This commit is contained in:
167
packages/hive-mind-hooks-codex-desktop/tests/parity.test.ts
Normal file
167
packages/hive-mind-hooks-codex-desktop/tests/parity.test.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { describe, expect, it, afterEach } from 'vitest';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
// codex-desktop is a THIN RE-EXPORT — install/uninstall/verify come from the
|
||||
// codex package via `export * from '@waggle/hive-mind-hooks-codex'`. Importing
|
||||
// them through the desktop barrel proves the re-export is wired and that the
|
||||
// SAME installer writes the SAME ~/.codex/hooks.json + pointer as codex.
|
||||
import { install, uninstall } from '../src/index.js';
|
||||
import { install as codexInstall } from '@waggle/hive-mind-hooks-codex';
|
||||
import { HIVE_MIND_MARKER } from '@waggle/hive-mind-hooks-codex';
|
||||
|
||||
interface TestEnv {
|
||||
home: string;
|
||||
hooksDir: string;
|
||||
configPath: string;
|
||||
pointerPath: string;
|
||||
}
|
||||
|
||||
/** Codex Desktop shares ~/.codex/ — same config + pointer paths as the CLI. */
|
||||
async function bootstrap(initial: Record<string, unknown> | undefined): Promise<TestEnv> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'hmcdxd-parity-'));
|
||||
const codexDir = join(home, '.codex');
|
||||
await mkdir(codexDir, { recursive: true });
|
||||
const configPath = join(codexDir, 'hooks.json');
|
||||
if (initial !== undefined) {
|
||||
await writeFile(configPath, JSON.stringify(initial, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
const hooksDir = resolve(home, 'fake-dist', 'hooks');
|
||||
await mkdir(hooksDir, { recursive: true });
|
||||
return { home, hooksDir, configPath, pointerPath: join(codexDir, 'hive-mind-install.json') };
|
||||
}
|
||||
|
||||
function sha256(s: string): string {
|
||||
return createHash('sha256').update(s, 'utf-8').digest('hex');
|
||||
}
|
||||
|
||||
describe('parity (codex-desktop ↔ codex)', () => {
|
||||
let env: TestEnv;
|
||||
|
||||
afterEach(async () => {
|
||||
if (env) await rm(env.home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── shared ~/.codex/ surface ──────────────────────────────────────────
|
||||
|
||||
it('installs into the SAME ~/.codex/hooks.json the Codex CLI uses', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
expect(existsSync(env.configPath)).toBe(false);
|
||||
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
|
||||
// The config + pointer land at the shared ~/.codex/ paths.
|
||||
expect(result.paths.configPath).toBe(env.configPath);
|
||||
expect(result.pointerPath).toBe(env.pointerPath);
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
expect(existsSync(env.pointerPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('writes the hive group (marker) + all four lifecycle events', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
|
||||
const after = JSON.parse(await readFile(env.configPath, 'utf-8')) as {
|
||||
hooks: Record<string, Array<{ _hiveMindShim?: string }>>;
|
||||
};
|
||||
expect(Object.keys(after.hooks).sort()).toEqual(
|
||||
['PreCompact', 'SessionStart', 'Stop', 'UserPromptSubmit'],
|
||||
);
|
||||
// Every group we added carries the codex hive marker.
|
||||
for (const event of Object.keys(after.hooks)) {
|
||||
expect(after.hooks[event][0]._hiveMindShim).toBe(HIVE_MIND_MARKER);
|
||||
}
|
||||
});
|
||||
|
||||
it('writes a pointer carrying the codex installed_hooks list', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
const pointer = JSON.parse(await readFile(result.pointerPath, 'utf-8')) as Record<string, unknown>;
|
||||
expect(pointer['installed_hooks']).toEqual(
|
||||
['session-start', 'user-prompt-submit', 'stop', 'pre-compact'],
|
||||
);
|
||||
expect(pointer['config_path']).toBe(env.configPath);
|
||||
});
|
||||
|
||||
// ── byte-for-byte identical to the codex installer ────────────────────
|
||||
|
||||
it('produces a byte-identical hooks.json to the codex installer (same inputs)', async () => {
|
||||
const initial = {
|
||||
hooks: {
|
||||
SessionStart: [
|
||||
{ matcher: 'startup', hooks: [{ type: 'command', command: 'node /existing/ctx.js' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
const fixedTs = '2026-04-28T10:30:45.123Z';
|
||||
|
||||
// Desktop install into env A.
|
||||
env = await bootstrap(initial);
|
||||
await install({
|
||||
home: env.home,
|
||||
hooksDir: env.hooksDir,
|
||||
now: () => new Date(fixedTs),
|
||||
});
|
||||
const desktopConfig = await readFile(env.configPath, 'utf-8');
|
||||
|
||||
// Codex install into env B with IDENTICAL inputs — same hooksDir so the
|
||||
// embedded hook command strings (which carry the absolute hooksDir path)
|
||||
// match. Only the HOME differs (each install writes to its own ~/.codex/),
|
||||
// and HOME does not appear in the emitted config.
|
||||
const envB = await bootstrap(initial);
|
||||
try {
|
||||
await codexInstall({
|
||||
home: envB.home,
|
||||
hooksDir: env.hooksDir,
|
||||
now: () => new Date(fixedTs),
|
||||
});
|
||||
const codexConfig = await readFile(envB.configPath, 'utf-8');
|
||||
expect(sha256(desktopConfig)).toBe(sha256(codexConfig));
|
||||
} finally {
|
||||
await rm(envB.home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── reversibility: uninstall restores the shared file ─────────────────
|
||||
|
||||
it('uninstall removes the hooks.json we created and leaves NO orphan', async () => {
|
||||
env = await bootstrap(undefined);
|
||||
const result = await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(result.createdByUs).toBe(true);
|
||||
expect(existsSync(env.configPath)).toBe(true);
|
||||
|
||||
const u = await uninstall({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(u.createdRemoved).toBe(true);
|
||||
expect(u.restoredFrom).toBeNull();
|
||||
expect(existsSync(env.configPath)).toBe(false);
|
||||
expect(existsSync(env.pointerPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('uninstall restores a pre-existing hooks.json byte-identically', async () => {
|
||||
const initial = {
|
||||
schemaVersion: 1,
|
||||
hooks: {
|
||||
SessionStart: [
|
||||
{ matcher: 'startup', hooks: [{ type: 'command', command: 'node /existing/ctx.js' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
env = await bootstrap(initial);
|
||||
const preInstall = await readFile(env.configPath, 'utf-8');
|
||||
const preHash = sha256(preInstall);
|
||||
|
||||
await install({ home: env.home, hooksDir: env.hooksDir });
|
||||
const afterInstall = await readFile(env.configPath, 'utf-8');
|
||||
expect(sha256(afterInstall)).not.toBe(preHash);
|
||||
|
||||
const u = await uninstall({ home: env.home, hooksDir: env.hooksDir });
|
||||
expect(u.createdRemoved).toBe(false);
|
||||
const afterUninstall = await readFile(env.configPath, 'utf-8');
|
||||
expect(sha256(afterUninstall)).toBe(preHash);
|
||||
expect(afterUninstall).toBe(preInstall);
|
||||
// backup + pointer cleaned up.
|
||||
expect(existsSync(env.pointerPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user