296 lines
7.7 KiB
TypeScript
296 lines
7.7 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,
|
|
detectLegacyDiscordTokenKeys,
|
|
loadRegisteredGroupsSummary,
|
|
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 legacy service-based channel auth names as configured channels', () => {
|
|
expect(
|
|
detectChannelAuth(
|
|
{},
|
|
{
|
|
DISCORD_CLAUDE_BOT_TOKEN: 'legacy-owner-token',
|
|
DISCORD_CODEX_MAIN_BOT_TOKEN: 'legacy-reviewer-token',
|
|
DISCORD_CODEX_REVIEW_BOT_TOKEN: 'legacy-arbiter-token',
|
|
},
|
|
),
|
|
).toEqual({});
|
|
});
|
|
|
|
it('detects legacy service-based discord token names from env file and process env', () => {
|
|
expect(
|
|
detectLegacyDiscordTokenKeys(
|
|
{
|
|
DISCORD_BOT_TOKEN: 'legacy-owner-token',
|
|
},
|
|
{
|
|
DISCORD_CODEX_MAIN_BOT_TOKEN: 'legacy-reviewer-token',
|
|
DISCORD_CODEX_REVIEW_BOT_TOKEN: 'legacy-arbiter-token',
|
|
},
|
|
),
|
|
).toEqual([
|
|
'DISCORD_BOT_TOKEN',
|
|
'DISCORD_CODEX_MAIN_BOT_TOKEN',
|
|
'DISCORD_CODEX_REVIEW_BOT_TOKEN',
|
|
]);
|
|
});
|
|
|
|
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 registered_groups (
|
|
jid TEXT NOT NULL,
|
|
agent_type TEXT
|
|
);
|
|
`);
|
|
db.exec(`
|
|
CREATE TABLE paired_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
chat_jid TEXT NOT NULL,
|
|
status TEXT NOT NULL
|
|
);
|
|
`);
|
|
db.exec(`
|
|
INSERT INTO registered_groups (jid, agent_type) VALUES
|
|
('group-1', 'claude-code'),
|
|
('group-1', 'codex'),
|
|
('group-2', 'claude-code');
|
|
`);
|
|
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 registered group 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 registered_groups (
|
|
jid TEXT PRIMARY KEY,
|
|
agent_type TEXT
|
|
);
|
|
`);
|
|
db.exec(`
|
|
INSERT INTO registered_groups (jid, agent_type) VALUES
|
|
('group-1', 'claude-code'),
|
|
('group-2', 'codex'),
|
|
('group-3', 'codex');
|
|
`);
|
|
db.close();
|
|
|
|
expect(loadRegisteredGroupsSummary(dbPath)).toEqual({
|
|
registeredGroups: 3,
|
|
groupsByAgent: {
|
|
'claude-code': 1,
|
|
codex: 2,
|
|
},
|
|
});
|
|
});
|
|
|
|
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',
|
|
},
|
|
2,
|
|
{ codex: 1 },
|
|
),
|
|
).toMatchObject({
|
|
status: 'success',
|
|
configuredChannels: ['discord', 'discord-review', 'discord-arbiter'],
|
|
codexConfigured: true,
|
|
reviewConfigured: true,
|
|
servicesSummary: { ejclaw: 'running' },
|
|
});
|
|
});
|
|
|
|
it('fails verification when any required gate is missing', () => {
|
|
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'stopped' }];
|
|
|
|
expect(
|
|
buildVerifySummary(
|
|
services,
|
|
[],
|
|
'missing',
|
|
{},
|
|
0,
|
|
{},
|
|
),
|
|
).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' },
|
|
1,
|
|
{},
|
|
),
|
|
).toMatchObject({
|
|
status: 'failed',
|
|
configuredChannels: ['discord-review'],
|
|
codexConfigured: true,
|
|
reviewConfigured: 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' },
|
|
1,
|
|
{},
|
|
{ tribunalRooms: 1 },
|
|
),
|
|
).toMatchObject({
|
|
status: 'failed',
|
|
configuredChannels: ['discord'],
|
|
codexConfigured: false,
|
|
reviewConfigured: 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',
|
|
},
|
|
1,
|
|
{},
|
|
{ tribunalRooms: 1, activeArbiterTasks: 1 },
|
|
),
|
|
).toMatchObject({
|
|
status: 'failed',
|
|
configuredChannels: ['discord', 'discord-review'],
|
|
codexConfigured: true,
|
|
reviewConfigured: false,
|
|
activeArbiterTasks: 1,
|
|
});
|
|
});
|
|
|
|
it('fails verification when legacy discord token names are still configured', () => {
|
|
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }];
|
|
|
|
expect(
|
|
buildVerifySummary(
|
|
services,
|
|
[],
|
|
'configured',
|
|
{
|
|
discord: 'configured',
|
|
'discord-review': 'configured',
|
|
'discord-arbiter': 'configured',
|
|
},
|
|
1,
|
|
{},
|
|
{ legacyDiscordTokenKeys: ['DISCORD_BOT_TOKEN'] },
|
|
),
|
|
).toMatchObject({
|
|
status: 'failed',
|
|
legacyDiscordTokenKeys: ['DISCORD_BOT_TOKEN'],
|
|
});
|
|
});
|
|
});
|