fix(websearch): harden settings-profile hook setup

- fail enabled settings-profile create and launch flows when hook preparation is still unusable

- refresh stale shared hook binaries without rewriting unchanged installs or failing benign races

- add rollback and regression coverage for disabled, stale, invalid, copy, import, and launch paths
This commit is contained in:
Tam Nhu Tran committed 2026-03-28 09:51:13 -04:00
1 parent fbbdd80830
commit 0e6965d205
12 files changed
+549 -46

No files matched your search

@@ -27,6 +27,10 @@ describe('ensureProfileHooks', () => {
return path.join(tempHome, '.ccs');
}
function getBundledHookContents(): string {
return fs.readFileSync(path.join(process.cwd(), 'lib', 'hooks', 'websearch-transformer.cjs'), 'utf8');
}
afterEach(() => {
mock.restore();
@@ -69,7 +73,7 @@ describe('ensureProfileHooks', () => {
const hookPath = getHookPath();
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
fs.writeFileSync(hookPath, '// existing hook', 'utf8');
fs.writeFileSync(hookPath, getBundledHookContents(), 'utf8');
const copyFileSpy = spyOn(fs, 'copyFileSync').mockImplementation(() => {
throw new Error('copy skipped');
@@ -80,10 +84,74 @@ describe('ensureProfileHooks', () => {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(ensured).toBe(true);
expect(copyFileSpy).toHaveBeenCalled();
expect(copyFileSpy).not.toHaveBeenCalled();
expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe(`node "${hookPath}"`);
});
it('does not rewrite the shared hook binary when it is already installed', () => {
setupTempHome();
expect(ensureProfileHooks('glm')).toBe(true);
const firstMtime = fs.statSync(getHookPath()).mtimeMs;
const waitUntil = Date.now() + 25;
while (Date.now() < waitUntil) {
// Give the filesystem timestamp a chance to advance if a rewrite occurs.
}
expect(ensureProfileHooks('glm')).toBe(true);
const secondMtime = fs.statSync(getHookPath()).mtimeMs;
expect(secondMtime).toBe(firstMtime);
});
it('refreshes a stale shared hook binary when the bundled script has changed', () => {
setupTempHome();
const hookPath = getHookPath();
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
fs.writeFileSync(hookPath, '// stale hook', 'utf8');
expect(ensureProfileHooks('glm')).toBe(true);
const installedHook = fs.readFileSync(hookPath, 'utf8');
expect(installedHook).not.toBe('// stale hook');
expect(installedHook).toContain('CCS WebSearch Hook');
});
it('succeeds when another process installs the hook during a failed local install', () => {
setupTempHome();
const hookPath = getHookPath();
const originalCopyFileSync = fs.copyFileSync;
const copyFileSpy = spyOn(fs, 'copyFileSync').mockImplementation((source, destination) => {
originalCopyFileSync(source, hookPath);
throw new Error(`simulated concurrent winner while copying to ${String(destination)}`);
});
const ensured = ensureProfileHooks('glm');
const settingsPath = path.join(getCcsDir(), 'glm.settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(ensured).toBe(true);
expect(copyFileSpy).toHaveBeenCalled();
expect(fs.existsSync(hookPath)).toBe(true);
expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe(`node "${hookPath}"`);
});
it('returns false when the hook path exists but is unusable', () => {
setupTempHome();
const hookPath = getHookPath();
fs.mkdirSync(hookPath, { recursive: true });
const ensured = ensureProfileHooks('glm');
expect(ensured).toBe(false);
expect(fs.statSync(hookPath).isDirectory()).toBe(true);
expect(fs.existsSync(path.join(getCcsDir(), 'glm.settings.json'))).toBe(false);
});
it('returns false for invalid profile names without creating files', () => {
setupTempHome();