oh-my-codex 0.21.37 → 0.21.39

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.
Files changed (46) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/doctor-warning-copy.test.js +53 -0
  4. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  5. package/dist/cli/__tests__/index.test.js +2 -2
  6. package/dist/cli/__tests__/index.test.js.map +1 -1
  7. package/dist/cli/__tests__/launch-fallback.test.js +55 -4
  8. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  9. package/dist/cli/__tests__/session-search-help.test.js +2 -2
  10. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-install-mode.test.js +122 -0
  12. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-refresh.test.js +2 -2
  14. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  15. package/dist/cli/__tests__/team.test.js +17 -12
  16. package/dist/cli/__tests__/team.test.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +3 -1
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/config/__tests__/models.test.js +2 -1
  21. package/dist/config/__tests__/models.test.js.map +1 -1
  22. package/dist/config/generator.d.ts.map +1 -1
  23. package/dist/config/generator.js +25 -4
  24. package/dist/config/generator.js.map +1 -1
  25. package/dist/config/models.d.ts +1 -1
  26. package/dist/config/models.d.ts.map +1 -1
  27. package/dist/config/models.js +1 -1
  28. package/dist/config/models.js.map +1 -1
  29. package/dist/scripts/__tests__/run-test-files.test.js +192 -2
  30. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  31. package/dist/scripts/run-test-files.js +33 -14
  32. package/dist/scripts/run-test-files.js.map +1 -1
  33. package/dist/team/__tests__/runtime.test.js +32 -4
  34. package/dist/team/__tests__/runtime.test.js.map +1 -1
  35. package/dist/team/__tests__/state-root.test.js +5 -5
  36. package/dist/team/__tests__/state-root.test.js.map +1 -1
  37. package/dist/vscode/__tests__/index.test.js +82 -20
  38. package/dist/vscode/__tests__/index.test.js.map +1 -1
  39. package/dist/vscode/index.d.ts +2 -0
  40. package/dist/vscode/index.d.ts.map +1 -1
  41. package/dist/vscode/index.js +3 -3
  42. package/dist/vscode/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  45. package/src/scripts/__tests__/run-test-files.test.ts +193 -2
  46. package/src/scripts/run-test-files.ts +31 -14
@@ -1,9 +1,11 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, writeFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, resolve } from 'node:path';
5
5
  import { describe, it } from 'node:test';
6
+ import { pathToFileURL } from 'node:url';
6
7
  import assert from 'node:assert/strict';
8
+ import { computeOmxPluginCacheClaimDigest } from '../../cli/plugin-marketplace.js';
7
9
 
8
10
  function runCompiledRunner(root: string, envOverrides: Record<string, string> = {}, timeoutMs = 15_000) {
9
11
  return spawnSync(process.execPath, ['dist/scripts/run-test-files.js', root], {
@@ -18,6 +20,193 @@ function runCompiledRunner(root: string, envOverrides: Record<string, string> =
18
20
  }
19
21
 
20
22
  describe('run-test-files diagnostics', () => {
23
+ it('isolates personal config and plugin caches per file without changing protected inputs or tool access', async () => {
24
+ const wd = mkdtempSync(join(tmpdir(), 'omx-run-test-files-'));
25
+ try {
26
+ const personalHome = realpathSync(mkdtempSync(join(wd, 'personal-home-')));
27
+ const codexHome = join(personalHome, '.codex');
28
+ const pluginCache = join(codexHome, 'plugins', 'cache', 'test-marketplace', 'oh-my-codex', '1.0.0');
29
+ mkdirSync(join(pluginCache, '.codex-plugin'), { recursive: true });
30
+ const protectedFiles = new Map([
31
+ [join(codexHome, '.omx-config.json'), JSON.stringify({ agentReasoning: { explore: 'high' }, agentModels: { explore: 'personal-model' } })],
32
+ [join(pluginCache, '.codex-plugin', 'plugin.json'), JSON.stringify({ name: 'oh-my-codex', version: '1.0.0' })],
33
+ ]);
34
+ for (const [path, content] of protectedFiles) writeFileSync(path, content);
35
+ const markerPath = join(pluginCache, '.omx-complete');
36
+ const marker = `${JSON.stringify({ version: '1.0.0', claimDigest: await computeOmxPluginCacheClaimDigest(pluginCache) })}\n`;
37
+ writeFileSync(markerPath, marker);
38
+ protectedFiles.set(markerPath, marker);
39
+
40
+ const modelsUrl = pathToFileURL(resolve('dist/config/models.js')).href;
41
+ const pathsUrl = pathToFileURL(resolve('dist/utils/paths.js')).href;
42
+ const pluginUrl = pathToFileURL(resolve('dist/cli/plugin-marketplace.js')).href;
43
+ const testsDir = join(wd, '__tests__');
44
+ mkdirSync(testsDir);
45
+ for (const name of ['a', 'b']) {
46
+ writeFileSync(join(testsDir, `${name}.test.js`), [
47
+ "import { test } from 'node:test';",
48
+ "import assert from 'node:assert/strict';",
49
+ "import { writeFileSync, mkdirSync } from 'node:fs';",
50
+ "import { homedir } from 'node:os';",
51
+ "import { join } from 'node:path';",
52
+ "import { spawnSync } from 'node:child_process';",
53
+ `import { getAgentReasoningOverride, getAgentModelOverride } from ${JSON.stringify(modelsUrl)};`,
54
+ `import { codexHome } from ${JSON.stringify(pathsUrl)};`,
55
+ `import { discoverOmxPluginCacheDirs } from ${JSON.stringify(pluginUrl)};`,
56
+ "test('home is isolated before imports and remains overridable by fixtures', async () => {",
57
+ ` const personalCodexHome = ${JSON.stringify(codexHome)};`,
58
+ " assert.equal(getAgentReasoningOverride('explore', personalCodexHome), 'high');",
59
+ " assert.equal(getAgentModelOverride('explore', personalCodexHome), 'personal-model');",
60
+ ` assert.deepEqual(await discoverOmxPluginCacheDirs(personalCodexHome), [${JSON.stringify(pluginCache)}]);`,
61
+ ` assert.notEqual(homedir(), ${JSON.stringify(personalHome)});`,
62
+ " assert.equal(process.env.HOME, homedir());",
63
+ " assert.equal(process.env.USERPROFILE, homedir());",
64
+ " assert.equal(getAgentReasoningOverride('explore'), undefined);",
65
+ " assert.equal(getAgentModelOverride('explore'), undefined);",
66
+ " assert.deepEqual(await discoverOmxPluginCacheDirs(codexHome()), []);",
67
+ ` assert.equal(process.env.PATH, ${JSON.stringify(process.env.PATH)});`,
68
+ ` assert.equal(process.env.TMPDIR, ${JSON.stringify(process.env.TMPDIR)});`,
69
+ ` const tool = spawnSync(process.execPath, ['-e', ${JSON.stringify("process.stdout.write('tool reachable')")}], { encoding: 'utf8' });`,
70
+ " assert.equal(tool.status, 0, tool.stderr);",
71
+ " assert.equal(tool.stdout, 'tool reachable');",
72
+ ` writeFileSync(${JSON.stringify(join(wd, `${name}-home.txt`))}, homedir());`,
73
+ " mkdirSync(codexHome(), { recursive: true });",
74
+ " writeFileSync(join(codexHome(), '.omx-config.json'), JSON.stringify({ agentReasoning: { explore: 'low' } }));",
75
+ " assert.equal(getAgentReasoningOverride('explore'), 'low');",
76
+ ` process.env.HOME = ${JSON.stringify(personalHome)};`,
77
+ ` process.env.USERPROFILE = ${JSON.stringify(personalHome)};`,
78
+ " assert.equal(getAgentReasoningOverride('explore'), 'high');",
79
+ "});",
80
+ '',
81
+ ].join('\n'));
82
+ }
83
+
84
+ for (const forceExit of ['0', '1']) {
85
+ const result = runCompiledRunner(testsDir, { HOME: personalHome, USERPROFILE: personalHome, OMX_NODE_TEST_FORCE_EXIT: forceExit });
86
+ assert.equal(result.status, 0, `${result.stderr}\n${result.stdout}`);
87
+ const homes = ['a', 'b'].map((name) => readFileSync(join(wd, `${name}-home.txt`), 'utf8'));
88
+ assert.notEqual(homes[0], homes[1]);
89
+ for (const home of homes) assert.equal(existsSync(home), false, 'completed test homes must be removed');
90
+ for (const [path, content] of protectedFiles) assert.equal(readFileSync(path, 'utf8'), content);
91
+ }
92
+ } finally {
93
+ rmSync(wd, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ it('removes isolated homes after failing test files in both runner modes', () => {
98
+ const wd = mkdtempSync(join(tmpdir(), 'omx-run-test-files-'));
99
+ try {
100
+ const testPath = join(wd, 'failure.test.js');
101
+ const homeRecord = join(wd, 'home.txt');
102
+ writeFileSync(testPath, [
103
+ "import { test } from 'node:test';",
104
+ "import assert from 'node:assert/strict';",
105
+ "import { writeFileSync } from 'node:fs';",
106
+ "import { homedir } from 'node:os';",
107
+ `writeFileSync(${JSON.stringify(homeRecord)}, homedir());`,
108
+ "test('fails', () => assert.fail('expected fixture failure'));",
109
+ '',
110
+ ].join('\n'));
111
+ for (const forceExit of ['0', '1']) {
112
+ const result = runCompiledRunner(testPath, { OMX_NODE_TEST_FORCE_EXIT: forceExit });
113
+ assert.notEqual(result.status, 0, `${result.stderr}\n${result.stdout}`);
114
+ assert.match(result.stdout, /expected fixture failure/);
115
+ assert.equal(existsSync(readFileSync(homeRecord, 'utf8')), false);
116
+ }
117
+ } finally {
118
+ rmSync(wd, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ it('keeps the isolated home until a force-terminated child closes', () => {
123
+ const wd = mkdtempSync(join(tmpdir(), 'omx-run-test-files-'));
124
+ try {
125
+ const recordPath = join(wd, 'child-close.json');
126
+ const preloadPath = join(wd, 'observe-close.mjs');
127
+ writeFileSync(preloadPath, [
128
+ "import childProcess from 'node:child_process';",
129
+ "import { existsSync, writeFileSync } from 'node:fs';",
130
+ "import { syncBuiltinESMExports } from 'node:module';",
131
+ 'const originalSpawn = childProcess.spawn;',
132
+ 'childProcess.spawn = (...args) => {',
133
+ ' const child = originalSpawn(...args);',
134
+ ' const home = args[2].env.HOME;',
135
+ ' child.once(\'close\', () => {',
136
+ ` writeFileSync(${JSON.stringify(recordPath)}, JSON.stringify({ home, exists: existsSync(home) }));`,
137
+ ' });',
138
+ ' return child;',
139
+ '};',
140
+ 'syncBuiltinESMExports();',
141
+ '',
142
+ ].join('\n'));
143
+ const testPath = join(wd, 'exit-block.test.js');
144
+ writeFileSync(testPath, [
145
+ "import { test } from 'node:test';",
146
+ "test('passes but cannot exit normally', () => {",
147
+ " process.on('SIGTERM', () => {});",
148
+ " process.on('exit', () => { while (true) {} });",
149
+ '});',
150
+ '',
151
+ ].join('\n'));
152
+ const result = spawnSync(process.execPath, ['--import', pathToFileURL(preloadPath).href, 'dist/scripts/run-test-files.js', testPath], {
153
+ encoding: 'utf8',
154
+ env: {
155
+ ...process.env,
156
+ OMX_NODE_TEST_FORCE_EXIT: '1',
157
+ OMX_NODE_TEST_FORCE_EXIT_GRACE_MS: '100',
158
+ OMX_NODE_TEST_RUNNER_TIMEOUT_MS: '2000',
159
+ },
160
+ timeout: 5_000,
161
+ });
162
+
163
+ assert.equal(result.status, 0, `${result.stderr}\n${result.stdout}`);
164
+ assert.match(result.stderr, /force-exit completion grace elapsed/);
165
+ assert.equal(existsSync(recordPath), true, 'runner must observe child close before exiting');
166
+ const record = JSON.parse(readFileSync(recordPath, 'utf8')) as { home: string; exists: boolean };
167
+ assert.equal(record.exists, true, 'isolated home must remain available until child close');
168
+ assert.equal(existsSync(record.home), false, 'isolated home must be removed after child close');
169
+ } finally {
170
+ rmSync(wd, { recursive: true, force: true });
171
+ }
172
+ });
173
+
174
+ it('can preserve the caller home for explicit diagnostics independently of runtime env', () => {
175
+ const wd = mkdtempSync(join(tmpdir(), 'omx-run-test-files-'));
176
+ try {
177
+ const home = join(wd, 'personal-home');
178
+ mkdirSync(home);
179
+ const markerPath = join(home, 'protected-input.txt');
180
+ writeFileSync(markerPath, 'unchanged input\n');
181
+ const testPath = join(wd, 'home-preserve.test.js');
182
+ writeFileSync(testPath, [
183
+ "import { test } from 'node:test';",
184
+ "import assert from 'node:assert/strict';",
185
+ "import { homedir } from 'node:os';",
186
+ "test('home diagnostic opt-in does not preserve runtime env', () => {",
187
+ ` assert.equal(homedir(), ${JSON.stringify(home)});`,
188
+ ` assert.equal(process.env.HOME, ${JSON.stringify(home)});`,
189
+ ` assert.equal(process.env.USERPROFILE, ${JSON.stringify(home)});`,
190
+ " assert.equal(process.env.CODEX_HOME, undefined);",
191
+ " assert.equal(process.env.OMX_ROOT, undefined);",
192
+ "});",
193
+ '',
194
+ ].join('\n'));
195
+
196
+ const result = runCompiledRunner(testPath, {
197
+ HOME: home,
198
+ USERPROFILE: home,
199
+ OMX_NODE_TEST_PRESERVE_HOME: '1',
200
+ CODEX_HOME: '/tmp/live-codex-home',
201
+ OMX_ROOT: '/tmp/live-omx-root',
202
+ });
203
+ assert.equal(result.status, 0, `${result.stderr}\n${result.stdout}`);
204
+ assert.equal(readFileSync(markerPath, 'utf8'), 'unchanged input\n');
205
+ } finally {
206
+ rmSync(wd, { recursive: true, force: true });
207
+ }
208
+ });
209
+
21
210
  it('applies a bounded node --test timeout so hanging tests fail with file context', () => {
22
211
  const wd = mkdtempSync(join(tmpdir(), 'omx-run-test-files-'));
23
212
  try {
@@ -398,6 +587,7 @@ describe('run-test-files diagnostics', () => {
398
587
  "import assert from 'node:assert/strict';",
399
588
  "test('runner env allowlist is narrow', () => {",
400
589
  " assert.equal(process.env.OMX_EXPLORE_BIN, '/tmp/fake-explore');",
590
+ " assert.equal(process.env.OMX_RUNTIME_BINARY, '/tmp/fake-runtime');",
401
591
  " assert.equal(process.env.OMX_NODE_TEST_CONCURRENCY, '1');",
402
592
  " assert.equal(process.env.OMX_ROOT, undefined);",
403
593
  " assert.equal(process.env.CODEX_HOME, undefined);",
@@ -408,6 +598,7 @@ describe('run-test-files diagnostics', () => {
408
598
 
409
599
  const result = runCompiledRunner(wd, {
410
600
  OMX_EXPLORE_BIN: '/tmp/fake-explore',
601
+ OMX_RUNTIME_BINARY: '/tmp/fake-runtime',
411
602
  OMX_NODE_TEST_CONCURRENCY: '1',
412
603
  OMX_ROOT: '/tmp/live-omx-root',
413
604
  CODEX_HOME: '/tmp/live-codex-home',
@@ -1,5 +1,6 @@
1
1
  import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
2
- import { readdirSync, statSync } from 'node:fs';
2
+ import { mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
4
5
 
5
6
  const DEFAULT_TEST_TIMEOUT_MS = 0;
@@ -13,6 +14,7 @@ const PRESERVED_TEST_ENV_KEYS = new Set([
13
14
  'OMX_NODE_TEST_CONCURRENCY',
14
15
  'OMX_NODE_TEST_FORCE_EXIT',
15
16
  'OMX_NODE_TEST_FORCE_EXIT_GRACE_MS',
17
+ 'OMX_NODE_TEST_PRESERVE_HOME',
16
18
  'OMX_NODE_TEST_PRESERVE_RUNTIME_ENV',
17
19
  'OMX_NODE_TEST_RUNNER_TIMEOUT_MS',
18
20
  'OMX_NODE_TEST_TIMEOUT_MS',
@@ -123,6 +125,7 @@ const runnerTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_RUNNER_TIMEOUT_
123
125
  const forceExitGraceMs = parseTimeoutMs(process.env.OMX_NODE_TEST_FORCE_EXIT_GRACE_MS, DEFAULT_FORCE_EXIT_GRACE_MS);
124
126
  const testConcurrency = parseTestConcurrency(process.env);
125
127
  const forceExit = parseBooleanEnv(process.env.OMX_NODE_TEST_FORCE_EXIT);
128
+ const preserveHome = parseBooleanEnv(process.env.OMX_NODE_TEST_PRESERVE_HOME);
126
129
  const sharedTestArgs = ['--test'];
127
130
  if (testTimeoutMs > 0) {
128
131
  sharedTestArgs.push(`--test-timeout=${testTimeoutMs}`);
@@ -139,7 +142,7 @@ console.error(
139
142
  testTimeoutMs > 0 ? ` with per-test timeout ${testTimeoutMs}ms` : ' with per-test timeout disabled'
140
143
  }${testConcurrency ? `, test concurrency ${testConcurrency}` : ', default test concurrency'}${
141
144
  forceExit ? `, force exit enabled with ${forceExitGraceMs}ms completion grace` : ', force exit disabled'
142
- }${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}, with per-file process isolation`,
145
+ }${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}, with per-file process isolation and ${preserveHome ? 'caller home preserved' : 'isolated homes'}`,
143
146
  );
144
147
 
145
148
  const childEnv = buildChildEnv(process.env);
@@ -179,7 +182,7 @@ function terminateChild(child: ChildProcess): void {
179
182
  signalChild(child, 'SIGKILL');
180
183
  }
181
184
 
182
- function runFileWithCompletionForceExit(file: string): Promise<TestRunResult> {
185
+ function runFileWithCompletionForceExit(file: string, env: NodeJS.ProcessEnv): Promise<TestRunResult> {
183
186
  return new Promise((resolveRun) => {
184
187
  let finished = false;
185
188
  let sawFailure = false;
@@ -196,7 +199,7 @@ function runFileWithCompletionForceExit(file: string): Promise<TestRunResult> {
196
199
 
197
200
  const child = spawn(process.execPath, [...sharedTestArgs, file], {
198
201
  stdio: ['ignore', 'pipe', 'pipe'],
199
- env: childEnv,
202
+ env,
200
203
  detached: !isWindows(),
201
204
  });
202
205
 
@@ -206,18 +209,22 @@ function runFileWithCompletionForceExit(file: string): Promise<TestRunResult> {
206
209
  if (completionTimer) clearTimeout(completionTimer);
207
210
  if (runnerTimer) clearTimeout(runnerTimer);
208
211
  console.error(`[run-test-files] ${file}: ${reason}; status ${status ?? 'unknown'}`);
212
+ // Signals start termination; close confirms this child and its stdio are
213
+ // finished before main removes the file's isolated home.
214
+ child.once('close', () => {
215
+ child.unref();
216
+ resolveRun({
217
+ status,
218
+ signal,
219
+ timedOut: reason.startsWith('runner timeout'),
220
+ abnormal: status === null || signal !== null || reason.startsWith('node --test failed to spawn'),
221
+ });
222
+ });
209
223
  if (terminate) {
210
224
  terminateChild(child);
211
225
  }
212
226
  child.stdout?.destroy();
213
227
  child.stderr?.destroy();
214
- child.unref();
215
- resolveRun({
216
- status,
217
- signal,
218
- timedOut: reason.startsWith('runner timeout'),
219
- abnormal: status === null || signal !== null || reason.startsWith('node --test failed to spawn'),
220
- });
221
228
  }
222
229
 
223
230
  function markFailure(): void {
@@ -323,10 +330,10 @@ function runFileWithCompletionForceExit(file: string): Promise<TestRunResult> {
323
330
  });
324
331
  }
325
332
 
326
- function runFileSync(file: string): TestRunResult {
333
+ function runFileSync(file: string, env: NodeJS.ProcessEnv): TestRunResult {
327
334
  const result = spawnSync(process.execPath, [...sharedTestArgs, file], {
328
335
  stdio: 'inherit',
329
- env: childEnv,
336
+ env,
330
337
  timeout: runnerTimeoutMs > 0 ? runnerTimeoutMs : undefined,
331
338
  killSignal: 'SIGTERM',
332
339
  });
@@ -351,7 +358,17 @@ async function main(): Promise<void> {
351
358
  let runnerTimedOut = false;
352
359
 
353
360
  for (const file of files) {
354
- const result = forceExit ? await runFileWithCompletionForceExit(file) : runFileSync(file);
361
+ // Runtime env scrubbing does not isolate personal config or plugin caches.
362
+ // Keep tool paths and temporary-directory settings, but start each file with
363
+ // an empty home unless the caller explicitly opts into home diagnostics.
364
+ const home = preserveHome ? undefined : mkdtempSync(join(tmpdir(), 'omx-node-test-home-'));
365
+ const env = home ? { ...childEnv, HOME: home, USERPROFILE: home } : childEnv;
366
+ let result: TestRunResult;
367
+ try {
368
+ result = forceExit ? await runFileWithCompletionForceExit(file, env) : runFileSync(file, env);
369
+ } finally {
370
+ if (home) rmSync(home, { recursive: true, force: true });
371
+ }
355
372
 
356
373
  if (result.status === 0) {
357
374
  continue;