refactor: remove legacy config and state aliases (PR5)
This commit is contained in:
@@ -169,7 +169,7 @@ describe('environment step legacy-room handling', () => {
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('exits with failure when legacy json state files need explicit migration', async () => {
|
||||
it('exits with failure when unexpected data state files are present', async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-env-run-'));
|
||||
tempDirs.push(tempDir);
|
||||
process.chdir(tempDir);
|
||||
|
||||
@@ -65,11 +65,9 @@ export async function run(_args: string[]): Promise<void> {
|
||||
hasAuth,
|
||||
assignedRooms: roomState.assignedRooms,
|
||||
legacyRegisteredGroupRows: roomState.legacyRegisteredGroupRows,
|
||||
hasLegacyRegisteredGroupsJson: roomState.hasLegacyRegisteredGroupsJson,
|
||||
legacyRoomMigrationRequired: roomState.legacyRoomMigrationRequired,
|
||||
pendingLegacyJsonStateFiles: roomState.pendingLegacyJsonStateFiles,
|
||||
legacyJsonStateMigrationRequired:
|
||||
roomState.legacyJsonStateMigrationRequired,
|
||||
unexpectedDataStateFiles: roomState.unexpectedDataStateFiles,
|
||||
unexpectedDataStateDetected: roomState.unexpectedDataStateDetected,
|
||||
hasBubblewrap,
|
||||
hasSocat,
|
||||
apparmorRestrictUnprivilegedUserns,
|
||||
@@ -87,12 +85,9 @@ export async function run(_args: string[]): Promise<void> {
|
||||
HAS_ASSIGNED_ROOMS: roomState.assignedRooms > 0,
|
||||
ASSIGNED_ROOMS: roomState.assignedRooms,
|
||||
LEGACY_REGISTERED_GROUP_ROWS: roomState.legacyRegisteredGroupRows,
|
||||
HAS_LEGACY_REGISTERED_GROUPS_JSON: roomState.hasLegacyRegisteredGroupsJson,
|
||||
LEGACY_ROOM_MIGRATION_REQUIRED: roomState.legacyRoomMigrationRequired,
|
||||
PENDING_LEGACY_JSON_STATE_FILES:
|
||||
roomState.pendingLegacyJsonStateFiles.join(','),
|
||||
LEGACY_JSON_STATE_MIGRATION_REQUIRED:
|
||||
roomState.legacyJsonStateMigrationRequired,
|
||||
UNEXPECTED_DATA_STATE_FILES: roomState.unexpectedDataStateFiles.join(','),
|
||||
UNEXPECTED_DATA_STATE_DETECTED: roomState.unexpectedDataStateDetected,
|
||||
HAS_BWRAP: hasBubblewrap,
|
||||
HAS_SOCAT: hasSocat,
|
||||
APPARMOR_RESTRICT_UNPRIVILEGED_USERNS:
|
||||
|
||||
@@ -13,7 +13,6 @@ const STEPS: Record<
|
||||
runners: () => import('./runners.js'),
|
||||
groups: () => import('./groups.js'),
|
||||
register: () => import('./register.js'),
|
||||
'migrate-json-state': () => import('./migrate-json-state.js'),
|
||||
'migrate-room-registrations': () => import('./migrate-room-registrations.js'),
|
||||
'restart-stack': () => import('./restart-stack.js'),
|
||||
service: () => import('./service.js'),
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
describe('migrate json state step', () => {
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not migrate legacy json state during startup and leaves files in place', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-json-init-'),
|
||||
);
|
||||
tempRoots.push(tempRoot);
|
||||
const storeDir = path.join(tempRoot, 'store');
|
||||
const dataDir = path.join(tempRoot, 'data');
|
||||
fs.mkdirSync(storeDir, { recursive: true });
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
vi.stubEnv('EJCLAW_STORE_DIR', storeDir);
|
||||
vi.stubEnv('EJCLAW_DATA_DIR', dataDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'router_state.json'),
|
||||
JSON.stringify({ last_timestamp: '1234' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'sessions.json'),
|
||||
JSON.stringify({ 'group-a': 'session-123' }),
|
||||
);
|
||||
|
||||
const { _initTestDatabase, initDatabase } = await import('../src/db.js');
|
||||
|
||||
expect(() => initDatabase()).toThrow(
|
||||
/Legacy JSON state migration required before startup/,
|
||||
);
|
||||
expect(fs.existsSync(path.join(dataDir, 'router_state.json'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(dataDir, 'sessions.json'))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'router_state.json.migrated')),
|
||||
).toBe(false);
|
||||
expect(fs.existsSync(path.join(dataDir, 'sessions.json.migrated'))).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
_initTestDatabase();
|
||||
});
|
||||
|
||||
it('migrates router_state.json and sessions.json only via explicit setup step', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-json-step-'),
|
||||
);
|
||||
tempRoots.push(tempRoot);
|
||||
const storeDir = path.join(tempRoot, 'store');
|
||||
const dataDir = path.join(tempRoot, 'data');
|
||||
fs.mkdirSync(storeDir, { recursive: true });
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
vi.stubEnv('EJCLAW_STORE_DIR', storeDir);
|
||||
vi.stubEnv('EJCLAW_DATA_DIR', dataDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'router_state.json'),
|
||||
JSON.stringify({
|
||||
last_timestamp: '1234',
|
||||
last_agent_timestamp: {
|
||||
'dc:test-room': '5678',
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'sessions.json'),
|
||||
JSON.stringify({
|
||||
'group-a': 'session-123',
|
||||
'group-b': 'session-456',
|
||||
}),
|
||||
);
|
||||
|
||||
const emitStatusMock = vi.fn();
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: emitStatusMock,
|
||||
}));
|
||||
|
||||
const { run } = await import('./migrate-json-state.js');
|
||||
|
||||
await run([]);
|
||||
|
||||
const db = new Database(path.join(storeDir, 'messages.db'), {
|
||||
readonly: true,
|
||||
});
|
||||
expect(
|
||||
db.prepare('SELECT key, value FROM router_state ORDER BY key').all(),
|
||||
).toEqual([
|
||||
{
|
||||
key: 'last_agent_seq',
|
||||
value: '{"dc:test-room":"5678"}',
|
||||
},
|
||||
{
|
||||
key: 'last_seq',
|
||||
value: '1234',
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT group_folder, agent_type, session_id
|
||||
FROM sessions
|
||||
ORDER BY group_folder`,
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
group_folder: 'group-a',
|
||||
agent_type: 'claude-code',
|
||||
session_id: 'session-123',
|
||||
},
|
||||
{
|
||||
group_folder: 'group-b',
|
||||
agent_type: 'claude-code',
|
||||
session_id: 'session-456',
|
||||
},
|
||||
]);
|
||||
db.close();
|
||||
|
||||
expect(fs.existsSync(path.join(dataDir, 'router_state.json'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dataDir, 'sessions.json'))).toBe(false);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'router_state.json.migrated')),
|
||||
).toBe(true);
|
||||
expect(fs.existsSync(path.join(dataDir, 'sessions.json.migrated'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(emitStatusMock).toHaveBeenLastCalledWith(
|
||||
'MIGRATE_JSON_STATE',
|
||||
expect.objectContaining({
|
||||
MIGRATED_ROUTER_STATE_KEYS: 2,
|
||||
MIGRATED_SESSIONS: 2,
|
||||
RENAMED_ROUTER_STATE_JSON: true,
|
||||
RENAMED_SESSIONS_JSON: true,
|
||||
STATUS: 'success',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { DATA_DIR, STORE_DIR } from '../src/config.js';
|
||||
import {
|
||||
initializeDatabaseSchema,
|
||||
openPersistentDatabase,
|
||||
} from '../src/db/bootstrap.js';
|
||||
import { setRouterStateInDatabase } from '../src/db/router-state.js';
|
||||
import { setSessionInDatabase } from '../src/db/sessions.js';
|
||||
import { readJsonFile } from '../src/utils.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
interface JsonMigrationReport {
|
||||
migratedRouterStateKeys: number;
|
||||
migratedSessions: number;
|
||||
renamedRouterStateJson: boolean;
|
||||
renamedSessionsJson: boolean;
|
||||
}
|
||||
|
||||
function renameMigratedFile(filePath: string): void {
|
||||
fs.renameSync(filePath, `${filePath}.migrated`);
|
||||
}
|
||||
|
||||
function migrateRouterStateJson(
|
||||
database: ReturnType<typeof openPersistentDatabase>,
|
||||
): {
|
||||
migratedKeys: number;
|
||||
renamed: boolean;
|
||||
} {
|
||||
const routerStatePath = path.join(DATA_DIR, 'router_state.json');
|
||||
const routerState = readJsonFile(routerStatePath) as {
|
||||
last_seq?: string;
|
||||
last_timestamp?: string;
|
||||
last_agent_seq?: Record<string, string>;
|
||||
last_agent_timestamp?: Record<string, string>;
|
||||
} | null;
|
||||
if (!routerState) {
|
||||
return { migratedKeys: 0, renamed: false };
|
||||
}
|
||||
|
||||
const lastSeq = routerState.last_seq ?? routerState.last_timestamp;
|
||||
const lastAgentSeq =
|
||||
routerState.last_agent_seq ?? routerState.last_agent_timestamp;
|
||||
let migratedKeys = 0;
|
||||
if (lastSeq) {
|
||||
setRouterStateInDatabase(database, 'last_seq', lastSeq);
|
||||
migratedKeys += 1;
|
||||
}
|
||||
if (lastAgentSeq) {
|
||||
setRouterStateInDatabase(
|
||||
database,
|
||||
'last_agent_seq',
|
||||
JSON.stringify(lastAgentSeq),
|
||||
);
|
||||
migratedKeys += 1;
|
||||
}
|
||||
renameMigratedFile(routerStatePath);
|
||||
return { migratedKeys, renamed: true };
|
||||
}
|
||||
|
||||
function migrateSessionsJson(
|
||||
database: ReturnType<typeof openPersistentDatabase>,
|
||||
): {
|
||||
migratedSessions: number;
|
||||
renamed: boolean;
|
||||
} {
|
||||
const sessionsPath = path.join(DATA_DIR, 'sessions.json');
|
||||
const sessions = readJsonFile(sessionsPath) as Record<string, string> | null;
|
||||
if (!sessions) {
|
||||
return { migratedSessions: 0, renamed: false };
|
||||
}
|
||||
|
||||
for (const [groupFolder, sessionId] of Object.entries(sessions)) {
|
||||
setSessionInDatabase(database, groupFolder, 'claude-code', sessionId);
|
||||
}
|
||||
renameMigratedFile(sessionsPath);
|
||||
return {
|
||||
migratedSessions: Object.keys(sessions).length,
|
||||
renamed: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
fs.mkdirSync(STORE_DIR, { recursive: true });
|
||||
const database = openPersistentDatabase();
|
||||
initializeDatabaseSchema(database);
|
||||
|
||||
const report: JsonMigrationReport = {
|
||||
migratedRouterStateKeys: 0,
|
||||
migratedSessions: 0,
|
||||
renamedRouterStateJson: false,
|
||||
renamedSessionsJson: false,
|
||||
};
|
||||
|
||||
database.transaction(() => {
|
||||
const routerStateResult = migrateRouterStateJson(database);
|
||||
report.migratedRouterStateKeys = routerStateResult.migratedKeys;
|
||||
report.renamedRouterStateJson = routerStateResult.renamed;
|
||||
|
||||
const sessionsResult = migrateSessionsJson(database);
|
||||
report.migratedSessions = sessionsResult.migratedSessions;
|
||||
report.renamedSessionsJson = sessionsResult.renamed;
|
||||
})();
|
||||
|
||||
database.close();
|
||||
|
||||
emitStatus('MIGRATE_JSON_STATE', {
|
||||
MIGRATED_ROUTER_STATE_KEYS: report.migratedRouterStateKeys,
|
||||
MIGRATED_SESSIONS: report.migratedSessions,
|
||||
RENAMED_ROUTER_STATE_JSON: report.renamedRouterStateJson,
|
||||
RENAMED_SESSIONS_JSON: report.renamedSessionsJson,
|
||||
STATUS: 'success',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ describe('migrate room registrations step', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('migrates pending legacy rows and legacy json into canonical room tables idempotently', async () => {
|
||||
it('migrates pending legacy rows into canonical room tables idempotently', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-rooms-'),
|
||||
);
|
||||
@@ -103,31 +103,10 @@ describe('migrate room registrations step', () => {
|
||||
);
|
||||
legacyDb.close();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'registered_groups.json'),
|
||||
JSON.stringify({
|
||||
'dc:json-room': {
|
||||
name: 'JSON Room',
|
||||
folder: 'json-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-02-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
requiresTrigger: true,
|
||||
isMain: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const emitStatusMock = vi.fn();
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: emitStatusMock,
|
||||
}));
|
||||
vi.doMock('../src/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { run } = await import('./migrate-room-registrations.js');
|
||||
|
||||
@@ -143,12 +122,6 @@ describe('migrate room registrations step', () => {
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
chat_jid: 'dc:json-room',
|
||||
room_mode: 'single',
|
||||
mode_source: 'inferred',
|
||||
owner_agent_type: 'claude-code',
|
||||
},
|
||||
{
|
||||
chat_jid: 'dc:legacy-room',
|
||||
room_mode: 'tribunal',
|
||||
@@ -165,11 +138,6 @@ describe('migrate room registrations step', () => {
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
chat_jid: 'dc:json-room',
|
||||
role: 'owner',
|
||||
agent_type: 'claude-code',
|
||||
},
|
||||
{
|
||||
chat_jid: 'dc:legacy-room',
|
||||
role: 'owner',
|
||||
@@ -201,22 +169,13 @@ describe('migrate room registrations step', () => {
|
||||
]);
|
||||
migratedDb.close();
|
||||
|
||||
expect(fs.existsSync(path.join(dataDir, 'registered_groups.json'))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'registered_groups.json.migrated')),
|
||||
).toBe(true);
|
||||
expect(emitStatusMock).toHaveBeenLastCalledWith(
|
||||
'MIGRATE_ROOM_REGISTRATIONS',
|
||||
expect.objectContaining({
|
||||
MIGRATED_ROOMS: 2,
|
||||
MIGRATED_ROLE_OVERRIDES: 3,
|
||||
MIGRATED_ROOMS: 1,
|
||||
MIGRATED_ROLE_OVERRIDES: 2,
|
||||
SKIPPED_ROOMS: 0,
|
||||
MIGRATED_JSON_ROOMS: 1,
|
||||
SKIPPED_JSON_ROOMS: 0,
|
||||
BACKED_UP_LEGACY_ROWS: 2,
|
||||
RENAMED_LEGACY_JSON: true,
|
||||
STATUS: 'success',
|
||||
}),
|
||||
);
|
||||
@@ -229,10 +188,7 @@ describe('migrate room registrations step', () => {
|
||||
MIGRATED_ROOMS: 0,
|
||||
MIGRATED_ROLE_OVERRIDES: 0,
|
||||
SKIPPED_ROOMS: 0,
|
||||
MIGRATED_JSON_ROOMS: 0,
|
||||
SKIPPED_JSON_ROOMS: 0,
|
||||
BACKED_UP_LEGACY_ROWS: 0,
|
||||
RENAMED_LEGACY_JSON: false,
|
||||
STATUS: 'success',
|
||||
}),
|
||||
);
|
||||
@@ -370,12 +326,6 @@ describe('migrate room registrations step', () => {
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: emitStatusMock,
|
||||
}));
|
||||
vi.doMock('../src/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { run } = await import('./migrate-room-registrations.js');
|
||||
|
||||
@@ -417,9 +367,9 @@ describe('migrate room registrations step', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fails and leaves registered_groups.json in place when it contains invalid entries', async () => {
|
||||
it('fails when unexpected data state files are still present', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-rooms-invalid-json-'),
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-rooms-legacy-files-'),
|
||||
);
|
||||
tempRoots.push(tempRoot);
|
||||
const storeDir = path.join(tempRoot, 'store');
|
||||
@@ -429,298 +379,12 @@ describe('migrate room registrations step', () => {
|
||||
vi.stubEnv('EJCLAW_STORE_DIR', storeDir);
|
||||
vi.stubEnv('EJCLAW_DATA_DIR', dataDir);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'registered_groups.json'),
|
||||
JSON.stringify({
|
||||
'dc:bad-room': {
|
||||
name: 'Bad Room',
|
||||
folder: '../bad',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-02-01T00:00:00.000Z',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: vi.fn(),
|
||||
}));
|
||||
vi.doMock('../src/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
fs.writeFileSync(path.join(dataDir, 'registered_groups.json'), '{}');
|
||||
|
||||
const { run } = await import('./migrate-room-registrations.js');
|
||||
|
||||
await expect(run([])).rejects.toThrow(
|
||||
/Invalid legacy registered_groups\.json folder/,
|
||||
/Unexpected data state files detected before room migration/,
|
||||
);
|
||||
expect(fs.existsSync(path.join(dataDir, 'registered_groups.json'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'registered_groups.json.migrated')),
|
||||
).toBe(false);
|
||||
|
||||
const migratedDb = new Database(path.join(storeDir, 'messages.db'));
|
||||
expect(
|
||||
migratedDb
|
||||
.prepare(
|
||||
`SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name IN ('room_settings', 'registered_groups_legacy_backup')`,
|
||||
)
|
||||
.all(),
|
||||
).toEqual([{ name: 'room_settings' }]);
|
||||
expect(
|
||||
migratedDb.prepare('SELECT COUNT(*) AS count FROM room_settings').get(),
|
||||
).toEqual({ count: 0 });
|
||||
migratedDb.close();
|
||||
});
|
||||
|
||||
it('treats identical registered_groups.json entries as idempotent when room_settings already exists', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-rooms-json-idempotent-'),
|
||||
);
|
||||
tempRoots.push(tempRoot);
|
||||
const storeDir = path.join(tempRoot, 'store');
|
||||
const dataDir = path.join(tempRoot, 'data');
|
||||
fs.mkdirSync(storeDir, { recursive: true });
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
vi.stubEnv('EJCLAW_STORE_DIR', storeDir);
|
||||
vi.stubEnv('EJCLAW_DATA_DIR', dataDir);
|
||||
|
||||
const dbPath = path.join(storeDir, '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,
|
||||
created_at 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)
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
`INSERT INTO room_settings (
|
||||
chat_jid,
|
||||
room_mode,
|
||||
mode_source,
|
||||
name,
|
||||
folder,
|
||||
trigger_pattern,
|
||||
requires_trigger,
|
||||
is_main,
|
||||
owner_agent_type,
|
||||
work_dir,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
'dc:json-room',
|
||||
'single',
|
||||
'inferred',
|
||||
'JSON Room',
|
||||
'json-room',
|
||||
'@Andy',
|
||||
1,
|
||||
0,
|
||||
'claude-code',
|
||||
null,
|
||||
'2024-02-01T00:00:00.000Z',
|
||||
'2024-02-01T00:00:00.000Z',
|
||||
);
|
||||
db.close();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'registered_groups.json'),
|
||||
JSON.stringify({
|
||||
'dc:json-room': {
|
||||
name: 'JSON Room',
|
||||
folder: 'json-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-02-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
requiresTrigger: true,
|
||||
isMain: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const emitStatusMock = vi.fn();
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: emitStatusMock,
|
||||
}));
|
||||
vi.doMock('../src/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { run } = await import('./migrate-room-registrations.js');
|
||||
|
||||
await run([]);
|
||||
|
||||
const migratedDb = new Database(dbPath, { readonly: true });
|
||||
expect(
|
||||
migratedDb
|
||||
.prepare(
|
||||
`SELECT chat_jid, role, agent_type
|
||||
FROM room_role_overrides
|
||||
ORDER BY chat_jid, role`,
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
chat_jid: 'dc:json-room',
|
||||
role: 'owner',
|
||||
agent_type: 'claude-code',
|
||||
},
|
||||
]);
|
||||
migratedDb.close();
|
||||
|
||||
expect(fs.existsSync(path.join(dataDir, 'registered_groups.json'))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'registered_groups.json.migrated')),
|
||||
).toBe(true);
|
||||
expect(emitStatusMock).toHaveBeenLastCalledWith(
|
||||
'MIGRATE_ROOM_REGISTRATIONS',
|
||||
expect.objectContaining({
|
||||
MIGRATED_ROOMS: 0,
|
||||
MIGRATED_JSON_ROOMS: 0,
|
||||
MIGRATED_ROLE_OVERRIDES: 1,
|
||||
RENAMED_LEGACY_JSON: true,
|
||||
STATUS: 'success',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails and preserves registered_groups.json when table and json owner overrides conflict', async () => {
|
||||
const tempRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ejclaw-migrate-rooms-json-conflict-'),
|
||||
);
|
||||
tempRoots.push(tempRoot);
|
||||
const storeDir = path.join(tempRoot, 'store');
|
||||
const dataDir = path.join(tempRoot, 'data');
|
||||
fs.mkdirSync(storeDir, { recursive: true });
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
vi.stubEnv('EJCLAW_STORE_DIR', storeDir);
|
||||
vi.stubEnv('EJCLAW_DATA_DIR', dataDir);
|
||||
|
||||
const dbPath = path.join(storeDir, 'messages.db');
|
||||
const db = new Database(dbPath);
|
||||
db.exec(`
|
||||
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 DEFAULT 'claude-code',
|
||||
work_dir TEXT,
|
||||
PRIMARY KEY (jid, agent_type),
|
||||
UNIQUE (folder, agent_type)
|
||||
)
|
||||
`);
|
||||
db.prepare(
|
||||
`INSERT INTO registered_groups (
|
||||
jid,
|
||||
name,
|
||||
folder,
|
||||
trigger_pattern,
|
||||
added_at,
|
||||
agent_config,
|
||||
requires_trigger,
|
||||
is_main,
|
||||
agent_type,
|
||||
work_dir
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
'dc:shared-room',
|
||||
'Shared Room',
|
||||
'shared-room',
|
||||
'@Andy',
|
||||
'2024-03-01T00:00:00.000Z',
|
||||
'{"source":"table"}',
|
||||
1,
|
||||
0,
|
||||
'claude-code',
|
||||
null,
|
||||
);
|
||||
db.close();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'registered_groups.json'),
|
||||
JSON.stringify({
|
||||
'dc:shared-room': {
|
||||
name: 'Shared Room',
|
||||
folder: 'shared-room',
|
||||
trigger: '@Andy',
|
||||
added_at: '2024-03-01T00:00:00.000Z',
|
||||
agentType: 'claude-code',
|
||||
agentConfig: { source: 'json' },
|
||||
requiresTrigger: true,
|
||||
isMain: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock('./status.js', () => ({
|
||||
emitStatus: vi.fn(),
|
||||
}));
|
||||
vi.doMock('../src/logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { run } = await import('./migrate-room-registrations.js');
|
||||
|
||||
await expect(run([])).rejects.toThrow(
|
||||
/owner override conflicts with existing owner override/,
|
||||
);
|
||||
|
||||
const migratedDb = new Database(dbPath, { readonly: true });
|
||||
expect(
|
||||
migratedDb.prepare('SELECT COUNT(*) AS count FROM room_settings').get(),
|
||||
).toEqual({ count: 0 });
|
||||
expect(
|
||||
migratedDb
|
||||
.prepare('SELECT COUNT(*) AS count FROM room_role_overrides')
|
||||
.get(),
|
||||
).toEqual({ count: 0 });
|
||||
migratedDb.close();
|
||||
|
||||
expect(fs.existsSync(path.join(dataDir, 'registered_groups.json'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(dataDir, 'registered_groups.json.migrated')),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
import { DATA_DIR, STORE_DIR } from '../src/config.js';
|
||||
import { listUnexpectedDataStateFiles } from '../src/data-state-files.js';
|
||||
import {
|
||||
initializeDatabaseSchema,
|
||||
openPersistentDatabase,
|
||||
@@ -11,112 +11,16 @@ import {
|
||||
import {
|
||||
buildLegacyRoomMigrationPlan,
|
||||
getPendingLegacyRegisteredGroupJids,
|
||||
getStoredRoomSettingsRowFromDatabase,
|
||||
insertStoredRoomSettingsFromMigration,
|
||||
normalizeStoredAgentType,
|
||||
upsertRoomRoleOverride,
|
||||
} from '../src/db/room-registration.js';
|
||||
import type { RegisteredGroup } from '../src/types.js';
|
||||
import { isValidGroupFolder } from '../src/group-folder.js';
|
||||
import { logger } from '../src/logger.js';
|
||||
import { readJsonFile } from '../src/utils.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
interface MigrationReport {
|
||||
migratedRooms: number;
|
||||
migratedRoleOverrides: number;
|
||||
skippedRooms: number;
|
||||
migratedJsonRooms: number;
|
||||
skippedJsonRooms: number;
|
||||
backedUpLegacyRows: number;
|
||||
renamedLegacyJson: boolean;
|
||||
}
|
||||
|
||||
interface StoredOwnerRoleOverride {
|
||||
agentType: string;
|
||||
agentConfig?: RegisteredGroup['agentConfig'];
|
||||
}
|
||||
|
||||
function normalizeAgentConfigValue(
|
||||
agentConfig: RegisteredGroup['agentConfig'] | undefined,
|
||||
): string {
|
||||
return JSON.stringify(agentConfig ?? null);
|
||||
}
|
||||
|
||||
function jsonEntryMatchesStoredRoom(
|
||||
jid: string,
|
||||
group: RegisteredGroup,
|
||||
database: Database,
|
||||
): boolean {
|
||||
const existing = getStoredRoomSettingsRowFromDatabase(database, jid);
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ownerAgentType =
|
||||
normalizeStoredAgentType(group.agentType) ?? 'claude-code';
|
||||
return (
|
||||
existing.roomMode === 'single' &&
|
||||
existing.name === group.name &&
|
||||
existing.folder === group.folder &&
|
||||
existing.trigger === group.trigger &&
|
||||
(existing.requiresTrigger ?? true) === (group.requiresTrigger ?? true) &&
|
||||
(existing.isMain ?? false) === (group.isMain ?? false) &&
|
||||
existing.ownerAgentType === ownerAgentType &&
|
||||
(existing.workDir ?? null) === (group.workDir ?? null)
|
||||
);
|
||||
}
|
||||
|
||||
function getStoredOwnerRoleOverride(
|
||||
database: Database,
|
||||
jid: string,
|
||||
): StoredOwnerRoleOverride | undefined {
|
||||
try {
|
||||
const row = database
|
||||
.prepare(
|
||||
`SELECT agent_type, agent_config_json
|
||||
FROM room_role_overrides
|
||||
WHERE chat_jid = ?
|
||||
AND role = 'owner'`,
|
||||
)
|
||||
.get(jid) as
|
||||
| {
|
||||
agent_type: string | null;
|
||||
agent_config_json: string | null;
|
||||
}
|
||||
| undefined;
|
||||
const agentType = normalizeStoredAgentType(row?.agent_type);
|
||||
if (!agentType) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
agentType,
|
||||
agentConfig: row?.agent_config_json
|
||||
? JSON.parse(row.agent_config_json)
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonEntryMatchesStoredOwnerOverride(
|
||||
database: Database,
|
||||
jid: string,
|
||||
group: RegisteredGroup,
|
||||
): boolean {
|
||||
const existingOverride = getStoredOwnerRoleOverride(database, jid);
|
||||
if (!existingOverride) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ownerAgentType =
|
||||
normalizeStoredAgentType(group.agentType) ?? 'claude-code';
|
||||
return (
|
||||
existingOverride.agentType === ownerAgentType &&
|
||||
normalizeAgentConfigValue(existingOverride.agentConfig) ===
|
||||
normalizeAgentConfigValue(group.agentConfig)
|
||||
);
|
||||
}
|
||||
|
||||
function ensureLegacyBackupTable(database: Database): void {
|
||||
@@ -217,125 +121,15 @@ function migrateLegacyRegisteredGroupsTable(
|
||||
}
|
||||
}
|
||||
|
||||
function loadValidatedLegacyRegisteredGroupsJson(
|
||||
database: Database,
|
||||
): Array<{ jid: string; group: RegisteredGroup }> {
|
||||
const legacyJsonPath = path.join(DATA_DIR, 'registered_groups.json');
|
||||
const legacyJson = readJsonFile(legacyJsonPath) as Record<
|
||||
string,
|
||||
RegisteredGroup
|
||||
> | null;
|
||||
|
||||
if (!legacyJson) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = Object.entries(legacyJson);
|
||||
for (const [jid, group] of entries) {
|
||||
if (!isValidGroupFolder(group.folder)) {
|
||||
throw new Error(
|
||||
`Invalid legacy registered_groups.json folder for ${jid}: ${group.folder}`,
|
||||
);
|
||||
}
|
||||
|
||||
const existing = getStoredRoomSettingsRowFromDatabase(database, jid);
|
||||
if (existing && !jsonEntryMatchesStoredRoom(jid, group, database)) {
|
||||
throw new Error(
|
||||
`Legacy registered_groups.json entry collides with existing room_settings row for ${jid}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return entries.map(([jid, group]) => ({ jid, group }));
|
||||
}
|
||||
|
||||
function migrateLegacyRegisteredGroupsJson(
|
||||
database: Database,
|
||||
projectRoot: string,
|
||||
report: MigrationReport,
|
||||
): void {
|
||||
const legacyJsonPath = path.join(DATA_DIR, 'registered_groups.json');
|
||||
const legacyJsonEntries = loadValidatedLegacyRegisteredGroupsJson(database);
|
||||
if (legacyJsonEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const insertRoom = database.prepare(
|
||||
`INSERT INTO room_settings (
|
||||
chat_jid,
|
||||
room_mode,
|
||||
mode_source,
|
||||
name,
|
||||
folder,
|
||||
trigger_pattern,
|
||||
requires_trigger,
|
||||
is_main,
|
||||
owner_agent_type,
|
||||
work_dir,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(chat_jid) DO NOTHING`,
|
||||
);
|
||||
|
||||
for (const { jid, group } of legacyJsonEntries) {
|
||||
const createdAt = group.added_at || new Date().toISOString();
|
||||
const ownerAgentType =
|
||||
normalizeStoredAgentType(group.agentType) ?? 'claude-code';
|
||||
const existing = getStoredRoomSettingsRowFromDatabase(database, jid);
|
||||
const existingOwnerOverride = getStoredOwnerRoleOverride(database, jid);
|
||||
|
||||
if (!existing) {
|
||||
insertRoom.run(
|
||||
jid,
|
||||
'single',
|
||||
'inferred',
|
||||
group.name,
|
||||
group.folder,
|
||||
group.trigger,
|
||||
(group.requiresTrigger ?? true) ? 1 : 0,
|
||||
(group.isMain ?? false) ? 1 : 0,
|
||||
ownerAgentType,
|
||||
group.workDir ?? null,
|
||||
createdAt,
|
||||
createdAt,
|
||||
);
|
||||
report.migratedRooms += 1;
|
||||
report.migratedJsonRooms += 1;
|
||||
}
|
||||
|
||||
if (
|
||||
existingOwnerOverride &&
|
||||
!jsonEntryMatchesStoredOwnerOverride(database, jid, group)
|
||||
) {
|
||||
throw new Error(
|
||||
`Legacy registered_groups.json owner override conflicts with existing owner override for ${jid}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!existingOwnerOverride) {
|
||||
upsertRoomRoleOverride(database, jid, {
|
||||
role: 'owner',
|
||||
agentType: ownerAgentType,
|
||||
agentConfig: group.agentConfig,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
});
|
||||
report.migratedRoleOverrides += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fs.renameSync(legacyJsonPath, `${legacyJsonPath}.migrated`);
|
||||
report.renamedLegacyJson = true;
|
||||
logger.info(
|
||||
{ legacyJsonPath, projectRoot },
|
||||
'Renamed registered_groups.json after explicit migration',
|
||||
);
|
||||
}
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
fs.mkdirSync(STORE_DIR, { recursive: true });
|
||||
const projectRoot = process.cwd();
|
||||
const unexpectedDataStateFiles = listUnexpectedDataStateFiles(DATA_DIR);
|
||||
if (unexpectedDataStateFiles.length > 0) {
|
||||
throw new Error(
|
||||
`Unexpected data state files detected before room migration (files=${unexpectedDataStateFiles.join(',')})`,
|
||||
);
|
||||
}
|
||||
|
||||
const database = openPersistentDatabase();
|
||||
initializeDatabaseSchema(database);
|
||||
|
||||
@@ -343,16 +137,12 @@ export async function run(_args: string[]): Promise<void> {
|
||||
migratedRooms: 0,
|
||||
migratedRoleOverrides: 0,
|
||||
skippedRooms: 0,
|
||||
migratedJsonRooms: 0,
|
||||
skippedJsonRooms: 0,
|
||||
backedUpLegacyRows: 0,
|
||||
renamedLegacyJson: false,
|
||||
};
|
||||
|
||||
database.transaction(() => {
|
||||
report.backedUpLegacyRows = backupPendingLegacyRows(database);
|
||||
migrateLegacyRegisteredGroupsTable(database, report);
|
||||
migrateLegacyRegisteredGroupsJson(database, projectRoot, report);
|
||||
})();
|
||||
|
||||
database.close();
|
||||
@@ -361,10 +151,7 @@ export async function run(_args: string[]): Promise<void> {
|
||||
MIGRATED_ROOMS: report.migratedRooms,
|
||||
MIGRATED_ROLE_OVERRIDES: report.migratedRoleOverrides,
|
||||
SKIPPED_ROOMS: report.skippedRooms,
|
||||
MIGRATED_JSON_ROOMS: report.migratedJsonRooms,
|
||||
SKIPPED_JSON_ROOMS: report.skippedJsonRooms,
|
||||
BACKED_UP_LEGACY_ROWS: report.backedUpLegacyRows,
|
||||
RENAMED_LEGACY_JSON: report.renamedLegacyJson,
|
||||
STATUS: 'success',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
|
||||
@@ -4,16 +4,16 @@ import path from 'path';
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
import { STORE_DIR } from '../src/config.js';
|
||||
import { listUnexpectedDataStateFiles } from '../src/data-state-files.js';
|
||||
import { countPendingLegacyRegisteredGroupRows } from '../src/db/room-registration.js';
|
||||
|
||||
export interface AssignedRoomsSummary {
|
||||
assignedRooms: number;
|
||||
roomsByOwnerAgent: Record<string, number>;
|
||||
legacyRegisteredGroupRows: number;
|
||||
hasLegacyRegisteredGroupsJson: boolean;
|
||||
legacyRoomMigrationRequired: boolean;
|
||||
pendingLegacyJsonStateFiles: string[];
|
||||
legacyJsonStateMigrationRequired: boolean;
|
||||
unexpectedDataStateFiles: string[];
|
||||
unexpectedDataStateDetected: boolean;
|
||||
}
|
||||
|
||||
export interface LegacyMigrationGuidance {
|
||||
@@ -25,50 +25,36 @@ function getDataDir(projectRoot: string): string {
|
||||
return process.env.EJCLAW_DATA_DIR || path.join(projectRoot, 'data');
|
||||
}
|
||||
|
||||
function getPendingLegacyJsonStateFiles(projectRoot: string): string[] {
|
||||
const dataDir = getDataDir(projectRoot);
|
||||
return ['router_state.json', 'sessions.json'].filter((filename) =>
|
||||
fs.existsSync(path.join(dataDir, filename)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getLegacyMigrationGuidance(
|
||||
summary: Pick<
|
||||
AssignedRoomsSummary,
|
||||
'legacyRoomMigrationRequired' | 'legacyJsonStateMigrationRequired'
|
||||
'legacyRoomMigrationRequired' | 'unexpectedDataStateDetected'
|
||||
>,
|
||||
target: 'setup' | 'verification',
|
||||
): LegacyMigrationGuidance | undefined {
|
||||
if (
|
||||
!summary.legacyRoomMigrationRequired &&
|
||||
!summary.legacyJsonStateMigrationRequired
|
||||
!summary.unexpectedDataStateDetected
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const steps: string[] = [];
|
||||
if (summary.legacyRoomMigrationRequired) {
|
||||
steps.push('bun setup/index.ts --step migrate-room-registrations');
|
||||
}
|
||||
if (summary.legacyJsonStateMigrationRequired) {
|
||||
steps.push('bun setup/index.ts --step migrate-json-state');
|
||||
}
|
||||
|
||||
const error =
|
||||
summary.legacyRoomMigrationRequired &&
|
||||
summary.legacyJsonStateMigrationRequired
|
||||
? 'legacy_migration_required'
|
||||
summary.legacyRoomMigrationRequired && summary.unexpectedDataStateDetected
|
||||
? 'legacy_migration_and_unexpected_data_state_detected'
|
||||
: summary.legacyRoomMigrationRequired
|
||||
? 'legacy_room_migration_required'
|
||||
: 'legacy_json_state_migration_required';
|
||||
const commandText =
|
||||
steps.length === 1
|
||||
? `Run \`${steps[0]}\` before continuing with ${target}`
|
||||
: `Run \`${steps[0]}\` and \`${steps[1]}\` before continuing with ${target}`;
|
||||
: 'unexpected_data_state_files_detected';
|
||||
const nextStep =
|
||||
summary.legacyRoomMigrationRequired && summary.unexpectedDataStateDetected
|
||||
? `Run \`bun setup/index.ts --step migrate-room-registrations\` and remove or archive unexpected data state files before continuing with ${target}`
|
||||
: summary.legacyRoomMigrationRequired
|
||||
? `Run \`bun setup/index.ts --step migrate-room-registrations\` before continuing with ${target}`
|
||||
: `Remove or archive unexpected data state files before continuing with ${target}`;
|
||||
|
||||
return {
|
||||
error,
|
||||
nextStep: commandText,
|
||||
nextStep,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,23 +113,18 @@ export function detectRoomRegistrationState(
|
||||
}
|
||||
}
|
||||
|
||||
const hasLegacyRegisteredGroupsJson = fs.existsSync(
|
||||
path.join(getDataDir(projectRoot), 'registered_groups.json'),
|
||||
const unexpectedDataStateFiles = listUnexpectedDataStateFiles(
|
||||
getDataDir(projectRoot),
|
||||
);
|
||||
const pendingLegacyJsonStateFiles =
|
||||
getPendingLegacyJsonStateFiles(projectRoot);
|
||||
const legacyRoomMigrationRequired =
|
||||
legacyRegisteredGroupRows > 0 || hasLegacyRegisteredGroupsJson;
|
||||
const legacyJsonStateMigrationRequired =
|
||||
pendingLegacyJsonStateFiles.length > 0;
|
||||
const legacyRoomMigrationRequired = legacyRegisteredGroupRows > 0;
|
||||
const unexpectedDataStateDetected = unexpectedDataStateFiles.length > 0;
|
||||
|
||||
return {
|
||||
assignedRooms,
|
||||
roomsByOwnerAgent,
|
||||
legacyRegisteredGroupRows,
|
||||
hasLegacyRegisteredGroupsJson,
|
||||
legacyRoomMigrationRequired,
|
||||
pendingLegacyJsonStateFiles,
|
||||
legacyJsonStateMigrationRequired,
|
||||
unexpectedDataStateFiles,
|
||||
unexpectedDataStateDetected,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,10 +128,9 @@ describe('verify state helpers', () => {
|
||||
codex: 2,
|
||||
},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,10 +156,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 0,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 1,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: true,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -268,10 +266,9 @@ describe('verify state helpers', () => {
|
||||
codex: 1,
|
||||
},
|
||||
legacyRegisteredGroupRows: 1,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: true,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -396,15 +393,14 @@ describe('verify state helpers', () => {
|
||||
codex: 1,
|
||||
},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('marks legacy registered_groups.json as migration-required', () => {
|
||||
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 });
|
||||
@@ -417,14 +413,13 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 0,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: true,
|
||||
legacyRoomMigrationRequired: true,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
unexpectedDataStateFiles: ['registered_groups.json'],
|
||||
unexpectedDataStateDetected: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('marks pending legacy json state files as migration-required', () => {
|
||||
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 });
|
||||
@@ -441,10 +436,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 0,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: ['router_state.json', 'sessions.json'],
|
||||
legacyJsonStateMigrationRequired: true,
|
||||
unexpectedDataStateFiles: ['router_state.json', 'sessions.json'],
|
||||
unexpectedDataStateDetected: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -474,10 +468,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 2,
|
||||
roomsByOwnerAgent: { codex: 1 },
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
),
|
||||
).toMatchObject({
|
||||
@@ -502,10 +495,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 0,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
),
|
||||
).toMatchObject({
|
||||
@@ -528,10 +520,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 1,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
),
|
||||
).toMatchObject({
|
||||
@@ -556,10 +547,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 1,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
{ tribunalRooms: 1 },
|
||||
),
|
||||
@@ -588,10 +578,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 1,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
{ tribunalRooms: 1, activeArbiterTasks: 1 },
|
||||
),
|
||||
@@ -617,10 +606,9 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 0,
|
||||
roomsByOwnerAgent: {},
|
||||
legacyRegisteredGroupRows: 2,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: true,
|
||||
pendingLegacyJsonStateFiles: [],
|
||||
legacyJsonStateMigrationRequired: false,
|
||||
unexpectedDataStateFiles: [],
|
||||
unexpectedDataStateDetected: false,
|
||||
},
|
||||
),
|
||||
).toMatchObject({
|
||||
@@ -630,7 +618,7 @@ describe('verify state helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails verification when legacy json state migration is still required', () => {
|
||||
it('fails verification when unexpected data state files are still present', () => {
|
||||
const services: ServiceCheck[] = [{ name: 'ejclaw', status: 'running' }];
|
||||
|
||||
expect(
|
||||
@@ -643,16 +631,15 @@ describe('verify state helpers', () => {
|
||||
assignedRooms: 1,
|
||||
roomsByOwnerAgent: { codex: 1 },
|
||||
legacyRegisteredGroupRows: 0,
|
||||
hasLegacyRegisteredGroupsJson: false,
|
||||
legacyRoomMigrationRequired: false,
|
||||
pendingLegacyJsonStateFiles: ['router_state.json'],
|
||||
legacyJsonStateMigrationRequired: true,
|
||||
unexpectedDataStateFiles: ['router_state.json'],
|
||||
unexpectedDataStateDetected: true,
|
||||
},
|
||||
),
|
||||
).toMatchObject({
|
||||
status: 'failed',
|
||||
legacyJsonStateMigrationRequired: true,
|
||||
pendingLegacyJsonStateFiles: ['router_state.json'],
|
||||
unexpectedDataStateDetected: true,
|
||||
unexpectedDataStateFiles: ['router_state.json'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,7 +162,7 @@ export function buildVerifySummary(
|
||||
arbiterRoutingSatisfied &&
|
||||
roomSummary.assignedRooms > 0 &&
|
||||
!roomSummary.legacyRoomMigrationRequired &&
|
||||
!roomSummary.legacyJsonStateMigrationRequired
|
||||
!roomSummary.unexpectedDataStateDetected
|
||||
? 'success'
|
||||
: 'failed';
|
||||
|
||||
|
||||
@@ -95,13 +95,9 @@ export async function run(_args: string[]): Promise<void> {
|
||||
ASSIGNED_ROOMS: roomSummary.assignedRooms,
|
||||
ROOMS_BY_OWNER_AGENT: JSON.stringify(roomSummary.roomsByOwnerAgent),
|
||||
LEGACY_REGISTERED_GROUP_ROWS: roomSummary.legacyRegisteredGroupRows,
|
||||
HAS_LEGACY_REGISTERED_GROUPS_JSON:
|
||||
roomSummary.hasLegacyRegisteredGroupsJson,
|
||||
LEGACY_ROOM_MIGRATION_REQUIRED: roomSummary.legacyRoomMigrationRequired,
|
||||
PENDING_LEGACY_JSON_STATE_FILES:
|
||||
roomSummary.pendingLegacyJsonStateFiles.join(','),
|
||||
LEGACY_JSON_STATE_MIGRATION_REQUIRED:
|
||||
roomSummary.legacyJsonStateMigrationRequired,
|
||||
UNEXPECTED_DATA_STATE_FILES: roomSummary.unexpectedDataStateFiles.join(','),
|
||||
UNEXPECTED_DATA_STATE_DETECTED: roomSummary.unexpectedDataStateDetected,
|
||||
REVIEWER_CHANNEL_CONFIGURED: reviewerChannelConfigured,
|
||||
ARBITER_CHANNEL_CONFIGURED: arbiterChannelConfigured,
|
||||
...(legacyMigrationGuidance
|
||||
|
||||
Reference in New Issue
Block a user