test: second-pass coverage for the memorable bridge

Policy lookup outcomes (plain dir, repo without policy, corrupt .git/config
fails closed); repoPolicyTier timeoutMs; line/col at line starts, after blank
lines, CRLF and first char; tolerant first-JSON-object parsing; keyed rate
limit; uninstall never creates a config just to say off and flips consent in
a GSTACK_STATE_ROOT outside the removed state dir.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-09-09 03:33:11 +00:00
co-authored by Claude Fable 5.1
parent abf5d96400
commit 3b4f955181
4 changed files with 126 additions and 8 deletions
+34
View File
@@ -376,6 +376,8 @@ describe('the Memorable arm stays quiet when nothing of its is registered', () =
expect(result.stdout).not.toContain('Memorable UserPromptSubmit hook');
const s = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
expect(s.hooks.UserPromptSubmit[0].hooks[0].command).toBe('/Users/me/my-own-hook');
// the consent flip only runs when the key reads on: no config file is created just to say off
expect(fs.existsSync(path.join(mockHome, '.gstack', 'config.yaml'))).toBe(false);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
@@ -436,3 +438,35 @@ describe('hook cleanup under lock contention is loud, never silent (review-army)
// per-test budget is too tight on a busy box.
}, 30000);
});
describe('the consent key never outlives the hook, even when the config lives outside the removed state dir', () => {
test('full uninstall (no --keep-state) with GSTACK_STATE_ROOT elsewhere: memorable_recall flips off there', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-uninstall-memo-root-'));
try {
const mockHome = path.join(tmp, 'home');
const otherRoot = path.join(tmp, 'elsewhere');
const installRoot = path.join(mockHome, '.claude', 'skills', 'gstack');
const installBin = path.join(installRoot, 'bin');
fs.mkdirSync(installBin, { recursive: true });
fs.mkdirSync(otherRoot, { recursive: true });
for (const b of ['gstack-uninstall', 'gstack-settings-hook', 'gstack-session-update', 'gstack-config']) {
const dst = path.join(installBin, b);
fs.copyFileSync(path.join(ROOT, 'bin', b), dst);
fs.chmodSync(dst, 0o755);
}
const settingsFile = path.join(mockHome, '.claude', 'settings.json');
fs.writeFileSync(settingsFile, JSON.stringify({ hooks: {} }));
fs.mkdirSync(path.join(mockHome, '.gstack'), { recursive: true });
const env = { ...process.env, HOME: mockHome, GSTACK_SETTINGS_FILE: settingsFile, GSTACK_STATE_ROOT: otherRoot };
expect(spawnSync('bash', [path.join(installBin, 'gstack-config'), 'set', 'memorable_recall', 'on'], { env, timeout: 20_000 }).status).toBe(0);
const result = spawnSync('bash', [path.join(installBin, 'gstack-uninstall'), '--force'], {
stdio: 'pipe', timeout: 30_000, encoding: 'utf-8', cwd: tmp, env,
});
expect(result.status).toBe(0);
expect(fs.existsSync(path.join(mockHome, '.gstack'))).toBe(false); // the default state dir went
expect(fs.readFileSync(path.join(otherRoot, 'config.yaml'), 'utf-8')).toMatch(/memorable_recall: off/); // the real config did not keep consent
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});