@fixy/codex-adapter 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/adapter.test.d.ts +2 -0
- package/dist/__tests__/adapter.test.d.ts.map +1 -0
- package/dist/__tests__/adapter.test.js +165 -0
- package/dist/__tests__/adapter.test.js.map +1 -0
- package/dist/__tests__/parse.test.d.ts +2 -0
- package/dist/__tests__/parse.test.d.ts.map +1 -0
- package/dist/__tests__/parse.test.js +78 -0
- package/dist/__tests__/parse.test.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/adapter.test.ts +195 -0
- package/src/__tests__/parse.test.ts +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createCodexAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let originalPath;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-codex-test-'));
|
|
10
|
+
originalPath = process.env.PATH ?? '';
|
|
11
|
+
const mockCodex = path.join(tmpDir, 'codex');
|
|
12
|
+
fs.writeFileSync(mockCodex, `#!/bin/bash
|
|
13
|
+
if [[ "$1" == "--version" ]]; then
|
|
14
|
+
echo "codex-cli 0.112.0"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [[ "$1" == "exec" ]]; then
|
|
19
|
+
# Check for resume subcommand
|
|
20
|
+
THREAD_ID="mock-thread-001"
|
|
21
|
+
if [[ "$2" == "resume" ]]; then
|
|
22
|
+
THREAD_ID="$3"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Emit noise to stderr (should be filtered)
|
|
26
|
+
echo "2026-04-12T11:46:37.355150Z ERROR codex_core::skills::loader: failed to stat skills entry" >&2
|
|
27
|
+
|
|
28
|
+
echo '{"type":"thread.started","thread_id":"'"\$THREAD_ID"'"}'
|
|
29
|
+
echo '{"type":"turn.started"}'
|
|
30
|
+
echo '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Mock codex response."}}'
|
|
31
|
+
echo '{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":10}}'
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "Unknown command" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
`, { mode: 0o755 });
|
|
38
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
39
|
+
});
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
process.env.PATH = originalPath;
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
function makeCtx(overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
runId: 'test-run-001',
|
|
47
|
+
agent: { id: 'codex', name: 'Codex CLI' },
|
|
48
|
+
threadContext: {
|
|
49
|
+
threadId: 'thread-001',
|
|
50
|
+
projectRoot: tmpDir,
|
|
51
|
+
worktreePath: tmpDir,
|
|
52
|
+
repoRef: null,
|
|
53
|
+
},
|
|
54
|
+
messages: [],
|
|
55
|
+
prompt: 'Hello Codex',
|
|
56
|
+
session: null,
|
|
57
|
+
onLog: () => { },
|
|
58
|
+
onMeta: () => { },
|
|
59
|
+
onSpawn: () => { },
|
|
60
|
+
signal: AbortSignal.timeout(30_000),
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
describe('CodexAdapter.probe()', () => {
|
|
65
|
+
it('finds the mock codex binary and reports available=true', async () => {
|
|
66
|
+
const adapter = createCodexAdapter();
|
|
67
|
+
const result = await adapter.probe();
|
|
68
|
+
expect(result.available).toBe(true);
|
|
69
|
+
expect(result.version).toContain('0.112.0');
|
|
70
|
+
});
|
|
71
|
+
it('reports available=false when codex is not on PATH', async () => {
|
|
72
|
+
const adapter = createCodexAdapter();
|
|
73
|
+
const savedPath = process.env.PATH;
|
|
74
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
75
|
+
try {
|
|
76
|
+
const result = await adapter.probe();
|
|
77
|
+
expect(result.available).toBe(false);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
process.env.PATH = savedPath;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('CodexAdapter.execute()', () => {
|
|
85
|
+
it('executes with mock codex and returns expected result', async () => {
|
|
86
|
+
const adapter = createCodexAdapter();
|
|
87
|
+
let metaCalled = false;
|
|
88
|
+
let spawnPid = null;
|
|
89
|
+
const ctx = makeCtx({
|
|
90
|
+
onMeta: (_meta) => {
|
|
91
|
+
metaCalled = true;
|
|
92
|
+
},
|
|
93
|
+
onSpawn: (pid) => {
|
|
94
|
+
spawnPid = pid;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const result = await adapter.execute(ctx);
|
|
98
|
+
expect(result.summary).toBe('Mock codex response.');
|
|
99
|
+
expect(result.session).not.toBeNull();
|
|
100
|
+
expect(result.session?.sessionId).toBe('mock-thread-001');
|
|
101
|
+
expect(result.exitCode).toBe(0);
|
|
102
|
+
expect(result.timedOut).toBe(false);
|
|
103
|
+
expect(result.patches).toEqual([]);
|
|
104
|
+
expect(metaCalled).toBe(true);
|
|
105
|
+
expect(typeof spawnPid).toBe('number');
|
|
106
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
it('resumes a session by passing the thread id to codex exec resume', async () => {
|
|
109
|
+
const adapter = createCodexAdapter();
|
|
110
|
+
const ctx = makeCtx({
|
|
111
|
+
session: { sessionId: 'resume-thread-42', params: {} },
|
|
112
|
+
});
|
|
113
|
+
const result = await adapter.execute(ctx);
|
|
114
|
+
expect(result.session).not.toBeNull();
|
|
115
|
+
expect(result.session?.sessionId).toBe('resume-thread-42');
|
|
116
|
+
});
|
|
117
|
+
it('filters codex startup noise from stderr', async () => {
|
|
118
|
+
const adapter = createCodexAdapter();
|
|
119
|
+
const stderrChunks = [];
|
|
120
|
+
const ctx = makeCtx({
|
|
121
|
+
onLog: (stream, chunk) => {
|
|
122
|
+
if (stream === 'stderr')
|
|
123
|
+
stderrChunks.push(chunk);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const result = await adapter.execute(ctx);
|
|
127
|
+
// The mock writes an ERROR codex_core line to stderr — it should be filtered
|
|
128
|
+
expect(stderrChunks.join('')).not.toContain('codex_core::skills::loader');
|
|
129
|
+
expect(result.warnings.length).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
it('forwards only agent_message text to stdout onLog, not raw JSON', async () => {
|
|
132
|
+
const adapter = createCodexAdapter();
|
|
133
|
+
const stdoutChunks = [];
|
|
134
|
+
const ctx = makeCtx({
|
|
135
|
+
onLog: (stream, chunk) => {
|
|
136
|
+
if (stream === 'stdout')
|
|
137
|
+
stdoutChunks.push(chunk);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
await adapter.execute(ctx);
|
|
141
|
+
const output = stdoutChunks.join('');
|
|
142
|
+
expect(output).toContain('Mock codex response.');
|
|
143
|
+
expect(output).not.toContain('thread.started');
|
|
144
|
+
expect(output).not.toContain('turn.started');
|
|
145
|
+
expect(output).not.toContain('turn.completed');
|
|
146
|
+
});
|
|
147
|
+
it('inherits CODEX_HOME from process.env', async () => {
|
|
148
|
+
const adapter = createCodexAdapter();
|
|
149
|
+
let metaEnv = {};
|
|
150
|
+
process.env['CODEX_HOME'] = '/custom/codex/home';
|
|
151
|
+
const ctx = makeCtx({
|
|
152
|
+
onMeta: (meta) => {
|
|
153
|
+
metaEnv = meta.env;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
await adapter.execute(ctx);
|
|
158
|
+
expect(metaEnv['CODEX_HOME']).toBe('/custom/codex/home');
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
delete process.env['CODEX_HOME'];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.js","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,IAAI,MAAc,CAAC;AACnB,IAAI,YAAoB,CAAC;AAEzB,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACpE,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAEtC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,EAAE,CAAC,aAAa,CACd,SAAS,EACT;;;;;;;;;;;;;;;;;;;;;;;;;CAyBH,EACG,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,GAAG,GAAG,YAAY,CAAC;AACjD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,GAAG,EAAE;IACZ,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,YAAY,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,SAAS,OAAO,CAAC,YAA2C,EAAE;IAC5D,OAAO;QACL,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE;QACzC,aAAa,EAAE;YACb,QAAQ,EAAE,YAAY;YACtB,WAAW,EAAE,MAAM;YACnB,YAAY,EAAE,MAAM;YACpB,OAAO,EAAE,IAAI;SACd;QACD,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;QACf,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC;QACjB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;QACnC,GAAG,SAAS;KACsB,CAAC;AACvC,CAAC;AAED,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAErC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,4BAA4B,CAAC;QAEhD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACrC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,IAAI,QAAQ,GAAkB,IAAI,CAAC;QAEnC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,MAAM,EAAE,CAAC,KAAyB,EAAE,EAAE;gBACpC,UAAU,GAAG,IAAI,CAAC;YACpB,CAAC;YACD,OAAO,EAAE,CAAC,GAAW,EAAE,EAAE;gBACvB,QAAQ,GAAG,GAAG,CAAC;YACjB,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACnC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QAErC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,OAAO,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,EAAE;SACvD,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;gBACvB,IAAI,MAAM,KAAK,QAAQ;oBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpD,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE1C,6EAA6E;QAC7E,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;gBACvB,IAAI,MAAM,KAAK,QAAQ;oBAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpD,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE3B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,IAAI,OAAO,GAA2B,EAAE,CAAC;QAEzC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAAC;QAEjD,MAAM,GAAG,GAAG,OAAO,CAAC;YAClB,MAAM,EAAE,CAAC,IAAwB,EAAE,EAAE;gBACnC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;YACrB,CAAC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAC3D,CAAC;gBAAS,CAAC;YACT,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexStreamJson } from '../parse.js';
|
|
3
|
+
describe('parseCodexStreamJson', () => {
|
|
4
|
+
it('extracts sessionId from thread.started and text from item.completed', () => {
|
|
5
|
+
const input = [
|
|
6
|
+
JSON.stringify({ type: 'thread.started', thread_id: '019d8183-e8b0-7e33-a2f8-29d950432756' }),
|
|
7
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
8
|
+
JSON.stringify({
|
|
9
|
+
type: 'item.completed',
|
|
10
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Hello.' },
|
|
11
|
+
}),
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
type: 'turn.completed',
|
|
14
|
+
usage: { input_tokens: 100, cached_input_tokens: 50, output_tokens: 10 },
|
|
15
|
+
}),
|
|
16
|
+
].join('\n');
|
|
17
|
+
const result = parseCodexStreamJson(input);
|
|
18
|
+
expect(result.sessionId).toBe('019d8183-e8b0-7e33-a2f8-29d950432756');
|
|
19
|
+
expect(result.summary).toBe('Hello.');
|
|
20
|
+
});
|
|
21
|
+
it('concatenates multiple agent_message items', () => {
|
|
22
|
+
const input = [
|
|
23
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-multi' }),
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
type: 'item.completed',
|
|
26
|
+
item: { id: 'item_0', type: 'agent_message', text: 'First part.' },
|
|
27
|
+
}),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: 'item.completed',
|
|
30
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Second part.' },
|
|
31
|
+
}),
|
|
32
|
+
].join('\n');
|
|
33
|
+
const result = parseCodexStreamJson(input);
|
|
34
|
+
expect(result.sessionId).toBe('thread-multi');
|
|
35
|
+
expect(result.summary).toContain('First part.');
|
|
36
|
+
expect(result.summary).toContain('Second part.');
|
|
37
|
+
});
|
|
38
|
+
it('ignores non-agent_message items', () => {
|
|
39
|
+
const input = [
|
|
40
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-filter' }),
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
type: 'item.completed',
|
|
43
|
+
item: { id: 'item_0', type: 'tool_call', text: 'should be ignored' },
|
|
44
|
+
}),
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
type: 'item.completed',
|
|
47
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Visible text.' },
|
|
48
|
+
}),
|
|
49
|
+
].join('\n');
|
|
50
|
+
const result = parseCodexStreamJson(input);
|
|
51
|
+
expect(result.summary).toBe('Visible text.');
|
|
52
|
+
expect(result.summary).not.toContain('should be ignored');
|
|
53
|
+
});
|
|
54
|
+
it('falls back to raw stdout for non-JSON output', () => {
|
|
55
|
+
const input = 'This is plain text output\nNot JSON\n';
|
|
56
|
+
const result = parseCodexStreamJson(input);
|
|
57
|
+
expect(result.sessionId).toBeNull();
|
|
58
|
+
expect(result.summary).toBe(input);
|
|
59
|
+
});
|
|
60
|
+
it('returns null sessionId and empty summary for empty string', () => {
|
|
61
|
+
const result = parseCodexStreamJson('');
|
|
62
|
+
expect(result.sessionId).toBeNull();
|
|
63
|
+
expect(result.summary).toBe('');
|
|
64
|
+
});
|
|
65
|
+
it('handles missing thread_id in thread.started gracefully', () => {
|
|
66
|
+
const input = [
|
|
67
|
+
JSON.stringify({ type: 'thread.started' }),
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
type: 'item.completed',
|
|
70
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Response without session.' },
|
|
71
|
+
}),
|
|
72
|
+
].join('\n');
|
|
73
|
+
const result = parseCodexStreamJson(input);
|
|
74
|
+
expect(result.sessionId).toBeNull();
|
|
75
|
+
expect(result.summary).toBe('Response without session.');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=parse.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.js","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixy/codex-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@fixy/
|
|
15
|
-
"@fixy/
|
|
14
|
+
"@fixy/adapter-utils": "0.0.5",
|
|
15
|
+
"@fixy/core": "0.0.5"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/node": "^
|
|
18
|
+
"@types/node": "^25.6.0"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"scripts": {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createCodexAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import type { FixyExecutionContext, FixyInvocationMeta } from '@fixy/core';
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let originalPath: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-codex-test-'));
|
|
13
|
+
originalPath = process.env.PATH ?? '';
|
|
14
|
+
|
|
15
|
+
const mockCodex = path.join(tmpDir, 'codex');
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
mockCodex,
|
|
18
|
+
`#!/bin/bash
|
|
19
|
+
if [[ "$1" == "--version" ]]; then
|
|
20
|
+
echo "codex-cli 0.112.0"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ "$1" == "exec" ]]; then
|
|
25
|
+
# Check for resume subcommand
|
|
26
|
+
THREAD_ID="mock-thread-001"
|
|
27
|
+
if [[ "$2" == "resume" ]]; then
|
|
28
|
+
THREAD_ID="$3"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Emit noise to stderr (should be filtered)
|
|
32
|
+
echo "2026-04-12T11:46:37.355150Z ERROR codex_core::skills::loader: failed to stat skills entry" >&2
|
|
33
|
+
|
|
34
|
+
echo '{"type":"thread.started","thread_id":"'"\$THREAD_ID"'"}'
|
|
35
|
+
echo '{"type":"turn.started"}'
|
|
36
|
+
echo '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Mock codex response."}}'
|
|
37
|
+
echo '{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":10}}'
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "Unknown command" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
`,
|
|
44
|
+
{ mode: 0o755 },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
process.env.PATH = originalPath;
|
|
52
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeCtx(overrides: Partial<FixyExecutionContext> = {}): FixyExecutionContext {
|
|
56
|
+
return {
|
|
57
|
+
runId: 'test-run-001',
|
|
58
|
+
agent: { id: 'codex', name: 'Codex CLI' },
|
|
59
|
+
threadContext: {
|
|
60
|
+
threadId: 'thread-001',
|
|
61
|
+
projectRoot: tmpDir,
|
|
62
|
+
worktreePath: tmpDir,
|
|
63
|
+
repoRef: null,
|
|
64
|
+
},
|
|
65
|
+
messages: [],
|
|
66
|
+
prompt: 'Hello Codex',
|
|
67
|
+
session: null,
|
|
68
|
+
onLog: () => {},
|
|
69
|
+
onMeta: () => {},
|
|
70
|
+
onSpawn: () => {},
|
|
71
|
+
signal: AbortSignal.timeout(30_000),
|
|
72
|
+
...overrides,
|
|
73
|
+
} as unknown as FixyExecutionContext;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('CodexAdapter.probe()', () => {
|
|
77
|
+
it('finds the mock codex binary and reports available=true', async () => {
|
|
78
|
+
const adapter = createCodexAdapter();
|
|
79
|
+
const result = await adapter.probe();
|
|
80
|
+
|
|
81
|
+
expect(result.available).toBe(true);
|
|
82
|
+
expect(result.version).toContain('0.112.0');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reports available=false when codex is not on PATH', async () => {
|
|
86
|
+
const adapter = createCodexAdapter();
|
|
87
|
+
const savedPath = process.env.PATH;
|
|
88
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await adapter.probe();
|
|
92
|
+
expect(result.available).toBe(false);
|
|
93
|
+
} finally {
|
|
94
|
+
process.env.PATH = savedPath;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('CodexAdapter.execute()', () => {
|
|
100
|
+
it('executes with mock codex and returns expected result', async () => {
|
|
101
|
+
const adapter = createCodexAdapter();
|
|
102
|
+
let metaCalled = false;
|
|
103
|
+
let spawnPid: number | null = null;
|
|
104
|
+
|
|
105
|
+
const ctx = makeCtx({
|
|
106
|
+
onMeta: (_meta: FixyInvocationMeta) => {
|
|
107
|
+
metaCalled = true;
|
|
108
|
+
},
|
|
109
|
+
onSpawn: (pid: number) => {
|
|
110
|
+
spawnPid = pid;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await adapter.execute(ctx);
|
|
115
|
+
|
|
116
|
+
expect(result.summary).toBe('Mock codex response.');
|
|
117
|
+
expect(result.session).not.toBeNull();
|
|
118
|
+
expect(result.session?.sessionId).toBe('mock-thread-001');
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
expect(result.timedOut).toBe(false);
|
|
121
|
+
expect(result.patches).toEqual([]);
|
|
122
|
+
expect(metaCalled).toBe(true);
|
|
123
|
+
expect(typeof spawnPid).toBe('number');
|
|
124
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('resumes a session by passing the thread id to codex exec resume', async () => {
|
|
128
|
+
const adapter = createCodexAdapter();
|
|
129
|
+
|
|
130
|
+
const ctx = makeCtx({
|
|
131
|
+
session: { sessionId: 'resume-thread-42', params: {} },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await adapter.execute(ctx);
|
|
135
|
+
|
|
136
|
+
expect(result.session).not.toBeNull();
|
|
137
|
+
expect(result.session?.sessionId).toBe('resume-thread-42');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('filters codex startup noise from stderr', async () => {
|
|
141
|
+
const adapter = createCodexAdapter();
|
|
142
|
+
const stderrChunks: string[] = [];
|
|
143
|
+
|
|
144
|
+
const ctx = makeCtx({
|
|
145
|
+
onLog: (stream, chunk) => {
|
|
146
|
+
if (stream === 'stderr') stderrChunks.push(chunk);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await adapter.execute(ctx);
|
|
151
|
+
|
|
152
|
+
// The mock writes an ERROR codex_core line to stderr — it should be filtered
|
|
153
|
+
expect(stderrChunks.join('')).not.toContain('codex_core::skills::loader');
|
|
154
|
+
expect(result.warnings.length).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('forwards only agent_message text to stdout onLog, not raw JSON', async () => {
|
|
158
|
+
const adapter = createCodexAdapter();
|
|
159
|
+
const stdoutChunks: string[] = [];
|
|
160
|
+
|
|
161
|
+
const ctx = makeCtx({
|
|
162
|
+
onLog: (stream, chunk) => {
|
|
163
|
+
if (stream === 'stdout') stdoutChunks.push(chunk);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await adapter.execute(ctx);
|
|
168
|
+
|
|
169
|
+
const output = stdoutChunks.join('');
|
|
170
|
+
expect(output).toContain('Mock codex response.');
|
|
171
|
+
expect(output).not.toContain('thread.started');
|
|
172
|
+
expect(output).not.toContain('turn.started');
|
|
173
|
+
expect(output).not.toContain('turn.completed');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('inherits CODEX_HOME from process.env', async () => {
|
|
177
|
+
const adapter = createCodexAdapter();
|
|
178
|
+
let metaEnv: Record<string, string> = {};
|
|
179
|
+
|
|
180
|
+
process.env['CODEX_HOME'] = '/custom/codex/home';
|
|
181
|
+
|
|
182
|
+
const ctx = makeCtx({
|
|
183
|
+
onMeta: (meta: FixyInvocationMeta) => {
|
|
184
|
+
metaEnv = meta.env;
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await adapter.execute(ctx);
|
|
190
|
+
expect(metaEnv['CODEX_HOME']).toBe('/custom/codex/home');
|
|
191
|
+
} finally {
|
|
192
|
+
delete process.env['CODEX_HOME'];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexStreamJson } from '../parse.js';
|
|
3
|
+
|
|
4
|
+
describe('parseCodexStreamJson', () => {
|
|
5
|
+
it('extracts sessionId from thread.started and text from item.completed', () => {
|
|
6
|
+
const input = [
|
|
7
|
+
JSON.stringify({ type: 'thread.started', thread_id: '019d8183-e8b0-7e33-a2f8-29d950432756' }),
|
|
8
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
9
|
+
JSON.stringify({
|
|
10
|
+
type: 'item.completed',
|
|
11
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Hello.' },
|
|
12
|
+
}),
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
type: 'turn.completed',
|
|
15
|
+
usage: { input_tokens: 100, cached_input_tokens: 50, output_tokens: 10 },
|
|
16
|
+
}),
|
|
17
|
+
].join('\n');
|
|
18
|
+
|
|
19
|
+
const result = parseCodexStreamJson(input);
|
|
20
|
+
|
|
21
|
+
expect(result.sessionId).toBe('019d8183-e8b0-7e33-a2f8-29d950432756');
|
|
22
|
+
expect(result.summary).toBe('Hello.');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('concatenates multiple agent_message items', () => {
|
|
26
|
+
const input = [
|
|
27
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-multi' }),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
type: 'item.completed',
|
|
30
|
+
item: { id: 'item_0', type: 'agent_message', text: 'First part.' },
|
|
31
|
+
}),
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
type: 'item.completed',
|
|
34
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Second part.' },
|
|
35
|
+
}),
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
const result = parseCodexStreamJson(input);
|
|
39
|
+
|
|
40
|
+
expect(result.sessionId).toBe('thread-multi');
|
|
41
|
+
expect(result.summary).toContain('First part.');
|
|
42
|
+
expect(result.summary).toContain('Second part.');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('ignores non-agent_message items', () => {
|
|
46
|
+
const input = [
|
|
47
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'thread-filter' }),
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
type: 'item.completed',
|
|
50
|
+
item: { id: 'item_0', type: 'tool_call', text: 'should be ignored' },
|
|
51
|
+
}),
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
type: 'item.completed',
|
|
54
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Visible text.' },
|
|
55
|
+
}),
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const result = parseCodexStreamJson(input);
|
|
59
|
+
|
|
60
|
+
expect(result.summary).toBe('Visible text.');
|
|
61
|
+
expect(result.summary).not.toContain('should be ignored');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to raw stdout for non-JSON output', () => {
|
|
65
|
+
const input = 'This is plain text output\nNot JSON\n';
|
|
66
|
+
|
|
67
|
+
const result = parseCodexStreamJson(input);
|
|
68
|
+
|
|
69
|
+
expect(result.sessionId).toBeNull();
|
|
70
|
+
expect(result.summary).toBe(input);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null sessionId and empty summary for empty string', () => {
|
|
74
|
+
const result = parseCodexStreamJson('');
|
|
75
|
+
|
|
76
|
+
expect(result.sessionId).toBeNull();
|
|
77
|
+
expect(result.summary).toBe('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles missing thread_id in thread.started gracefully', () => {
|
|
81
|
+
const input = [
|
|
82
|
+
JSON.stringify({ type: 'thread.started' }),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
type: 'item.completed',
|
|
85
|
+
item: { id: 'item_0', type: 'agent_message', text: 'Response without session.' },
|
|
86
|
+
}),
|
|
87
|
+
].join('\n');
|
|
88
|
+
|
|
89
|
+
const result = parseCodexStreamJson(input);
|
|
90
|
+
|
|
91
|
+
expect(result.sessionId).toBeNull();
|
|
92
|
+
expect(result.summary).toBe('Response without session.');
|
|
93
|
+
});
|
|
94
|
+
});
|