feat: package Pi extensions as Bun workspaces
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user