All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Detailed comparison showing: - Existing doc addresses L-015 (premature completion) - New doc addresses context exhaustion (multi-issue orchestration) - ~20% overlap (both use non-AI coordinator, mechanical gates) - 80% complementary (different problems, different solutions) Recommends merging into comprehensive document (already done). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
480 lines
13 KiB
TypeScript
480 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { QualityOrchestratorService } from "./quality-orchestrator.service";
|
|
import { TokenBudgetService } from "../token-budget/token-budget.service";
|
|
import type {
|
|
QualityGate,
|
|
CompletionClaim,
|
|
OrchestrationConfig,
|
|
CompletionValidation,
|
|
} from "./interfaces";
|
|
|
|
describe("QualityOrchestratorService", () => {
|
|
let service: QualityOrchestratorService;
|
|
|
|
const mockWorkspaceId = "workspace-1";
|
|
const mockTaskId = "task-1";
|
|
const mockAgentId = "agent-1";
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
QualityOrchestratorService,
|
|
{
|
|
provide: TokenBudgetService,
|
|
useValue: {
|
|
checkSuspiciousDoneClaim: vi.fn().mockResolvedValue({ suspicious: false }),
|
|
},
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<QualityOrchestratorService>(QualityOrchestratorService);
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
describe("validateCompletion", () => {
|
|
const claim: CompletionClaim = {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Task completed successfully",
|
|
filesChanged: ["src/test.ts"],
|
|
};
|
|
|
|
const config: OrchestrationConfig = {
|
|
workspaceId: mockWorkspaceId,
|
|
gates: [
|
|
{
|
|
id: "build",
|
|
name: "Build Check",
|
|
description: "Verify code compiles",
|
|
type: "build",
|
|
command: "echo 'build success'",
|
|
required: true,
|
|
order: 1,
|
|
},
|
|
],
|
|
maxContinuations: 3,
|
|
continuationBudget: 10000,
|
|
strictMode: false,
|
|
};
|
|
|
|
it("should accept completion when all required gates pass", async () => {
|
|
const result = await service.validateCompletion(claim, config);
|
|
|
|
expect(result.verdict).toBe("accepted");
|
|
expect(result.allGatesPassed).toBe(true);
|
|
expect(result.requiredGatesFailed).toHaveLength(0);
|
|
});
|
|
|
|
it("should reject completion when required gates fail", async () => {
|
|
const failingConfig: OrchestrationConfig = {
|
|
...config,
|
|
gates: [
|
|
{
|
|
id: "build",
|
|
name: "Build Check",
|
|
description: "Verify code compiles",
|
|
type: "build",
|
|
command: "exit 1",
|
|
required: true,
|
|
order: 1,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await service.validateCompletion(claim, failingConfig);
|
|
|
|
expect(result.verdict).toBe("rejected");
|
|
expect(result.allGatesPassed).toBe(false);
|
|
expect(result.requiredGatesFailed).toContain("build");
|
|
});
|
|
|
|
it("should accept when optional gates fail but required gates pass", async () => {
|
|
const mixedConfig: OrchestrationConfig = {
|
|
...config,
|
|
gates: [
|
|
{
|
|
id: "build",
|
|
name: "Build Check",
|
|
description: "Verify code compiles",
|
|
type: "build",
|
|
command: "echo 'success'",
|
|
required: true,
|
|
order: 1,
|
|
},
|
|
{
|
|
id: "coverage",
|
|
name: "Coverage Check",
|
|
description: "Check coverage",
|
|
type: "coverage",
|
|
command: "exit 1",
|
|
required: false,
|
|
order: 2,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await service.validateCompletion(claim, mixedConfig);
|
|
|
|
expect(result.verdict).toBe("accepted");
|
|
expect(result.allGatesPassed).toBe(false);
|
|
expect(result.requiredGatesFailed).toHaveLength(0);
|
|
});
|
|
|
|
it("should provide feedback when gates fail", async () => {
|
|
const failingConfig: OrchestrationConfig = {
|
|
...config,
|
|
gates: [
|
|
{
|
|
id: "test",
|
|
name: "Test Suite",
|
|
description: "Run tests",
|
|
type: "test",
|
|
command: "exit 1",
|
|
required: true,
|
|
order: 1,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await service.validateCompletion(claim, failingConfig);
|
|
|
|
expect(result.feedback).toBeDefined();
|
|
expect(result.suggestedActions).toBeDefined();
|
|
expect(result.suggestedActions!.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should run gates in order", async () => {
|
|
const orderedConfig: OrchestrationConfig = {
|
|
...config,
|
|
gates: [
|
|
{
|
|
id: "gate3",
|
|
name: "Third Gate",
|
|
description: "Third",
|
|
type: "custom",
|
|
command: "echo 'third'",
|
|
required: true,
|
|
order: 3,
|
|
},
|
|
{
|
|
id: "gate1",
|
|
name: "First Gate",
|
|
description: "First",
|
|
type: "custom",
|
|
command: "echo 'first'",
|
|
required: true,
|
|
order: 1,
|
|
},
|
|
{
|
|
id: "gate2",
|
|
name: "Second Gate",
|
|
description: "Second",
|
|
type: "custom",
|
|
command: "echo 'second'",
|
|
required: true,
|
|
order: 2,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await service.validateCompletion(claim, orderedConfig);
|
|
|
|
expect(result.gateResults[0].gateId).toBe("gate1");
|
|
expect(result.gateResults[1].gateId).toBe("gate2");
|
|
expect(result.gateResults[2].gateId).toBe("gate3");
|
|
});
|
|
});
|
|
|
|
describe("runGate", () => {
|
|
it("should successfully run a gate with passing command", async () => {
|
|
const gate: QualityGate = {
|
|
id: "test-gate",
|
|
name: "Test Gate",
|
|
description: "Test description",
|
|
type: "custom",
|
|
command: "echo 'success'",
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
const result = await service.runGate(gate);
|
|
|
|
expect(result.gateId).toBe("test-gate");
|
|
expect(result.gateName).toBe("Test Gate");
|
|
expect(result.passed).toBe(true);
|
|
expect(result.duration).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should fail a gate with failing command", async () => {
|
|
const gate: QualityGate = {
|
|
id: "fail-gate",
|
|
name: "Failing Gate",
|
|
description: "Should fail",
|
|
type: "custom",
|
|
command: "exit 1",
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
const result = await service.runGate(gate);
|
|
|
|
expect(result.passed).toBe(false);
|
|
expect(result.error).toBeDefined();
|
|
});
|
|
|
|
it("should capture output from gate execution", async () => {
|
|
const gate: QualityGate = {
|
|
id: "output-gate",
|
|
name: "Output Gate",
|
|
description: "Captures output",
|
|
type: "custom",
|
|
command: "echo 'test output'",
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
const result = await service.runGate(gate);
|
|
|
|
expect(result.output).toContain("test output");
|
|
});
|
|
|
|
it("should validate expected output pattern", async () => {
|
|
const gate: QualityGate = {
|
|
id: "pattern-gate",
|
|
name: "Pattern Gate",
|
|
description: "Checks output pattern",
|
|
type: "custom",
|
|
command: "echo 'coverage: 90%'",
|
|
expectedOutput: /coverage: \d+%/,
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
const result = await service.runGate(gate);
|
|
|
|
expect(result.passed).toBe(true);
|
|
});
|
|
|
|
it("should fail when expected output pattern does not match", async () => {
|
|
const gate: QualityGate = {
|
|
id: "bad-pattern-gate",
|
|
name: "Bad Pattern Gate",
|
|
description: "Pattern should not match",
|
|
type: "custom",
|
|
command: "echo 'no coverage info'",
|
|
expectedOutput: /coverage: \d+%/,
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
const result = await service.runGate(gate);
|
|
|
|
expect(result.passed).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("shouldContinue", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["test"],
|
|
verdict: "needs-continuation",
|
|
};
|
|
|
|
const config: OrchestrationConfig = {
|
|
workspaceId: mockWorkspaceId,
|
|
gates: [],
|
|
maxContinuations: 3,
|
|
continuationBudget: 10000,
|
|
strictMode: false,
|
|
};
|
|
|
|
it("should continue when under max continuations", () => {
|
|
const result = service.shouldContinue(validation, 1, config);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("should not continue when at max continuations", () => {
|
|
const result = service.shouldContinue(validation, 3, config);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should not continue when validation is accepted", () => {
|
|
const acceptedValidation: CompletionValidation = {
|
|
...validation,
|
|
verdict: "accepted",
|
|
allGatesPassed: true,
|
|
requiredGatesFailed: [],
|
|
};
|
|
|
|
const result = service.shouldContinue(acceptedValidation, 1, config);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("generateContinuationPrompt", () => {
|
|
it("should generate prompt with failed gate information", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [
|
|
{
|
|
gateId: "test",
|
|
gateName: "Test Suite",
|
|
passed: false,
|
|
error: "Tests failed",
|
|
duration: 1000,
|
|
},
|
|
],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["test"],
|
|
verdict: "needs-continuation",
|
|
};
|
|
|
|
const prompt = service.generateContinuationPrompt(validation);
|
|
|
|
expect(prompt).toContain("Test Suite");
|
|
expect(prompt).toContain("failed");
|
|
});
|
|
|
|
it("should include suggested actions in prompt", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["lint"],
|
|
verdict: "needs-continuation",
|
|
suggestedActions: ["Run: pnpm lint --fix", "Check code style"],
|
|
};
|
|
|
|
const prompt = service.generateContinuationPrompt(validation);
|
|
|
|
expect(prompt).toContain("pnpm lint --fix");
|
|
expect(prompt).toContain("Check code style");
|
|
});
|
|
});
|
|
|
|
describe("generateRejectionFeedback", () => {
|
|
it("should generate detailed rejection feedback", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [
|
|
{
|
|
gateId: "build",
|
|
gateName: "Build Check",
|
|
passed: false,
|
|
error: "Compilation error",
|
|
duration: 500,
|
|
},
|
|
],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["build"],
|
|
verdict: "rejected",
|
|
};
|
|
|
|
const feedback = service.generateRejectionFeedback(validation);
|
|
|
|
expect(feedback).toContain("rejected");
|
|
expect(feedback).toContain("Build Check");
|
|
});
|
|
});
|
|
|
|
describe("getDefaultGates", () => {
|
|
it("should return default gates for workspace", () => {
|
|
const gates = service.getDefaultGates(mockWorkspaceId);
|
|
|
|
expect(gates).toBeDefined();
|
|
expect(gates.length).toBeGreaterThan(0);
|
|
expect(gates.some((g) => g.id === "build")).toBe(true);
|
|
expect(gates.some((g) => g.id === "lint")).toBe(true);
|
|
expect(gates.some((g) => g.id === "test")).toBe(true);
|
|
});
|
|
|
|
it("should return gates in correct order", () => {
|
|
const gates = service.getDefaultGates(mockWorkspaceId);
|
|
|
|
for (let i = 1; i < gates.length; i++) {
|
|
expect(gates[i].order).toBeGreaterThanOrEqual(gates[i - 1].order);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("recordContinuation", () => {
|
|
it("should record continuation attempt", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["test"],
|
|
verdict: "needs-continuation",
|
|
};
|
|
|
|
expect(() => service.recordContinuation(mockTaskId, 1, validation)).not.toThrow();
|
|
});
|
|
|
|
it("should handle multiple continuation records", () => {
|
|
const validation: CompletionValidation = {
|
|
claim: {
|
|
taskId: mockTaskId,
|
|
agentId: mockAgentId,
|
|
workspaceId: mockWorkspaceId,
|
|
claimedAt: new Date(),
|
|
message: "Done",
|
|
filesChanged: [],
|
|
},
|
|
gateResults: [],
|
|
allGatesPassed: false,
|
|
requiredGatesFailed: ["test"],
|
|
verdict: "needs-continuation",
|
|
};
|
|
|
|
service.recordContinuation(mockTaskId, 1, validation);
|
|
service.recordContinuation(mockTaskId, 2, validation);
|
|
|
|
expect(() => service.recordContinuation(mockTaskId, 3, validation)).not.toThrow();
|
|
});
|
|
});
|
|
});
|