Files
harness-tools/__archived/extensions/workflow/notifications.test.ts
T

290 lines
8.7 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import * as path from "node:path";
import test from "node:test";
import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import {
DEFAULT_WORKFLOW_CONFIG,
type WorkflowRun,
} from "./domain.ts";
import {
reconcileWorkflowDeliveries,
workflowNotificationType,
} from "./notifications.ts";
import { WorkflowStore } from "./store.ts";
async function withTempDir(run: (directory: string) => Promise<void>) {
const directory = await mkdtemp(path.join(tmpdir(), "workflow-delivery-"));
try {
await run(directory);
} finally {
await rm(directory, { recursive: true, force: true });
}
}
function workflow(background = false): WorkflowRun {
const gateId = "gate-1";
return {
runId: "run-1",
revision: 0,
mode: "feature",
status: "waiting",
background,
request: "Build search",
featureName: "search",
parentSessionIds: ["session-1"],
lease: {
processId: 100,
activeParentSessionId: "session-1",
heartbeatAt: 1,
},
workspace: {
state: "ready",
sourceRoot: "/repo",
root: "/repo/.worktrees/run-1",
branch: "feature/search",
baseHead: "abc123",
},
effectiveConfig: structuredClone(DEFAULT_WORKFLOW_CONFIG),
pinnedProfiles: [],
preflightEligibility: [],
facts: {
stage: "design",
taskKind: "feature",
requestTerms: ["search"],
candidatePaths: [],
languages: [],
tags: [],
riskTags: [],
},
stages: [],
gates: [
{
id: gateId,
kind: "plan-approval",
status: "open",
revision: 0,
openedAt: 1,
},
],
decisions: [],
assignments: [],
prototypeCandidates: [],
prototypeCleanupCompleted: false,
deliveries: [
{
key: `gate:run-1:${gateId}:0`,
state: "pending",
createdAt: 1,
},
],
commandReceipts: [],
uiImpactEvidence: [],
affectedUiSurfaces: [],
mutatorProfileNames: [],
reviewCycles: 0,
verificationCycles: 0,
};
}
function deliveryHarness(entries: unknown[], persistImmediately = true) {
const sent: Array<{ message: unknown; options: unknown }> = [];
const pi = {
sendMessage(message: unknown, options: unknown) {
sent.push({ message, options });
if (persistImmediately) {
const record = message as Record<string, unknown>;
entries.push({
type: "message",
message: {
role: "custom",
customType: record.customType,
details: record.details,
},
});
}
},
} as unknown as Pick<ExtensionAPI, "sendMessage">;
const ctx = {
sessionManager: {
getSessionId: () => "session-1",
getEntries: () => entries,
},
} as unknown as ExtensionContext;
return { pi, ctx, sent };
}
test("delivery receipt closes the send/save crash window exactly once", async () => {
await withTempDir(async (directory) => {
const store = new WorkflowStore(path.join(directory, "state"));
await store.createRun(workflow());
const entries: unknown[] = [];
const harness = deliveryHarness(entries);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.equal(harness.sent.length, 1);
const message = harness.sent[0]?.message as Record<string, unknown>;
assert.equal(message.customType, workflowNotificationType);
assert.match(String(message.content), /workflow search is waiting/);
assert.equal(
(message.details as Record<string, unknown>).runId,
"run-1",
);
const delivered = await store.loadRun("run-1");
assert.equal(delivered.deliveries[0]?.state, "sent");
assert.equal(delivered.revision, 0);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.equal(harness.sent.length, 1);
});
});
test("queued foreground delivery suppresses immediate duplicates and retries a missing receipt", async () => {
await withTempDir(async (directory) => {
const originalNow = Date.now;
let now = 1_000;
Date.now = () => now;
try {
const store = new WorkflowStore(path.join(directory, "state"));
const run = workflow();
await store.createRun(run);
const entries: unknown[] = [];
const harness = deliveryHarness(entries, false);
const runtime = {
store,
engineForContext: () => Promise.reject(new Error("unused")),
};
await reconcileWorkflowDeliveries(harness.pi, runtime, harness.ctx);
now++;
await reconcileWorkflowDeliveries(harness.pi, runtime, harness.ctx);
assert.equal(harness.sent.length, 1);
assert.deepEqual(harness.sent[0]?.options, { deliverAs: "steer" });
now += 30_000;
await reconcileWorkflowDeliveries(harness.pi, runtime, harness.ctx);
assert.equal(harness.sent.length, 2);
const sentMessage = harness.sent[1]?.message as Record<string, unknown>;
entries.push({
type: "message",
message: {
role: "custom",
customType: sentMessage.customType,
details: sentMessage.details,
},
});
await reconcileWorkflowDeliveries(harness.pi, runtime, harness.ctx);
assert.equal(harness.sent.length, 2);
assert.equal((await store.loadRun("run-1")).deliveries[0]?.state, "sent");
} finally {
Date.now = originalNow;
}
});
});
test("obsolete Gate deliveries settle without sending contradictory notices", async () => {
await withTempDir(async (directory) => {
const store = new WorkflowStore(path.join(directory, "state"));
const run = workflow();
run.status = "running";
run.gates[0]!.status = "satisfied";
await store.createRun(run);
const harness = deliveryHarness([]);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.equal(harness.sent.length, 0);
assert.equal((await store.loadRun("run-1")).deliveries[0]?.state, "sent");
});
});
test("an existing receiver receipt marks pending delivery without resending", async () => {
await withTempDir(async (directory) => {
const store = new WorkflowStore(path.join(directory, "state"));
const run = workflow(true);
await store.createRun(run);
const entries = [
{
type: "message",
message: {
role: "custom",
customType: workflowNotificationType,
details: { deliveryKey: run.deliveries[0]?.key },
},
},
];
const harness = deliveryHarness(entries);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.equal(harness.sent.length, 0);
assert.equal(
(await store.loadRun("run-1")).deliveries[0]?.state,
"sent",
);
});
});
test("terminal deliveries include the persisted report and settle once", async () => {
await withTempDir(async (directory) => {
const store = new WorkflowStore(path.join(directory, "state"));
const run = workflow(true);
run.status = "completed";
run.gates = [];
run.reportArtifact = {
path: "/state/report.md",
digest: "digest",
revision: 4,
};
run.deliveries = [
{
key: "terminal:run-1:completed:4",
state: "pending",
createdAt: 4,
},
];
await store.createRun(run);
const harness = deliveryHarness([]);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.equal(harness.sent.length, 1);
assert.match(
String((harness.sent[0]?.message as Record<string, unknown>).content),
/workflow search finished with status completed.*Report: \/state\/report.md/,
);
assert.equal((await store.loadRun("run-1")).deliveries[0]?.state, "sent");
});
});
test("background deliveries request a follow-up turn", async () => {
await withTempDir(async (directory) => {
const store = new WorkflowStore(path.join(directory, "state"));
await store.createRun(workflow(true));
const harness = deliveryHarness([]);
await reconcileWorkflowDeliveries(
harness.pi,
{ store, engineForContext: () => Promise.reject(new Error("unused")) },
harness.ctx,
);
assert.deepEqual(harness.sent[0]?.options, {
deliverAs: "followUp",
triggerTurn: true,
});
});
});