refactor: extract verify state helpers

This commit is contained in:
Eyejoker
2026-03-31 06:51:54 +09:00
parent 30f19f994b
commit b9541a7c5b
3 changed files with 286 additions and 84 deletions

131
setup/verify-state.test.ts Normal file
View File

@@ -0,0 +1,131 @@
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,
loadRegisteredGroupsSummary,
} 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 channel auth from either env source', () => {
expect(
detectChannelAuth(
{ DISCORD_BOT_TOKEN: '' },
{ DISCORD_BOT_TOKEN: 'discord-token' },
),
).toEqual({
discord: 'configured',
});
});
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',
},
{
kind: 'codex',
name: 'ejclaw-codex',
description: 'Codex',
launchdLabel: 'com.ejclaw.codex',
logName: 'ejclaw-codex',
},
];
expect(
buildVerifySummary(
services,
serviceDefs,
'configured',
{ discord: 'configured' },
2,
{ codex: 1 },
),
).toMatchObject({
status: 'success',
configuredChannels: ['discord'],
codexConfigured: true,
reviewConfigured: false,
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' },
});
});
});

133
setup/verify-state.ts Normal file
View File

@@ -0,0 +1,133 @@
import fs from 'fs';
import path from 'path';
import { Database } from 'bun:sqlite';
import { STORE_DIR } from '../src/config.js';
import { readEnvFile } from '../src/env.js';
import type { ServiceDef } from './service-defs.js';
import type { ServiceCheck } from './verify-services.js';
export type CredentialsStatus = 'configured' | 'missing';
export type VerifyStatus = 'success' | 'failed';
export interface RegisteredGroupsSummary {
registeredGroups: number;
groupsByAgent: Record<string, number>;
}
export interface VerifySummary extends RegisteredGroupsSummary {
status: VerifyStatus;
servicesSummary: Record<string, string>;
configuredChannels: string[];
channelAuth: Record<string, string>;
codexConfigured: boolean;
reviewConfigured: boolean;
}
export function detectCredentials(projectRoot: string): CredentialsStatus {
const envFile = path.join(projectRoot, '.env');
if (!fs.existsSync(envFile)) {
return 'missing';
}
const envContent = fs.readFileSync(envFile, 'utf-8');
return /^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)
? 'configured'
: 'missing';
}
export function detectChannelAuth(
envVars = readEnvFile(['DISCORD_BOT_TOKEN']),
processEnv: NodeJS.ProcessEnv = process.env,
): Record<string, string> {
const channelAuth: Record<string, string> = {};
if (processEnv.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) {
channelAuth.discord = 'configured';
}
return channelAuth;
}
export function loadRegisteredGroupsSummary(
dbPath = path.join(STORE_DIR, 'messages.db'),
): RegisteredGroupsSummary {
let registeredGroups = 0;
const groupsByAgent: Record<string, number> = {};
if (!fs.existsSync(dbPath)) {
return { registeredGroups, groupsByAgent };
}
try {
const db = new Database(dbPath, { readonly: true });
const row = db
.prepare('SELECT COUNT(*) as count FROM registered_groups')
.get() as { count: number };
registeredGroups = row.count;
try {
const rows = db
.prepare(
'SELECT agent_type, COUNT(*) as count FROM registered_groups GROUP BY agent_type',
)
.all() as { agent_type: string; count: number }[];
for (const current of rows) {
groupsByAgent[current.agent_type || 'unknown'] = current.count;
}
} catch {
// agent_type column might not exist in older schema
}
db.close();
} catch {
// Table might not exist
}
return { registeredGroups, groupsByAgent };
}
export function buildVerifySummary(
services: ServiceCheck[],
serviceDefs: ServiceDef[],
credentials: CredentialsStatus,
channelAuth: Record<string, string>,
registeredGroups: number,
groupsByAgent: Record<string, number>,
): VerifySummary {
const configuredChannels = Object.keys(channelAuth);
const allConfiguredServicesRunning = services.every(
(service) => service.status === 'running',
);
const codexConfigured = serviceDefs.some(
(service) => service.kind === 'codex',
);
const reviewConfigured = serviceDefs.some(
(service) => service.kind === 'review',
);
const status =
allConfiguredServicesRunning &&
credentials === 'configured' &&
configuredChannels.length > 0 &&
registeredGroups > 0
? 'success'
: 'failed';
const servicesSummary: Record<string, string> = {};
for (const service of services) {
servicesSummary[service.name] = service.status;
}
return {
status,
servicesSummary,
configuredChannels,
channelAuth,
registeredGroups,
groupsByAgent,
codexConfigured,
reviewConfigured,
};
}

View File

@@ -9,17 +9,16 @@
* *
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks. * Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
*/ */
import fs from 'fs';
import path from 'path';
import { Database } from 'bun:sqlite';
import { STORE_DIR } from '../src/config.js';
import { readEnvFile } from '../src/env.js';
import { logger } from '../src/logger.js'; import { logger } from '../src/logger.js';
import { getServiceManager } from './platform.js'; import { getServiceManager } from './platform.js';
import { getServiceDefs } from './service-defs.js'; import { getServiceDefs } from './service-defs.js';
import { emitStatus } from './status.js'; import { emitStatus } from './status.js';
import {
buildVerifySummary,
detectChannelAuth,
detectCredentials,
loadRegisteredGroupsSummary,
} from './verify-state.js';
import { getServiceChecks } from './verify-services.js'; import { getServiceChecks } from './verify-services.js';
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -40,84 +39,23 @@ export async function run(_args: string[]): Promise<void> {
logger.info({ service: svc.name, status: svc.status }, 'Service status'); logger.info({ service: svc.name, status: svc.status }, 'Service status');
} }
// 2. Check credentials const credentials = detectCredentials(projectRoot);
let credentials = 'missing'; const channelAuth = detectChannelAuth();
const envFile = path.join(projectRoot, '.env'); const { registeredGroups, groupsByAgent } = loadRegisteredGroupsSummary();
if (fs.existsSync(envFile)) { const {
const envContent = fs.readFileSync(envFile, 'utf-8'); status,
if (/^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=/m.test(envContent)) { servicesSummary,
credentials = 'configured'; configuredChannels,
} codexConfigured,
} reviewConfigured,
} = buildVerifySummary(
// 3. Check channel auth (detect configured channels by credentials) services,
const envVars = readEnvFile(['DISCORD_BOT_TOKEN']); serviceDefs,
credentials,
const channelAuth: Record<string, string> = {}; channelAuth,
registeredGroups,
if (process.env.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) { groupsByAgent,
channelAuth.discord = 'configured';
}
const configuredChannels = Object.keys(channelAuth);
const anyChannelConfigured = configuredChannels.length > 0;
// 4. Check registered groups (using better-sqlite3, not sqlite3 CLI)
let registeredGroups = 0;
let groupsByAgent: Record<string, number> = {};
const dbPath = path.join(STORE_DIR, 'messages.db');
if (fs.existsSync(dbPath)) {
try {
const db = new Database(dbPath, { readonly: true });
const row = db
.prepare('SELECT COUNT(*) as count FROM registered_groups')
.get() as { count: number };
registeredGroups = row.count;
// Count by agent type
try {
const rows = db
.prepare(
'SELECT agent_type, COUNT(*) as count FROM registered_groups GROUP BY agent_type',
)
.all() as { agent_type: string; count: number }[];
for (const r of rows) {
groupsByAgent[r.agent_type || 'unknown'] = r.count;
}
} catch {
// agent_type column might not exist in older schema
}
db.close();
} catch {
// Table might not exist
}
}
// Determine overall status
const allConfiguredServicesRunning = services.every(
(service) => service.status === 'running',
); );
const codexConfigured = serviceDefs.some(
(service) => service.kind === 'codex',
);
const reviewConfigured = serviceDefs.some(
(service) => service.kind === 'review',
);
const status =
allConfiguredServicesRunning &&
credentials !== 'missing' &&
anyChannelConfigured &&
registeredGroups > 0
? 'success'
: 'failed';
// Build service status summary
const servicesSummary: Record<string, string> = {};
for (const svc of services) {
servicesSummary[svc.name] = svc.status;
}
logger.info({ status, channelAuth, servicesSummary }, 'Verification complete'); logger.info({ status, channelAuth, servicesSummary }, 'Verification complete');