refactor: extract verify state helpers
This commit is contained in:
131
setup/verify-state.test.ts
Normal file
131
setup/verify-state.test.ts
Normal 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
133
setup/verify-state.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
106
setup/verify.ts
106
setup/verify.ts
@@ -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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user