Files
EJClaw/setup/verify-state.test.ts

659 lines
18 KiB
TypeScript

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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: true,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: true,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: false,
},
);
});
it('marks legacy registered_groups.json as migration-required', () => {
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,
hasLegacyRegisteredGroupsJson: true,
legacyRoomMigrationRequired: true,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: false,
});
});
it('marks pending legacy json state files as migration-required', () => {
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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: ['router_state.json', 'sessions.json'],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: 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,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: true,
pendingLegacyJsonStateFiles: [],
legacyJsonStateMigrationRequired: false,
},
),
).toMatchObject({
status: 'failed',
legacyRoomMigrationRequired: true,
legacyRegisteredGroupRows: 2,
});
});
it('fails verification when legacy json state migration is still required', () => {
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }];
expect(
buildVerifySummary(
services,
[],
'configured',
{ discord: 'configured' },
{
assignedRooms: 1,
roomsByOwnerAgent: { codex: 1 },
legacyRegisteredGroupRows: 0,
hasLegacyRegisteredGroupsJson: false,
legacyRoomMigrationRequired: false,
pendingLegacyJsonStateFiles: ['router_state.json'],
legacyJsonStateMigrationRequired: true,
},
),
).toMatchObject({
status: 'failed',
legacyJsonStateMigrationRequired: true,
pendingLegacyJsonStateFiles: ['router_state.json'],
});
});
});