Codex compresses request bodies with zstd. The agent venv is Python 3.11, which has no zstd in its standard library, so every Codex request failed on a fresh install. The matching zstandard build now ships beside the bridge; the status endpoint reports ok=false when bodies cannot be read, and the IDE then leaves Codex's config alone. A running bridge from an older build is replaced, and "Sync now" makes the bridge re-read the IDE session at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link'
|
|
import {
|
|
CODEX_LINK_LINE,
|
|
CODEX_LINK_REFRESH_URL,
|
|
CODEX_LINK_STATUS_URL,
|
|
CODEX_LINK_STOP_URL,
|
|
codexBridgeArgs,
|
|
codexBridgeConfig
|
|
} from './codex-link-config'
|
|
|
|
const PATHS: CodexLinkPaths = {
|
|
settingsFile: 'D:\\ide\\codex-baglama.json',
|
|
bridgeHome: 'D:\\ide\\codex-bridge',
|
|
hermesHome: 'D:\\ide\\agent',
|
|
bridgeScript: 'D:\\app\\codex-bridge\\bridge.py',
|
|
pythonw: 'D:\\ide\\agent\\venv\\Scripts\\pythonw.exe',
|
|
python: 'D:\\ide\\agent\\venv\\Scripts\\python.exe',
|
|
codexConfig: 'D:\\home\\.codex\\config.toml'
|
|
}
|
|
|
|
const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json`
|
|
const BACKUP = `${PATHS.codexConfig}.aiturk-yedek`
|
|
const STATUS_OK = { ok: true, surum: '2', ide_durum: 'tamam', ide_model: 7, ozet: true }
|
|
const USER_CONFIG = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n'
|
|
|
|
interface World {
|
|
/** What the machine looks like; mutated by the fake effects. */
|
|
files: Map<string, string>
|
|
/** Is something answering on the bridge port? */
|
|
listening: boolean
|
|
/** Version the listening bridge reports before any spawn; a spawn brings the current one. */
|
|
staleVersion: null | string
|
|
/** Does a spawn bring the bridge up? `false` = it never answers. */
|
|
spawnWorks: boolean
|
|
/** Spawned bridge only starts answering after this many status probes. */
|
|
probesUntilUp: number
|
|
autostart: null | string
|
|
regFails: boolean
|
|
/** Every observable effect, in order. */
|
|
events: string[]
|
|
clock: number
|
|
}
|
|
|
|
function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {}) {
|
|
const world: World = {
|
|
autostart: null,
|
|
clock: 0,
|
|
events: [],
|
|
files: new Map([
|
|
[PATHS.codexConfig, USER_CONFIG],
|
|
[PATHS.bridgeScript, '# bridge'],
|
|
[PATHS.pythonw, ''],
|
|
[PATHS.python, '']
|
|
]),
|
|
listening: false,
|
|
staleVersion: null,
|
|
probesUntilUp: 0,
|
|
regFails: false,
|
|
spawnWorks: true,
|
|
...patch
|
|
}
|
|
|
|
let spawned = false
|
|
let probesSinceSpawn = 0
|
|
|
|
const deps: CodexLinkDeps = {
|
|
paths: PATHS,
|
|
exists: file => world.files.has(file),
|
|
readText: file => world.files.get(file) ?? null,
|
|
writeTextAtomic: (file, text) => {
|
|
world.events.push(`write ${file}`)
|
|
world.files.set(file, text)
|
|
},
|
|
backupOnce: (source, target) => {
|
|
if (!world.files.has(target)) {
|
|
world.events.push(`backup ${target}`)
|
|
world.files.set(target, world.files.get(source) ?? '')
|
|
}
|
|
},
|
|
spawnBridge: (command, args, env) => {
|
|
world.events.push(`spawn ${command} ${JSON.stringify(args)} ${JSON.stringify(env)}`)
|
|
spawned = true
|
|
probesSinceSpawn = 0
|
|
world.staleVersion = null
|
|
},
|
|
request: async (method, url) => {
|
|
if (method === 'GET' && url === CODEX_LINK_STATUS_URL) {
|
|
if (spawned && world.spawnWorks && !world.listening) {
|
|
probesSinceSpawn += 1
|
|
|
|
if (probesSinceSpawn > world.probesUntilUp) {
|
|
world.listening = true
|
|
}
|
|
}
|
|
|
|
if (world.listening) {
|
|
world.events.push('status ok')
|
|
|
|
return world.staleVersion ? { ...STATUS_OK, surum: world.staleVersion } : STATUS_OK
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
if (method === 'POST' && url === CODEX_LINK_REFRESH_URL) {
|
|
world.events.push('refresh')
|
|
|
|
return world.listening ? STATUS_OK : null
|
|
}
|
|
|
|
if (method === 'POST' && url === CODEX_LINK_STOP_URL) {
|
|
world.events.push('stop')
|
|
|
|
if (!world.listening) {
|
|
return null
|
|
}
|
|
|
|
world.listening = false
|
|
|
|
return { ok: true }
|
|
}
|
|
|
|
throw new Error(`unexpected request ${method} ${url}`)
|
|
},
|
|
setAutostart: async command => {
|
|
world.events.push('autostart set')
|
|
|
|
if (world.regFails) {
|
|
throw new Error('reg add exited 1')
|
|
}
|
|
|
|
world.autostart = command
|
|
},
|
|
clearAutostart: async () => {
|
|
world.events.push('autostart clear')
|
|
world.autostart = null
|
|
},
|
|
sleep: async ms => {
|
|
world.clock += ms
|
|
},
|
|
now: () => world.clock,
|
|
log: () => undefined
|
|
}
|
|
|
|
return { link: createCodexLink(deps), world }
|
|
}
|
|
|
|
const writes = (world: World) => world.events.filter(event => event.startsWith('write '))
|
|
const spawns = (world: World) => world.events.filter(event => event.startsWith('spawn '))
|
|
// Effects that change the machine (a status probe does not).
|
|
const effects = (world: World) => world.events.filter(event => event !== 'status ok')
|
|
|
|
describe('applyCodexLink', () => {
|
|
it('touches nothing when Codex is not installed', async () => {
|
|
const { link, world } = createWorld()
|
|
world.files.delete(PATHS.codexConfig)
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status).toMatchObject({ enabled: true, codexFound: false, bridgeRunning: false, lastError: null })
|
|
expect(effects(world)).toEqual([])
|
|
expect(world.autostart).toBeNull()
|
|
})
|
|
|
|
it('touches nothing when Codex already uses another bridge', async () => {
|
|
const foreign = 'openai_base_url = "http://127.0.0.1:8799/backend-api/codex"\n[features]\n'
|
|
const { link, world } = createWorld({ listening: true })
|
|
world.files.set(PATHS.codexConfig, foreign)
|
|
|
|
const status = await link.apply({ sync: true })
|
|
|
|
expect(status).toMatchObject({ enabled: true, codexFound: true, foreign: true, bridgeRunning: true })
|
|
expect(effects(world)).toEqual([])
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(foreign)
|
|
})
|
|
|
|
it('waits for the IDE setup: no venv, no changes', async () => {
|
|
const { link, world } = createWorld()
|
|
world.files.delete(PATHS.pythonw)
|
|
world.files.delete(PATHS.python)
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status).toMatchObject({ enabled: true, codexFound: true, venvReady: false, foreign: false })
|
|
expect(effects(world)).toEqual([])
|
|
})
|
|
|
|
it('brings everything up in the safe order', async () => {
|
|
const { link, world } = createWorld({ probesUntilUp: 2 })
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status).toEqual({
|
|
enabled: true,
|
|
codexFound: true,
|
|
venvReady: true,
|
|
foreign: false,
|
|
bridgeRunning: true,
|
|
ideDurum: 'tamam',
|
|
ideModel: 7,
|
|
ozet: true,
|
|
lastError: null
|
|
})
|
|
|
|
const env = { AITURK_CODEX_BRIDGE_HOME: PATHS.bridgeHome, HERMES_HOME: PATHS.hermesHome }
|
|
|
|
const args = codexBridgeArgs({
|
|
bridgeHome: PATHS.bridgeHome,
|
|
bridgeScript: PATHS.bridgeScript,
|
|
hermesHome: PATHS.hermesHome
|
|
})
|
|
|
|
// bridge.json → start → verified answer → config.toml (backed up) → login item
|
|
expect(world.events).toEqual([
|
|
`write ${BRIDGE_JSON}`,
|
|
`spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`,
|
|
'status ok',
|
|
`backup ${BACKUP}`,
|
|
`write ${PATHS.codexConfig}`,
|
|
'autostart set'
|
|
])
|
|
|
|
expect(JSON.parse(world.files.get(BRIDGE_JSON) ?? '')).toEqual(codexBridgeConfig())
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
expect(world.files.get(BACKUP)).toBe(USER_CONFIG)
|
|
expect(world.autostart).toBe(
|
|
`"${PATHS.pythonw}" "${PATHS.bridgeScript}" --home "${PATHS.bridgeHome}" --hermes-home "${PATHS.hermesHome}"`
|
|
)
|
|
})
|
|
|
|
it('never points Codex at a bridge that did not come up', async () => {
|
|
const { link, world } = createWorld({ spawnWorks: false })
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status).toMatchObject({ enabled: true, bridgeRunning: false, lastError: 'bridge-start-failed' })
|
|
expect(spawns(world)).toHaveLength(1)
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
|
|
expect(world.files.has(BACKUP)).toBe(false)
|
|
expect(world.autostart).toBeNull()
|
|
expect(writes(world)).toEqual([`write ${BRIDGE_JSON}`])
|
|
// The start poll is bounded by its one budget.
|
|
expect(world.clock).toBe(CODEX_BRIDGE_START_BUDGET_MS)
|
|
})
|
|
|
|
it('takes its own line back out when the bridge can no longer be started', async () => {
|
|
const { link, world } = createWorld({ spawnWorks: false })
|
|
world.files.set(PATHS.codexConfig, `${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status.lastError).toBe('bridge-start-failed')
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
|
|
})
|
|
|
|
it('does not start a second bridge when one is already listening', async () => {
|
|
const { link, world } = createWorld({ listening: true })
|
|
|
|
await link.apply()
|
|
await link.apply({ sync: true })
|
|
|
|
expect(spawns(world)).toEqual([])
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
// The config is written once; the login item is refreshed on every apply.
|
|
expect(writes(world).filter(event => event === `write ${PATHS.codexConfig}`)).toHaveLength(1)
|
|
expect(world.events.filter(event => event === 'autostart set')).toHaveLength(2)
|
|
})
|
|
|
|
it('replaces a bridge left running by an older IDE build', async () => {
|
|
const { link, world } = createWorld({ listening: true, staleVersion: '0' })
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(world.events.indexOf('stop')).toBeGreaterThanOrEqual(0)
|
|
expect(spawns(world)).toHaveLength(1)
|
|
expect(world.events.indexOf('stop')).toBeLessThan(world.events.findIndex(event => event.startsWith('spawn')))
|
|
expect(status.bridgeRunning).toBe(true)
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
})
|
|
|
|
it('asks a running bridge to re-read the IDE session on every apply', async () => {
|
|
const { link, world } = createWorld({ listening: true })
|
|
|
|
await link.apply({ sync: true })
|
|
|
|
expect(world.events).toContain('refresh')
|
|
expect(spawns(world)).toEqual([])
|
|
})
|
|
|
|
it('never overwrites the first backup', async () => {
|
|
const { link, world } = createWorld({ listening: true })
|
|
world.files.set(BACKUP, 'the original original')
|
|
|
|
await link.apply()
|
|
|
|
expect(world.files.get(BACKUP)).toBe('the original original')
|
|
})
|
|
|
|
it('falls back to python.exe when pythonw.exe is missing', async () => {
|
|
const { link, world } = createWorld()
|
|
world.files.delete(PATHS.pythonw)
|
|
|
|
await link.apply()
|
|
|
|
expect(spawns(world)[0]).toContain(`spawn ${PATHS.python} `)
|
|
expect(world.autostart?.startsWith(`"${PATHS.python}" `)).toBe(true)
|
|
})
|
|
|
|
it('reports a missing bridge script instead of starting anything', async () => {
|
|
const { link, world } = createWorld()
|
|
world.files.delete(PATHS.bridgeScript)
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status.lastError).toBe('bridge-missing')
|
|
expect(effects(world)).toEqual([])
|
|
})
|
|
|
|
it('surfaces a registry failure but keeps the working link', async () => {
|
|
const { link, world } = createWorld({ listening: true, regFails: true })
|
|
|
|
const status = await link.apply()
|
|
|
|
expect(status).toMatchObject({ bridgeRunning: true, lastError: 'autostart-failed' })
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
})
|
|
|
|
it('undoes everything when turned off', async () => {
|
|
const { link, world } = createWorld({ listening: true })
|
|
|
|
await link.apply()
|
|
world.events.length = 0
|
|
|
|
const status = await link.apply({ enabled: false })
|
|
|
|
expect(status).toMatchObject({ enabled: false, codexFound: true, bridgeRunning: false, lastError: null })
|
|
expect(world.events).toEqual([
|
|
`write ${PATHS.settingsFile}`,
|
|
`write ${PATHS.codexConfig}`,
|
|
'autostart clear',
|
|
'stop'
|
|
])
|
|
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({ enabled: false, kapsam: 'ide' })
|
|
// Byte for byte what the user had before the feature touched it.
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
|
|
expect(world.autostart).toBeNull()
|
|
expect(world.listening).toBe(false)
|
|
})
|
|
|
|
it('stays off across applies and comes back when turned on', async () => {
|
|
const { link, world } = createWorld()
|
|
world.files.set(PATHS.settingsFile, '{"enabled": false, "kapsam": "ide", "not": "ours"}')
|
|
|
|
expect(await link.apply()).toMatchObject({ enabled: false, bridgeRunning: false })
|
|
expect(spawns(world)).toEqual([])
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
|
|
|
|
expect(await link.apply({ enabled: true })).toMatchObject({ enabled: true, bridgeRunning: true })
|
|
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({
|
|
enabled: true,
|
|
kapsam: 'ide',
|
|
not: 'ours'
|
|
})
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
|
|
})
|
|
|
|
it('does not stop a bridge that serves a foreign config when turned off', async () => {
|
|
const foreign = 'openai_base_url = "http://127.0.0.1:8799/backend-api/codex"\n'
|
|
const { link, world } = createWorld({ listening: true })
|
|
world.files.set(PATHS.codexConfig, foreign)
|
|
|
|
const status = await link.apply({ enabled: false })
|
|
|
|
expect(status).toMatchObject({ enabled: false, foreign: true, bridgeRunning: true })
|
|
expect(world.events).not.toContain('stop')
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(foreign)
|
|
expect(world.listening).toBe(true)
|
|
})
|
|
|
|
it('runs concurrent applies one after another', async () => {
|
|
const { link, world } = createWorld({ probesUntilUp: 3 })
|
|
|
|
const [first, second] = await Promise.all([link.apply(), link.apply({ enabled: false })])
|
|
|
|
expect(first.enabled).toBe(true)
|
|
expect(second).toMatchObject({ enabled: false, bridgeRunning: false })
|
|
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
|
|
expect(world.autostart).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('getStatus', () => {
|
|
it('reads without changing anything', async () => {
|
|
const { link, world } = createWorld({ listening: true })
|
|
|
|
const status = await link.getStatus()
|
|
|
|
expect(status).toMatchObject({
|
|
enabled: true,
|
|
codexFound: true,
|
|
venvReady: true,
|
|
foreign: false,
|
|
bridgeRunning: true,
|
|
ideDurum: 'tamam',
|
|
ideModel: 7,
|
|
ozet: true
|
|
})
|
|
expect(effects(world)).toEqual([])
|
|
})
|
|
|
|
it('reports a bridge that is not answering', async () => {
|
|
const { link, world } = createWorld()
|
|
|
|
expect(await link.getStatus()).toMatchObject({ bridgeRunning: false, ideDurum: null, ideModel: null, ozet: null })
|
|
expect(effects(world)).toEqual([])
|
|
})
|
|
})
|