100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { AutonomyProjectRuntime } from "../src/services/autonomyRuntime.js";
|
|
import { buildAutoHumanAttentionPriority, PORTFOLIO_CATALOG_CACHE_MS, shouldRefreshPortfolioCatalog, summarizeDeliveryActivity, summarizeLiveExecution } from "../src/services/portfolioControlPlane.js";
|
|
|
|
const autoProject = (patch: Partial<AutonomyProjectRuntime> = {}): AutonomyProjectRuntime => ({
|
|
projectId: "project-db-id",
|
|
projectSlug: "demo",
|
|
projectName: "Demo",
|
|
enabled: true,
|
|
status: "running",
|
|
lifecycle: "ready",
|
|
pendingWorkCount: 3,
|
|
completedWorkCount: 1,
|
|
maxWorkItemsPerRun: 5,
|
|
maxRunMinutes: 30,
|
|
stalled: false,
|
|
...patch,
|
|
});
|
|
|
|
describe("portfolio delivery activity semantics", () => {
|
|
it("counts stored delivery phases separately from executor liveness", () => {
|
|
expect(summarizeDeliveryActivity({
|
|
draft: 2,
|
|
approved: 3,
|
|
executing: 4,
|
|
validating: 5,
|
|
pull_request_open: 6,
|
|
merged: 7,
|
|
verified: 8,
|
|
released: 9,
|
|
})).toEqual({
|
|
active: 9,
|
|
queued: 5,
|
|
waitingExternal: 6,
|
|
mergedAwaitingVerification: 7,
|
|
completed: 17,
|
|
});
|
|
});
|
|
|
|
it("defaults missing delivery buckets to zero", () => {
|
|
expect(summarizeDeliveryActivity({})).toEqual({
|
|
active: 0,
|
|
queued: 0,
|
|
waitingExternal: 0,
|
|
mergedAwaitingVerification: 0,
|
|
completed: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("confirmed execution", () => {
|
|
const now = Date.parse("2026-09-09T12:00:00Z");
|
|
const working = autoProject({ activeWorkKind: "implement", activeRunId: "run-1", executorRunId: "run-1", executorStatus: "working", executorLastSeenAt: new Date(now - 1000).toISOString(), leaseExpiresAt: new Date(now + 60000).toISOString() });
|
|
it("requires enabled state, valid lease and fresh working heartbeat together", () => {
|
|
expect(summarizeLiveExecution([working], 120, now).verifiedActiveProjects).toBe(1);
|
|
for (const patch of [{ enabled: false }, { executorStatus: "stale" as const }, { executorStatus: "idle" as const }, { activeWorkKind: undefined }, { leaseExpiresAt: new Date(now).toISOString() }, { executorLastSeenAt: new Date(now - 121000).toISOString() }, { executorLastSeenAt: "invalid" }, { executorLastSeenAt: new Date(now + 1000).toISOString() }]) {
|
|
expect(summarizeLiveExecution([{ ...working, ...patch }], 120, now).verifiedActiveProjects).toBe(0);
|
|
}
|
|
});
|
|
it("distinguishes paused automation from orphaned claims", () => {
|
|
expect(summarizeLiveExecution([{ ...working, executorRunId: "different-run" }], 120, now).verifiedActiveProjects).toBe(0);
|
|
expect(summarizeLiveExecution([{ ...working, activeRunId: undefined }], 120, now).verifiedActiveProjects).toBe(0);
|
|
expect(summarizeLiveExecution([{ ...working, enabled: false }], 120, now)).toMatchObject({ verifiedActiveProjects: 0, enabledProjects: 0, unconfirmedLeaseProjects: 0 });
|
|
expect(summarizeLiveExecution([{ ...working, executorStatus: "offline" }], 120, now).unconfirmedLeaseProjects).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("portfolio Auto human attention", () => {
|
|
it("surfaces true human Auto blockers as portfolio attention", () => {
|
|
const priority = buildAutoHumanAttentionPriority([
|
|
autoProject({ status: "blocked", blockerKind: "human", blockerReason: "Owner decision required" }),
|
|
autoProject({ projectId: "retry-db", projectSlug: "retry", projectName: "Retry", status: "blocked", blockerKind: "retryable", blockerReason: "Runner timeout" }),
|
|
]);
|
|
|
|
expect(priority).toMatchObject({
|
|
id: "auto-human-blockers",
|
|
projectId: "project-db-id",
|
|
requiresHumanAttention: true,
|
|
count: 1,
|
|
route: "/projects/demo/attention",
|
|
});
|
|
});
|
|
|
|
it("does not turn retryable or disabled Auto state into human attention", () => {
|
|
expect(buildAutoHumanAttentionPriority([
|
|
autoProject({ status: "blocked", blockerKind: "retryable" }),
|
|
autoProject({ enabled: false, status: "blocked", blockerKind: "human" }),
|
|
])).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("portfolio catalog freshness", () => {
|
|
it("serves a recent durable catalog before asking Gitea for another inventory", () => {
|
|
const now = Date.parse("2026-09-13T12:00:00.000Z");
|
|
expect(shouldRefreshPortfolioCatalog(new Date(now - PORTFOLIO_CATALOG_CACHE_MS), now)).toBe(false);
|
|
expect(shouldRefreshPortfolioCatalog(new Date(now - PORTFOLIO_CATALOG_CACHE_MS - 1), now)).toBe(true);
|
|
expect(shouldRefreshPortfolioCatalog(undefined, now)).toBe(true);
|
|
});
|
|
});
|