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
288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
addCodexLinkLine,
|
|
CODEX_LINK_LINE,
|
|
CODEX_LINK_RUN_COMMAND_MAX,
|
|
codexAutostartPlan,
|
|
codexBridgeArgs,
|
|
codexBridgeCommandLine,
|
|
codexBridgeConfig,
|
|
codexBridgeLauncherSource,
|
|
inspectCodexConfig,
|
|
parseCodexLinkSettings,
|
|
removeCodexLinkLine,
|
|
serializeCodexLinkSettings
|
|
} from './codex-link-config'
|
|
|
|
const FOREIGN = 'openai_base_url = "http://127.0.0.1:8799/backend-api/codex"'
|
|
|
|
// Add then remove must hand the user's file back exactly as it was.
|
|
function expectRoundTrip(original: string) {
|
|
const added = addCodexLinkLine(original)
|
|
|
|
expect(added.changed).toBe(true)
|
|
expect(added.owner).toBe('ours')
|
|
expect(inspectCodexConfig(added.text)).toBe('ours')
|
|
|
|
const removed = removeCodexLinkLine(added.text)
|
|
|
|
expect(removed.changed).toBe(true)
|
|
expect(removed.text).toBe(original)
|
|
|
|
return added.text
|
|
}
|
|
|
|
describe('the contract line', () => {
|
|
it('is the exact text Codex and the bridge agree on', () => {
|
|
expect(CODEX_LINK_LINE).toBe(
|
|
'openai_base_url = "http://127.0.0.1:8795/backend-api/codex" # AITURK IDE Codex Baglama'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('addCodexLinkLine', () => {
|
|
it('writes the line into an empty file', () => {
|
|
expect(expectRoundTrip('')).toBe(`${CODEX_LINK_LINE}\n`)
|
|
})
|
|
|
|
it('puts the line before the first table when the file only has tables', () => {
|
|
const original = '[projects."C:\\\\work"]\ntrust_level = "trusted"\n\n[mcp_servers.x]\ncommand = "node"\n'
|
|
const text = expectRoundTrip(original)
|
|
|
|
expect(text.indexOf(CODEX_LINK_LINE)).toBeLessThan(text.indexOf('[projects.'))
|
|
expect(text.endsWith(original)).toBe(true)
|
|
})
|
|
|
|
it('keeps existing top-level keys and tables intact', () => {
|
|
const original = '# my config\nmodel = "gpt-6"\napproval_policy = "never"\n\n[features]\nweb = true\n'
|
|
const text = expectRoundTrip(original)
|
|
|
|
expect(text.indexOf(CODEX_LINK_LINE)).toBeLessThan(text.indexOf('[features]'))
|
|
expect(text).toContain('model = "gpt-6"\napproval_policy = "never"\n\n[features]\nweb = true\n')
|
|
})
|
|
|
|
it('preserves a CRLF file and writes its own line with CRLF', () => {
|
|
const original = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n'
|
|
const text = expectRoundTrip(original)
|
|
|
|
expect(text).toBe(`${CODEX_LINK_LINE}\r\n${original}`)
|
|
expect(text.replace(/\r\n/g, '')).not.toMatch(/[\r\n]/)
|
|
})
|
|
|
|
it('keeps a byte-order mark at the very start', () => {
|
|
const text = expectRoundTrip('\uFEFFmodel = "gpt-6"\n')
|
|
|
|
expect(text.startsWith(`\uFEFF${CODEX_LINK_LINE}\n`)).toBe(true)
|
|
})
|
|
|
|
it('keeps a file without a trailing newline exactly as it was', () => {
|
|
expectRoundTrip('model = "gpt-6"\n[features]\nweb = true')
|
|
})
|
|
|
|
it('is idempotent once our line is there', () => {
|
|
const once = addCodexLinkLine('model = "gpt-6"\n[features]\n').text
|
|
const twice = addCodexLinkLine(once)
|
|
|
|
expect(twice.changed).toBe(false)
|
|
expect(twice.owner).toBe('ours')
|
|
expect(twice.text).toBe(once)
|
|
})
|
|
|
|
it('refreshes a stale copy of our own line in place', () => {
|
|
const stale = 'model = "gpt-6"\nopenai_base_url = "http://127.0.0.1:1/x" # AITURK IDE Codex Baglama\n[features]\n'
|
|
const edit = addCodexLinkLine(stale)
|
|
|
|
expect(edit.changed).toBe(true)
|
|
expect(edit.text).toBe(`model = "gpt-6"\n${CODEX_LINK_LINE}\n[features]\n`)
|
|
})
|
|
|
|
it('still owns its line after Codex rewrote the config and dropped the comment', () => {
|
|
const rewritten = 'openai_base_url = "http://127.0.0.1:8795/backend-api/codex"\nmodel = "x"\n'
|
|
|
|
expect(inspectCodexConfig(rewritten)).toBe('ours')
|
|
expect(removeCodexLinkLine(rewritten).text).toBe('model = "x"\n')
|
|
// Another local port is somebody else's bridge.
|
|
expect(inspectCodexConfig('openai_base_url = "http://127.0.0.1:8799/backend-api/codex"\n')).toBe('foreign')
|
|
})
|
|
|
|
it('does not touch a foreign openai_base_url and reports it', () => {
|
|
for (const original of [
|
|
`${FOREIGN}\n[features]\n`,
|
|
`model = "gpt-6"\r\n ${FOREIGN} # my own bridge\r\n`,
|
|
'"openai_base_url" = "https://example.test/v1"\n',
|
|
"'openai_base_url'='https://example.test/v1'\n"
|
|
]) {
|
|
const edit = addCodexLinkLine(original)
|
|
|
|
expect(edit).toEqual({ changed: false, owner: 'foreign', text: original })
|
|
expect(inspectCodexConfig(original)).toBe('foreign')
|
|
}
|
|
})
|
|
|
|
it('does not treat the same key under a table as top-level', () => {
|
|
const original = 'model = "gpt-6"\n\n[model_providers.mine]\nopenai_base_url = "https://example.test/v1"\n'
|
|
|
|
expect(inspectCodexConfig(original)).toBe('absent')
|
|
|
|
const text = expectRoundTrip(original)
|
|
|
|
expect(text.indexOf(CODEX_LINK_LINE)).toBeLessThan(text.indexOf('[model_providers.mine]'))
|
|
expect(text).toContain('[model_providers.mine]\nopenai_base_url = "https://example.test/v1"\n')
|
|
})
|
|
|
|
it('ignores the key inside a comment', () => {
|
|
const original = '# openai_base_url = "https://example.test/v1"\n #openai_base_url = "x"\nmodel = "gpt-6"\n'
|
|
|
|
expect(inspectCodexConfig(original)).toBe('absent')
|
|
expectRoundTrip(original)
|
|
})
|
|
|
|
it('ignores look-alikes inside multi-line values', () => {
|
|
// Neither the "key" inside the string nor the "[table]" inside it is real,
|
|
// so the genuine foreign key after it must still be found.
|
|
const inString = 'notes = """\nopenai_base_url = "nope"\n[not.a.table]\n"""\nmodel = "gpt-6"\n'
|
|
|
|
expect(inspectCodexConfig(inString)).toBe('absent')
|
|
expect(inspectCodexConfig(`${inString}${FOREIGN}\n`)).toBe('foreign')
|
|
|
|
const inArray = 'notify = [\n "a",\n ["b"]\n]\n'
|
|
|
|
expect(inspectCodexConfig(`${inArray}${FOREIGN}\n`)).toBe('foreign')
|
|
expect(inspectCodexConfig(`literal = '''\n[x]\n'''\n${FOREIGN}\n`)).toBe('foreign')
|
|
})
|
|
|
|
it('is not fooled by a bracket or hash inside a quoted value', () => {
|
|
const original = `title = "a [b] # c"\n${FOREIGN}\n`
|
|
|
|
expect(inspectCodexConfig(original)).toBe('foreign')
|
|
})
|
|
})
|
|
|
|
describe('removeCodexLinkLine', () => {
|
|
it('removes only our line, wherever the user moved it', () => {
|
|
const text = `model = "gpt-6"\r\n${CODEX_LINK_LINE}\r\napproval_policy = "never"\r\n[features]\r\n`
|
|
const edit = removeCodexLinkLine(text)
|
|
|
|
expect(edit).toEqual({
|
|
changed: true,
|
|
owner: 'absent',
|
|
text: 'model = "gpt-6"\r\napproval_policy = "never"\r\n[features]\r\n'
|
|
})
|
|
})
|
|
|
|
it('leaves a file without our line untouched', () => {
|
|
for (const original of ['', 'model = "gpt-6"\n', `${FOREIGN}\n[features]\n`]) {
|
|
expect(removeCodexLinkLine(original).changed).toBe(false)
|
|
expect(removeCodexLinkLine(original).text).toBe(original)
|
|
}
|
|
})
|
|
|
|
it('never removes a foreign line', () => {
|
|
expect(removeCodexLinkLine(`${FOREIGN}\n`)).toEqual({ changed: false, owner: 'foreign', text: `${FOREIGN}\n` })
|
|
})
|
|
|
|
it('leaves our marker alone when it sits under a table', () => {
|
|
const original = `[model_providers.mine]\n${CODEX_LINK_LINE}\n`
|
|
|
|
expect(removeCodexLinkLine(original)).toEqual({ changed: false, owner: 'absent', text: original })
|
|
})
|
|
})
|
|
|
|
describe('preference file', () => {
|
|
it('is on by default when the file is missing or malformed', () => {
|
|
for (const raw of [null, undefined, '', '{', 'null', '[]', '"off"', '{"kapsam":"ide"}', '{"enabled":"false"}']) {
|
|
expect(parseCodexLinkSettings(raw).enabled).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('is off only when explicitly disabled', () => {
|
|
expect(parseCodexLinkSettings('{"enabled": false, "kapsam": "ide"}').enabled).toBe(false)
|
|
expect(parseCodexLinkSettings('{"enabled": true}').enabled).toBe(true)
|
|
})
|
|
|
|
it('writes the contract shape and keeps unknown fields', () => {
|
|
expect(JSON.parse(serializeCodexLinkSettings({}, false))).toEqual({ enabled: false, kapsam: 'ide' })
|
|
|
|
const { stored } = parseCodexLinkSettings('{"enabled": false, "kapsam": "hepsi", "gelecek": {"a": 1}}')
|
|
|
|
expect(JSON.parse(serializeCodexLinkSettings(stored, true))).toEqual({
|
|
enabled: true,
|
|
gelecek: { a: 1 },
|
|
kapsam: 'hepsi'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('bridge configuration and command', () => {
|
|
const launch = {
|
|
bridgeHome: 'C:\\Users\\a b\\AppData\\Local\\TurkServis\\AITURK-IDE\\codex-bridge',
|
|
bridgeScript: 'C:\\Program Files\\AITURK IDE\\resources\\codex-bridge\\bridge.py',
|
|
hermesHome: 'C:\\Users\\a b\\AppData\\Local\\TurkServis\\AITURK-IDE\\agent'
|
|
}
|
|
|
|
const python = `${launch.hermesHome}\\hermes-agent\\venv\\Scripts\\pythonw.exe`
|
|
|
|
it('writes the agreed bridge.json', () => {
|
|
expect(codexBridgeConfig()).toEqual({
|
|
port: 8795,
|
|
chatgpt_backend: 'https://chatgpt.com',
|
|
routes: {},
|
|
nine_router_catalog: { enabled: false },
|
|
pano_kanca_user: false,
|
|
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
|
|
})
|
|
})
|
|
|
|
it('passes the same arguments to the spawn and the login item', () => {
|
|
expect(codexBridgeArgs(launch)).toEqual([
|
|
launch.bridgeScript,
|
|
'--home',
|
|
launch.bridgeHome,
|
|
'--hermes-home',
|
|
launch.hermesHome
|
|
])
|
|
expect(codexBridgeCommandLine(python, launch)).toBe(
|
|
`"${python}" "${launch.bridgeScript}" --home "${launch.bridgeHome}" --hermes-home "${launch.hermesHome}"`
|
|
)
|
|
})
|
|
|
|
it('drops a trailing separator that would escape the closing quote', () => {
|
|
expect(
|
|
codexBridgeCommandLine('C:\\py\\pythonw.exe', {
|
|
bridgeHome: 'C:\\d\\',
|
|
bridgeScript: 'C:\\b.py',
|
|
hermesHome: 'C:\\h\\'
|
|
})
|
|
).toBe('"C:\\py\\pythonw.exe" "C:\\b.py" --home "C:\\d" --hermes-home "C:\\h"')
|
|
})
|
|
|
|
it('uses the direct command for the login item when it fits a Run value', () => {
|
|
const short = { bridgeHome: 'C:\\d', bridgeScript: 'C:\\b\\bridge.py', hermesHome: 'C:\\h' }
|
|
|
|
expect(codexAutostartPlan('C:\\py\\pythonw.exe', short, 'C:\\d\\baslat.pyw')).toEqual({
|
|
command: '"C:\\py\\pythonw.exe" "C:\\b\\bridge.py" --home "C:\\d" --hermes-home "C:\\h"',
|
|
launcher: null
|
|
})
|
|
})
|
|
|
|
it('falls back to a launcher when the direct command is too long for a Run value', () => {
|
|
expect(codexBridgeCommandLine(python, launch).length).toBeGreaterThan(CODEX_LINK_RUN_COMMAND_MAX)
|
|
|
|
const launcherPath = `${launch.bridgeHome}\\baslat.pyw`
|
|
const plan = codexAutostartPlan(python, launch, launcherPath)
|
|
|
|
expect(plan?.command).toBe(`"${python}" "${launcherPath}"`)
|
|
expect(plan?.command.length).toBeLessThanOrEqual(CODEX_LINK_RUN_COMMAND_MAX)
|
|
expect(plan?.launcher).toEqual({ path: launcherPath, source: codexBridgeLauncherSource(launch) })
|
|
// The launcher carries the very same argv as the direct command.
|
|
expect(plan?.launcher?.source).toContain(`ARGV = ${JSON.stringify(codexBridgeArgs(launch))}`)
|
|
})
|
|
|
|
it('registers nothing when not even the launcher command fits', () => {
|
|
const deep = `C:\\${'x'.repeat(300)}`
|
|
|
|
expect(codexAutostartPlan(`${deep}\\pythonw.exe`, launch, `${deep}\\baslat.pyw`)).toBeNull()
|
|
})
|
|
})
|