Convex control plane (Slices 1-4 prerequisites for Slice 5): - Versioned Definition/Design persistence: revisions preserve history instead of deleting prior slices/approvals - Fenced conversation turn queue: attempt-numbered leases prevent stale worker overwrites; expired turns reconciled by cron - Fenced Run/Attempt claiming: only queued attempts from running Runs can be claimed; lease expiry checks on every finish/checkpoint - Multi-slice progression: successful slice marks next slice ready instead of completing the whole Work; completed Runs blocked from retry - Typed AttemptClassification in schema and resolver decisions table - Durable resolver decisions for normal outcomes, cancellation, and lease-expiry recovery - Persistent artifacts and delivery metadata with provenance, source revision, verification status, and controlled delivery transitions - High-impact open questions block definition approval - Question submission gated to defining/awaiting-approval states only - Delivery status updates validate JSON metadata before persisting - Planner failures recorded as durable blocked events - Approval identity uses canonical tokenIdentifier Primitives: - decodeDefinition separates decode from validate - Design validation enforces 1-4 slices with unique IDs and valid deps - Artifact and delivery draft schemas with verified-revision binding - Delivery status transition table Package management: - Consolidated shared deps into single catalog (hono, valibot, streamdown, @types/react, @types/node, @tailwindcss/*, react-native) - Removed cross-version Hono boundary: flue() exported as Fetchable - Deleted bun.lock and regenerated from clean catalog resolution Lint/format: - Removed .eslintignore; oxc uses native ignorePatterns in oxlint.config.ts - Deleted redundant packages/backend/.oxlintrc.json - Added repos/** and scripts/** to ignore patterns - Convex-specific rule overrides for ES2022 target constraints - Fixed all oxc errors in fluePersistence.ts and agents/src/db.ts - Fixed all formatting across changed files
175 lines
5.4 KiB
TypeScript
175 lines
5.4 KiB
TypeScript
import { Effect } from "effect";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import { FakeHarnessLive } from "./harness-runtime";
|
|
import { defaultCodingKitV0, resolveOutcome, shouldRetry } from "./resolver";
|
|
import { decodeDefinition, validateDefinition } from "./work-definition";
|
|
import { validateDesignPacket } from "./work-design";
|
|
import { canTransitionWork } from "./work-lifecycle";
|
|
|
|
const definition = {
|
|
acceptanceCriteria: ["terminal outcome"],
|
|
affectedUsers: ["founders"],
|
|
assumptions: [],
|
|
constraints: [],
|
|
desiredOutcome: "A visible terminal simulation exists",
|
|
inScope: ["fake execution"],
|
|
outOfScope: ["real sandboxing"],
|
|
problem: "A deterministic build path is needed",
|
|
questions: [
|
|
{
|
|
alternatives: [],
|
|
id: "q1",
|
|
impact: "high",
|
|
prompt: "Which runtime?",
|
|
status: "open",
|
|
},
|
|
],
|
|
requiredArtifacts: ["events"],
|
|
risk: "medium",
|
|
version: 1,
|
|
};
|
|
|
|
const probeOutcome = (
|
|
classification: Parameters<typeof resolveOutcome>[0]["classification"],
|
|
retryable = false
|
|
) => ({ classification, retryable, summary: "probe" });
|
|
|
|
describe("work resolution contracts", () => {
|
|
test("high-impact open questions block definition approval", async () => {
|
|
await expect(
|
|
Effect.runPromise(decodeDefinition(definition))
|
|
).resolves.toEqual(definition);
|
|
await expect(
|
|
Effect.runPromise(validateDefinition(definition))
|
|
).rejects.toThrow(/High-impact/u);
|
|
});
|
|
|
|
test("question identities are unique within a Definition version", async () => {
|
|
await expect(
|
|
Effect.runPromise(
|
|
decodeDefinition({
|
|
...definition,
|
|
questions: [definition.questions[0], definition.questions[0]],
|
|
})
|
|
)
|
|
).rejects.toThrow(/Question ID must be unique/u);
|
|
});
|
|
|
|
test("observable slices and terminal fake outcomes are enforced", async () => {
|
|
const approved = await Effect.runPromise(
|
|
validateDefinition({ ...definition, questions: [] })
|
|
);
|
|
const design = await Effect.runPromise(
|
|
validateDesignPacket({
|
|
architectureSummary: "small",
|
|
callFlowDelta: [],
|
|
concerns: [],
|
|
definitionVersion: approved.version,
|
|
evidenceRequirements: ["event"],
|
|
fileTreeDelta: [],
|
|
impactMap: { files: [], modules: [], risks: [], summary: "small" },
|
|
invariants: ["terminal"],
|
|
keyInterfaces: [],
|
|
slices: [
|
|
{
|
|
codeBoundaries: ["runtime"],
|
|
dependsOn: [],
|
|
evidenceRequirements: ["event"],
|
|
id: "s1",
|
|
objective: "one",
|
|
observableBehavior: "visible",
|
|
reviewRequired: false,
|
|
title: "one",
|
|
verification: ["assert"],
|
|
},
|
|
],
|
|
tradeoffs: [],
|
|
version: 1,
|
|
})
|
|
);
|
|
expect(design.slices).toHaveLength(1);
|
|
const [, outcome] = await Effect.runPromise(
|
|
FakeHarnessLive(() => 1).run({ scenario: "success" })
|
|
);
|
|
expect(outcome.classification).toBe("Succeeded");
|
|
expect(outcome.summary).toMatch(/no implementation/u);
|
|
const [, firstTransient] = await Effect.runPromise(
|
|
FakeHarnessLive(() => 1).run({
|
|
attemptNumber: 1,
|
|
scenario: "transient-failure-then-success",
|
|
})
|
|
);
|
|
const [, recoveredTransient] = await Effect.runPromise(
|
|
FakeHarnessLive(() => 1).run({
|
|
attemptNumber: 2,
|
|
scenario: "transient-failure-then-success",
|
|
})
|
|
);
|
|
expect(firstTransient.classification).toBe("RetryableFailure");
|
|
expect(recoveredTransient.classification).toBe("Succeeded");
|
|
expect(
|
|
shouldRetry(
|
|
{
|
|
classification: "RetryableFailure",
|
|
retryable: true,
|
|
summary: "retry",
|
|
},
|
|
1,
|
|
defaultCodingKitV0.retryPolicy
|
|
)
|
|
).toBe(true);
|
|
expect(canTransitionWork("ready", "executing")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolver decisions", () => {
|
|
const policy = defaultCodingKitV0.retryPolicy;
|
|
|
|
test("retries a retryable failure while the budget remains", () => {
|
|
expect(
|
|
resolveOutcome(probeOutcome("RetryableFailure", true), 1, policy)
|
|
).toEqual({ kind: "retry" });
|
|
});
|
|
|
|
test("settles RetryableFailure as failed once the budget is exhausted", () => {
|
|
expect(
|
|
resolveOutcome(probeOutcome("RetryableFailure", true), 3, policy)
|
|
).toEqual({
|
|
kind: "terminal",
|
|
workStatus: "failed",
|
|
});
|
|
});
|
|
|
|
test("maps each terminal classification to its Work status", () => {
|
|
expect(resolveOutcome(probeOutcome("Succeeded"), 1, policy)).toEqual({
|
|
kind: "terminal",
|
|
workStatus: "completed",
|
|
});
|
|
expect(resolveOutcome(probeOutcome("PermanentFailure"), 1, policy)).toEqual(
|
|
{
|
|
kind: "terminal",
|
|
workStatus: "failed",
|
|
}
|
|
);
|
|
expect(resolveOutcome(probeOutcome("NeedsInput"), 1, policy)).toEqual({
|
|
kind: "terminal",
|
|
workStatus: "needs-input",
|
|
});
|
|
expect(resolveOutcome(probeOutcome("Blocked"), 1, policy)).toEqual({
|
|
kind: "terminal",
|
|
workStatus: "blocked",
|
|
});
|
|
expect(resolveOutcome(probeOutcome("Cancelled"), 1, policy)).toEqual({
|
|
kind: "terminal",
|
|
workStatus: "ready",
|
|
});
|
|
});
|
|
|
|
test("completed and cancelled are not silently reopened", () => {
|
|
expect(canTransitionWork("completed", "defining")).toBe(false);
|
|
expect(canTransitionWork("cancelled", "proposed")).toBe(false);
|
|
expect(canTransitionWork("ready", "executing")).toBe(true);
|
|
});
|
|
});
|