Files
projectbrain-Public/backend/test/feature-delivery-policy.test.ts

183 lines
8.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { ApprovalState, DeliveryStatus, DeliveryValidationStatus } from "@prisma/client";
const mocks = vi.hoisted(() => {
const tx = {
deliveryPlan: { create: vi.fn(), update: vi.fn() },
initiative: { update: vi.fn() },
outboxEvent: { create: vi.fn() },
deliveryRun: { create: vi.fn(), update: vi.fn() },
deliveryValidation: { create: vi.fn(), deleteMany: vi.fn() },
findingInvestigation: { findUnique: vi.fn(), update: vi.fn() },
repositoryFinding: { update: vi.fn() },
};
const prisma = {
project: { findFirst: vi.fn() },
initiative: { findFirstOrThrow: vi.fn() },
deliveryPlan: { findFirst: vi.fn(), findMany: vi.fn() },
giteaRepositoryLink: { findFirst: vi.fn() },
deliveryRun: { findFirst: vi.fn() },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
};
return {
prisma,
tx,
createInitiativeForProject: vi.fn(),
};
});
vi.mock("../src/db/prisma.js", () => ({ prisma: mocks.prisma }));
vi.mock("../src/repositories/initiativesRepository.js", () => ({ createInitiativeForProject: mocks.createInitiativeForProject }));
import { createFeatureDelivery, startFeatureDelivery, recordFeatureDeliveryActionValidation, reportedValidationResult, type FeatureDeliveryInput } from "../src/repositories/featureDeliveryRepository.js";
const baseInput = (riskLevel: FeatureDeliveryInput["riskLevel"]): FeatureDeliveryInput => ({
title: "Intent-first onboarding",
intent: "Make onboarding understandable for nontechnical users.",
priority: "normal",
problem: "The current onboarding exposes too many technical concepts.",
desiredOutcome: "A short intent-first onboarding flow.",
users: ["new users"],
goals: ["reduce cognitive load"],
nonGoals: ["remove technical capabilities"],
acceptanceCriteria: ["normal users can create a project without understanding MCP or Git internals"],
implementationPlan: ["simplify the primary flow", "preserve technical drill-downs"],
risks: ["navigation changes can affect deep links"],
validationCommands: ["pnpm test"],
riskLevel,
});
const plan = (approvalState: ApprovalState, status: DeliveryStatus) => ({
id: "delivery-db-1",
slug: "delivery-1",
projectId: "project-db-1",
initiativeId: "initiative-db-1",
title: "Intent-first onboarding",
intent: "Make onboarding understandable for nontechnical users.",
priority: "normal",
problem: "The current onboarding exposes too many technical concepts.",
desiredOutcome: "A short intent-first onboarding flow.",
usersJson: ["new users"],
goalsJson: ["reduce cognitive load"],
nonGoalsJson: [],
acceptanceCriteriaJson: ["normal users understand the flow"],
implementationPlanJson: ["simplify the primary flow"],
risksJson: ["navigation changes"],
validationCommandsJson: ["pnpm test"],
status,
approvalState,
approvedAt: approvalState === ApprovalState.approved ? new Date("2026-08-31T20:00:00.000Z") : null,
approvedBy: approvalState === ApprovalState.approved ? "chatgpt" : null,
reviewFeedback: null,
version: 1,
createdBy: "chatgpt",
updatedBy: "chatgpt",
createdAt: new Date("2026-08-31T20:00:00.000Z"),
updatedAt: new Date("2026-08-31T20:00:00.000Z"),
initiative: null,
runs: [],
});
beforeEach(() => {
vi.clearAllMocks();
mocks.prisma.project.findFirst.mockResolvedValue({ id: "project-db-1", slug: "projectbrain" });
mocks.createInitiativeForProject.mockResolvedValue({ id: "initiative-1" });
mocks.prisma.initiative.findFirstOrThrow.mockResolvedValue({ id: "initiative-db-1", slug: "initiative-1" });
mocks.tx.initiative.update.mockResolvedValue({});
mocks.tx.outboxEvent.create.mockResolvedValue({});
mocks.tx.findingInvestigation.findUnique.mockResolvedValue(null);
});
describe("feature delivery risk authorization", () => {
it.each([
["low", "AUTO_ALLOWED"],
["medium", "AUTO_ALLOWED_WITH_AUDIT"],
] as const)("auto-authorizes %s reversible work", async (riskLevel, policyDecision) => {
const created = plan(ApprovalState.approved, DeliveryStatus.approved);
mocks.tx.deliveryPlan.create.mockResolvedValue(created);
const result = await createFeatureDelivery("projectbrain", baseInput(riskLevel), "chatgpt");
expect(result.approvalState).toBe(ApprovalState.approved);
expect(result.spec.status).toBe(DeliveryStatus.approved);
expect(mocks.tx.deliveryPlan.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
approvalState: ApprovalState.approved,
status: DeliveryStatus.approved,
}),
}));
expect(mocks.tx.outboxEvent.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
eventType: "delivery.plan.auto_authorized",
payloadJson: expect.objectContaining({ riskLevel, policyDecision }),
}),
}));
});
it.each(["high", "destructive"] as const)("requires a human decision for %s work", async (riskLevel) => {
const created = plan(ApprovalState.pending, DeliveryStatus.awaiting_approval);
mocks.tx.deliveryPlan.create.mockResolvedValue(created);
const result = await createFeatureDelivery("projectbrain", baseInput(riskLevel), "chatgpt");
expect(result.approvalState).toBe(ApprovalState.pending);
expect(result.spec.status).toBe(DeliveryStatus.awaiting_approval);
expect(mocks.tx.deliveryPlan.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
approvalState: ApprovalState.pending,
status: DeliveryStatus.awaiting_approval,
approvedAt: null,
approvedBy: null,
}),
}));
expect(mocks.tx.outboxEvent.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
eventType: "delivery.plan.user_decision_required",
payloadJson: expect.objectContaining({ riskLevel, policyDecision: "USER_DECISION_REQUIRED" }),
}),
}));
});
it("fails closed when a pending high-risk delivery tries to start repository execution", async () => {
mocks.prisma.deliveryPlan.findFirst.mockResolvedValue(plan(ApprovalState.pending, DeliveryStatus.awaiting_approval));
await expect(startFeatureDelivery("projectbrain", "delivery-1", "chatgpt/high-risk-change"))
.rejects.toThrow("FEATURE_DELIVERY_APPROVAL_REQUIRED");
expect(mocks.prisma.giteaRepositoryLink.findFirst).not.toHaveBeenCalled();
expect(mocks.tx.deliveryRun.create).not.toHaveBeenCalled();
});
});
describe("Actions evidence scope", () => {
it("records only the observed workflow, preserving separately reported checks", async () => {
const current = { ...plan(ApprovalState.approved, DeliveryStatus.validating),
validationCommandsJson: ["dotnet test", "CI / build-test (pull_request)", "Live disposable provider smoke"],
runs: [{ id: "run-1", validations: [], changes: [] }] };
mocks.prisma.deliveryPlan.findFirst.mockResolvedValue(current);
mocks.tx.deliveryPlan.update.mockResolvedValue(current);
await recordFeatureDeliveryActionValidation("projectbrain", "delivery-1", {
provider: "gitea_actions", runId: 9205, runNumber: 500, url: "https://example.test/actions/runs/9205",
workflowPath: "managed-validation.yml@refs/heads/chatgpt/work", headSha: "a".repeat(40),
conclusion: "success", verifiedAt: "2026-09-08T00:00:00Z",
});
expect(mocks.tx.deliveryValidation.create).toHaveBeenCalledTimes(1);
expect(mocks.tx.deliveryValidation.create).toHaveBeenCalledWith({ data: expect.objectContaining({
command: "Gitea Actions: managed-validation.yml@refs/heads/chatgpt/work", status: "passed", headSha: "a".repeat(40),
}) });
expect(mocks.tx.deliveryValidation.deleteMany).toHaveBeenCalledWith({ where: { deliveryRunId: "run-1", provider: "gitea_actions" } });
});
it.each(["CI / build-test (pull_request)", "Live provider smoke", "dotnet test"])("does not present legacy inferred %s as execution", (command) => {
expect(reportedValidationResult({ command, status: DeliveryValidationStatus.passed, provider: "gitea_actions", summary: "managed-validation.yml" }))
.toEqual({ command, status: "not_run", summary: expect.stringContaining("Legacy inferred result") });
});
it("preserves explicit reports and directly observed workflow results", () => {
expect(reportedValidationResult({ command: "Live provider smoke", status: DeliveryValidationStatus.skipped, provider: null, summary: "Credentials unavailable" }).status).toBe("not_run");
expect(reportedValidationResult({ command: "Local contract suite", status: DeliveryValidationStatus.failed, provider: null, summary: "Failed" }).status).toBe("failed");
expect(reportedValidationResult({ command: "CI / build-test (pull_request)", status: DeliveryValidationStatus.failed, provider: "gitea_actions", summary: "ci.yml" }).status).toBe("failed");
expect(reportedValidationResult({ command: "Gitea Actions: ci.yml", status: DeliveryValidationStatus.passed, provider: "gitea_actions", summary: "ci.yml" }).status).toBe("passed");
});
});