115 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|