/** * Regression tests for F2 (SECURE_MODULE_PUBLISH.md): `module secret set * celilo-registry publish_tokens` clobbers the whole newline-separated list. * `registry token add`/`rm` must read-modify-write instead. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb, getDb } from '../../db/client'; import { modules } from '../../db/schema'; import { CrossModuleDataManager } from '../../services/cross-module-data-manager'; import { resetTestDbPath } from '../../test-utils/db-path'; import { handleRegistryTokenAdd, handleRegistryTokenRm } from './registry-token'; /** Read back the stored publish_tokens list via the production read path. */ async function readStoredTokens(): Promise { const manager = new CrossModuleDataManager(); await manager.initialize(); const raw = manager.getSecret('celilo-registry', 'publish_tokens'); if (!raw) return []; return raw .split('\n') .map((l) => l.trim()) .filter((l) => l.length > 0); } describe('registry token add/rm (F2 clobber fix)', () => { let dataDir: string; beforeEach(async () => { dataDir = await mkdtemp(join(tmpdir(), 'celilo-registry-token-')); process.env.CELILO_DATA_DIR = dataDir; process.env.CELILO_DB_PATH = join(dataDir, 'celilo.db'); // Seed the celilo-registry module row (FK target for the secrets row). getDb() .insert(modules) .values({ id: 'celilo-registry', name: 'Celilo Registry', version: '1.0.0', sourcePath: '/test', manifestData: {}, }) .run(); }); afterEach(async () => { closeDb(); delete process.env.CELILO_DATA_DIR; resetTestDbPath(); if (existsSync(dataDir)) { await rm(dataDir, { recursive: true, force: true }); } }); test('add appends without clobbering existing tokens (F2 regression)', async () => { await handleRegistryTokenAdd(['token-a']); await handleRegistryTokenAdd(['token-b']); await handleRegistryTokenAdd(['token-c']); const result = await handleRegistryTokenAdd(['token-d']); expect(result.success).toBe(true); // The whole point of F2: adding token-d must NOT wipe a/b/c. expect((await readStoredTokens()).sort()).toEqual(['token-a', 'token-b', 'token-c', 'token-d']); }); test('rm removes one line, leaves the others', async () => { await handleRegistryTokenAdd(['token-a']); await handleRegistryTokenAdd(['token-b']); await handleRegistryTokenAdd(['token-c']); const result = await handleRegistryTokenRm(['token-b']); expect(result.success).toBe(true); expect((await readStoredTokens()).sort()).toEqual(['token-a', 'token-c']); }); test('add is idempotent — no duplicate line', async () => { await handleRegistryTokenAdd(['token-a']); const result = await handleRegistryTokenAdd(['token-a']); expect(result.success).toBe(true); expect(await readStoredTokens()).toEqual(['token-a']); }); test('rm of an absent token errors', async () => { await handleRegistryTokenAdd(['token-a']); const result = await handleRegistryTokenRm(['token-x']); expect(result.success).toBe(false); }); test('add requires a token argument', async () => { const result = await handleRegistryTokenAdd([]); expect(result.success).toBe(false); }); test('errors when celilo-registry module is not installed', async () => { getDb().delete(modules).run(); const result = await handleRegistryTokenAdd(['token-a']); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('celilo-registry'); } }); });