setup: narrow legacy room registration surface
This commit is contained in:
@@ -4,7 +4,7 @@ import path from 'path';
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { Database } from 'bun:sqlite';
|
import { Database } from 'bun:sqlite';
|
||||||
import { detectAssignedRooms, run as runEnvironment } from './environment.js';
|
import { run as runEnvironment } from './environment.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the environment check step.
|
* Tests for the environment check step.
|
||||||
@@ -20,77 +20,6 @@ describe('environment detection', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('assigned rooms detection', () => {
|
|
||||||
const tempDirs: string[] = [];
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
for (const dir of tempDirs.splice(0)) {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false when the database does not exist', () => {
|
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-env-'));
|
|
||||||
tempDirs.push(tempDir);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
detectAssignedRooms({ dbPath: path.join(tempDir, 'messages.db') }),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false for an empty room_settings table', () => {
|
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-env-'));
|
|
||||||
tempDirs.push(tempDir);
|
|
||||||
const dbPath = path.join(tempDir, 'messages.db');
|
|
||||||
const db = new Database(dbPath);
|
|
||||||
db.exec(`CREATE TABLE room_settings (chat_jid TEXT PRIMARY KEY)`);
|
|
||||||
db.close();
|
|
||||||
|
|
||||||
expect(detectAssignedRooms({ dbPath })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true when canonical room assignments exist', () => {
|
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-env-'));
|
|
||||||
tempDirs.push(tempDir);
|
|
||||||
const dbPath = path.join(tempDir, '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.prepare(
|
|
||||||
`INSERT INTO room_settings (chat_jid, room_mode, updated_at)
|
|
||||||
VALUES (?, ?, ?)`,
|
|
||||||
).run('dc:room-1', 'single', '2024-01-01T00:00:00.000Z');
|
|
||||||
db.close();
|
|
||||||
|
|
||||||
expect(detectAssignedRooms({ dbPath })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false when only legacy registered_groups rows exist', () => {
|
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-env-'));
|
|
||||||
tempDirs.push(tempDir);
|
|
||||||
const dbPath = path.join(tempDir, '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 ('dc:legacy-room', 'claude-code')
|
|
||||||
`);
|
|
||||||
db.close();
|
|
||||||
|
|
||||||
expect(detectAssignedRooms({ dbPath })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('environment step legacy-room handling', () => {
|
describe('environment step legacy-room handling', () => {
|
||||||
const originalCwd = process.cwd();
|
const originalCwd = process.cwd();
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from './platform.js';
|
} from './platform.js';
|
||||||
import {
|
import {
|
||||||
detectRoomRegistrationState,
|
detectRoomRegistrationState,
|
||||||
getLegacyMigrationGuidance,
|
getRoomRegistrationGateFailure,
|
||||||
} from './room-registration-state.js';
|
} from './room-registration-state.js';
|
||||||
import { emitStatus } from './status.js';
|
import { emitStatus } from './status.js';
|
||||||
|
|
||||||
@@ -51,11 +51,11 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
projectRoot,
|
projectRoot,
|
||||||
dbPath: path.join(projectRoot, 'store', 'messages.db'),
|
dbPath: path.join(projectRoot, 'store', 'messages.db'),
|
||||||
});
|
});
|
||||||
const legacyMigrationGuidance = getLegacyMigrationGuidance(
|
const roomRegistrationGateFailure = getRoomRegistrationGateFailure(
|
||||||
roomState,
|
roomState,
|
||||||
'setup',
|
'setup',
|
||||||
);
|
);
|
||||||
const status = legacyMigrationGuidance ? 'failed' : 'success';
|
const status = roomRegistrationGateFailure ? 'failed' : 'success';
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
@@ -94,10 +94,10 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
apparmorRestrictUnprivilegedUserns ?? 'n/a',
|
apparmorRestrictUnprivilegedUserns ?? 'n/a',
|
||||||
HAS_BWRAP_READONLY_SANDBOX_CAPABILITY:
|
HAS_BWRAP_READONLY_SANDBOX_CAPABILITY:
|
||||||
hasBubblewrapReadonlySandboxCapability,
|
hasBubblewrapReadonlySandboxCapability,
|
||||||
...(legacyMigrationGuidance
|
...(roomRegistrationGateFailure
|
||||||
? {
|
? {
|
||||||
ERROR: legacyMigrationGuidance.error,
|
ERROR: roomRegistrationGateFailure.error,
|
||||||
NEXT_STEP: legacyMigrationGuidance.nextStep,
|
NEXT_STEP: roomRegistrationGateFailure.nextStep,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
STATUS: status,
|
STATUS: status,
|
||||||
|
|||||||
299
setup/room-registration-state.test.ts
Normal file
299
setup/room-registration-state.test.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
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 {
|
||||||
|
detectRoomRegistrationState,
|
||||||
|
getRoomRegistrationGateFailure,
|
||||||
|
} from './room-registration-state.js';
|
||||||
|
|
||||||
|
describe('room registration state', () => {
|
||||||
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of tempRoots.splice(0)) {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports no assigned rooms when the database does not exist', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-room-reg-'));
|
||||||
|
tempRoots.push(tempRoot);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
detectRoomRegistrationState({
|
||||||
|
projectRoot: tempRoot,
|
||||||
|
dbPath: path.join(tempRoot, 'messages.db'),
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
assignedRooms: 0,
|
||||||
|
roomsByOwnerAgent: {},
|
||||||
|
legacyRegisteredGroupRows: 0,
|
||||||
|
legacyRoomMigrationRequired: false,
|
||||||
|
unexpectedDataStateFiles: [],
|
||||||
|
unexpectedDataStateDetected: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts canonical room_settings rows and owner-agent breakdowns', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-room-reg-'));
|
||||||
|
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(
|
||||||
|
detectRoomRegistrationState({ projectRoot: tempRoot, 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-room-reg-'));
|
||||||
|
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(
|
||||||
|
detectRoomRegistrationState({ dbPath, projectRoot: tempRoot }),
|
||||||
|
).toEqual({
|
||||||
|
assignedRooms: 0,
|
||||||
|
roomsByOwnerAgent: {},
|
||||||
|
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-room-reg-'));
|
||||||
|
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(
|
||||||
|
detectRoomRegistrationState({ dbPath, projectRoot: tempRoot }),
|
||||||
|
).toEqual({
|
||||||
|
assignedRooms: 1,
|
||||||
|
roomsByOwnerAgent: {
|
||||||
|
codex: 1,
|
||||||
|
},
|
||||||
|
legacyRegisteredGroupRows: 0,
|
||||||
|
legacyRoomMigrationRequired: false,
|
||||||
|
unexpectedDataStateFiles: [],
|
||||||
|
unexpectedDataStateDetected: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks unexpected data-state files as blockers', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-room-reg-'));
|
||||||
|
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"}}',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempRoot, 'data', 'router_state.json'),
|
||||||
|
JSON.stringify({ last_timestamp: '1234' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detectRoomRegistrationState({ projectRoot: tempRoot })).toEqual({
|
||||||
|
assignedRooms: 0,
|
||||||
|
roomsByOwnerAgent: {},
|
||||||
|
legacyRegisteredGroupRows: 0,
|
||||||
|
legacyRoomMigrationRequired: false,
|
||||||
|
unexpectedDataStateFiles: ['registered_groups.json', 'router_state.json'],
|
||||||
|
unexpectedDataStateDetected: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no gate failure when room-registration state is clean', () => {
|
||||||
|
expect(
|
||||||
|
getRoomRegistrationGateFailure(
|
||||||
|
{
|
||||||
|
legacyRoomMigrationRequired: false,
|
||||||
|
unexpectedDataStateDetected: false,
|
||||||
|
},
|
||||||
|
'setup',
|
||||||
|
),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a targeted gate failure when legacy room migration is pending', () => {
|
||||||
|
expect(
|
||||||
|
getRoomRegistrationGateFailure(
|
||||||
|
{
|
||||||
|
legacyRoomMigrationRequired: true,
|
||||||
|
unexpectedDataStateDetected: false,
|
||||||
|
},
|
||||||
|
'verification',
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
error: 'legacy_room_migration_required',
|
||||||
|
nextStep:
|
||||||
|
'Run `bun setup/index.ts --step migrate-room-registrations` before continuing with verification',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a combined gate failure when legacy migration and unexpected data files are both present', () => {
|
||||||
|
expect(
|
||||||
|
getRoomRegistrationGateFailure(
|
||||||
|
{
|
||||||
|
legacyRoomMigrationRequired: true,
|
||||||
|
unexpectedDataStateDetected: true,
|
||||||
|
},
|
||||||
|
'setup',
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
error: 'legacy_migration_and_unexpected_data_state_detected',
|
||||||
|
nextStep:
|
||||||
|
'Run `bun setup/index.ts --step migrate-room-registrations` and remove or archive unexpected data state files before continuing with setup',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,7 +16,7 @@ export interface AssignedRoomsSummary {
|
|||||||
unexpectedDataStateDetected: boolean;
|
unexpectedDataStateDetected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyMigrationGuidance {
|
export interface RoomRegistrationGateFailure {
|
||||||
error: string;
|
error: string;
|
||||||
nextStep: string;
|
nextStep: string;
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,13 @@ function getDataDir(projectRoot: string): string {
|
|||||||
return process.env.EJCLAW_DATA_DIR || path.join(projectRoot, 'data');
|
return process.env.EJCLAW_DATA_DIR || path.join(projectRoot, 'data');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLegacyMigrationGuidance(
|
export function getRoomRegistrationGateFailure(
|
||||||
summary: Pick<
|
summary: Pick<
|
||||||
AssignedRoomsSummary,
|
AssignedRoomsSummary,
|
||||||
'legacyRoomMigrationRequired' | 'unexpectedDataStateDetected'
|
'legacyRoomMigrationRequired' | 'unexpectedDataStateDetected'
|
||||||
>,
|
>,
|
||||||
target: 'setup' | 'verification',
|
target: 'setup' | 'verification',
|
||||||
): LegacyMigrationGuidance | undefined {
|
): RoomRegistrationGateFailure | undefined {
|
||||||
if (
|
if (
|
||||||
!summary.legacyRoomMigrationRequired &&
|
!summary.legacyRoomMigrationRequired &&
|
||||||
!summary.unexpectedDataStateDetected
|
!summary.unexpectedDataStateDetected
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
buildVerifySummary,
|
buildVerifySummary,
|
||||||
detectChannelAuth,
|
detectChannelAuth,
|
||||||
detectCredentials,
|
detectCredentials,
|
||||||
loadAssignedRoomsSummary,
|
|
||||||
loadRoleRoutingRequirementsSummary,
|
loadRoleRoutingRequirementsSummary,
|
||||||
} from './verify-state.js';
|
} from './verify-state.js';
|
||||||
|
|
||||||
@@ -100,348 +99,6 @@ describe('verify state helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
it('builds a successful verification summary when all gates pass', () => {
|
||||||
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }];
|
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }];
|
||||||
const serviceDefs: ServiceDef[] = [
|
const serviceDefs: ServiceDef[] = [
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import path from 'path';
|
|||||||
|
|
||||||
import { logger } from '../src/logger.js';
|
import { logger } from '../src/logger.js';
|
||||||
import { getServiceManager } from './platform.js';
|
import { getServiceManager } from './platform.js';
|
||||||
import { getLegacyMigrationGuidance } from './room-registration-state.js';
|
import { getRoomRegistrationGateFailure } from './room-registration-state.js';
|
||||||
import { getServiceDefs } from './service-defs.js';
|
import { getServiceDefs } from './service-defs.js';
|
||||||
import { emitStatus } from './status.js';
|
import { emitStatus } from './status.js';
|
||||||
import {
|
import {
|
||||||
@@ -66,7 +66,7 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
activeArbiterTasks,
|
activeArbiterTasks,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const legacyMigrationGuidance = getLegacyMigrationGuidance(
|
const roomRegistrationGateFailure = getRoomRegistrationGateFailure(
|
||||||
roomSummary,
|
roomSummary,
|
||||||
'verification',
|
'verification',
|
||||||
);
|
);
|
||||||
@@ -100,10 +100,10 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
UNEXPECTED_DATA_STATE_DETECTED: roomSummary.unexpectedDataStateDetected,
|
UNEXPECTED_DATA_STATE_DETECTED: roomSummary.unexpectedDataStateDetected,
|
||||||
REVIEWER_CHANNEL_CONFIGURED: reviewerChannelConfigured,
|
REVIEWER_CHANNEL_CONFIGURED: reviewerChannelConfigured,
|
||||||
ARBITER_CHANNEL_CONFIGURED: arbiterChannelConfigured,
|
ARBITER_CHANNEL_CONFIGURED: arbiterChannelConfigured,
|
||||||
...(legacyMigrationGuidance
|
...(roomRegistrationGateFailure
|
||||||
? {
|
? {
|
||||||
ERROR: legacyMigrationGuidance.error,
|
ERROR: roomRegistrationGateFailure.error,
|
||||||
NEXT_STEP: legacyMigrationGuidance.nextStep,
|
NEXT_STEP: roomRegistrationGateFailure.nextStep,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
STATUS: status,
|
STATUS: status,
|
||||||
|
|||||||
@@ -2271,6 +2271,25 @@ describe('message query LIMIT', () => {
|
|||||||
// --- RegisteredGroup isMain round-trip ---
|
// --- RegisteredGroup isMain round-trip ---
|
||||||
|
|
||||||
describe('registered group isMain', () => {
|
describe('registered group isMain', () => {
|
||||||
|
it('projects trigger metadata from canonical room settings into registered groups', () => {
|
||||||
|
_setRegisteredGroupForTests('dc:triggered', {
|
||||||
|
name: 'Triggered Room',
|
||||||
|
folder: 'triggered-room',
|
||||||
|
trigger: '@Andy',
|
||||||
|
added_at: '2024-01-01T00:00:00.000Z',
|
||||||
|
requiresTrigger: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getRegisteredGroup('dc:triggered')).toMatchObject({
|
||||||
|
trigger: '@Andy',
|
||||||
|
requiresTrigger: true,
|
||||||
|
});
|
||||||
|
expect(getAllRoomBindings()['dc:triggered']).toMatchObject({
|
||||||
|
trigger: '@Andy',
|
||||||
|
requiresTrigger: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('persists isMain=true through set/get round-trip', () => {
|
it('persists isMain=true through set/get round-trip', () => {
|
||||||
_setRegisteredGroupForTests('dc:main', {
|
_setRegisteredGroupForTests('dc:main', {
|
||||||
name: 'Main Chat',
|
name: 'Main Chat',
|
||||||
|
|||||||
@@ -244,8 +244,10 @@ export function buildRegisteredGroupFromStoredSettings(
|
|||||||
jid: stored.chatJid,
|
jid: stored.chatJid,
|
||||||
name: stored.name || stored.chatJid,
|
name: stored.name || stored.chatJid,
|
||||||
folder: stored.folder,
|
folder: stored.folder,
|
||||||
|
trigger: stored.trigger,
|
||||||
added_at: capabilityMetadata?.createdAt || new Date(0).toISOString(),
|
added_at: capabilityMetadata?.createdAt || new Date(0).toISOString(),
|
||||||
agentConfig: capabilityMetadata?.agentConfig,
|
agentConfig: capabilityMetadata?.agentConfig,
|
||||||
|
requiresTrigger: stored.requiresTrigger ?? false,
|
||||||
isMain: stored.isMain ? true : undefined,
|
isMain: stored.isMain ? true : undefined,
|
||||||
agentType: resolvedAgentType,
|
agentType: resolvedAgentType,
|
||||||
workDir: stored.workDir,
|
workDir: stored.workDir,
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export interface RegisteredGroup {
|
|||||||
trigger?: string;
|
trigger?: string;
|
||||||
added_at: string;
|
added_at: string;
|
||||||
agentConfig?: AgentConfig;
|
agentConfig?: AgentConfig;
|
||||||
requiresTrigger?: boolean; // Compatibility-only legacy field
|
requiresTrigger?: boolean; // Whether non-paired messages must match the room trigger
|
||||||
isMain?: boolean; // True for the main control group (no trigger, elevated privileges)
|
isMain?: boolean; // True for the main control group (no trigger, elevated privileges)
|
||||||
agentType?: AgentType;
|
agentType?: AgentType;
|
||||||
workDir?: string; // Working directory for the agent (defaults to group folder)
|
workDir?: string; // Working directory for the agent (defaults to group folder)
|
||||||
|
|||||||
Reference in New Issue
Block a user