Files
aiturk-hermes-ide/apps/desktop/electron/codex-link-client.test.ts
T

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)
})
})