oh-my-codex 0.18.10 → 0.18.11
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +2 -6
- package/dist/catalog/__tests__/schema.test.js +6 -0
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-spark-routing.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js +79 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -0
- package/dist/cli/__tests__/explore.test.js +47 -1175
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +26 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +121 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts +2 -21
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +25 -624
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +18 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -10
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-routing.test.js +5 -8
- package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +6 -6
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
- package/dist/hooks/explore-routing.d.ts +1 -1
- package/dist/hooks/explore-routing.d.ts.map +1 -1
- package/dist/hooks/explore-routing.js +3 -8
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +101 -1
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/constants.d.ts +10 -0
- package/dist/hud/constants.d.ts.map +1 -1
- package/dist/hud/constants.js +19 -0
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/reconcile.d.ts +5 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +20 -2
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +3 -3
- package/dist/scripts/__tests__/codex-native-hook.test.js +2 -2
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +2 -2
- package/templates/AGENTS.md +1 -2
- package/templates/catalog-manifest.json +7 -0
|
@@ -1,376 +1,63 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { chmod, mkdtemp, readFile, rm, mkdir, symlink, writeFile } from 'node:fs/promises';
|
|
6
|
-
import { createServer } from 'node:http';
|
|
7
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
8
5
|
import { tmpdir } from 'node:os';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const repoRoot = join(testDir, '..', '..', '..');
|
|
17
|
-
const omxBin = join(repoRoot, 'dist', 'cli', 'omx.js');
|
|
18
|
-
const nodeWrapper = join(cwd, '.omx-test-node.sh');
|
|
19
|
-
if (!existsSync(nodeWrapper)) {
|
|
20
|
-
writeFileSync(nodeWrapper, '#!/bin/sh\nexec node "$@"\n');
|
|
21
|
-
chmodSync(nodeWrapper, 0o755);
|
|
22
|
-
}
|
|
23
|
-
const r = spawnSync(nodeWrapper, [omxBin, ...argv], {
|
|
24
|
-
cwd,
|
|
25
|
-
encoding: 'utf-8',
|
|
26
|
-
env: { ...process.env, CODEX_HOME: '', ...envOverrides },
|
|
27
|
-
});
|
|
28
|
-
return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '', error: r.error?.message };
|
|
29
|
-
}
|
|
30
|
-
function normalizeDarwinTmpPath(value) {
|
|
31
|
-
return process.platform === 'darwin' ? value.replaceAll('/private/var/', '/var/') : value;
|
|
32
|
-
}
|
|
33
|
-
function shouldSkipForSpawnPermissions(err) {
|
|
34
|
-
return typeof err === 'string' && /(EPERM|EACCES)/i.test(err);
|
|
35
|
-
}
|
|
36
|
-
async function runExploreCommandForTest(cwd, argv, envOverrides = {}) {
|
|
37
|
-
const stdoutChunks = [];
|
|
38
|
-
const stderrChunks = [];
|
|
39
|
-
const originalStdout = process.stdout.write.bind(process.stdout);
|
|
40
|
-
const originalStderr = process.stderr.write.bind(process.stderr);
|
|
41
|
-
const originalExitCode = process.exitCode;
|
|
42
|
-
const previousEnv = new Map();
|
|
43
|
-
for (const [key, value] of Object.entries(envOverrides)) {
|
|
44
|
-
previousEnv.set(key, process.env[key]);
|
|
45
|
-
process.env[key] = value;
|
|
46
|
-
}
|
|
47
|
-
process.stdout.write = ((chunk) => {
|
|
48
|
-
stdoutChunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
|
|
49
|
-
return true;
|
|
50
|
-
});
|
|
51
|
-
process.stderr.write = ((chunk) => {
|
|
52
|
-
stderrChunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
|
|
53
|
-
return true;
|
|
54
|
-
});
|
|
55
|
-
const originalCwd = process.cwd();
|
|
56
|
-
process.exitCode = 0;
|
|
6
|
+
import { assertBuiltinExploreHarnessSupported, exploreCommand, EXPLORE_DEPRECATION_MESSAGE, EXPLORE_HELP, getBuiltinExploreHarnessUnsupportedReason, packagedExploreHarnessBinaryName, resolvePackagedExploreHarnessCommand, } from '../explore.js';
|
|
7
|
+
async function captureExploreCommand(args) {
|
|
8
|
+
const originalLog = console.log;
|
|
9
|
+
let stdout = '';
|
|
10
|
+
console.log = (...parts) => {
|
|
11
|
+
stdout += `${parts.map((part) => String(part)).join(' ')}\n`;
|
|
12
|
+
};
|
|
57
13
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
14
|
+
await exploreCommand(args);
|
|
15
|
+
return { stdout, threw: false };
|
|
60
16
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const [key, value] of previousEnv.entries()) {
|
|
64
|
-
if (value === undefined)
|
|
65
|
-
delete process.env[key];
|
|
66
|
-
else
|
|
67
|
-
process.env[key] = value;
|
|
68
|
-
}
|
|
69
|
-
process.stdout.write = originalStdout;
|
|
70
|
-
process.stderr.write = originalStderr;
|
|
17
|
+
catch (error) {
|
|
18
|
+
return { stdout, threw: true, error: error instanceof Error ? error.message : String(error) };
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return { stdout: stdoutChunks.join(''), stderr: stderrChunks.join(''), exitCode };
|
|
75
|
-
}
|
|
76
|
-
async function createExploreTestPath(wd) {
|
|
77
|
-
const binDir = join(wd, 'test-bin');
|
|
78
|
-
await mkdir(binDir, { recursive: true });
|
|
79
|
-
const rgPath = join(binDir, process.platform === 'win32' ? 'rg.cmd' : 'rg');
|
|
80
|
-
const lines = process.platform === 'win32'
|
|
81
|
-
? ['@echo off', 'echo ripgrep 14.0.0', '']
|
|
82
|
-
: ['#!/bin/sh', 'echo "ripgrep 14.0.0"', ''];
|
|
83
|
-
await writeFile(rgPath, lines.join(process.platform === 'win32' ? '\r\n' : '\n'));
|
|
84
|
-
if (process.platform !== 'win32') {
|
|
85
|
-
await chmod(rgPath, 0o755);
|
|
20
|
+
finally {
|
|
21
|
+
console.log = originalLog;
|
|
86
22
|
}
|
|
87
|
-
return `${binDir}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH || ''}`;
|
|
88
|
-
}
|
|
89
|
-
async function writeEnvNodeCodexStub(wd, capturePath) {
|
|
90
|
-
const stub = join(wd, 'codex-stub.sh');
|
|
91
|
-
const argvPath = join(wd, 'codex-argv.txt');
|
|
92
|
-
const allowedStdoutPath = join(wd, 'allowed.stdout.txt');
|
|
93
|
-
const allowedStderrPath = join(wd, 'allowed.stderr.txt');
|
|
94
|
-
const blockedStdoutPath = join(wd, 'blocked.stdout.txt');
|
|
95
|
-
const blockedStderrPath = join(wd, 'blocked.stderr.txt');
|
|
96
|
-
await writeFile(stub, `#!/bin/sh
|
|
97
|
-
set -eu
|
|
98
|
-
output_path=''
|
|
99
|
-
: > ${JSON.stringify(argvPath)}
|
|
100
|
-
while [ "$#" -gt 0 ]; do
|
|
101
|
-
printf '%s\n' "$1" >> ${JSON.stringify(argvPath)}
|
|
102
|
-
if [ "$1" = "-o" ] && [ "$#" -ge 2 ]; then
|
|
103
|
-
output_path="$2"
|
|
104
|
-
shift 2
|
|
105
|
-
continue
|
|
106
|
-
fi
|
|
107
|
-
shift
|
|
108
|
-
done
|
|
109
|
-
|
|
110
|
-
if [ -z "$output_path" ]; then
|
|
111
|
-
printf 'missing -o output path\n' >&2
|
|
112
|
-
exit 1
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
bash -lc 'rg --version' > ${JSON.stringify(allowedStdoutPath)} 2> ${JSON.stringify(allowedStderrPath)}
|
|
116
|
-
allowed_status=$?
|
|
117
|
-
set +e
|
|
118
|
-
bash -lc 'node --version' > ${JSON.stringify(blockedStdoutPath)} 2> ${JSON.stringify(blockedStderrPath)}
|
|
119
|
-
blocked_status=$?
|
|
120
|
-
set -e
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
printf 'PATH=%s\n' "$PATH"
|
|
124
|
-
printf 'SHELL=%s\n' "\${SHELL:-}"
|
|
125
|
-
printf 'ALLOWED_STATUS=%s\n' "$allowed_status"
|
|
126
|
-
printf 'BLOCKED_STATUS=%s\n' "$blocked_status"
|
|
127
|
-
printf -- '--ARGV--\n'
|
|
128
|
-
cat ${JSON.stringify(argvPath)}
|
|
129
|
-
printf -- '--ALLOWED_STDOUT--\n'
|
|
130
|
-
cat ${JSON.stringify(allowedStdoutPath)}
|
|
131
|
-
printf -- '--ALLOWED_STDERR--\n'
|
|
132
|
-
cat ${JSON.stringify(allowedStderrPath)}
|
|
133
|
-
printf -- '--BLOCKED_STDOUT--\n'
|
|
134
|
-
cat ${JSON.stringify(blockedStdoutPath)}
|
|
135
|
-
printf -- '--BLOCKED_STDERR--\n'
|
|
136
|
-
cat ${JSON.stringify(blockedStderrPath)}
|
|
137
|
-
} > ${JSON.stringify(capturePath)}
|
|
138
|
-
|
|
139
|
-
printf '# Answer\nHarness completed\n' > "$output_path"
|
|
140
|
-
`);
|
|
141
|
-
await chmod(stub, 0o755);
|
|
142
|
-
return stub;
|
|
143
|
-
}
|
|
144
|
-
async function writePosixPackageManagerCodexShim(wd, capturePath) {
|
|
145
|
-
const packageRoot = join(wd, 'node_modules', '@openai', 'codex');
|
|
146
|
-
const binDir = join(wd, 'node_modules', '.bin');
|
|
147
|
-
const entrypointPath = join(packageRoot, 'bin', 'codex.js');
|
|
148
|
-
const shimPath = join(binDir, 'codex');
|
|
149
|
-
await mkdir(join(packageRoot, 'bin'), { recursive: true });
|
|
150
|
-
await mkdir(binDir, { recursive: true });
|
|
151
|
-
await writeFile(entrypointPath, `const fs = require('node:fs');
|
|
152
|
-
const path = require('node:path');
|
|
153
|
-
|
|
154
|
-
const args = process.argv.slice(2);
|
|
155
|
-
let outputPath = '';
|
|
156
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
157
|
-
const value = args[i];
|
|
158
|
-
if (value === '-o' && i + 1 < args.length) {
|
|
159
|
-
outputPath = args[i + 1];
|
|
160
|
-
i += 1;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (!outputPath) {
|
|
164
|
-
process.stderr.write('missing -o output path\\n');
|
|
165
|
-
process.exit(1);
|
|
166
23
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const stub = join(wd, 'codex-scenario-stub.sh');
|
|
189
|
-
await writeFile(stub, `#!/bin/sh
|
|
190
|
-
set -eu
|
|
191
|
-
output_path=''
|
|
192
|
-
model=''
|
|
193
|
-
while [ "$#" -gt 0 ]; do
|
|
194
|
-
case "$1" in
|
|
195
|
-
-o)
|
|
196
|
-
output_path="$2"
|
|
197
|
-
shift 2
|
|
198
|
-
;;
|
|
199
|
-
-m)
|
|
200
|
-
model="$2"
|
|
201
|
-
shift 2
|
|
202
|
-
;;
|
|
203
|
-
*)
|
|
204
|
-
shift
|
|
205
|
-
;;
|
|
206
|
-
esac
|
|
207
|
-
done
|
|
208
|
-
if [ -z "$output_path" ]; then
|
|
209
|
-
printf 'missing -o output path\n' >&2
|
|
210
|
-
exit 1
|
|
211
|
-
fi
|
|
212
|
-
if [ -z "$model" ]; then
|
|
213
|
-
printf 'missing -m model\n' >&2
|
|
214
|
-
exit 1
|
|
215
|
-
fi
|
|
216
|
-
${body}
|
|
217
|
-
`);
|
|
218
|
-
await chmod(stub, 0o755);
|
|
219
|
-
return stub;
|
|
220
|
-
}
|
|
221
|
-
async function writeExploreHarnessScenarioStub(wd, body) {
|
|
222
|
-
const stub = join(wd, 'explore-scenario-stub.sh');
|
|
223
|
-
await writeFile(stub, `#!/bin/sh
|
|
224
|
-
set -eu
|
|
225
|
-
${body}
|
|
226
|
-
`);
|
|
227
|
-
await chmod(stub, 0o755);
|
|
228
|
-
return stub;
|
|
229
|
-
}
|
|
230
|
-
describe('parseExploreArgs', () => {
|
|
231
|
-
it('parses --prompt form', () => {
|
|
232
|
-
assert.deepEqual(parseExploreArgs(['--prompt', 'find', 'auth']), { prompt: 'find auth' });
|
|
233
|
-
});
|
|
234
|
-
it('parses --prompt= form', () => {
|
|
235
|
-
assert.deepEqual(parseExploreArgs(['--prompt=find auth']), { prompt: 'find auth' });
|
|
236
|
-
});
|
|
237
|
-
it('parses --prompt-file form', () => {
|
|
238
|
-
assert.deepEqual(parseExploreArgs(['--prompt-file', 'prompt.md']), { promptFile: 'prompt.md' });
|
|
239
|
-
});
|
|
240
|
-
it('throws on missing prompt', () => {
|
|
241
|
-
assert.throws(() => parseExploreArgs([]), new RegExp(EXPLORE_USAGE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
242
|
-
});
|
|
243
|
-
it('throws on unknown flag', () => {
|
|
244
|
-
assert.throws(() => parseExploreArgs(['--bogus']), /Unknown argument/);
|
|
245
|
-
});
|
|
246
|
-
it('rejects positional prompt text with a corrective --prompt hint', () => {
|
|
247
|
-
assert.throws(() => parseExploreArgs(['find package.json']), /Positional prompt text is not supported\. Use: omx explore --prompt "find package\.json"/);
|
|
248
|
-
assert.throws(() => parseExploreArgs(['find', 'package.json']), /Positional prompt text is not supported\. Use: omx explore --prompt "find package\.json"/);
|
|
249
|
-
});
|
|
250
|
-
it('rejects duplicate prompt sources', () => {
|
|
251
|
-
assert.throws(() => parseExploreArgs(['--prompt', 'find auth', '--prompt-file', 'prompt.md']), /Choose exactly one/);
|
|
252
|
-
});
|
|
253
|
-
it('rejects missing prompt-file value', () => {
|
|
254
|
-
assert.throws(() => parseExploreArgs(['--prompt-file']), /Missing path after --prompt-file/);
|
|
255
|
-
});
|
|
256
|
-
it('rejects missing prompt value', () => {
|
|
257
|
-
assert.throws(() => parseExploreArgs(['--prompt']), /Missing text after --prompt/);
|
|
24
|
+
describe('exploreCommand (hard-deprecated tombstone)', () => {
|
|
25
|
+
it('prints migration help for --help without throwing', async () => {
|
|
26
|
+
const result = await captureExploreCommand(['--help']);
|
|
27
|
+
assert.equal(result.threw, false);
|
|
28
|
+
assert.match(result.stdout, /Hard-deprecated legacy command surface/);
|
|
29
|
+
assert.match(result.stdout, /all fail intentionally/);
|
|
30
|
+
assert.match(result.stdout, /omx sparkshell/);
|
|
31
|
+
});
|
|
32
|
+
it('throws the deprecation message for bare invocation', async () => {
|
|
33
|
+
const result = await captureExploreCommand([]);
|
|
34
|
+
assert.equal(result.threw, true);
|
|
35
|
+
assert.match(result.error ?? '', /hard-deprecated and the direct command surface has been removed/);
|
|
36
|
+
});
|
|
37
|
+
it('throws the deprecation message for legacy --prompt usage', async () => {
|
|
38
|
+
const result = await captureExploreCommand(['--prompt', 'find package.json']);
|
|
39
|
+
assert.equal(result.threw, true);
|
|
40
|
+
assert.match(result.error ?? '', new RegExp(EXPLORE_DEPRECATION_MESSAGE.slice(0, 24)));
|
|
41
|
+
});
|
|
42
|
+
it('exposes deprecation help text', () => {
|
|
43
|
+
assert.match(EXPLORE_HELP, /Migration:/);
|
|
44
|
+
assert.match(EXPLORE_HELP, /normal Codex repository inspection tools\/subagents/);
|
|
258
45
|
});
|
|
259
46
|
});
|
|
260
|
-
describe('
|
|
261
|
-
it('
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const result = await runExploreCommandForTest(wd, ['--help']);
|
|
265
|
-
assert.equal(result.exitCode, 0);
|
|
266
|
-
assert.match(result.stdout, /Usage: omx explore --prompt "<prompt>"/);
|
|
267
|
-
assert.match(result.stdout, /omx explore --prompt-file <file>/);
|
|
268
|
-
assert.match(result.stdout, /Never use positional prompt text/i);
|
|
269
|
-
assert.match(result.stdout, /DEPRECATED: `omx explore` is deprecated/i);
|
|
270
|
-
assert.equal(result.stderr, '');
|
|
271
|
-
}
|
|
272
|
-
finally {
|
|
273
|
-
await rm(wd, { recursive: true, force: true });
|
|
274
|
-
}
|
|
47
|
+
describe('getBuiltinExploreHarnessUnsupportedReason', () => {
|
|
48
|
+
it('returns undefined on non-Windows platforms', () => {
|
|
49
|
+
assert.equal(getBuiltinExploreHarnessUnsupportedReason('linux', {}), undefined);
|
|
275
50
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-prompt-'));
|
|
280
|
-
try {
|
|
281
|
-
const promptPath = join(wd, 'prompt.md');
|
|
282
|
-
await writeFile(promptPath, ' find symbol refs \n');
|
|
283
|
-
assert.equal(await loadExplorePrompt({ promptFile: promptPath }), 'find symbol refs');
|
|
284
|
-
}
|
|
285
|
-
finally {
|
|
286
|
-
await rm(wd, { recursive: true, force: true });
|
|
287
|
-
}
|
|
51
|
+
it('reports a reason on Windows without an override', () => {
|
|
52
|
+
const reason = getBuiltinExploreHarnessUnsupportedReason('win32', {});
|
|
53
|
+
assert.match(reason ?? '', /not ready on Windows/);
|
|
288
54
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
it('injects wiki matches into the explore prompt when local wiki pages exist', async () => {
|
|
292
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-wiki-'));
|
|
293
|
-
try {
|
|
294
|
-
writePage(wd, {
|
|
295
|
-
filename: 'runtime.md',
|
|
296
|
-
frontmatter: {
|
|
297
|
-
title: 'Runtime Architecture',
|
|
298
|
-
tags: ['runtime', 'hooks'],
|
|
299
|
-
created: '2026-01-01T00:00:00.000Z',
|
|
300
|
-
updated: '2026-01-01T00:00:00.000Z',
|
|
301
|
-
sources: ['test'],
|
|
302
|
-
links: [],
|
|
303
|
-
category: 'architecture',
|
|
304
|
-
confidence: 'high',
|
|
305
|
-
schemaVersion: WIKI_SCHEMA_VERSION,
|
|
306
|
-
},
|
|
307
|
-
content: '\n# Runtime Architecture\n\nSessionStart uses native hooks and session-end uses runtime cleanup.\n',
|
|
308
|
-
});
|
|
309
|
-
const prompt = buildExplorePromptWithWikiContext('how does session-start work', wd);
|
|
310
|
-
assert.match(prompt, /\[OMX Wiki Context\]/);
|
|
311
|
-
assert.match(prompt, /Runtime Architecture/);
|
|
312
|
-
assert.match(prompt, /prefer repository-backed facts/i);
|
|
313
|
-
assert.match(prompt, /Wiki mismatch/);
|
|
314
|
-
assert.match(prompt, /Original Explore Prompt/);
|
|
315
|
-
assert.equal(existsSync(join(wd, 'omx_wiki', 'log.md')), false);
|
|
316
|
-
}
|
|
317
|
-
finally {
|
|
318
|
-
await rm(wd, { recursive: true, force: true });
|
|
319
|
-
}
|
|
55
|
+
it('returns undefined on Windows when OMX_EXPLORE_BIN is set', () => {
|
|
56
|
+
assert.equal(getBuiltinExploreHarnessUnsupportedReason('win32', { OMX_EXPLORE_BIN: '/tmp/harness' }), undefined);
|
|
320
57
|
});
|
|
321
|
-
it('
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
writePage(wd, {
|
|
325
|
-
filename: 'runtime.md',
|
|
326
|
-
frontmatter: {
|
|
327
|
-
title: 'Runtime Architecture',
|
|
328
|
-
tags: ['runtime', 'hooks'],
|
|
329
|
-
created: '2026-01-01T00:00:00.000Z',
|
|
330
|
-
updated: '2026-01-01T00:00:00.000Z',
|
|
331
|
-
sources: ['test'],
|
|
332
|
-
links: [],
|
|
333
|
-
category: 'architecture',
|
|
334
|
-
confidence: 'high',
|
|
335
|
-
schemaVersion: WIKI_SCHEMA_VERSION,
|
|
336
|
-
},
|
|
337
|
-
content: '\n# Runtime Architecture\n\nSessionStart uses native hooks and session-end uses runtime cleanup.\n',
|
|
338
|
-
});
|
|
339
|
-
buildExplorePromptWithWikiContext('session-start lifecycle', wd);
|
|
340
|
-
const logPath = join(wd, 'omx_wiki', 'log.md');
|
|
341
|
-
assert.equal(existsSync(logPath), false);
|
|
342
|
-
// sanity: direct query callers still log by default
|
|
343
|
-
const { queryWiki } = await import('../../wiki/index.js');
|
|
344
|
-
queryWiki(wd, 'session-start lifecycle');
|
|
345
|
-
assert.equal(existsSync(logPath), true);
|
|
346
|
-
assert.match(readFileSync(logPath, 'utf8'), /Query "session-start lifecycle"/);
|
|
347
|
-
}
|
|
348
|
-
finally {
|
|
349
|
-
await rm(wd, { recursive: true, force: true });
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
it('warns when wiki pages are missing or too weak', async () => {
|
|
353
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-no-wiki-'));
|
|
354
|
-
try {
|
|
355
|
-
const prompt = buildExplorePromptWithWikiContext('find auth', wd);
|
|
356
|
-
assert.match(prompt, /\[OMX Wiki Status\]/);
|
|
357
|
-
assert.match(prompt, /build an initial project wiki/i);
|
|
358
|
-
assert.match(prompt, /Original Explore Prompt/);
|
|
359
|
-
}
|
|
360
|
-
finally {
|
|
361
|
-
await rm(wd, { recursive: true, force: true });
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
it('warns when the wiki directory is missing entirely', async () => {
|
|
365
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-missing-wiki-'));
|
|
366
|
-
try {
|
|
367
|
-
const prompt = buildExplorePromptWithWikiContext('find auth', wd);
|
|
368
|
-
assert.match(prompt, /Wiki evidence is weak or missing/i);
|
|
369
|
-
assert.match(prompt, /build an initial project wiki/i);
|
|
370
|
-
}
|
|
371
|
-
finally {
|
|
372
|
-
await rm(wd, { recursive: true, force: true });
|
|
373
|
-
}
|
|
58
|
+
it('assert helper throws only on unsupported platforms', () => {
|
|
59
|
+
assert.doesNotThrow(() => assertBuiltinExploreHarnessSupported('linux', {}));
|
|
60
|
+
assert.throws(() => assertBuiltinExploreHarnessSupported('win32', {}), /not ready on Windows/);
|
|
374
61
|
});
|
|
375
62
|
});
|
|
376
63
|
describe('resolvePackagedExploreHarnessCommand', () => {
|
|
@@ -414,819 +101,4 @@ describe('resolvePackagedExploreHarnessCommand', () => {
|
|
|
414
101
|
}
|
|
415
102
|
});
|
|
416
103
|
});
|
|
417
|
-
describe('resolveExploreHarnessCommand', () => {
|
|
418
|
-
it('uses env override when provided', () => {
|
|
419
|
-
const resolved = resolveExploreHarnessCommand('/repo', { OMX_EXPLORE_BIN: '/tmp/omx-explore-stub' });
|
|
420
|
-
assert.deepEqual(resolved, { command: '/tmp/omx-explore-stub', args: [] });
|
|
421
|
-
});
|
|
422
|
-
it('prefers a packaged native harness binary when present', async () => {
|
|
423
|
-
await withPackagedExploreHarnessLock(async () => {
|
|
424
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-native-'));
|
|
425
|
-
try {
|
|
426
|
-
const binDir = join(wd, 'bin');
|
|
427
|
-
await mkdir(binDir, { recursive: true });
|
|
428
|
-
await writeFile(join(wd, 'package.json'), '{}\n');
|
|
429
|
-
await writeFile(join(binDir, 'omx-explore-harness.meta.json'), JSON.stringify({
|
|
430
|
-
binaryName: packagedExploreHarnessBinaryName(),
|
|
431
|
-
platform: process.platform,
|
|
432
|
-
arch: process.arch,
|
|
433
|
-
}));
|
|
434
|
-
const nativePath = join(binDir, packagedExploreHarnessBinaryName());
|
|
435
|
-
await writeFile(nativePath, '#!/bin/sh\necho native\n');
|
|
436
|
-
await chmod(nativePath, 0o755);
|
|
437
|
-
const resolved = resolveExploreHarnessCommand(wd, {});
|
|
438
|
-
assert.deepEqual(resolved, { command: nativePath, args: [] });
|
|
439
|
-
}
|
|
440
|
-
finally {
|
|
441
|
-
await rm(wd, { recursive: true, force: true });
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
it('uses an existing repo-built native harness before cargo fallback', async () => {
|
|
446
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-target-'));
|
|
447
|
-
try {
|
|
448
|
-
const targetDir = join(wd, 'target', 'release');
|
|
449
|
-
await mkdir(targetDir, { recursive: true });
|
|
450
|
-
await writeFile(join(wd, 'package.json'), '{}\n');
|
|
451
|
-
await writeFile(join(targetDir, packagedExploreHarnessBinaryName()), '#!/bin/sh\nexit 0\n');
|
|
452
|
-
await chmod(join(targetDir, packagedExploreHarnessBinaryName()), 0o755);
|
|
453
|
-
await mkdir(join(wd, 'crates', 'omx-explore'), { recursive: true });
|
|
454
|
-
await writeFile(join(wd, 'crates', 'omx-explore', 'Cargo.toml'), '[package]\nname="omx-explore-harness"\nversion="0.0.0"\n');
|
|
455
|
-
const repoBuilt = repoBuiltExploreHarnessCommand(wd);
|
|
456
|
-
assert.deepEqual(repoBuilt, { command: join(targetDir, packagedExploreHarnessBinaryName()), args: [] });
|
|
457
|
-
const resolved = resolveExploreHarnessCommand(wd, {});
|
|
458
|
-
assert.deepEqual(resolved, { command: join(targetDir, packagedExploreHarnessBinaryName()), args: [] });
|
|
459
|
-
}
|
|
460
|
-
finally {
|
|
461
|
-
await rm(wd, { recursive: true, force: true });
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
it('builds cargo fallback command otherwise', async () => {
|
|
465
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-fallback-'));
|
|
466
|
-
try {
|
|
467
|
-
const crateDir = join(wd, 'crates', 'omx-explore');
|
|
468
|
-
await mkdir(crateDir, { recursive: true });
|
|
469
|
-
await writeFile(join(wd, 'package.json'), '{}\n');
|
|
470
|
-
await writeFile(join(crateDir, 'Cargo.toml'), '[package]\nname = "omx-explore-harness"\nversion = "0.0.0"\n');
|
|
471
|
-
const cacheDir = join(wd, 'native-cache');
|
|
472
|
-
const resolved = resolveExploreHarnessCommand(wd, { OMX_NATIVE_CACHE_DIR: cacheDir });
|
|
473
|
-
assert.equal(resolved.command, 'cargo');
|
|
474
|
-
assert.ok(resolved.args.includes('--manifest-path'));
|
|
475
|
-
assert.ok(resolved.args.includes(join(wd, 'crates', 'omx-explore', 'Cargo.toml')));
|
|
476
|
-
assert.ok(resolved.args.includes('--target-dir'));
|
|
477
|
-
assert.ok(resolved.args.includes(join(cacheDir, 'cargo-target', 'omx-explore-harness')));
|
|
478
|
-
}
|
|
479
|
-
finally {
|
|
480
|
-
await rm(wd, { recursive: true, force: true });
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
it('hydrates a native harness for packaged installs before attempting cargo fallback', async () => {
|
|
484
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-hydrated-'));
|
|
485
|
-
try {
|
|
486
|
-
const assetRoot = join(wd, 'assets');
|
|
487
|
-
const cacheDir = join(wd, 'cache');
|
|
488
|
-
const stagingDir = join(wd, 'staging');
|
|
489
|
-
await mkdir(assetRoot, { recursive: true });
|
|
490
|
-
await mkdir(stagingDir, { recursive: true });
|
|
491
|
-
await writeFile(join(wd, 'package.json'), JSON.stringify({
|
|
492
|
-
version: '0.8.15',
|
|
493
|
-
repository: { url: 'git+https://github.com/Yeachan-Heo/oh-my-codex.git' },
|
|
494
|
-
}));
|
|
495
|
-
// Published packages intentionally ship src/scripts for postinstall, but
|
|
496
|
-
// that must not make them look like writable source checkouts.
|
|
497
|
-
await mkdir(join(wd, 'src', 'scripts'), { recursive: true });
|
|
498
|
-
await writeFile(join(wd, 'src', 'scripts', 'postinstall-bootstrap.js'), '');
|
|
499
|
-
await mkdir(join(wd, 'crates', 'omx-explore'), { recursive: true });
|
|
500
|
-
await writeFile(join(wd, 'crates', 'omx-explore', 'Cargo.toml'), '[package]\nname=\"omx-explore-harness\"\nversion=\"0.8.15\"\n');
|
|
501
|
-
const binaryPath = join(stagingDir, packagedExploreHarnessBinaryName());
|
|
502
|
-
await writeFile(binaryPath, '#!/bin/sh\necho hydrated-explore\n');
|
|
503
|
-
await chmod(binaryPath, 0o755);
|
|
504
|
-
const archiveName = `omx-explore-harness-${process.platform}-${process.arch}.tar.gz`;
|
|
505
|
-
const archivePath = join(assetRoot, archiveName);
|
|
506
|
-
const archive = spawnSync('tar', ['-czf', archivePath, '-C', stagingDir, packagedExploreHarnessBinaryName()], { encoding: 'utf-8' });
|
|
507
|
-
assert.equal(archive.status, 0, archive.stderr || archive.stdout);
|
|
508
|
-
const archiveBuffer = await readFile(archivePath);
|
|
509
|
-
const checksum = createHash('sha256').update(archiveBuffer).digest('hex');
|
|
510
|
-
const server = await new Promise((resolve) => {
|
|
511
|
-
const srv = createServer(async (req, res) => {
|
|
512
|
-
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
513
|
-
const filePath = join(assetRoot, url.pathname.replace(/^\//, ''));
|
|
514
|
-
try {
|
|
515
|
-
res.writeHead(200);
|
|
516
|
-
res.end(await readFile(filePath));
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
res.writeHead(404);
|
|
520
|
-
res.end('missing');
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
srv.listen(0, '127.0.0.1', () => {
|
|
524
|
-
const address = srv.address();
|
|
525
|
-
if (!address || typeof address === 'string')
|
|
526
|
-
throw new Error('bad address');
|
|
527
|
-
resolve({
|
|
528
|
-
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
529
|
-
close: () => new Promise((done, reject) => srv.close((err) => err ? reject(err) : done())),
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
});
|
|
533
|
-
try {
|
|
534
|
-
await writeFile(join(assetRoot, 'native-release-manifest.json'), JSON.stringify({
|
|
535
|
-
version: '0.8.15',
|
|
536
|
-
assets: [{
|
|
537
|
-
product: 'omx-explore-harness',
|
|
538
|
-
version: '0.8.15',
|
|
539
|
-
platform: process.platform,
|
|
540
|
-
arch: process.arch,
|
|
541
|
-
archive: archiveName,
|
|
542
|
-
binary: 'omx-explore-harness',
|
|
543
|
-
binary_path: 'omx-explore-harness',
|
|
544
|
-
sha256: checksum,
|
|
545
|
-
size: archiveBuffer.length,
|
|
546
|
-
download_url: `${server.baseUrl}/${archiveName}`,
|
|
547
|
-
}],
|
|
548
|
-
}, null, 2));
|
|
549
|
-
const resolved = await resolveExploreHarnessCommandWithHydration(wd, {
|
|
550
|
-
OMX_NATIVE_MANIFEST_URL: `${server.baseUrl}/native-release-manifest.json`,
|
|
551
|
-
OMX_NATIVE_CACHE_DIR: cacheDir,
|
|
552
|
-
});
|
|
553
|
-
assert.notEqual(resolved.command, 'cargo');
|
|
554
|
-
assert.match(resolved.command, /cache/);
|
|
555
|
-
}
|
|
556
|
-
finally {
|
|
557
|
-
await server.close();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
finally {
|
|
561
|
-
await rm(wd, { recursive: true, force: true });
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
it('reports a clean fallback error when the native manifest is unavailable for packaged installs', async () => {
|
|
565
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-missing-manifest-'));
|
|
566
|
-
try {
|
|
567
|
-
await writeFile(join(wd, 'package.json'), JSON.stringify({
|
|
568
|
-
version: '0.8.15',
|
|
569
|
-
repository: { url: 'git+https://github.com/Yeachan-Heo/oh-my-codex.git' },
|
|
570
|
-
}));
|
|
571
|
-
const server = await new Promise((resolve) => {
|
|
572
|
-
const srv = createServer((_req, res) => {
|
|
573
|
-
res.writeHead(404);
|
|
574
|
-
res.end('missing');
|
|
575
|
-
});
|
|
576
|
-
srv.listen(0, '127.0.0.1', () => {
|
|
577
|
-
const address = srv.address();
|
|
578
|
-
if (!address || typeof address === 'string')
|
|
579
|
-
throw new Error('bad address');
|
|
580
|
-
resolve({
|
|
581
|
-
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
582
|
-
close: () => new Promise((done, reject) => srv.close((err) => err ? reject(err) : done())),
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
try {
|
|
587
|
-
await assert.rejects(() => resolveExploreHarnessCommandWithHydration(wd, {
|
|
588
|
-
OMX_NATIVE_MANIFEST_URL: `${server.baseUrl}/native-release-manifest.json`,
|
|
589
|
-
OMX_NATIVE_CACHE_DIR: join(wd, 'cache'),
|
|
590
|
-
}), /no compatible native harness is available/);
|
|
591
|
-
}
|
|
592
|
-
finally {
|
|
593
|
-
await server.close();
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
finally {
|
|
597
|
-
await rm(wd, { recursive: true, force: true });
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
describe('buildExploreHarnessArgs', () => {
|
|
602
|
-
it('includes cwd, prompt, prompt contract, and constrained model settings', () => {
|
|
603
|
-
const wd = join(tmpdir(), 'omx-explore-arg-test');
|
|
604
|
-
const isolatedCodexHome = join(tmpdir(), `omx-explore-defaults-${process.pid}-${Date.now()}`);
|
|
605
|
-
const savedEnv = new Map();
|
|
606
|
-
for (const key of [
|
|
607
|
-
'CODEX_HOME',
|
|
608
|
-
'OMX_DEFAULT_FRONTIER_MODEL',
|
|
609
|
-
'OMX_DEFAULT_STANDARD_MODEL',
|
|
610
|
-
'OMX_DEFAULT_SPARK_MODEL',
|
|
611
|
-
'OMX_SPARK_MODEL',
|
|
612
|
-
]) {
|
|
613
|
-
savedEnv.set(key, process.env[key]);
|
|
614
|
-
delete process.env[key];
|
|
615
|
-
}
|
|
616
|
-
try {
|
|
617
|
-
const args = buildExploreHarnessArgs('find auth', wd, {
|
|
618
|
-
CODEX_HOME: isolatedCodexHome,
|
|
619
|
-
OMX_EXPLORE_SPARK_MODEL: 'spark-model',
|
|
620
|
-
}, '/pkg');
|
|
621
|
-
assert.deepEqual(args.slice(0, 3), ['--cwd', wd, '--prompt']);
|
|
622
|
-
assert.match(args[3] || '', /Original Explore Prompt/);
|
|
623
|
-
assert.match(args[3] || '', /find auth/);
|
|
624
|
-
assert.deepEqual(args.slice(4), [
|
|
625
|
-
'--prompt-file',
|
|
626
|
-
'/pkg/prompts/explore-harness.md',
|
|
627
|
-
'--instructions-file',
|
|
628
|
-
'/pkg/templates/model-instructions/explore-lightweight-AGENTS.md',
|
|
629
|
-
'--model-spark',
|
|
630
|
-
'spark-model',
|
|
631
|
-
'--model-fallback',
|
|
632
|
-
'gpt-5.5',
|
|
633
|
-
]);
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
for (const [key, value] of savedEnv.entries()) {
|
|
637
|
-
if (value === undefined)
|
|
638
|
-
delete process.env[key];
|
|
639
|
-
else
|
|
640
|
-
process.env[key] = value;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
it('honors configured env overrides for fallback model and instructions file', async () => {
|
|
645
|
-
const codexHome = await mkdtemp(join(tmpdir(), 'omx-explore-config-env-'));
|
|
646
|
-
await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
|
|
647
|
-
env: {
|
|
648
|
-
OMX_DEFAULT_STANDARD_MODEL: 'standard-local',
|
|
649
|
-
OMX_DEFAULT_SPARK_MODEL: 'spark-local',
|
|
650
|
-
OMX_EXPLORE_MODEL_INSTRUCTIONS_FILE: '/config/explore-instructions.md',
|
|
651
|
-
},
|
|
652
|
-
}));
|
|
653
|
-
try {
|
|
654
|
-
const wd = join(tmpdir(), 'omx-explore-arg-test');
|
|
655
|
-
const args = buildExploreHarnessArgs('find auth', wd, {
|
|
656
|
-
CODEX_HOME: codexHome,
|
|
657
|
-
}, '/pkg');
|
|
658
|
-
assert.deepEqual(args.slice(4), [
|
|
659
|
-
'--prompt-file',
|
|
660
|
-
'/pkg/prompts/explore-harness.md',
|
|
661
|
-
'--instructions-file',
|
|
662
|
-
'/config/explore-instructions.md',
|
|
663
|
-
'--model-spark',
|
|
664
|
-
'spark-local',
|
|
665
|
-
'--model-fallback',
|
|
666
|
-
'standard-local',
|
|
667
|
-
]);
|
|
668
|
-
}
|
|
669
|
-
finally {
|
|
670
|
-
await rm(codexHome, { recursive: true, force: true });
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
it('applies persisted project CODEX_HOME fallback before reading explore config overrides', async () => {
|
|
674
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-project-codex-home-'));
|
|
675
|
-
const badHome = join(wd, 'home-as-file');
|
|
676
|
-
await writeFile(badHome, 'not-a-directory');
|
|
677
|
-
await mkdir(join(wd, '.omx'), { recursive: true });
|
|
678
|
-
await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
|
|
679
|
-
await mkdir(join(wd, '.codex'), { recursive: true });
|
|
680
|
-
await writeFile(join(wd, '.codex', '.omx-config.json'), JSON.stringify({
|
|
681
|
-
env: {
|
|
682
|
-
OMX_DEFAULT_STANDARD_MODEL: 'standard-project',
|
|
683
|
-
OMX_DEFAULT_SPARK_MODEL: 'spark-project',
|
|
684
|
-
},
|
|
685
|
-
}));
|
|
686
|
-
try {
|
|
687
|
-
const env = resolveExploreEnv(wd, { HOME: badHome });
|
|
688
|
-
assert.equal(env.CODEX_HOME, join(wd, '.codex'));
|
|
689
|
-
const args = buildExploreHarnessArgs('find auth', wd, env, '/pkg');
|
|
690
|
-
assert.deepEqual(args.slice(4), [
|
|
691
|
-
'--prompt-file',
|
|
692
|
-
'/pkg/prompts/explore-harness.md',
|
|
693
|
-
'--instructions-file',
|
|
694
|
-
'/pkg/templates/model-instructions/explore-lightweight-AGENTS.md',
|
|
695
|
-
'--model-spark',
|
|
696
|
-
'spark-project',
|
|
697
|
-
'--model-fallback',
|
|
698
|
-
'standard-project',
|
|
699
|
-
]);
|
|
700
|
-
}
|
|
701
|
-
finally {
|
|
702
|
-
await rm(wd, { recursive: true, force: true });
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
describe('resolveExploreSparkShellRoute', () => {
|
|
707
|
-
it('keeps natural-language exploration prompts on the direct harness path', () => {
|
|
708
|
-
assert.equal(resolveExploreSparkShellRoute('which files define team routing'), undefined);
|
|
709
|
-
assert.equal(resolveExploreSparkShellRoute('map the relationship between hooks and tmux helpers'), undefined);
|
|
710
|
-
});
|
|
711
|
-
it('routes qualifying read-only git commands to sparkshell', () => {
|
|
712
|
-
assert.deepEqual(resolveExploreSparkShellRoute('git log --oneline'), {
|
|
713
|
-
argv: ['git', 'log', '--oneline'],
|
|
714
|
-
reason: 'long-output',
|
|
715
|
-
});
|
|
716
|
-
assert.deepEqual(resolveExploreSparkShellRoute('run git diff --stat'), {
|
|
717
|
-
argv: ['git', 'diff', '--stat'],
|
|
718
|
-
reason: 'long-output',
|
|
719
|
-
});
|
|
720
|
-
});
|
|
721
|
-
it('rejects non-read-only or shell-unsafe commands for sparkshell routing', () => {
|
|
722
|
-
assert.equal(resolveExploreSparkShellRoute('git commit -m test'), undefined);
|
|
723
|
-
assert.equal(resolveExploreSparkShellRoute('npm test'), undefined);
|
|
724
|
-
assert.equal(resolveExploreSparkShellRoute('git log | head'), undefined);
|
|
725
|
-
assert.equal(resolveExploreSparkShellRoute('find /tmp -maxdepth 1'), undefined);
|
|
726
|
-
});
|
|
727
|
-
});
|
|
728
|
-
describe('exploreCommand', () => {
|
|
729
|
-
it('answers simple text lookups with the local fast-path before spawning Codex-backed harnesses', async () => {
|
|
730
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-local-fast-path-'));
|
|
731
|
-
try {
|
|
732
|
-
await mkdir(join(wd, 'src'), { recursive: true });
|
|
733
|
-
await writeFile(join(wd, 'src', 'auth.ts'), 'export const token = "local-only";\n');
|
|
734
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
735
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf harness-should-not-run\n');
|
|
736
|
-
await chmod(harnessStub, 0o755);
|
|
737
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'search for local-only'], {
|
|
738
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
739
|
-
});
|
|
740
|
-
assert.equal(result.exitCode, 0);
|
|
741
|
-
assert.match(result.stdout, /local fast-path used \(text lookup\)/);
|
|
742
|
-
assert.match(result.stdout, /src\/auth\.ts:1/);
|
|
743
|
-
assert.equal(result.stderr, `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
744
|
-
}
|
|
745
|
-
finally {
|
|
746
|
-
await rm(wd, { recursive: true, force: true });
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
it('answers explicit file-read prompts with bounded file content instead of metadata only', async () => {
|
|
750
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-local-file-read-'));
|
|
751
|
-
try {
|
|
752
|
-
await writeFile(join(wd, 'README.md'), '# Demo README\n\nThis content must be visible.\n');
|
|
753
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
754
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf harness-should-not-run\n');
|
|
755
|
-
await chmod(harnessStub, 0o755);
|
|
756
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'read README.md'], {
|
|
757
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
758
|
-
});
|
|
759
|
-
assert.equal(result.exitCode, 0);
|
|
760
|
-
assert.match(result.stdout, /local fast-path used \(file lookup\)/);
|
|
761
|
-
assert.match(result.stdout, /README\.md \(\d+ bytes; showing up to/);
|
|
762
|
-
assert.match(result.stdout, /# Demo README/);
|
|
763
|
-
assert.match(result.stdout, /This content must be visible\./);
|
|
764
|
-
assert.doesNotMatch(result.stdout.trim(), /^.*README\.md \(\d+ bytes\)$/);
|
|
765
|
-
assert.equal(result.stderr, `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
766
|
-
}
|
|
767
|
-
finally {
|
|
768
|
-
await rm(wd, { recursive: true, force: true });
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
it('marks explicit file-read fast-path output when file content is truncated', async () => {
|
|
772
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-local-file-truncated-'));
|
|
773
|
-
try {
|
|
774
|
-
await writeFile(join(wd, 'README.md'), `# Demo README\n${'x'.repeat(20_000)}\n`);
|
|
775
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
776
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf harness-should-not-run\n');
|
|
777
|
-
await chmod(harnessStub, 0o755);
|
|
778
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'show README.md'], {
|
|
779
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
780
|
-
});
|
|
781
|
-
assert.equal(result.exitCode, 0);
|
|
782
|
-
assert.match(result.stdout, /# Demo README/);
|
|
783
|
-
assert.match(result.stdout, /\[truncated: file exceeds local fast-path limit/);
|
|
784
|
-
assert.equal(result.stderr, `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
785
|
-
}
|
|
786
|
-
finally {
|
|
787
|
-
await rm(wd, { recursive: true, force: true });
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
it('does not follow symlinks in explicit file-read local fast-path lookups', async () => {
|
|
791
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-local-symlink-'));
|
|
792
|
-
const outside = await mkdtemp(join(tmpdir(), 'omx-explore-local-outside-'));
|
|
793
|
-
try {
|
|
794
|
-
await writeFile(join(outside, 'secret.txt'), 'outside-secret-should-not-print\n');
|
|
795
|
-
await symlink(join(outside, 'secret.txt'), join(wd, 'leak.txt'));
|
|
796
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
797
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf "harness-fallback\\n"\n');
|
|
798
|
-
await chmod(harnessStub, 0o755);
|
|
799
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'read leak.txt'], {
|
|
800
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
801
|
-
});
|
|
802
|
-
assert.equal(result.exitCode, 0);
|
|
803
|
-
assert.match(result.stdout, /harness-fallback/);
|
|
804
|
-
assert.doesNotMatch(result.stdout, /outside-secret-should-not-print/);
|
|
805
|
-
assert.doesNotMatch(result.stdout, /local fast-path used/);
|
|
806
|
-
}
|
|
807
|
-
finally {
|
|
808
|
-
await rm(wd, { recursive: true, force: true });
|
|
809
|
-
await rm(outside, { recursive: true, force: true });
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
it('does not read oversized files during text-search local fast-path lookups', async () => {
|
|
813
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-local-large-text-'));
|
|
814
|
-
try {
|
|
815
|
-
await writeFile(join(wd, 'large.txt'), `${'x'.repeat(20_000)}\nlarge-only-needle\n`);
|
|
816
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
817
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf "harness-fallback\\n"\n');
|
|
818
|
-
await chmod(harnessStub, 0o755);
|
|
819
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'search for large-only-needle'], {
|
|
820
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
821
|
-
});
|
|
822
|
-
assert.equal(result.exitCode, 0);
|
|
823
|
-
assert.match(result.stdout, /harness-fallback/);
|
|
824
|
-
assert.doesNotMatch(result.stdout, /local fast-path used \(text lookup\)/);
|
|
825
|
-
assert.equal(result.stderr, `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
826
|
-
}
|
|
827
|
-
finally {
|
|
828
|
-
await rm(wd, { recursive: true, force: true });
|
|
829
|
-
}
|
|
830
|
-
});
|
|
831
|
-
it('times out Codex-backed harness process trees with an explicit runaway error', async () => {
|
|
832
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-timeout-'));
|
|
833
|
-
try {
|
|
834
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
835
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf "started\\n"\nsleep 5\n');
|
|
836
|
-
await chmod(harnessStub, 0o755);
|
|
837
|
-
await assert.rejects(() => runExploreCommandForTest(wd, ['--prompt', 'map the runtime timeout behavior'], {
|
|
838
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
839
|
-
OMX_EXPLORE_TIMEOUT_MS: '50',
|
|
840
|
-
}), /harness timed out after 50ms; terminated the process tree/);
|
|
841
|
-
}
|
|
842
|
-
finally {
|
|
843
|
-
await rm(wd, { recursive: true, force: true });
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
it('emits verbose telemetry when explore uses the sparkshell backend', async () => {
|
|
847
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-sparkshell-telemetry-'));
|
|
848
|
-
try {
|
|
849
|
-
const sparkshellStub = join(wd, 'sparkshell-stub.sh');
|
|
850
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
851
|
-
await writeFile(sparkshellStub, `#!/bin/sh\nprintf '# Answer\\n- telemetry route\\n'\n`);
|
|
852
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf harness-should-not-run\n');
|
|
853
|
-
await chmod(sparkshellStub, 0o755);
|
|
854
|
-
await chmod(harnessStub, 0o755);
|
|
855
|
-
const result = runOmx(wd, ['explore', '--verbose', '--prompt', 'git log --oneline'], {
|
|
856
|
-
OMX_SPARKSHELL_BIN: sparkshellStub,
|
|
857
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
858
|
-
});
|
|
859
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
860
|
-
return;
|
|
861
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
862
|
-
assert.match(result.stderr, /backend=sparkshell reason=long-output/);
|
|
863
|
-
assert.match(result.stdout, /telemetry route/);
|
|
864
|
-
}
|
|
865
|
-
finally {
|
|
866
|
-
await rm(wd, { recursive: true, force: true });
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
it('routes qualifying read-only shell commands through sparkshell instead of the direct harness', async () => {
|
|
870
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-sparkshell-route-'));
|
|
871
|
-
try {
|
|
872
|
-
const sparkshellStub = join(wd, 'sparkshell-stub.sh');
|
|
873
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
874
|
-
const capturePath = join(wd, 'sparkshell-capture.txt');
|
|
875
|
-
await writeFile(sparkshellStub, `#!/bin/sh\nprintf '%s\n' "$@" > ${JSON.stringify(capturePath)}\nprintf '# Answer\n- routed via sparkshell\n'\n`);
|
|
876
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf harness-should-not-run\n');
|
|
877
|
-
await chmod(sparkshellStub, 0o755);
|
|
878
|
-
await chmod(harnessStub, 0o755);
|
|
879
|
-
const result = runOmx(wd, ['explore', '--prompt', 'git log --oneline'], {
|
|
880
|
-
OMX_SPARKSHELL_BIN: sparkshellStub,
|
|
881
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
882
|
-
});
|
|
883
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
884
|
-
return;
|
|
885
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
886
|
-
assert.equal(result.stdout, '# Answer\n- routed via sparkshell\n');
|
|
887
|
-
assert.equal(result.stderr, `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
888
|
-
const captured = (await readFile(capturePath, 'utf-8')).trim().split('\n');
|
|
889
|
-
assert.deepEqual(captured, ['git', 'log', '--oneline']);
|
|
890
|
-
}
|
|
891
|
-
finally {
|
|
892
|
-
await rm(wd, { recursive: true, force: true });
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
it('falls back to the explore harness when sparkshell backend is unavailable', async () => {
|
|
896
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-sparkshell-fallback-'));
|
|
897
|
-
try {
|
|
898
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
899
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf "%s\\n" "# Answer" "- fallback harness recovered the lookup"\n');
|
|
900
|
-
await chmod(harnessStub, 0o755);
|
|
901
|
-
const result = runOmx(wd, ['explore', '--prompt', 'git log --oneline'], {
|
|
902
|
-
OMX_SPARKSHELL_BIN: join(wd, 'missing-sparkshell'),
|
|
903
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
904
|
-
});
|
|
905
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
906
|
-
return;
|
|
907
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
908
|
-
assert.match(result.stderr, /sparkshell backend unavailable/);
|
|
909
|
-
assert.match(result.stderr, /Falling back to the explore harness/);
|
|
910
|
-
assert.match(result.stdout, /fallback harness recovered the lookup/);
|
|
911
|
-
}
|
|
912
|
-
finally {
|
|
913
|
-
await rm(wd, { recursive: true, force: true });
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
it('falls back to the explore harness when sparkshell is GLIBC-incompatible', async () => {
|
|
917
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-sparkshell-glibc-'));
|
|
918
|
-
try {
|
|
919
|
-
const sparkshellStub = join(wd, 'sparkshell-stub.sh');
|
|
920
|
-
const harnessStub = join(wd, 'explore-stub.sh');
|
|
921
|
-
await writeFile(sparkshellStub, "#!/bin/sh\necho \"omx-sparkshell: /lib/x86_64-linux-gnu/libc.so.6: version \\`GLIBC_2.39' not found\" 1>&2\nexit 1\n");
|
|
922
|
-
await writeFile(harnessStub, '#!/bin/sh\nprintf "%s\\n" "# Answer" "- fallback harness recovered the lookup"\n');
|
|
923
|
-
await chmod(sparkshellStub, 0o755);
|
|
924
|
-
await chmod(harnessStub, 0o755);
|
|
925
|
-
const result = runOmx(wd, ['explore', '--prompt', 'git log --oneline'], {
|
|
926
|
-
OMX_SPARKSHELL_BIN: sparkshellStub,
|
|
927
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
928
|
-
});
|
|
929
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
930
|
-
return;
|
|
931
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
932
|
-
assert.match(result.stderr, /GLIBC symbols/i);
|
|
933
|
-
assert.match(result.stderr, /Falling back to the explore harness/);
|
|
934
|
-
assert.match(result.stdout, /fallback harness recovered the lookup/);
|
|
935
|
-
assert.doesNotMatch(result.stderr, /version `GLIBC_2\.39' not found/);
|
|
936
|
-
}
|
|
937
|
-
finally {
|
|
938
|
-
await rm(wd, { recursive: true, force: true });
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
it('passes prompt to harness and preserves markdown stdout', async () => {
|
|
942
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-cmd-'));
|
|
943
|
-
try {
|
|
944
|
-
const stub = join(wd, 'explore-stub.sh');
|
|
945
|
-
const capturePath = join(wd, 'capture.txt');
|
|
946
|
-
await writeFile(stub, `#!/bin/sh\nprintf '%s\n' \"$@\" > ${JSON.stringify(capturePath)}\nprintf '# Files\\n- demo\\n'\n`);
|
|
947
|
-
await chmod(stub, 0o755);
|
|
948
|
-
const stdoutChunks = [];
|
|
949
|
-
const stderrChunks = [];
|
|
950
|
-
const originalStdout = process.stdout.write.bind(process.stdout);
|
|
951
|
-
const originalStderr = process.stderr.write.bind(process.stderr);
|
|
952
|
-
process.stdout.write = ((chunk) => {
|
|
953
|
-
stdoutChunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
|
|
954
|
-
return true;
|
|
955
|
-
});
|
|
956
|
-
process.stderr.write = ((chunk) => {
|
|
957
|
-
stderrChunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
|
|
958
|
-
return true;
|
|
959
|
-
});
|
|
960
|
-
const originalEnv = process.env.OMX_EXPLORE_BIN;
|
|
961
|
-
process.env.OMX_EXPLORE_BIN = stub;
|
|
962
|
-
const originalCwd = process.cwd();
|
|
963
|
-
process.chdir(wd);
|
|
964
|
-
try {
|
|
965
|
-
await exploreCommand(['--prompt', 'find', 'auth']);
|
|
966
|
-
}
|
|
967
|
-
finally {
|
|
968
|
-
process.chdir(originalCwd);
|
|
969
|
-
if (originalEnv === undefined)
|
|
970
|
-
delete process.env.OMX_EXPLORE_BIN;
|
|
971
|
-
else
|
|
972
|
-
process.env.OMX_EXPLORE_BIN = originalEnv;
|
|
973
|
-
process.stdout.write = originalStdout;
|
|
974
|
-
process.stderr.write = originalStderr;
|
|
975
|
-
}
|
|
976
|
-
assert.equal(stderrChunks.join(''), `${EXPLORE_DEPRECATION_NOTICE}\n`);
|
|
977
|
-
assert.equal(stdoutChunks.join(''), '# Files\n- demo\n');
|
|
978
|
-
const captured = (await readFile(capturePath, 'utf-8')).trim().split('\n');
|
|
979
|
-
assert.ok(captured.includes('--prompt'));
|
|
980
|
-
assert.ok(captured.includes('find auth'));
|
|
981
|
-
assert.ok(captured.includes('--model-spark'));
|
|
982
|
-
assert.ok(captured.includes('--model-fallback'));
|
|
983
|
-
}
|
|
984
|
-
finally {
|
|
985
|
-
await rm(wd, { recursive: true, force: true });
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
it('works end-to-end through omx explore', async () => {
|
|
989
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-e2e-'));
|
|
990
|
-
try {
|
|
991
|
-
const stub = join(wd, 'explore-stub.sh');
|
|
992
|
-
await writeFile(stub, '#!/bin/sh\nprintf "# Answer\\nReady to proceed\\n"\n');
|
|
993
|
-
await chmod(stub, 0o755);
|
|
994
|
-
const result = runOmx(wd, ['explore', '--prompt', 'find auth'], { OMX_EXPLORE_BIN: stub });
|
|
995
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
996
|
-
return;
|
|
997
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
998
|
-
assert.equal(result.stdout, '# Answer\nReady to proceed\n');
|
|
999
|
-
}
|
|
1000
|
-
finally {
|
|
1001
|
-
await rm(wd, { recursive: true, force: true });
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
it('passes project-local CODEX_HOME to the harness when persisted setup scope is project', async () => {
|
|
1005
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-project-codex-home-e2e-'));
|
|
1006
|
-
try {
|
|
1007
|
-
const stub = join(wd, 'explore-stub.sh');
|
|
1008
|
-
const capturePath = join(wd, 'capture.txt');
|
|
1009
|
-
const badHome = join(wd, 'home-as-file');
|
|
1010
|
-
await writeFile(badHome, 'not-a-directory');
|
|
1011
|
-
await mkdir(join(wd, '.omx'), { recursive: true });
|
|
1012
|
-
await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
|
|
1013
|
-
await writeFile(stub, `#!/bin/sh\nprintf 'CODEX_HOME=%s\\n' \"$CODEX_HOME\" > ${JSON.stringify(capturePath)}\nprintf '# Answer\\nReady to proceed\\n'\n`);
|
|
1014
|
-
await chmod(stub, 0o755);
|
|
1015
|
-
const result = runOmx(wd, ['explore', '--prompt', 'find auth'], {
|
|
1016
|
-
HOME: badHome,
|
|
1017
|
-
OMX_EXPLORE_BIN: stub,
|
|
1018
|
-
});
|
|
1019
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
1020
|
-
return;
|
|
1021
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1022
|
-
assert.equal(result.stdout, '# Answer\nReady to proceed\n');
|
|
1023
|
-
assert.equal(normalizeDarwinTmpPath(await readFile(capturePath, 'utf-8')), `CODEX_HOME=${normalizeDarwinTmpPath(join(wd, '.codex'))}\n`);
|
|
1024
|
-
}
|
|
1025
|
-
finally {
|
|
1026
|
-
await rm(wd, { recursive: true, force: true });
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
it('launches an env-node codex binary while keeping model shell commands allowlisted', async () => {
|
|
1030
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-harness-e2e-'));
|
|
1031
|
-
try {
|
|
1032
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1033
|
-
const capturePath = join(wd, 'capture.json');
|
|
1034
|
-
const codexStub = await writeEnvNodeCodexStub(wd, capturePath);
|
|
1035
|
-
const testPath = await createExploreTestPath(wd);
|
|
1036
|
-
const result = runOmx(wd, ['explore', '--prompt', 'find buildTmuxPaneCommand'], {
|
|
1037
|
-
OMX_EXPLORE_CODEX_BIN: codexStub,
|
|
1038
|
-
PATH: testPath,
|
|
1039
|
-
});
|
|
1040
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
1041
|
-
return;
|
|
1042
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1043
|
-
assert.equal(result.stdout, '# Answer\nHarness completed\n');
|
|
1044
|
-
const captured = await readFile(capturePath, 'utf-8');
|
|
1045
|
-
assert.match(captured, /PATH=.*omx-explore-allowlist-/);
|
|
1046
|
-
assert.match(captured, /SHELL=.*omx-explore-allowlist-.*\/bin\/bash$/m);
|
|
1047
|
-
assert.match(captured, /ALLOWED_STATUS=0/);
|
|
1048
|
-
assert.match(captured, /BLOCKED_STATUS=(?!0)\d+/);
|
|
1049
|
-
assert.match(captured, /--ARGV--[\s\S]*\nexec\n/);
|
|
1050
|
-
assert.match(captured, /model_instructions_file=.*explore-lightweight-AGENTS\.md/);
|
|
1051
|
-
assert.match(captured, /--ALLOWED_STDOUT--[\s\S]*ripgrep/i);
|
|
1052
|
-
assert.match(captured, /--BLOCKED_STDERR--[\s\S]*not on the omx explore allowlist/);
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
finally {
|
|
1056
|
-
await rm(wd, { recursive: true, force: true });
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
it('bypasses a POSIX package-manager codex shim without broadening the allowlisted PATH', async () => {
|
|
1060
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-harness-posix-shim-'));
|
|
1061
|
-
try {
|
|
1062
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1063
|
-
const capturePath = join(wd, 'capture.txt');
|
|
1064
|
-
const codexShim = await writePosixPackageManagerCodexShim(wd, capturePath);
|
|
1065
|
-
const testPath = await createExploreTestPath(wd);
|
|
1066
|
-
const result = runOmx(wd, ['explore', '--prompt', 'find buildTmuxPaneCommand'], {
|
|
1067
|
-
OMX_EXPLORE_CODEX_BIN: codexShim,
|
|
1068
|
-
PATH: testPath,
|
|
1069
|
-
});
|
|
1070
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
1071
|
-
return;
|
|
1072
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1073
|
-
assert.equal(result.stdout, '# Answer\nHarness completed\n');
|
|
1074
|
-
const captured = await readFile(capturePath, 'utf-8');
|
|
1075
|
-
assert.match(captured, /ARGV0=.*\/node$/m);
|
|
1076
|
-
assert.match(captured, /ARGV1=.*node_modules\/@openai\/codex\/bin\/codex\.js$/m);
|
|
1077
|
-
assert.match(captured, /PATH=.*omx-explore-allowlist-/);
|
|
1078
|
-
assert.doesNotMatch(captured, /PATH=.*node_modules\/\.bin/);
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
finally {
|
|
1082
|
-
await rm(wd, { recursive: true, force: true });
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
it('supports --prompt-file end-to-end with the harness', async () => {
|
|
1086
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-harness-prompt-file-'));
|
|
1087
|
-
try {
|
|
1088
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1089
|
-
const capturePath = join(wd, 'capture.json');
|
|
1090
|
-
const codexStub = await writeEnvNodeCodexStub(wd, capturePath);
|
|
1091
|
-
const testPath = await createExploreTestPath(wd);
|
|
1092
|
-
const promptPath = join(wd, 'prompt.md');
|
|
1093
|
-
await writeFile(promptPath, 'find prompt-file support\n');
|
|
1094
|
-
const result = runOmx(wd, ['explore', '--prompt-file', promptPath], {
|
|
1095
|
-
OMX_EXPLORE_CODEX_BIN: codexStub,
|
|
1096
|
-
PATH: testPath,
|
|
1097
|
-
});
|
|
1098
|
-
if (shouldSkipForSpawnPermissions(result.error))
|
|
1099
|
-
return;
|
|
1100
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1101
|
-
assert.equal(result.stdout, '# Answer\nHarness completed\n');
|
|
1102
|
-
const captured = await readFile(capturePath, 'utf-8');
|
|
1103
|
-
assert.match(captured, /--ARGV--[\s\S]*find prompt-file support/);
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
finally {
|
|
1107
|
-
await rm(wd, { recursive: true, force: true });
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
it('preserves must-preserve facts in a long noisy summary fixture', async () => {
|
|
1111
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-fidelity-'));
|
|
1112
|
-
try {
|
|
1113
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1114
|
-
const harnessStub = await writeExploreHarnessScenarioStub(wd, `
|
|
1115
|
-
printf '%s\n' '# Answer' '## Critical facts' '- MUST: summary mode stayed read-only' '- MUST: blocked command stayed node --version' '- MUST: next command is omx team status <team-name>' '' '## Noise'
|
|
1116
|
-
i=0
|
|
1117
|
-
while [ "$i" -lt 80 ]; do
|
|
1118
|
-
printf '%s\n' "- distractor line $i"
|
|
1119
|
-
i=$((i + 1))
|
|
1120
|
-
done
|
|
1121
|
-
exit 0
|
|
1122
|
-
`);
|
|
1123
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'surface the critical facts'], {
|
|
1124
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
1125
|
-
});
|
|
1126
|
-
assert.equal(result.exitCode, 0, result.stderr || result.stdout);
|
|
1127
|
-
assert.match(result.stdout, /MUST: summary mode stayed read-only/);
|
|
1128
|
-
assert.match(result.stdout, /MUST: blocked command stayed node --version/);
|
|
1129
|
-
assert.match(result.stdout, /MUST: next command is omx team status <team-name>/);
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
finally {
|
|
1133
|
-
await rm(wd, { recursive: true, force: true });
|
|
1134
|
-
}
|
|
1135
|
-
});
|
|
1136
|
-
it('preserves buried critical facts in adversarial noisy output', async () => {
|
|
1137
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-adversarial-'));
|
|
1138
|
-
try {
|
|
1139
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1140
|
-
const harnessStub = await writeExploreHarnessScenarioStub(wd, `
|
|
1141
|
-
printf '# Answer\n'
|
|
1142
|
-
i=0
|
|
1143
|
-
while [ "$i" -lt 40 ]; do
|
|
1144
|
-
printf '%s\n' "- noise before signal $i"
|
|
1145
|
-
i=$((i + 1))
|
|
1146
|
-
done
|
|
1147
|
-
printf '%s\n' '- MUST: fallback route remained available'
|
|
1148
|
-
i=0
|
|
1149
|
-
while [ "$i" -lt 40 ]; do
|
|
1150
|
-
printf '%s\n' "- noise after signal $i"
|
|
1151
|
-
i=$((i + 1))
|
|
1152
|
-
done
|
|
1153
|
-
printf '%s\n' '- MUST: stderr guidance stayed actionable'
|
|
1154
|
-
printf '%s\n' '- MUST: semantic facts survive compression'
|
|
1155
|
-
exit 0
|
|
1156
|
-
`);
|
|
1157
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'extract buried signals'], {
|
|
1158
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
1159
|
-
});
|
|
1160
|
-
assert.equal(result.exitCode, 0, result.stderr || result.stdout);
|
|
1161
|
-
assert.match(result.stdout, /MUST: fallback route remained available/);
|
|
1162
|
-
assert.match(result.stdout, /MUST: stderr guidance stayed actionable/);
|
|
1163
|
-
assert.match(result.stdout, /MUST: semantic facts survive compression/);
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
finally {
|
|
1167
|
-
await rm(wd, { recursive: true, force: true });
|
|
1168
|
-
}
|
|
1169
|
-
});
|
|
1170
|
-
it('falls back after spark failure with explicit output notice and actionable stderr guidance', async () => {
|
|
1171
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-fallback-success-'));
|
|
1172
|
-
try {
|
|
1173
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1174
|
-
const harnessStub = await writeExploreHarnessScenarioStub(wd, `
|
|
1175
|
-
printf '[omx explore] fallback-attempt=model from=\`%s\` to=\`gpt-5.5\` reason=spark_attempt_failed exit=17. Cost/behavior boundary changed if fallback succeeds; stdout fallback notice is emitted only after successful fallback output.\n' "\${OMX_EXPLORE_SPARK_MODEL:-spark-test-model}" >&2
|
|
1176
|
-
printf '[omx explore] spark model \`%s\` unavailable or failed (exit 17). Falling back to \`gpt-5.5\`.\n' "\${OMX_EXPLORE_SPARK_MODEL:-spark-test-model}" >&2
|
|
1177
|
-
printf '[omx explore] spark stderr: spark timed out; retry with the frontier fallback\n' >&2
|
|
1178
|
-
printf '%s\n' '## OMX Explore fallback' '- fallback: model' '- from: \`spark-test-model\`' '- to: \`gpt-5.5\`' '- reason: spark attempt failed with exit 17' '- boundary: cost/behavior may differ from the low-cost spark path' '' '# Answer' '- recovered with fallback model' '- MUST: actionable recovery path remained available'
|
|
1179
|
-
`);
|
|
1180
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'validate fallback recovery'], {
|
|
1181
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
1182
|
-
OMX_EXPLORE_SPARK_MODEL: 'spark-test-model',
|
|
1183
|
-
});
|
|
1184
|
-
assert.equal(result.exitCode, 0, result.stderr || result.stdout);
|
|
1185
|
-
assert.match(result.stderr, /fallback-attempt=model from=`spark-test-model` to=`gpt-5\.5` reason=spark_attempt_failed exit=17/);
|
|
1186
|
-
assert.match(result.stderr, /stdout fallback notice is emitted only after successful fallback output/);
|
|
1187
|
-
assert.match(result.stderr, /spark model `spark-test-model` unavailable or failed \(exit 17\)/);
|
|
1188
|
-
assert.match(result.stderr, /spark stderr: spark timed out; retry with the frontier fallback/);
|
|
1189
|
-
assert.match(result.stdout, /## OMX Explore fallback/);
|
|
1190
|
-
assert.match(result.stdout, /fallback: model/);
|
|
1191
|
-
assert.match(result.stdout, /from: `spark-test-model`/);
|
|
1192
|
-
assert.match(result.stdout, /to: `gpt-5\.5`/);
|
|
1193
|
-
assert.match(result.stdout, /cost\/behavior may differ from the low-cost spark path/);
|
|
1194
|
-
assert.match(result.stdout, /recovered with fallback model/);
|
|
1195
|
-
assert.match(result.stdout, /MUST: actionable recovery path remained available/);
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
finally {
|
|
1199
|
-
await rm(wd, { recursive: true, force: true });
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
it('reports both failed attempts with codes and final actionable stderr end-to-end', async () => {
|
|
1203
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-explore-fallback-failure-'));
|
|
1204
|
-
try {
|
|
1205
|
-
await withPackagedExploreHarnessHidden(async () => {
|
|
1206
|
-
const harnessStub = await writeExploreHarnessScenarioStub(wd, `
|
|
1207
|
-
printf '[omx explore] fallback-attempt=model from=\`%s\` to=\`gpt-5.5\` reason=spark_attempt_failed exit=23. Cost/behavior boundary changed if fallback succeeds; stdout fallback notice is emitted only after successful fallback output.\n' "\${OMX_EXPLORE_SPARK_MODEL:-spark-test-model}" >&2
|
|
1208
|
-
printf '[omx explore] spark model \`%s\` unavailable or failed (exit 23). Falling back to \`gpt-5.5\`.\n' "\${OMX_EXPLORE_SPARK_MODEL:-spark-test-model}" >&2
|
|
1209
|
-
printf '[omx explore] spark stderr: spark backend unavailable; install the fallback runtime\n' >&2
|
|
1210
|
-
printf '[omx explore] both spark (\`%s\`) and fallback (\`gpt-5.5\`) attempts failed (codes 23 / 29). Last stderr: fallback backend unavailable; set OMX_EXPLORE_BIN to a working harness\n' "\${OMX_EXPLORE_SPARK_MODEL:-spark-test-model}" >&2
|
|
1211
|
-
exit 1
|
|
1212
|
-
`);
|
|
1213
|
-
const result = await runExploreCommandForTest(wd, ['--prompt', 'validate failure guidance'], {
|
|
1214
|
-
OMX_EXPLORE_BIN: harnessStub,
|
|
1215
|
-
OMX_EXPLORE_SPARK_MODEL: 'spark-test-model',
|
|
1216
|
-
});
|
|
1217
|
-
assert.equal(result.exitCode, 1, result.stderr || result.stdout);
|
|
1218
|
-
assert.match(result.stderr, /fallback-attempt=model from=`spark-test-model` to=`gpt-5\.5` reason=spark_attempt_failed exit=23/);
|
|
1219
|
-
assert.match(result.stderr, /stdout fallback notice is emitted only after successful fallback output/);
|
|
1220
|
-
assert.doesNotMatch(result.stderr, /output includes a fallback notice/);
|
|
1221
|
-
assert.doesNotMatch(result.stdout, /## OMX Explore fallback/);
|
|
1222
|
-
assert.match(result.stderr, /spark model `spark-test-model` unavailable or failed \(exit 23\)/);
|
|
1223
|
-
assert.match(result.stderr, /spark stderr: spark backend unavailable; install the fallback runtime/);
|
|
1224
|
-
assert.match(result.stderr, /both spark \(`spark-test-model`\) and fallback \(`gpt-5\.5`\) attempts failed \(codes 23 \/ 29\)\. Last stderr: fallback backend unavailable; set OMX_EXPLORE_BIN to a working harness/);
|
|
1225
|
-
});
|
|
1226
|
-
}
|
|
1227
|
-
finally {
|
|
1228
|
-
await rm(wd, { recursive: true, force: true });
|
|
1229
|
-
}
|
|
1230
|
-
});
|
|
1231
|
-
});
|
|
1232
104
|
//# sourceMappingURL=explore.test.js.map
|