174 lines
6.8 KiB
TypeScript
174 lines
6.8 KiB
TypeScript
import { createHash } from 'node:crypto'
|
|
import { EventEmitter } from 'node:events'
|
|
import { PassThrough } from 'node:stream'
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { parseControlResponse, validateControlRequest } from '../codex-link-control'
|
|
import controlSchema from '../codex-link-control.schema.json'
|
|
import { CODEX_LINK_CONTROL_SCHEMA_HASH } from '../codex-link-control.types'
|
|
import type { CodexLinkControlRequest } from '../codex-link-control.types'
|
|
import { canonicalJson } from '../codex-link-json'
|
|
|
|
import { createCodexLinkClient } from './codex-link-client'
|
|
|
|
const request: CodexLinkControlRequest = {
|
|
contract_version: 'codex-link/1',
|
|
kind: 'settings_get',
|
|
request_id: 'req-1',
|
|
operation_id: 'op-1'
|
|
}
|
|
|
|
const response = {
|
|
...request,
|
|
kind: 'settings_result',
|
|
settings: {
|
|
settings_revision: 1,
|
|
enabled: true,
|
|
compression_enabled: true,
|
|
threshold_tokens: 250000,
|
|
source_binding_handle: null,
|
|
compact_route_id: null,
|
|
legacy_intent_imported: false,
|
|
ownership_manifest_revision: 0,
|
|
activation_state: 'pending_runtime_acceptance'
|
|
},
|
|
available_sources: []
|
|
}
|
|
|
|
function harness(reply: string = JSON.stringify(response), env: Record<string, string> = {}) {
|
|
let stdin = ''
|
|
|
|
const child = Object.assign(new EventEmitter(), {
|
|
stdout: new PassThrough(),
|
|
stderr: new PassThrough(),
|
|
stdin: new PassThrough(),
|
|
kill: vi.fn()
|
|
})
|
|
|
|
child.stdin.on('data', chunk => {
|
|
stdin += chunk.toString()
|
|
})
|
|
child.stdin.on('finish', () =>
|
|
queueMicrotask(() => {
|
|
const bytes = Buffer.from(reply)
|
|
|
|
// UTF-8 chunks may end inside a multi-byte character.
|
|
for (const byte of bytes) {
|
|
child.stdout.write(Buffer.from([byte]))
|
|
}
|
|
|
|
child.stderr.write('diagnostic that must not become a response')
|
|
child.emit('close', 0)
|
|
})
|
|
)
|
|
const spawn = vi.fn(() => child)
|
|
|
|
const control = createCodexLinkClient({
|
|
spawn,
|
|
existsSync: () => true,
|
|
platform: 'win32',
|
|
env: { LOCALAPPDATA: 'C:\\Users\\Example User\\AppData\\Local', SystemRoot: 'C:\\Windows', ...env }
|
|
})
|
|
|
|
return { control, spawn, child, stdin: () => stdin }
|
|
}
|
|
|
|
describe('Codex connection control boundary', () => {
|
|
it('pins the generated control-only schema', () => {
|
|
expect('sha256:' + createHash('sha256').update(canonicalJson(controlSchema)).digest('hex')).toBe(CODEX_LINK_CONTROL_SCHEMA_HASH)
|
|
expect(controlSchema.$defs).not.toHaveProperty('runtime_attach')
|
|
expect(controlSchema.$defs).not.toHaveProperty('checkpoint_commit')
|
|
})
|
|
it('uses only fixed command arguments and sends the exact request on stdin with EOF', async () => {
|
|
const h = harness()
|
|
expect(await h.control(request)).toEqual(response)
|
|
expect(h.stdin()).toBe(JSON.stringify(request) + '\n')
|
|
const [file, args, options] = h.spawn.mock.calls[0] as unknown as [string, string[], any]
|
|
expect(file).toBe('C:\\Windows\\System32\\cmd.exe')
|
|
expect(args).toEqual([
|
|
'/d',
|
|
'/s',
|
|
'/c',
|
|
'""C:\\Users\\Example User\\AppData\\Local\\TurkServis\\AITURK\\program\\bin\\aiturk.cmd" codex-link control --stdin"'
|
|
])
|
|
expect(options).toMatchObject({ shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
})
|
|
it('rejects runtime and arbitrary path/credential commands before spawning', async () => {
|
|
const h = harness()
|
|
await expect(h.control({ ...request, kind: 'runtime_attach' })).rejects.toThrow()
|
|
await expect(h.control({ ...request, path: 'C:\\private' })).rejects.toThrow()
|
|
expect(h.spawn).not.toHaveBeenCalled()
|
|
})
|
|
it('rejects shell expansion in the entrypoint path', async () => {
|
|
const h = harness(undefined, { LOCALAPPDATA: 'C:\\Users\\%PATH%\\Local' })
|
|
await expect(h.control(request)).rejects.toThrow('invalid_entrypoint')
|
|
expect(h.spawn).not.toHaveBeenCalled()
|
|
})
|
|
it('binds the response to both request and operation identity', () => {
|
|
expect(() =>
|
|
parseControlResponse(JSON.stringify({ ...response, operation_id: 'other-chat-op' }), request)
|
|
).toThrow()
|
|
expect(() => parseControlResponse(JSON.stringify({ ...response, request_id: 'other-request' }), request)).toThrow()
|
|
})
|
|
it('rejects duplicate keys, floats, trailing JSON and invalid UTF8', () => {
|
|
expect(() =>
|
|
parseControlResponse(
|
|
JSON.stringify(response).replace('"settings_revision":1', '"settings_revision":1,"settings_revision":2'),
|
|
request
|
|
)
|
|
).toThrow('duplicate_json_key')
|
|
expect(() => parseControlResponse(JSON.stringify(response).replace('250000', '250000.0'), request)).toThrow(
|
|
'floats_or_non_json_type'
|
|
)
|
|
expect(() => parseControlResponse(JSON.stringify(response) + '{}', request)).toThrow()
|
|
expect(() => parseControlResponse(new Uint8Array([0xc3, 0x28]), request)).toThrow()
|
|
})
|
|
it('rejects oversized requests and unsafe integer settings', () => {
|
|
expect(() =>
|
|
validateControlRequest({
|
|
...request,
|
|
kind: 'settings_update',
|
|
expected_settings_revision: 1,
|
|
patch: { threshold_tokens: Number.MAX_SAFE_INTEGER + 1 }
|
|
})
|
|
).toThrow()
|
|
expect(() => validateControlRequest({ ...request, operation_id: 'x'.repeat(70000) })).toThrow()
|
|
})
|
|
it('does not retry a write after a broken reply', async () => {
|
|
const h = harness('broken')
|
|
await expect(h.control({ ...request, kind: 'catalog_sync' })).rejects.toThrow('outcome_unknown')
|
|
expect(h.spawn).toHaveBeenCalledTimes(1)
|
|
})
|
|
it('preserves a structured CLI rejection returned with exit code one', async () => {
|
|
const rejection = { ...request, kind: 'control_error', settings_revision: 2, error: { code: 'stale_settings_cas', message: 'Ayarlar değişti.', recovery_action: 'retry_control_read' } }
|
|
const h = harness()
|
|
h.child.stdin.removeAllListeners('finish')
|
|
h.child.stdin.on('finish', () => queueMicrotask(() => {
|
|
h.child.stdout.write(JSON.stringify(rejection))
|
|
h.child.emit('close', 1)
|
|
}))
|
|
expect(await h.control(request)).toEqual(rejection)
|
|
expect(h.spawn).toHaveBeenCalledTimes(1)
|
|
})
|
|
it('preserves exact Unicode when response bytes arrive separately', async () => {
|
|
const h = harness(
|
|
JSON.stringify({
|
|
...response,
|
|
available_sources: [{ source_binding_handle: 'source-a', label: 'Türkçe é/é 🧪', authorized: true }]
|
|
})
|
|
)
|
|
|
|
const result = await h.control(request)
|
|
expect(result.kind === 'settings_result' && result.available_sources[0].label).toBe('Türkçe é/é 🧪')
|
|
})
|
|
it('bounds stdout and terminates only the short proxy', async () => {
|
|
const h = harness('x'.repeat(1024 * 1024 + 1))
|
|
// Avoid a million small chunks: replace the synthetic finish listener.
|
|
h.child.stdin.removeAllListeners('finish')
|
|
h.child.stdin.on('finish', () => queueMicrotask(() => h.child.stdout.emit('data', Buffer.alloc(1024 * 1024 + 1))))
|
|
await expect(h.control(request)).rejects.toThrow('outcome_unknown')
|
|
expect(h.child.kill).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|