import fs from 'fs'; import os from 'os'; import path from 'path'; import { afterEach, describe, expect, it } from 'vitest'; import { Database } from 'bun:sqlite'; import type { ServiceDef } from './service-defs.js'; import type { ServiceCheck } from './verify-services.js'; import { buildVerifySummary, detectChannelAuth, detectCredentials, loadAssignedRoomsSummary, loadRoleRoutingRequirementsSummary, } from './verify-state.js'; describe('verify state helpers', () => { const tempRoots: string[] = []; afterEach(() => { for (const root of tempRoots.splice(0)) { fs.rmSync(root, { recursive: true, force: true }); } }); it('detects configured credentials from .env', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); fs.writeFileSync( path.join(tempRoot, '.env'), 'CLAUDE_CODE_OAUTH_TOKEN=test-token\n', ); expect(detectCredentials(tempRoot)).toBe('configured'); }); it('detects canonical role-based channel auth names from process env', () => { expect( detectChannelAuth( {}, { DISCORD_OWNER_BOT_TOKEN: 'owner-token', DISCORD_REVIEWER_BOT_TOKEN: 'reviewer-token', DISCORD_ARBITER_BOT_TOKEN: 'arbiter-token', }, ), ).toEqual({ discord: 'configured', 'discord-review': 'configured', 'discord-arbiter': 'configured', }); }); it('does not treat unknown discord token names as configured channels', () => { expect( detectChannelAuth( {}, { DISCORD_UNUSED_BOT_TOKEN: 'unknown-token', }, ), ).toEqual({}); }); it('loads paired-room routing requirements from the sqlite store', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); const dbPath = path.join(tempRoot, 'messages.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE room_settings ( chat_jid TEXT PRIMARY KEY, room_mode TEXT NOT NULL, updated_at TEXT NOT NULL ); `); db.exec(` CREATE TABLE paired_tasks ( id TEXT PRIMARY KEY, chat_jid TEXT NOT NULL, status TEXT NOT NULL ); `); db.exec(` INSERT INTO room_settings (chat_jid, room_mode, updated_at) VALUES ('group-1', 'tribunal', '2024-01-01T00:00:00.000Z'), ('group-2', 'single', '2024-01-01T00:00:00.000Z'); `); db.exec(` INSERT INTO paired_tasks (id, chat_jid, status) VALUES ('task-1', 'group-1', 'arbiter_requested'), ('task-2', 'group-2', 'completed'); `); db.close(); expect(loadRoleRoutingRequirementsSummary({ dbPath })).toEqual({ tribunalRooms: 1, activeArbiterTasks: 1, }); }); it('loads assigned room counts from the sqlite store', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); const dbPath = path.join(tempRoot, 'messages.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE room_settings ( chat_jid TEXT PRIMARY KEY, room_mode TEXT NOT NULL, owner_agent_type TEXT, updated_at TEXT NOT NULL ); `); db.exec(` INSERT INTO room_settings (chat_jid, room_mode, owner_agent_type, updated_at) VALUES ('group-1', 'single', 'claude-code', '2024-01-01T00:00:00.000Z'), ('group-2', 'single', 'codex', '2024-01-01T00:00:00.000Z'), ('group-3', 'tribunal', 'codex', '2024-01-01T00:00:00.000Z'); `); db.close(); expect(loadAssignedRoomsSummary({ dbPath })).toEqual({ assignedRooms: 3, roomsByOwnerAgent: { 'claude-code': 1, codex: 2, }, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }); }); it('marks legacy-only sqlite registrations as migration-required', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); const dbPath = path.join(tempRoot, 'messages.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE registered_groups ( jid TEXT PRIMARY KEY, agent_type TEXT ) `); db.exec(` INSERT INTO registered_groups (jid, agent_type) VALUES ('legacy-room', 'claude-code') `); db.close(); expect(loadAssignedRoomsSummary({ dbPath, projectRoot: tempRoot })).toEqual( { assignedRooms: 0, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 1, legacyRoomMigrationRequired: true, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ); }); it('marks mixed canonical and pending legacy sqlite registrations as migration-required', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); const dbPath = path.join(tempRoot, 'messages.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE room_settings ( chat_jid TEXT PRIMARY KEY, room_mode TEXT NOT NULL, mode_source TEXT NOT NULL DEFAULT 'explicit', name TEXT, folder TEXT, trigger_pattern TEXT, requires_trigger INTEGER DEFAULT 1, is_main INTEGER DEFAULT 0, owner_agent_type TEXT, work_dir TEXT, updated_at TEXT NOT NULL ); CREATE TABLE room_role_overrides ( chat_jid TEXT NOT NULL, role TEXT NOT NULL, agent_type TEXT NOT NULL, agent_config_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (chat_jid, role) ); CREATE TABLE registered_groups ( jid TEXT NOT NULL, name TEXT NOT NULL, folder TEXT NOT NULL, trigger_pattern TEXT NOT NULL, added_at TEXT NOT NULL, agent_config TEXT, requires_trigger INTEGER DEFAULT 1, is_main INTEGER DEFAULT 0, agent_type TEXT NOT NULL, work_dir TEXT, PRIMARY KEY (jid, agent_type) ) `); db.exec(` INSERT INTO room_settings ( chat_jid, room_mode, mode_source, name, folder, trigger_pattern, requires_trigger, is_main, owner_agent_type, work_dir, updated_at ) VALUES ( 'canonical-room', 'single', 'explicit', 'Canonical Room', 'canonical-room', '@Andy', 1, 0, 'codex', NULL, '2024-01-01T00:00:00.000Z' ) `); db.exec(` INSERT INTO registered_groups ( jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir ) VALUES ( 'legacy-room', 'Legacy Room', 'legacy-room', '@Andy', '2024-01-01T00:00:00.000Z', NULL, 1, 0, 'claude-code', NULL ) `); db.close(); expect(loadAssignedRoomsSummary({ dbPath, projectRoot: tempRoot })).toEqual( { assignedRooms: 1, roomsByOwnerAgent: { codex: 1, }, legacyRegisteredGroupRows: 1, legacyRoomMigrationRequired: true, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ); }); it('ignores legacy projection rows when matching canonical room_settings already exist', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); const dbPath = path.join(tempRoot, 'messages.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE room_settings ( chat_jid TEXT PRIMARY KEY, room_mode TEXT NOT NULL, mode_source TEXT NOT NULL DEFAULT 'explicit', name TEXT, folder TEXT, trigger_pattern TEXT, requires_trigger INTEGER DEFAULT 1, is_main INTEGER DEFAULT 0, owner_agent_type TEXT, work_dir TEXT, updated_at TEXT NOT NULL ); CREATE TABLE room_role_overrides ( chat_jid TEXT NOT NULL, role TEXT NOT NULL, agent_type TEXT NOT NULL, agent_config_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (chat_jid, role) ); CREATE TABLE registered_groups ( jid TEXT NOT NULL, name TEXT NOT NULL, folder TEXT NOT NULL, trigger_pattern TEXT NOT NULL, added_at TEXT NOT NULL, agent_config TEXT, requires_trigger INTEGER DEFAULT 1, is_main INTEGER DEFAULT 0, agent_type TEXT NOT NULL, work_dir TEXT, PRIMARY KEY (jid, agent_type) ) `); db.exec(` INSERT INTO room_settings ( chat_jid, room_mode, mode_source, name, folder, trigger_pattern, requires_trigger, is_main, owner_agent_type, work_dir, updated_at ) VALUES ( 'same-room', 'single', 'explicit', 'Same Room', 'same-room', '@Andy', 1, 0, 'codex', NULL, '2024-01-01T00:00:00.000Z' ) `); db.exec(` INSERT INTO registered_groups ( jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir ) VALUES ( 'same-room', 'Same Room', 'same-room', '@Andy', '2024-01-01T00:00:00.000Z', NULL, 1, 0, 'codex', NULL ) `); db.exec(` INSERT INTO room_role_overrides ( chat_jid, role, agent_type, agent_config_json, created_at, updated_at ) VALUES ( 'same-room', 'owner', 'codex', NULL, '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z' ) `); db.close(); expect(loadAssignedRoomsSummary({ dbPath, projectRoot: tempRoot })).toEqual( { assignedRooms: 1, roomsByOwnerAgent: { codex: 1, }, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ); }); it('marks unexpected data-state files as setup blockers', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); fs.mkdirSync(path.join(tempRoot, 'data'), { recursive: true }); fs.writeFileSync( path.join(tempRoot, 'data', 'registered_groups.json'), '{"dc:legacy":{"name":"Legacy","folder":"legacy","trigger":"@Andy","added_at":"2024-01-01T00:00:00.000Z"}}', ); expect(loadAssignedRoomsSummary({ projectRoot: tempRoot })).toEqual({ assignedRooms: 0, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: ['registered_groups.json'], unexpectedDataStateDetected: true, }); }); it('marks unexpected router/session json files as setup blockers', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-verify-')); tempRoots.push(tempRoot); fs.mkdirSync(path.join(tempRoot, 'data'), { recursive: true }); fs.writeFileSync( path.join(tempRoot, 'data', 'router_state.json'), JSON.stringify({ last_timestamp: '1234' }), ); fs.writeFileSync( path.join(tempRoot, 'data', 'sessions.json'), JSON.stringify({ 'group-a': 'session-123' }), ); expect(loadAssignedRoomsSummary({ projectRoot: tempRoot })).toEqual({ assignedRooms: 0, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: ['router_state.json', 'sessions.json'], unexpectedDataStateDetected: true, }); }); it('builds a successful verification summary when all gates pass', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; const serviceDefs: ServiceDef[] = [ { kind: 'primary', name: 'ejclaw', description: 'EJClaw', launchdLabel: 'com.ejclaw', logName: 'ejclaw', }, ]; expect( buildVerifySummary( services, serviceDefs, 'configured', { discord: 'configured', 'discord-review': 'configured', 'discord-arbiter': 'configured', }, { assignedRooms: 2, roomsByOwnerAgent: { codex: 1 }, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ), ).toMatchObject({ status: 'success', configuredChannels: ['discord', 'discord-review', 'discord-arbiter'], reviewerChannelConfigured: true, arbiterChannelConfigured: true, servicesSummary: { ejclaw: 'running' }, }); }); it('fails verification when any required gate is missing', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'stopped' }]; expect( buildVerifySummary( services, [], 'missing', {}, { assignedRooms: 0, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ), ).toMatchObject({ status: 'failed', configuredChannels: [], servicesSummary: { ejclaw: 'stopped' }, }); }); it('fails verification when only the review channel is configured', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; expect( buildVerifySummary( services, [], 'configured', { 'discord-review': 'configured' }, { assignedRooms: 1, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ), ).toMatchObject({ status: 'failed', configuredChannels: ['discord-review'], reviewerChannelConfigured: true, arbiterChannelConfigured: false, servicesSummary: { ejclaw: 'running' }, }); }); it('fails verification when tribunal rooms exist but the reviewer channel is missing', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; expect( buildVerifySummary( services, [], 'configured', { discord: 'configured' }, { assignedRooms: 1, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, { tribunalRooms: 1 }, ), ).toMatchObject({ status: 'failed', configuredChannels: ['discord'], reviewerChannelConfigured: false, arbiterChannelConfigured: false, tribunalRooms: 1, }); }); it('fails verification when arbiter work is pending but the arbiter channel is missing', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; expect( buildVerifySummary( services, [], 'configured', { discord: 'configured', 'discord-review': 'configured', }, { assignedRooms: 1, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, { tribunalRooms: 1, activeArbiterTasks: 1 }, ), ).toMatchObject({ status: 'failed', configuredChannels: ['discord', 'discord-review'], reviewerChannelConfigured: true, arbiterChannelConfigured: false, activeArbiterTasks: 1, }); }); it('fails verification when legacy room migration is still required', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; expect( buildVerifySummary( services, [], 'configured', { discord: 'configured' }, { assignedRooms: 0, roomsByOwnerAgent: {}, legacyRegisteredGroupRows: 2, legacyRoomMigrationRequired: true, unexpectedDataStateFiles: [], unexpectedDataStateDetected: false, }, ), ).toMatchObject({ status: 'failed', legacyRoomMigrationRequired: true, legacyRegisteredGroupRows: 2, }); }); it('fails verification when unexpected data state files are still present', () => { const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }]; expect( buildVerifySummary( services, [], 'configured', { discord: 'configured' }, { assignedRooms: 1, roomsByOwnerAgent: { codex: 1 }, legacyRegisteredGroupRows: 0, legacyRoomMigrationRequired: false, unexpectedDataStateFiles: ['router_state.json'], unexpectedDataStateDetected: true, }, ), ).toMatchObject({ status: 'failed', unexpectedDataStateDetected: true, unexpectedDataStateFiles: ['router_state.json'], }); }); });