/** * Tests for `celilo module list`'s jail-exemption marker (per-module-jail-policy * task 3.1/3.4). * * A module with a recorded policy weaker than the system's is marked in the * human list and carries a `jailExemptions` roster entry in JSON. The negative * half is asserted with the same rigour: no row, no marker, empty roster — * a query bug that reported phantom exemptions would look exactly like a * correctly empty one otherwise. * * Isolation mirrors module-jail.test.ts: CELILO_DB_PATH / CELILO_DATA_DIR are * set before the SUT is imported. */ 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-list-jail-')); process.env.CELILO_DB_PATH = join(testRoot, 'celilo.db'); process.env.CELILO_DATA_DIR = join(testRoot, 'data'); const { getDb, closeDb } = await import('../../db/client'); const { moduleJailPolicies, modules, systemConfig } = await import('../../db/schema'); const { handleModuleList } = await import('./module-list'); import type { CommandResult, CommandSuccess } from '../types'; /** 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 insertModule(id: string): void { getDb() .insert(modules) .values({ id, name: id, sourcePath: `/tmp/${id}`, version: '1.0.0', manifestData: {} }) .run(); } function setModulePolicy(moduleId: string, policy: string): void { const typed = policy as 'auto' | 'off' | 'required'; getDb() .insert(moduleJailPolicies) .values({ moduleId, policy: typed, updatedAt: new Date() }) .onConflictDoUpdate({ target: moduleJailPolicies.moduleId, set: { policy: typed, updatedAt: new Date() }, }) .run(); } function setSystemPolicy(value: string): void { getDb() .insert(systemConfig) .values({ key: 'hooks.jail_policy', value }) .onConflictDoUpdate({ target: systemConfig.key, set: { value } }) .run(); } function clearModulePolicy(moduleId: string): void { getDb().delete(moduleJailPolicies).where(eq(moduleJailPolicies.moduleId, moduleId)).run(); } afterAll(() => { closeDb(); rmSync(testRoot, { recursive: true, force: true }); }); describe('module list — jail exemption marker (per-module-jail-policy task 3.1)', () => { beforeEach(() => { const db = getDb(); db.delete(moduleJailPolicies).run(); db.delete(systemConfig).where(eq(systemConfig.key, 'hooks.jail_policy')).run(); db.delete(modules).run(); insertModule('caddy'); }); test('a module weaker than the system is marked, with both policies', async () => { setSystemPolicy('auto'); setModulePolicy('caddy', 'off'); const result = await handleModuleList({}); expectOk(result); expect(result.message).toContain('caddy'); expect(result.message).toContain("[jail: off — weaker than the system's auto]"); }); test('a module with no per-module row carries no marker (the negative half of task 3.4)', async () => { setSystemPolicy('auto'); const result = await handleModuleList({}); expectOk(result); expect(result.message).not.toContain('jail:'); expect(result.message).not.toContain('weaker than'); }); test('a module whose row is not weaker than the system carries no marker', async () => { setSystemPolicy('auto'); setModulePolicy('caddy', 'required'); const result = await handleModuleList({}); expectOk(result); expect(result.message).not.toContain('jail:'); }); test('a cleared row stops the marker (the exemption is un-set, not hidden)', async () => { setSystemPolicy('auto'); setModulePolicy('caddy', 'off'); const before = await handleModuleList({}); expectOk(before); expect(before.message).toContain('[jail:'); clearModulePolicy('caddy'); const after = await handleModuleList({}); expectOk(after); expect(after.message).not.toContain('[jail:'); }); test('the JSON roster carries the exemptions', async () => { setSystemPolicy('auto'); setModulePolicy('caddy', 'off'); const result = await handleModuleList({ json: true }); expectOk(result); const parsed = JSON.parse(result.message) as { jailExemptions: Array<{ moduleId: string; policy: string; systemPolicy: string }>; }; expect(parsed.jailExemptions).toEqual([ { moduleId: 'caddy', policy: 'off', systemPolicy: 'auto' }, ]); }); test('the JSON roster is empty when nothing is exempted', async () => { const result = await handleModuleList({ json: true }); expectOk(result); const parsed = JSON.parse(result.message) as { jailExemptions: unknown[] }; expect(parsed.jailExemptions).toEqual([]); }); });