feat(settings): Codex Bağlama, on by default
Settings → Codex Bağlama runs the bundled bridge for an OpenAI Codex install on the same computer: the IDE's models and combos appear in Codex and switching models keeps the conversation. The IDE points Codex's openai_base_url at the bridge only after the bridge answers, keeps it running across logins, and removes every trace when the setting is turned off. A Codex that already uses another base URL is left untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
This commit is contained in:
1 parent
1ce5ab82cf
commit
dea67fc153
16 files changed
+2317
-2
No files matched your search
@@ -0,0 +1,403 @@
|
||||
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_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: '1', 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_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('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([])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user