Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions scripts/run-product-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ async function collectTests(directory, prefix = '') {
return files;
}

// 端末の本物のhost設定の置き場を指す変数(`src/setup-hosts.mjs`が読む)。試験は一時のHOMEを渡すが、
// これらが残っていると、CLIはHOMEでなくそちらを使い、利用者の本物の設定を書き換える。
export const HOST_CONFIG_ENV = Object.freeze(['CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'GROK_HOME']);

export function productTestEnvironment(parentEnv = process.env) {
// product gateはsuite単位ですでに全CPU並列である。各integration fixtureが
// sensor init用WASM poolまで最大8本prewarmするとnested oversubscriptionになり、
Expand All @@ -95,6 +99,7 @@ export function productTestEnvironment(parentEnv = process.env) {
const env = { ...parentEnv, LATTICE_DASHBOARD_AUTOSTART: '0',
LATTICE_SENSOR_PARSE_WORKERS: '1' };
delete env.FORCE_COLOR;
for (const name of HOST_CONFIG_ENV) delete env[name];
return env;
}

Expand Down
39 changes: 38 additions & 1 deletion test/hooks-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import nodeTest, { after, before } from 'node:test';
import { HOST_CONFIG_ENV } from '../scripts/run-product-tests.mjs';
import { resolveStableNodePath, runHooksCli } from '../src/hooks-cli.mjs';

const test = process.platform === 'win32' ? nodeTest.skip : nodeTest;
Expand Down Expand Up @@ -79,8 +80,12 @@ after(async () => {

function isolatedEnv({ home = suiteHome, stateHome = suiteState, configHome = suiteConfig,
extraEnv = {} } = {}) {
// 端末の本物のhost設定の置き場を指す変数は渡さない。残っていると、CLIは下のHOMEでなくそちらを使い、
// 利用者の本物の設定を書き換える。
const inherited = { ...process.env };
for (const name of HOST_CONFIG_ENV) delete inherited[name];
const env = {
...process.env,
...inherited,
...extraEnv,
HOME: home,
XDG_STATE_HOME: stateHome,
Expand Down Expand Up @@ -598,6 +603,38 @@ test('P4 F6: pending receipt回復はlock取得後にconfigを再読してcommit
});

// C3-1: host home dir不在はHOST_NOT_PRESENT exit 1で、dirを作らない。
// 試験を起こした端末が`CODEX_HOME`等を持っていても、CLIへ渡すのは試験のHOMEだけである。
// 2026-10-03: `CODEX_HOME`を持つ端末でこの試験を走らせ、共有の本物のCodex hooks.jsonを書き換えた。
test('試験のCLIは、端末のCODEX_HOME・CLAUDE_CONFIG_DIR・GROK_HOMEが指す本物の設定へ触れない', async (t) => {
const decoy = await mkdtemp(path.join(tmpdir(), 'lattice-hooks-decoy-'));
t.after(() => rm(decoy, { recursive: true, force: true }));
const saved = Object.fromEntries(HOST_CONFIG_ENV.map((name) => [name, process.env[name]]));
t.after(() => {
for (const [name, value] of Object.entries(saved)) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
});
// 本物の代わりのフォルダを指させる。中へ設定を置き、試験の後も1 byteも変わっていないことを見る。
const decoyHooks = path.join(decoy, 'hooks.json');
const decoySettings = path.join(decoy, 'settings.json');
await writeFile(decoyHooks, FOREIGN_CODEX_HOOKS, { mode: 0o600 });
await writeFile(decoySettings, '{}\n', { mode: 0o600 });
for (const name of HOST_CONFIG_ENV) process.env[name] = decoy;
const before = [await snapshotFile(decoyHooks), await snapshotFile(decoySettings)];

for (const host of ['codex', 'claude']) {
const fixture = await hooksFixture(t, host, { config: host === 'codex' ? FOREIGN_CODEX_HOOKS : '{}\n' });
assert.equal(runCli(['hooks', 'install', '--host', host], options(fixture)).status, 0, host);
assert.equal(handlers(await readJson(fixture.configPath))
.filter((item) => commandArgv(item.command).includes('hooks')).length, 1, host);
assert.equal(runCli(['hooks', 'uninstall', '--host', host], options(fixture)).status, 0, host);
}

assert.deepEqual([await snapshotFile(decoyHooks), await snapshotFile(decoySettings)], before);
assert.deepEqual((await readdir(decoy)).sort(), ['hooks.json', 'settings.json']);
});

test('P3 C3-1: host home dir不在は HOST_NOT_PRESENT exit 1、設定dirを作らずに終了する', async (t) => {
for (const host of ['claude', 'codex', 'cursor']) {
const fixture = await hooksFixture(t, host, { createHost: false });
Expand Down
10 changes: 10 additions & 0 deletions test/product-test-runner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ test('product test child envはFORCE_COLORを除去しdashboard autostartを無
});
});

test('product test child envは、端末の本物のhost設定を指す変数を渡さない', () => {
// これらが残ると、一時のHOMEを渡した試験のCLIが、利用者の本物のCodex・Claude・Grokの設定を書き換える。
const parentEnv = { PATH: '/fixture/bin', CODEX_HOME: '/real/.codex', CLAUDE_CONFIG_DIR: '/real/.claude',
GROK_HOME: '/real/.grok' };
const childEnv = productTestEnvironment(parentEnv);
for (const name of ['CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'GROK_HOME']) assert.equal(Object.hasOwn(childEnv, name), false, name);
assert.equal(childEnv.PATH, '/fixture/bin');
assert.equal(parentEnv.CODEX_HOME, '/real/.codex');
});

test('環境別profileはfocused再現用のsuiteだけを選びcoreの総当たりを複製しない', () => {
const retired = 'control-compiler.test.mjs';
const all = [
Expand Down
Loading