diff --git a/cli/__tests__/adapters.opencode.test.mjs b/cli/__tests__/adapters.opencode.test.mjs index e77eb53e9f..a5f76a7df5 100644 --- a/cli/__tests__/adapters.opencode.test.mjs +++ b/cli/__tests__/adapters.opencode.test.mjs @@ -1031,8 +1031,8 @@ describe('opencode external provider config', () => { }); describe('opencode adapter — spawn()', () => { - test('records both smoke-tested OpenCode versions', () => { - expect(TESTED_OPENCODE_VERSIONS).toEqual(['1.18.35', '1.19.0']); + test('records every smoke-tested OpenCode version', () => { + expect(TESTED_OPENCODE_VERSIONS).toEqual(['1.18.35', '1.19.0', '1.19.4']); }); test('a seat with no public sandbox declaration runs as trusted', async () => { @@ -1077,7 +1077,7 @@ describe('opencode adapter — spawn()', () => { }); }); - test.each(['1.18.35', '1.19.0'])('a public seat accepts smoke-tested OpenCode %s', async (version) => { + test.each(['1.18.35', '1.19.0', '1.19.4'])('a public seat accepts smoke-tested OpenCode %s', async (version) => { await withProcessEnv(ALLOW_UNTESTED_OPENCODE_VERSION_ENV, undefined, async () => { await withTemp(async (root) => { const workspace = join(root, 'workspace'); @@ -1137,7 +1137,7 @@ describe('opencode adapter — spawn()', () => { }); expect(spawn.calls).toHaveLength(1); expect(warning).toHaveBeenCalledWith( - '[opencode] installed OpenCode 1.20.0 is newer than smoke-tested versions 1.18.35, 1.19.0; trusted seat continues', + '[opencode] installed OpenCode 1.20.0 is newer than smoke-tested versions 1.18.35, 1.19.0, 1.19.4; trusted seat continues', ); } finally { warning.mockRestore(); @@ -1150,22 +1150,27 @@ describe('opencode adapter — spawn()', () => { { description: 'older', result: { status: 0, stdout: 'OpenCode 1.18.0\n' }, - message: /installed OpenCode 1\.18\.0 is older than smoke-tested versions 1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + message: /installed OpenCode 1\.18\.0 is older than smoke-tested versions 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, }, { description: 'between smoke-tested versions', result: { status: 0, stdout: 'OpenCode 1.18.40\n' }, - message: /installed OpenCode 1\.18\.40 differs from smoke-tested versions 1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + message: /installed OpenCode 1\.18\.40 differs from smoke-tested versions 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + }, + { + description: 'untested next-patch', + result: { status: 0, stdout: 'OpenCode 1.19.5\n' }, + message: /installed OpenCode 1\.19\.5 is newer than smoke-tested versions 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, }, { description: 'newer', result: { status: 0, stdout: 'OpenCode 1.20.0\n' }, - message: /installed OpenCode 1\.20\.0 is newer than smoke-tested versions 1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + message: /installed OpenCode 1\.20\.0 is newer than smoke-tested versions 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, }, { description: 'unavailable', result: { status: 1, stdout: '', stderr: 'version command failed' }, - message: /installed OpenCode version is unknown; smoke-tested versions: 1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + message: /installed OpenCode version is unknown; smoke-tested versions: 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, }, { description: 'update announcement containing multiple versions', @@ -1173,7 +1178,7 @@ describe('opencode adapter — spawn()', () => { status: 0, stdout: 'A new release is available: 1.18.35 -> 1.19.0\n', }, - message: /installed OpenCode version is unknown; smoke-tested versions: 1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + message: /installed OpenCode version is unknown; smoke-tested versions: 1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, }, ])('a public seat refuses an $description version before creating state or spawning', async ({ result, @@ -1241,7 +1246,7 @@ describe('opencode adapter — spawn()', () => { _binaryPath: '/usr/local/bin/opencode', _platform: 'darwin', _spawnImpl: spawn.impl, - })).rejects.toThrow(/refuse to run.*1\.20\.0.*1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/); + })).rejects.toThrow(/refuse to run.*1\.20\.0.*1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/); expect(spawn.calls).toHaveLength(0); expect(validateEnvironmentSpec({ version: 1, [ALLOW_UNTESTED_OPENCODE_VERSION_ENV]: '1', @@ -1292,11 +1297,11 @@ describe('opencode adapter — spawn()', () => { await opencode.spawn('public turn', spawnOptions); expect(spawn.calls).toHaveLength(1); expect(warning).toHaveBeenCalledWith( - '[opencode] installed OpenCode 1.20.0 is newer than smoke-tested versions 1.18.35, 1.19.0; continuing because the host process set COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1', + '[opencode] installed OpenCode 1.20.0 is newer than smoke-tested versions 1.18.35, 1.19.0, 1.19.4; continuing because the host process set COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1', ); } else { await expect(opencode.spawn('public turn', spawnOptions)).rejects.toThrow( - /refuse to run.*1\.20\.0.*1\.18\.35, 1\.19\.0.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, + /refuse to run.*1\.20\.0.*1\.18\.35, 1\.19\.0, 1\.19\.4.*COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION=1/, ); expect(spawn.calls).toHaveLength(0); expect(warning).not.toHaveBeenCalled(); diff --git a/cli/__tests__/adapters.qwen.test.mjs b/cli/__tests__/adapters.qwen.test.mjs index cbec37e616..1eee76b116 100644 --- a/cli/__tests__/adapters.qwen.test.mjs +++ b/cli/__tests__/adapters.qwen.test.mjs @@ -129,12 +129,34 @@ test('public launch isolates HOME and config, strips shadows, and never serializ }); }); -test('public version drift refuses before launching a provider proxy or child', async () => { +test('records every smoke-tested Qwen version', () => { + expect(TESTED_QWEN_VERSIONS).toEqual(['0.25.1', '0.26.2']); +}); + +test.each(['0.25.1', '0.26.2'])('a public seat accepts smoke-tested Qwen %s without the override', async (version) => { await withSeat(async (ctx) => { - spawnSyncMock.mockImplementation(() => ({ status: 0, stdout: '0.26.0\n' })); + spawnSyncMock.mockImplementation(() => ({ status: 0, stdout: `${version}\n` })); + const previous = process.env[ALLOW_UNTESTED_QWEN_VERSION_ENV]; + const warning = jest.spyOn(console, 'warn').mockImplementation(() => {}); + try { + delete process.env[ALLOW_UNTESTED_QWEN_VERSION_ENV]; + await expect(qwen.spawn('hello', { environment: { model: 'test-model' }, ...ctx, _spawnImpl: () => fakeChild() })) + .resolves.toMatchObject({ text: 'Qwen reply' }); + expect(warning).not.toHaveBeenCalled(); + } finally { + warning.mockRestore(); + if (previous === undefined) delete process.env[ALLOW_UNTESTED_QWEN_VERSION_ENV]; + else process.env[ALLOW_UNTESTED_QWEN_VERSION_ENV] = previous; + } + }); +}); + +test.each(['0.26.0', '0.26.3'])('public version drift to %s refuses before launching a provider proxy or child', async (version) => { + await withSeat(async (ctx) => { + spawnSyncMock.mockImplementation(() => ({ status: 0, stdout: `${version}\n` })); const spawn = jest.fn(); await expect(qwen.spawn('hello', { environment: { model: 'test-model' }, ...ctx, _spawnImpl: spawn })) - .rejects.toThrow(/Public Qwen seats refuse version 0.26.0/); + .rejects.toThrow(`Public Qwen seats refuse version ${version}; smoke-tested: 0.25.1, 0.26.2.`); expect(spawn).not.toHaveBeenCalled(); }); }); diff --git a/cli/src/lib/adapters/opencode.js b/cli/src/lib/adapters/opencode.js index 2f5687bf2d..b7394b425e 100644 --- a/cli/src/lib/adapters/opencode.js +++ b/cli/src/lib/adapters/opencode.js @@ -65,7 +65,7 @@ const ISOLATED_INSTANCE_PORT = 8766; const LOOPBACK_BRIDGE = fileURLToPath(new URL('../sandbox/opencode-loopback.mjs', import.meta.url)); const TERMINATION_GRACE_MS = 5000; // Bump only with a passing real sandbox-exec smoke against the new version. -const TESTED_OPENCODE_VERSIONS = Object.freeze(['1.18.35', '1.19.0']); +const TESTED_OPENCODE_VERSIONS = Object.freeze(['1.18.35', '1.19.0', '1.19.4']); const ALLOW_UNTESTED_OPENCODE_VERSION_ENV = 'COMMONLY_OPENCODE_ALLOW_UNTESTED_VERSION'; const VERSION_CHECK_TIMEOUT_MS = 5000; const OPENCODE_CONFIG_GITIGNORE = [ diff --git a/cli/src/lib/adapters/qwen.js b/cli/src/lib/adapters/qwen.js index cbb6f45d59..3713c920e7 100644 --- a/cli/src/lib/adapters/qwen.js +++ b/cli/src/lib/adapters/qwen.js @@ -1,4 +1,4 @@ -/** Qwen Code 0.25.1 wrapper. Public turns have fresh HOME, config and keys. */ +/** Qwen Code 0.25.1 and 0.26.2 wrapper. Public turns have fresh HOME, config and keys. */ import { requireSeatModel } from './seat-model.js'; import { spawn as childSpawn, spawnSync } from 'child_process'; import { lstatSync, realpathSync } from 'fs'; @@ -17,7 +17,7 @@ import { resolveProviderKeyFile, startProviderProxy } from '../provider-proxy.js import { adapterFailure, spawnCredentials } from '../upstream-refusal.js'; // Add a version only with a passing real clean-box sandbox smoke that names it. -const TESTED_QWEN_VERSIONS = Object.freeze(['0.25.1']); +const TESTED_QWEN_VERSIONS = Object.freeze(['0.25.1', '0.26.2']); const ALLOW_UNTESTED_QWEN_VERSION_ENV = 'COMMONLY_QWEN_ALLOW_UNTESTED_VERSION'; const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000; const TERMINATION_GRACE_MS = 5000;