Codex only received combos and the models named in the IDE config, so none of the models behind the providers added to 9Router showed up. Those single models are now always added; 9Router's combos stay hidden. The copy of the same model that comes through OmniRoute is still added only when the IDE config names it, so each model is listed once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
401 lines
13 KiB
TypeScript
401 lines
13 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 STATUS_OK = { ok: true, surum: '4', 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)
|
|
},
|
|
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 → login item
|
|
expect(world.events).toEqual([
|
|
`write ${BRIDGE_JSON}`,
|
|
`spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`,
|
|
'status ok',
|
|
`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.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.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('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([])
|
|
})
|
|
})
|