moving
This commit is contained in:
@@ -76,8 +76,15 @@ export const codexAdapter: EventAdapter = {
|
||||
extractParent(payload): string | undefined {
|
||||
return pickStringField(payload, 'parent_frame_id', 'prompt_frame_id');
|
||||
},
|
||||
// formatInject omitted ⇒ the shared SessionStart body uses CC's default
|
||||
// hookSpecificOutput shape (codex honors the CC inject convention).
|
||||
|
||||
formatInject(additionalContext): unknown {
|
||||
return {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
isDirectExecution,
|
||||
makePreCompactHandler,
|
||||
runHook,
|
||||
type HookRunOptions,
|
||||
@@ -20,15 +21,6 @@ export async function runPreCompact(opts: Partial<HookRunOptions> = {}): Promise
|
||||
});
|
||||
}
|
||||
|
||||
const isMain = (() => {
|
||||
try {
|
||||
if (typeof process.argv[1] !== 'string') return false;
|
||||
const url = new URL(`file://${process.argv[1].replace(/\\/g, '/')}`);
|
||||
return url.href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (isMain) {
|
||||
if (isDirectExecution(import.meta.url)) {
|
||||
void runPreCompact();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
isDirectExecution,
|
||||
makeSessionStartHandler,
|
||||
runHook,
|
||||
type HookRunOptions,
|
||||
@@ -23,15 +24,6 @@ export async function runSessionStart(opts: Partial<HookRunOptions> = {}): Promi
|
||||
});
|
||||
}
|
||||
|
||||
const isMain = (() => {
|
||||
try {
|
||||
if (typeof process.argv[1] !== 'string') return false;
|
||||
const url = new URL(`file://${process.argv[1].replace(/\\/g, '/')}`);
|
||||
return url.href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (isMain) {
|
||||
if (isDirectExecution(import.meta.url)) {
|
||||
void runSessionStart();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
isDirectExecution,
|
||||
makeStopHandler,
|
||||
runHook,
|
||||
type HookRunOptions,
|
||||
@@ -21,15 +22,6 @@ export async function runStop(opts: Partial<HookRunOptions> = {}): Promise<void>
|
||||
});
|
||||
}
|
||||
|
||||
const isMain = (() => {
|
||||
try {
|
||||
if (typeof process.argv[1] !== 'string') return false;
|
||||
const url = new URL(`file://${process.argv[1].replace(/\\/g, '/')}`);
|
||||
return url.href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (isMain) {
|
||||
if (isDirectExecution(import.meta.url)) {
|
||||
void runStop();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
isDirectExecution,
|
||||
makeUserPromptSubmitHandler,
|
||||
runHook,
|
||||
type HookRunOptions,
|
||||
@@ -19,15 +20,6 @@ export async function runUserPromptSubmit(opts: Partial<HookRunOptions> = {}): P
|
||||
});
|
||||
}
|
||||
|
||||
const isMain = (() => {
|
||||
try {
|
||||
if (typeof process.argv[1] !== 'string') return false;
|
||||
const url = new URL(`file://${process.argv[1].replace(/\\/g, '/')}`);
|
||||
return url.href === import.meta.url;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (isMain) {
|
||||
if (isDirectExecution(import.meta.url)) {
|
||||
void runUserPromptSubmit();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { dirname } from 'node:path';
|
||||
import { createLogger, type Logger } from '@waggle/hive-mind-shim-core';
|
||||
import {
|
||||
backupByteIdentical,
|
||||
hookCommandFor,
|
||||
hookScriptPath,
|
||||
jsonRegister,
|
||||
normalizeCliPath,
|
||||
@@ -34,7 +33,13 @@ import {
|
||||
type JsonRegisterEntry,
|
||||
type Lifecycle,
|
||||
} from '@waggle/hive-mind-hooks-core';
|
||||
import { resolvePaths, allHookBasenames, type CodexPaths, type ResolvePathsOptions } from './paths.js';
|
||||
import {
|
||||
resolvePaths,
|
||||
allHookBasenames,
|
||||
hookCommandFor,
|
||||
type CodexPaths,
|
||||
type ResolvePathsOptions,
|
||||
} from './paths.js';
|
||||
import { codexRegisterSpec } from './adapter.js';
|
||||
|
||||
export interface InstallResult {
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
* Codex's standalone `~/.codex/hooks.json` (NOT `~/.codex/config.toml` —
|
||||
* we stay out of the user's TOML and away from protected
|
||||
* `notify`/`profile`/`model_providers` keys). The Windows-safe backup
|
||||
* path + `--cli-path` quoting + hooks-dir resolution are reused verbatim
|
||||
* from `@waggle/hive-mind-hooks-core` so codex reads like the reference.
|
||||
* path + hooks-dir resolution reuse `@waggle/hive-mind-hooks-core`. Codex
|
||||
* needs its own Windows command encoder because its Rust runtime passes the
|
||||
* whole handler string through the selected host shell (PowerShell for a
|
||||
* normal local thread, with `cmd.exe /C` as the fallback).
|
||||
*/
|
||||
|
||||
import { homedir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { join, resolve, win32 } from 'node:path';
|
||||
import {
|
||||
backupPathFor,
|
||||
hookCommandFor,
|
||||
hookCommandFor as sharedHookCommandFor,
|
||||
hooksDirFromModuleUrl,
|
||||
} from '@waggle/hive-mind-hooks-core';
|
||||
|
||||
@@ -69,5 +71,77 @@ export function resolvePaths(opts: ResolvePathsOptions = {}): CodexPaths {
|
||||
return { codexDir, configPath, pointerPath, hooksDir };
|
||||
}
|
||||
|
||||
/** Re-export the shared Windows-safe helpers so codex modules read like CC. */
|
||||
export { backupPathFor, hookCommandFor };
|
||||
export interface HookCommandOptions {
|
||||
/** Explicit seam for cross-platform command-shape tests. */
|
||||
platform?: NodeJS.Platform;
|
||||
/** Explicit Windows runtime seam; production normally uses the launcher env override. */
|
||||
nodePath?: string;
|
||||
/** Explicit Windows root seam; production reads the OS-provided SystemRoot. */
|
||||
systemRoot?: string;
|
||||
}
|
||||
|
||||
function powershellLiteral(value: string): string {
|
||||
return `'${value.replaceAll("'", "''")}'`;
|
||||
}
|
||||
|
||||
function windowsPowerShellPath(systemRoot?: string): string {
|
||||
const root = win32.normalize(
|
||||
systemRoot?.trim()
|
||||
|| process.env.SystemRoot?.trim()
|
||||
|| 'C:\\Windows',
|
||||
);
|
||||
if (!/^[A-Za-z]:\\[A-Za-z0-9._\\-]+$/.test(root)) {
|
||||
throw new Error('Windows SystemRoot must be an absolute shell-safe path');
|
||||
}
|
||||
return win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
||||
}
|
||||
|
||||
function windowsHookCommand(
|
||||
nodePath: string,
|
||||
scriptPath: string,
|
||||
cliPath?: string,
|
||||
systemRoot?: string,
|
||||
): string {
|
||||
const args = [nodePath, scriptPath];
|
||||
if (cliPath && cliPath.length > 0) args.push('--cli-path', cliPath);
|
||||
|
||||
const script = [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
'try {',
|
||||
` & ${args.map(powershellLiteral).join(' ')}`,
|
||||
' if ($null -eq $LASTEXITCODE) { exit 1 }',
|
||||
' exit $LASTEXITCODE',
|
||||
'} catch {',
|
||||
' [Console]::Error.WriteLine($_.Exception.Message)',
|
||||
' exit 1',
|
||||
'}',
|
||||
].join('\r\n');
|
||||
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
||||
return `${windowsPowerShellPath(systemRoot)} -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Codex command that survives the host's exact shell dispatch.
|
||||
* POSIX keeps the shared `node "script" --cli-path "cli"` contract. On
|
||||
* Windows the trusted invocation is UTF-16LE encoded inside PowerShell so
|
||||
* both Codex's PowerShell host and its `cmd.exe /C` fallback can parse the
|
||||
* same command. The system PowerShell path is resolved to a validated
|
||||
* absolute literal so an untrusted workspace cannot win command lookup with
|
||||
* a repo-local `powershell.exe`.
|
||||
*/
|
||||
export function hookCommandFor(
|
||||
scriptPath: string,
|
||||
cliPath?: string,
|
||||
opts: HookCommandOptions = {},
|
||||
): string {
|
||||
if ((opts.platform ?? process.platform) !== 'win32') {
|
||||
return sharedHookCommandFor(scriptPath, cliPath);
|
||||
}
|
||||
|
||||
const nodePath = opts.nodePath?.trim()
|
||||
|| process.env.WAGGLE_HOOK_NODE_PATH?.trim()
|
||||
|| process.execPath;
|
||||
return windowsHookCommand(nodePath, scriptPath, cliPath, opts.systemRoot);
|
||||
}
|
||||
|
||||
export { backupPathFor };
|
||||
|
||||
@@ -26,12 +26,18 @@ describe('codex session-start handler', () => {
|
||||
expect(bridge.recallMemory).toHaveBeenCalledWith('', { limit: 1, scope: 'personal', workspace: null });
|
||||
expect(cap.stdout).toHaveLength(1);
|
||||
const parsed = JSON.parse(cap.stdout[0]) as {
|
||||
hookSpecificOutput: { source: string; additionalContext: string };
|
||||
hookSpecificOutput: { hookEventName: string; additionalContext: string };
|
||||
};
|
||||
// Codex has no custom formatInject ⇒ the default CC hookSpecificOutput shape,
|
||||
// stamped with source 'codex'.
|
||||
expect(parsed.hookSpecificOutput.source).toBe('codex');
|
||||
expect(parsed.hookSpecificOutput.additionalContext).toContain('past observation');
|
||||
expect(parsed).toEqual({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: expect.stringContaining('past observation'),
|
||||
},
|
||||
});
|
||||
expect(Object.keys(parsed.hookSpecificOutput).sort()).toEqual([
|
||||
'additionalContext',
|
||||
'hookEventName',
|
||||
]);
|
||||
expect(cap.exits).toEqual([0]);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ import { HIVE_MIND_MARKER } from '../src/adapter.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function decodedHookCommand(command: string): string {
|
||||
const match = /^[A-Za-z]:\\[A-Za-z0-9._\\-]+\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/=]+)$/.exec(command);
|
||||
return match ? Buffer.from(match[1], 'base64').toString('utf16le') : command;
|
||||
}
|
||||
|
||||
// This file's last test spawns the COMPILED CLI (dist/bin/codex-hooks.js).
|
||||
// dist/ is gitignored and a clean CI checkout runs no build step, so the
|
||||
// artifact is absent there — skip (don't fail) when it's missing. The other
|
||||
@@ -153,8 +158,18 @@ describe('install (codex)', () => {
|
||||
const after = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
|
||||
hooks: Record<string, Array<{ hooks: Array<{ command: string }> }>>;
|
||||
};
|
||||
expect(after.hooks.SessionStart[0].hooks[0].command).toContain(`--cli-path "${cliPath}"`);
|
||||
expect(after.hooks.Stop[0].hooks[0].command).toContain(`--cli-path "${cliPath}"`);
|
||||
for (const command of [
|
||||
after.hooks.SessionStart[0].hooks[0].command,
|
||||
after.hooks.Stop[0].hooks[0].command,
|
||||
]) {
|
||||
if (process.platform === 'win32') {
|
||||
expect(command).toMatch(/^[A-Za-z]:\\[A-Za-z0-9._\\-]+\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe .* -EncodedCommand [A-Za-z0-9+/=]+$/);
|
||||
expect(command).not.toContain('"');
|
||||
}
|
||||
const payload = decodedHookCommand(command);
|
||||
expect(payload).toContain('--cli-path');
|
||||
expect(payload).toContain(cliPath);
|
||||
}
|
||||
|
||||
const pointer = JSON.parse(await readFile(result.pointerPath, 'utf-8')) as Record<string, unknown>;
|
||||
expect(pointer['cli_path']).toBe(cliPath);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { copyFile, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { join, resolve } from 'node:path';
|
||||
import {
|
||||
@@ -33,26 +36,193 @@ describe('resolvePaths (codex)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hookCommandFor (codex, 2-arg shared helper)', () => {
|
||||
describe('hookCommandFor (codex)', () => {
|
||||
it('produces a quoted node invocation around the absolute script path', () => {
|
||||
const cmd = hookCommandFor(resolve('/abs/dist/hooks/session-start.js'));
|
||||
const cmd = hookCommandFor(resolve('/abs/dist/hooks/session-start.js'), undefined, { platform: 'linux' });
|
||||
expect(cmd).toMatch(/^node "[^"]+session-start\.js"$/);
|
||||
});
|
||||
|
||||
it('appends --cli-path when supplied', () => {
|
||||
const cmd = hookCommandFor(resolve('/abs/dist/hooks/session-start.js'), '/abs/cli/dist/index.js');
|
||||
const cmd = hookCommandFor(
|
||||
resolve('/abs/dist/hooks/session-start.js'),
|
||||
'/abs/cli/dist/index.js',
|
||||
{ platform: 'linux' },
|
||||
);
|
||||
expect(cmd).toMatch(/--cli-path "\/abs\/cli\/dist\/index\.js"$/);
|
||||
});
|
||||
|
||||
it('omits --cli-path when empty string is passed', () => {
|
||||
const cmd = hookCommandFor(resolve('/abs/dist/hooks/session-start.js'), '');
|
||||
const cmd = hookCommandFor(resolve('/abs/dist/hooks/session-start.js'), '', { platform: 'linux' });
|
||||
expect(cmd).not.toContain('--cli-path');
|
||||
});
|
||||
|
||||
it('preserves Windows-style paths (with spaces) inside the quotes', () => {
|
||||
const cmd = hookCommandFor('/abs/dist/hooks/stop.js', 'C:\\Program Files\\hive-mind\\dist\\index.js');
|
||||
expect(cmd).toContain('--cli-path "C:\\Program Files\\hive-mind\\dist\\index.js"');
|
||||
it('rejects a Windows root unsafe to embed in either host shell', () => {
|
||||
expect(() => hookCommandFor('C:\\hooks\\session-start.js', undefined, {
|
||||
platform: 'win32',
|
||||
nodePath: 'C:\\runtime\\node.exe',
|
||||
systemRoot: 'C:\\Windows & attacker',
|
||||
})).toThrow(/SystemRoot/);
|
||||
});
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'survives Codex Rust cmd.exe /C dispatch with spaces and apostrophes',
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "codex hook O'Brien "));
|
||||
try {
|
||||
const stdinMarker = 'CODEX_STDIN_čćžšđ_漢_🐝';
|
||||
const stdoutMarker = 'CODEX_STDOUT_čćžšđ_漢_🐝';
|
||||
const stderrMarker = 'CODEX_STDERR_čćžšđ_漢_🐝';
|
||||
const fixtureDir = join(root, "fixture dir O'Brien");
|
||||
const scriptPath = join(fixtureDir, "hook O'Brien.mjs");
|
||||
const cliPath = join(root, "cli dir O'Brien", 'index.js');
|
||||
const nodePath = join(root, "node runtime O'Brien.exe");
|
||||
const decoyPowerShell = join(root, 'powershell.exe');
|
||||
const rustSource = join(root, 'codex-runner.rs');
|
||||
const rustExe = join(root, 'codex-runner.exe');
|
||||
await mkdir(fixtureDir, { recursive: true });
|
||||
await copyFile(process.execPath, nodePath);
|
||||
await writeFile(decoyPowerShell, 'DECOY_WORKSPACE_POWERSHELL', 'utf8');
|
||||
await writeFile(scriptPath, [
|
||||
"let stdin = '';",
|
||||
`const stdoutMarker = ${JSON.stringify(stdoutMarker)};`,
|
||||
"process.stdin.setEncoding('utf8');",
|
||||
"for await (const chunk of process.stdin) stdin += chunk;",
|
||||
"process.stdout.write(JSON.stringify({ stdin, argv: process.argv.slice(2), stdoutMarker }));",
|
||||
`process.stderr.write(${JSON.stringify(stderrMarker)});`,
|
||||
'process.exitCode = 23;',
|
||||
'',
|
||||
].join('\n'), 'utf8');
|
||||
await writeFile(rustSource, String.raw`
|
||||
use std::env;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
fn main() {
|
||||
let command = env::var("CODEX_TEST_COMMAND").expect("CODEX_TEST_COMMAND");
|
||||
let comspec = env::var("ComSpec").unwrap_or_else(|_| "cmd.exe".to_string());
|
||||
let mut input = Vec::new();
|
||||
io::stdin().read_to_end(&mut input).expect("read stdin");
|
||||
let mut child = Command::new(comspec)
|
||||
.arg("/C")
|
||||
.arg(command)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn cmd.exe");
|
||||
child.stdin.take().expect("child stdin").write_all(&input).expect("forward stdin");
|
||||
let output = child.wait_with_output().expect("wait for cmd.exe");
|
||||
io::stdout().write_all(&output.stdout).expect("forward stdout");
|
||||
io::stderr().write_all(&output.stderr).expect("forward stderr");
|
||||
std::process::exit(output.status.code().unwrap_or(1));
|
||||
}
|
||||
`, 'utf8');
|
||||
|
||||
const compile = spawnSync('rustc', [rustSource, '-o', rustExe], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
});
|
||||
expect(compile.status, compile.stderr).toBe(0);
|
||||
|
||||
const command = hookCommandFor(scriptPath, cliPath, {
|
||||
platform: 'win32',
|
||||
nodePath,
|
||||
systemRoot: 'C:\\Windows',
|
||||
});
|
||||
expect(command).toMatch(/^C:\\Windows\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand [A-Za-z0-9+/=]+$/);
|
||||
expect(command).not.toContain('"');
|
||||
const payload = Buffer.from(command.split(' ').at(-1) as string, 'base64').toString('utf16le');
|
||||
expect(payload).toContain(`& '${nodePath.replaceAll("'", "''")}'`);
|
||||
|
||||
const run = spawnSync(rustExe, [], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_TEST_COMMAND: command },
|
||||
input: stdinMarker,
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
});
|
||||
expect(run.status).toBe(23);
|
||||
expect(JSON.parse(run.stdout)).toEqual({
|
||||
stdin: stdinMarker,
|
||||
argv: ['--cli-path', cliPath],
|
||||
stdoutMarker,
|
||||
});
|
||||
expect(run.stderr).toBe(stderrMarker);
|
||||
|
||||
const missingCommand = hookCommandFor(scriptPath, cliPath, {
|
||||
platform: 'win32',
|
||||
nodePath: join(root, 'missing-node.exe'),
|
||||
systemRoot: 'C:\\Windows',
|
||||
});
|
||||
const missingRun = spawnSync(rustExe, [], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CODEX_TEST_COMMAND: missingCommand },
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
});
|
||||
expect(missingRun.status, missingRun.stderr).not.toBe(0);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'survives Codex PowerShell host dispatch',
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'codex-powershell-host-'));
|
||||
try {
|
||||
const scriptPath = join(root, 'session-start.mjs');
|
||||
const decoyPowerShell = join(root, 'powershell.exe');
|
||||
await writeFile(scriptPath, 'process.stdin.pipe(process.stdout);\n', 'utf8');
|
||||
await writeFile(decoyPowerShell, 'DECOY_WORKSPACE_POWERSHELL', 'utf8');
|
||||
|
||||
const command = hookCommandFor(scriptPath, undefined, {
|
||||
platform: 'win32',
|
||||
nodePath: process.execPath,
|
||||
});
|
||||
const hostPowerShell = join(
|
||||
process.env.SystemRoot ?? 'C:\\Windows',
|
||||
'System32',
|
||||
'WindowsPowerShell',
|
||||
'v1.0',
|
||||
'powershell.exe',
|
||||
);
|
||||
const run = spawnSync(
|
||||
hostPowerShell,
|
||||
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command],
|
||||
{
|
||||
cwd: root,
|
||||
input: 'POWERSHELL_HOST_OK',
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(run.status, run.stderr).toBe(0);
|
||||
expect(run.stdout).toBe('POWERSHELL_HOST_OK');
|
||||
|
||||
const missingCommand = hookCommandFor(scriptPath, undefined, {
|
||||
platform: 'win32',
|
||||
nodePath: join(root, 'missing-node.exe'),
|
||||
});
|
||||
const missingRun = spawnSync(
|
||||
hostPowerShell,
|
||||
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', missingCommand],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
expect(missingRun.status, missingRun.stderr).not.toBe(0);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
15_000,
|
||||
);
|
||||
});
|
||||
|
||||
describe('backupPathFor (codex)', () => {
|
||||
|
||||
@@ -21,7 +21,9 @@ function entry(
|
||||
): JsonRegisterEntry {
|
||||
return {
|
||||
lifecycle,
|
||||
command: hookCommandFor(`${HOOKS_DIR}/${basename}.js`),
|
||||
// Registration-shape tests are platform-neutral; command dispatch itself
|
||||
// is covered by paths.test.ts against Codex's exact Windows Rust runner.
|
||||
command: hookCommandFor(`${HOOKS_DIR}/${basename}.js`, undefined, { platform: 'linux' }),
|
||||
timeout,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user