maestro/src/worker.reflection-dispatch.test.ts
2026-06-03 05:08:00 +00:00

115 lines
3.8 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { Worker } from './worker.js';
import type { AppConfig } from './config.js';
import type { Job } from './db/repository.js';
function makeConfig(): AppConfig {
return {
provider: {
model: 'test-model',
workers: [{ id: 'worker-1', endpoint: 'http://localhost:11434/v1', roles: ['auto', 'fast', 'quality', 'reflection'] }],
},
worktreeDir: '/tmp/worker-reflection-dispatch-test',
concurrency: 1,
maxMovements: 30,
retry: { maxAttempts: 3, backoffSeconds: [60, 300, 900] },
ask: { maxPerJob: 2 },
subtasks: { maxDepth: 2 },
tools: {
searxngUrl: 'http://localhost:8080',
visionModel: 'vision',
visionTimeout: 60,
visionMaxTokens: 1024,
webfetchTimeout: 30,
websearchTimeout: 15,
webfetchAllowedHosts: [],
},
};
}
function makeReflectionJob(): Job {
return {
id: 'job-reflect-1',
repo: 'local/reflection-job-orig-1',
issueNumber: 0,
prNumber: null,
status: 'running',
pieceName: 'reflection',
requiredRole: 'reflection',
requiredProfile: 'reflection',
currentMovement: null,
instruction: '',
branchName: null,
worktreePath: null,
attempt: 1,
maxAttempts: 3,
nextRetryAt: null,
errorSummary: null,
resumeMovement: null,
askCount: 0,
workerId: 'worker-1',
parentJobId: null,
subtaskDepth: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
taskKind: 'reflection',
payload: JSON.stringify({
originalJobId: 'job-orig-1',
userId: 'u-1',
pieceName: 'chat',
outcome: 'succeeded',
}),
ownerId: 'u-1',
visibility: 'private',
visibilityScopeOrgId: null,
browserSessionProfileId: null,
} as unknown as Job;
}
describe('worker reflection dispatch', () => {
it('a task_kind=reflection job reaches a terminal state without going through the agent loop', async () => {
// Track all updateJob calls so we can verify a terminal status is set.
const updateJobCalls: Array<[string, Record<string, unknown>]> = [];
const repo = {
lockIssue: vi.fn().mockResolvedValue(true),
unlockIssue: vi.fn().mockResolvedValue(undefined),
addAuditLog: vi.fn().mockResolvedValue(undefined),
updateWorkerNodeHealth: vi.fn().mockResolvedValue(undefined),
updateJob: vi.fn().mockImplementation((id: string, patch: Record<string, unknown>) => {
updateJobCalls.push([id, patch]);
return Promise.resolve(undefined);
}),
};
const worker = new Worker(
'worker-1',
'http://localhost:11434/v1',
'test-model',
repo as never,
makeConfig(),
);
const job = makeReflectionJob();
await (worker as unknown as { executeJob: (job: Job) => Promise<void> }).executeJob(job);
// handleReflectionJob must have called updateJob with a terminal status.
// Note: the reflection-runner skeleton returns 'failed', so the status will be
// 'failed' until Phase 3.2+ implements real logic. We accept either terminal status
// to avoid coupling the test to the stub's return value.
const terminalCall = updateJobCalls.find(
([id, patch]) =>
id === 'job-reflect-1' &&
(patch.status === 'succeeded' || patch.status === 'failed'),
);
expect(terminalCall).toBeDefined();
// The agent-loop path (loadPiece) must NOT have been called.
// We verify this indirectly: lockIssue was called (got past acquireJobOrRequeue),
// but no 'Piece not found' / loadPiece side-effects occurred
// (the mock repo has no loadPiece, so if it were called it would throw).
expect(repo.lockIssue).toHaveBeenCalledWith('local/reflection-job-orig-1', 0, 'job-reflect-1');
expect(repo.unlockIssue).toHaveBeenCalledWith('local/reflection-job-orig-1', 0);
});
});