refactor: simplify paired review system and add reviewer container isolation
Phase 1 — Strip over-engineering (-3,665 lines): - DB tables: 7 → 3 (remove executions, approvals, artifacts, events) - Task statuses: 11 → 5 (active, review_ready, in_review, merge_ready, completed) - Remove plan governance, event sourcing, gate/verdict, freshness guards - paired-execution-context: 980 → 299 lines - Session commands: remove /risk, /plan, /approve-plan, /request-plan-changes Phase 2 — Container isolation for reviewers: - Add container-runtime.ts (Docker abstraction) - Add credential-proxy.ts (API key injection without container exposure) - Add container-runner.ts (reviewer-specific read-only mount + tmpfs) - Add container/Dockerfile + agent-runner (Chromium, Claude Code, Codex) - agent-runner.ts: auto-route reviewer execution to container mode
This commit is contained in:
@@ -95,24 +95,13 @@ describe('paired /review command path', () => {
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
roomRoleContext: ownerContext,
|
||||
dedupeKey: 'msg-review-owner-1',
|
||||
});
|
||||
|
||||
const task = db.getLatestOpenPairedTaskForChat('dc:test');
|
||||
const events = db.listPairedEventsForTask(task!.id);
|
||||
|
||||
expect(result?.status).toBe('ready');
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.event_type).toBe('request_review');
|
||||
expect(events[0]?.dedupe_key).toBe('msg-review-owner-1');
|
||||
expect(events[0]?.source_fingerprint).toBeTruthy();
|
||||
expect(events[0]?.source_fingerprint).not.toBe(task?.source_ref);
|
||||
if (!result || result.status !== 'ready') {
|
||||
throw new Error('expected owner /review to prepare a ready snapshot');
|
||||
}
|
||||
expect(events[0]?.source_fingerprint).toBe(
|
||||
result.reviewerWorkspace.snapshot_source_fingerprint,
|
||||
);
|
||||
expect(result.reviewerWorkspace.snapshot_ref).toBeTruthy();
|
||||
});
|
||||
|
||||
it('reuses the db owner workspace when reviewer service handles /review', async () => {
|
||||
@@ -240,134 +229,12 @@ describe('paired /review command path', () => {
|
||||
status: 'pending',
|
||||
task: expect.objectContaining({
|
||||
id: task!.id,
|
||||
status: 'review_pending',
|
||||
status: 'review_ready',
|
||||
}),
|
||||
pendingReason: 'owner-workspace-not-ready',
|
||||
});
|
||||
expect(task?.status).toBe('review_pending');
|
||||
expect(task?.status).toBe('review_ready');
|
||||
expect(task?.review_requested_at).toBeTruthy();
|
||||
expect(db.listPairedEventsForTask(task!.id)).toEqual([]);
|
||||
expect(fs.existsSync(reviewerLocalOwnerDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('creates the first checkpointed request_review event only after owner workspace exists', async () => {
|
||||
const canonicalDir = path.join(tempRoot, 'canonical');
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
runGit(['init'], canonicalDir);
|
||||
runGit(['config', 'user.email', 'test@example.com'], canonicalDir);
|
||||
runGit(['config', 'user.name', 'EJClaw Test'], canonicalDir);
|
||||
fs.writeFileSync(path.join(canonicalDir, 'README.md'), 'original\n');
|
||||
runGit(['add', 'README.md'], canonicalDir);
|
||||
runGit(['commit', '-m', 'initial'], canonicalDir);
|
||||
|
||||
const group: RegisteredGroup = {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: '2026-03-28T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
workDir: canonicalDir,
|
||||
};
|
||||
|
||||
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-review');
|
||||
process.env.SERVICE_ID = 'codex-review';
|
||||
vi.resetModules();
|
||||
let { db, executionContext } = await loadModules();
|
||||
db.initDatabase();
|
||||
|
||||
const firstResult = executionContext.markRoomReviewReady({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
roomRoleContext: reviewerContext,
|
||||
dedupeKey: 'msg-review-pending-1',
|
||||
});
|
||||
|
||||
const task = db.getLatestOpenPairedTaskForChat('dc:test');
|
||||
expect(firstResult?.status).toBe('pending');
|
||||
expect(db.listPairedEventsForTask(task!.id)).toEqual([]);
|
||||
|
||||
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-owner');
|
||||
process.env.SERVICE_ID = 'codex-main';
|
||||
vi.resetModules();
|
||||
({ db, executionContext } = await loadModules());
|
||||
db.initDatabase();
|
||||
|
||||
executionContext.preparePairedExecutionContext({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
runId: 'run-owner',
|
||||
roomRoleContext: ownerContext,
|
||||
});
|
||||
|
||||
const secondResult = executionContext.markRoomReviewReady({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
roomRoleContext: ownerContext,
|
||||
dedupeKey: 'msg-review-owner-2',
|
||||
});
|
||||
|
||||
const events = db.listPairedEventsForTask(task!.id);
|
||||
expect(secondResult?.status).toBe('ready');
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.event_type).toBe('request_review');
|
||||
expect(events[0]?.dedupe_key).toBe('msg-review-owner-2');
|
||||
});
|
||||
|
||||
it('blocks /review when a high-risk task plan is not approved yet', async () => {
|
||||
const canonicalDir = path.join(tempRoot, 'canonical');
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
runGit(['init'], canonicalDir);
|
||||
runGit(['config', 'user.email', 'test@example.com'], canonicalDir);
|
||||
runGit(['config', 'user.name', 'EJClaw Test'], canonicalDir);
|
||||
fs.writeFileSync(path.join(canonicalDir, 'README.md'), 'original\n');
|
||||
runGit(['add', 'README.md'], canonicalDir);
|
||||
runGit(['commit', '-m', 'initial'], canonicalDir);
|
||||
|
||||
const group: RegisteredGroup = {
|
||||
name: 'Paired Room',
|
||||
folder: 'paired-room',
|
||||
trigger: '@codex',
|
||||
added_at: '2026-03-28T00:00:00.000Z',
|
||||
agentType: 'codex',
|
||||
workDir: canonicalDir,
|
||||
};
|
||||
|
||||
process.env.EJCLAW_DATA_DIR = path.join(tempRoot, 'data-owner');
|
||||
process.env.SERVICE_ID = 'codex-main';
|
||||
vi.resetModules();
|
||||
const { db, executionContext } = await loadModules();
|
||||
db.initDatabase();
|
||||
|
||||
const task = executionContext.preparePairedExecutionContext({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
runId: 'run-owner',
|
||||
roomRoleContext: ownerContext,
|
||||
})?.task;
|
||||
|
||||
expect(task).toBeTruthy();
|
||||
db.updatePairedTask(task!.id, {
|
||||
risk_level: 'high',
|
||||
plan_status: 'pending',
|
||||
status: 'plan_review_pending',
|
||||
updated_at: '2026-03-29T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const result = executionContext.markRoomReviewReady({
|
||||
group,
|
||||
chatJid: 'dc:test',
|
||||
roomRoleContext: ownerContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'blocked',
|
||||
task: expect.objectContaining({
|
||||
id: task!.id,
|
||||
risk_level: 'high',
|
||||
plan_status: 'pending',
|
||||
status: 'plan_review_pending',
|
||||
}),
|
||||
blockedReason: 'plan-review-required',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user