/** * Tests for `celilo module jail` (per-module-jail-policy task 2.1/2.2). * * The verb records, clears and shows one module's hook jail policy, and asks * over the bus interview when the write weakens the posture below the * system's. Strengthening writes directly; `--force` skips the ask. * * Isolation mirrors system-config.test.ts: CELILO_DB_PATH / CELILO_DATA_DIR / * EVENT_BUS_DB are set before the SUT is imported. bun test runs non-TTY, so * a weakening write with no responder listening fails fast with "No responder * is listening" — a successful direct weakening write in these tests is proof * the command never asked. */ import { afterAll, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { eq } from 'drizzle-orm'; const testRoot = mkdtempSync(join(tmpdir(), 'celilo-module-jail-')); const busDbPath = join(testRoot, 'bus.db'); process.env.CELILO_DB_PATH = join(testRoot, 'celilo.db'); process.env.CELILO_DATA_DIR = join(testRoot, 'data'); process.env.EVENT_BUS_DB = busDbPath; import type { CommandError, CommandResult, CommandSuccess } from '../types'; const { getDb, closeDb } = await import('../../db/client'); const { moduleJailPolicies, modules, systemConfig } = await import('../../db/schema'); const { handleModuleJail } = await import('./module-jail'); const { startProgrammaticResponder } = await import('../../services/programmatic-responder'); const MODULE_ID = 'jailmod'; /** Narrow the result union after asserting the branch, so `.message` typechecks. */ function expectOk(result: CommandResult): asserts result is CommandSuccess { expect(result.success).toBe(true); } function expectErr(result: CommandResult): asserts result is CommandError { expect(result.success).toBe(false); } function insertModule(id: string): void { getDb() .insert(modules) .values({ id, name: id, sourcePath: `/tmp/${id}`, version: '1.0.0', manifestData: {} }) .run(); } function storedPolicy(moduleId: string): string | undefined { return getDb() .select() .from(moduleJailPolicies) .where(eq(moduleJailPolicies.moduleId, moduleId)) .get()?.policy; } function setSystemPolicy(value: string | undefined): void { const db = getDb(); if (value === undefined) { db.delete(systemConfig).where(eq(systemConfig.key, 'hooks.jail_policy')).run(); } else { db.insert(systemConfig) .values({ key: 'hooks.jail_policy', value }) .onConflictDoUpdate({ target: systemConfig.key, set: { value } }) .run(); } } afterAll(() => { closeDb(); rmSync(testRoot, { recursive: true, force: true }); }); describe('module jail — the write form', () => { beforeEach(() => { getDb().delete(moduleJailPolicies).run(); getDb().delete(systemConfig).where(eq(systemConfig.key, 'hooks.jail_policy')).run(); if ( !getDb() .select() .from(modules) .all() .some((m) => m.id === MODULE_ID) ) { insertModule(MODULE_ID); } }); test('records a policy and the read form resolves through it', async () => { const set = await handleModuleJail([MODULE_ID, 'auto']); expect(set.success).toBe(true); expect(storedPolicy(MODULE_ID)).toBe('auto'); const show = await handleModuleJail([MODULE_ID]); expectOk(show); expect(show.message).toContain('effective: auto — source: module'); }); test('rejects a value outside the accepted set', async () => { const result = await handleModuleJail([MODULE_ID, 'sometimes']); expectErr(result); expect(result.error).toContain("Invalid jail policy 'sometimes'"); expect(storedPolicy(MODULE_ID)).toBeUndefined(); }); test('rejects an unknown module', async () => { const result = await handleModuleJail(['no-such-module', 'off']); expect(result.success).toBe(false); }); test('refuses to pass both a policy and --clear', async () => { const result = await handleModuleJail([MODULE_ID, 'off'], { clear: true }); expectErr(result); expect(result.error).toContain('not both'); }); test('module row beats the system value in the read form', async () => { setSystemPolicy('required'); await handleModuleJail([MODULE_ID, 'off'], { force: true }); const show = await handleModuleJail([MODULE_ID]); expectOk(show); expect(show.message).toContain('effective: off — source: module'); expect(show.message).toContain('hooks.jail_policy = required'); }); test('--clear removes the row and the module follows the system again', async () => { setSystemPolicy('auto'); await handleModuleJail([MODULE_ID, 'off'], { force: true }); expect(storedPolicy(MODULE_ID)).toBe('off'); const cleared = await handleModuleJail([MODULE_ID], { clear: true }); expectOk(cleared); expect(storedPolicy(MODULE_ID)).toBeUndefined(); expect(cleared.message).toContain('follows the system: auto'); const show = await handleModuleJail([MODULE_ID]); expectOk(show); expect(show.message).toContain('effective: auto — source: config'); }); test('--clear with no row succeeds', async () => { const result = await handleModuleJail([MODULE_ID], { clear: true }); expectOk(result); expect(result.message).toContain('already follows the system'); }); }); describe('module jail — the weakening interview gate', () => { beforeEach(() => { getDb().delete(moduleJailPolicies).run(); getDb().delete(systemConfig).where(eq(systemConfig.key, 'hooks.jail_policy')).run(); if ( !getDb() .select() .from(modules) .all() .some((m) => m.id === MODULE_ID) ) { insertModule(MODULE_ID); } }); test('weakening with no responder fails fast and writes nothing', async () => { setSystemPolicy('auto'); let threw: unknown; try { await handleModuleJail([MODULE_ID, 'off']); } catch (error) { threw = error; } expect(threw).toBeInstanceOf(Error); expect(String((threw as Error).message)).toContain('No responder is listening'); expect(storedPolicy(MODULE_ID)).toBeUndefined(); }); test('weakening writes after a confirming reply, and the question was asked', async () => { setSystemPolicy('auto'); const responder = startProgrammaticResponder({ busDbPath, db: getDb(), onMissing: 'skip', values: { interview: { 'module-jail-policy.weaken': true } }, }); try { const result = await handleModuleJail([MODULE_ID, 'off']); expect(result.success).toBe(true); expect(storedPolicy(MODULE_ID)).toBe('off'); const asked = responder.seenInterviewPayloads(); expect(asked.length).toBe(1); expect(asked[0].scope).toBe('module-jail-policy'); expect(asked[0].key).toBe('weaken'); expect(asked[0].message).toContain('unjailed'); } finally { responder.close(); } }); test('a denial writes nothing', async () => { setSystemPolicy('auto'); const responder = startProgrammaticResponder({ busDbPath, db: getDb(), onMissing: 'skip', values: { interview: { 'module-jail-policy.weaken': false } }, }); try { const result = await handleModuleJail([MODULE_ID, 'off']); expect(result.success).toBe(false); expect(storedPolicy(MODULE_ID)).toBeUndefined(); } finally { responder.close(); } }); test('weakening with --force writes directly and asks nothing', async () => { setSystemPolicy('required'); const result = await handleModuleJail([MODULE_ID, 'off'], { force: true }); expectOk(result); expect(storedPolicy(MODULE_ID)).toBe('off'); expect(result.message).toContain('weaker than the system'); }); test('strengthening writes directly with no responder listening', async () => { setSystemPolicy('off'); const result = await handleModuleJail([MODULE_ID, 'required']); expect(result.success).toBe(true); expect(storedPolicy(MODULE_ID)).toBe('required'); }); test('setting a policy equal to the system writes directly', async () => { setSystemPolicy('off'); const result = await handleModuleJail([MODULE_ID, 'off']); expect(result.success).toBe(true); expect(storedPolicy(MODULE_ID)).toBe('off'); }); });