chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,223 @@
|
||||
import type { Context } from "@earendil-works/chord";
|
||||
import { createSession, defineTask, MemoryStorage, type StorageWrite, type TaskId } from "@earendil-works/pi-durable";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { context, flush } from "./session-support.ts";
|
||||
import { aborted, deferred, eventually, openTasks } from "./task-support.ts";
|
||||
|
||||
/** Runs until aborted; its abort handler ends it `aborted`. */
|
||||
const Dormant = defineTask<null, { phase: "work" }, null>({
|
||||
name: "test.dormant",
|
||||
version: 1,
|
||||
initial: () => ({ phase: "work" }),
|
||||
phases: {
|
||||
work: async (_task, runtime) => {
|
||||
await aborted(runtime.signal);
|
||||
},
|
||||
},
|
||||
abort: async (_task, runtime, abortContext) => {
|
||||
await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), abortContext);
|
||||
},
|
||||
});
|
||||
|
||||
/** Holds every commit that abort-marks `target`, while set, until released. */
|
||||
class HoldMarks extends MemoryStorage {
|
||||
target: TaskId | undefined;
|
||||
readonly held = deferred();
|
||||
readonly release = deferred();
|
||||
override async commit(writes: readonly StorageWrite[], commitContext: Context) {
|
||||
if (
|
||||
writes.some((write) => write.type === "task" && write.value.id === this.target && write.value.abortRequested)
|
||||
) {
|
||||
this.held.resolve();
|
||||
await this.release.promise;
|
||||
}
|
||||
return super.commit(writes, commitContext);
|
||||
}
|
||||
}
|
||||
|
||||
describe("work below a cancelled owner", () => {
|
||||
it("does not start its next phase while the cascade that marks it has not committed", async () => {
|
||||
const storage = new HoldMarks();
|
||||
const entered = deferred();
|
||||
const advance = deferred();
|
||||
let ranNext = false;
|
||||
const Child = defineTask<null, { phase: "first" } | { phase: "next" }, null>({
|
||||
name: "test.child",
|
||||
version: 1,
|
||||
initial: () => ({ phase: "first" }),
|
||||
phases: {
|
||||
first: async (_task, runtime, phaseContext) => {
|
||||
entered.resolve();
|
||||
await advance.promise;
|
||||
await runtime.commit(() => ({ status: "running", checkpoint: { phase: "next" } }), phaseContext);
|
||||
},
|
||||
next: async (_task, runtime) => {
|
||||
ranNext = true;
|
||||
await aborted(runtime.signal);
|
||||
},
|
||||
},
|
||||
abort: async (_task, runtime, abortContext) => {
|
||||
await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), abortContext);
|
||||
},
|
||||
});
|
||||
const { harness } = await openTasks(storage, [Dormant, Child]);
|
||||
const root = await harness.root(context);
|
||||
const parent = await root.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { ownership: { kind: "conversation" } }),
|
||||
context,
|
||||
);
|
||||
const child = await root.commit(
|
||||
(tx) => tx.createTask(Child, null, { ownership: { kind: "task", taskId: parent } }),
|
||||
context,
|
||||
);
|
||||
storage.target = child;
|
||||
try {
|
||||
harness.resume();
|
||||
await entered.promise;
|
||||
const aborting = harness.abortTask(parent, context);
|
||||
await storage.held.promise;
|
||||
// The parent's request is durable, the cascade's mark of the child is not: its phase must not advance.
|
||||
advance.resolve();
|
||||
for (let i = 0; i < 5; i++) await flush();
|
||||
expect(ranNext).toBe(false);
|
||||
storage.release.resolve();
|
||||
expect((await harness.waitForTask(child, context)).state.outcome.status).toBe("aborted");
|
||||
await aborting;
|
||||
expect(ranNext).toBe(false);
|
||||
} finally {
|
||||
advance.resolve();
|
||||
storage.release.resolve();
|
||||
await harness.close(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("lets a failFast request in the same pass override a restart mark", async () => {
|
||||
const storage = new MemoryStorage();
|
||||
const session = createSession(storage);
|
||||
const conversation = await session.commit(
|
||||
(tx) => tx.createConversation({ ownership: { kind: "ownerless" } }),
|
||||
context,
|
||||
);
|
||||
const parent = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { conversationId: conversation.id, ownership: { kind: "conversation" } }),
|
||||
context,
|
||||
);
|
||||
const failed = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { ownership: { kind: "task", taskId: parent } }),
|
||||
context,
|
||||
);
|
||||
const child = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { ownership: { kind: "task", taskId: parent } }),
|
||||
context,
|
||||
);
|
||||
// A restart-abandoned owner that waits failFast on a failed child and a live one.
|
||||
const owner = (await storage.task(parent, context))!;
|
||||
const failedRecord = (await storage.task(failed, context))!;
|
||||
const waiting = {
|
||||
status: "waiting",
|
||||
checkpoint: { phase: "work" },
|
||||
on: [failed, child],
|
||||
policy: "failFast",
|
||||
} as const;
|
||||
const failure = { status: "failed", error: { message: "failed" } } as const;
|
||||
await storage.commit(
|
||||
[
|
||||
{ type: "task", value: { ...owner, abortRequested: true, abortReason: "restart", state: waiting } },
|
||||
{
|
||||
type: "task",
|
||||
value: { ...failedRecord, state: { status: "terminal", outcome: failure } } as typeof failedRecord,
|
||||
},
|
||||
],
|
||||
context,
|
||||
);
|
||||
// The child's definition is missing: a restart mark would keep it waiting, a request orphans it.
|
||||
const { harness } = await openTasks(storage, []);
|
||||
try {
|
||||
harness.resume();
|
||||
await eventually(async () => (await harness.getTask(child, context))?.state.status === "terminal");
|
||||
expect((await harness.getTask(child, context))?.state).toMatchObject({
|
||||
outcome: { status: "orphaned", reason: "missing_task" },
|
||||
});
|
||||
} finally {
|
||||
await harness.close(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("upgrades restart marks down the tree when an abort is requested above them", async () => {
|
||||
const storage = new MemoryStorage();
|
||||
const session = createSession(storage);
|
||||
const conversation = await session.commit(
|
||||
(tx) => tx.createConversation({ ownership: { kind: "ownerless" } }),
|
||||
context,
|
||||
);
|
||||
const top = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { conversationId: conversation.id, ownership: { kind: "conversation" } }),
|
||||
context,
|
||||
);
|
||||
const middle = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { ownership: { kind: "task", taskId: top }, abandonOnRestart: true }),
|
||||
context,
|
||||
);
|
||||
const Missing = defineTask<null, { phase: "work" }, null>({ ...Dormant.definition, name: "test.missing" });
|
||||
const leaf = await session.commit(
|
||||
(tx) => tx.createTask(Missing, null, { ownership: { kind: "task", taskId: middle } }),
|
||||
context,
|
||||
);
|
||||
// Only Dormant is registered: the leaf waits for its definition under the restart cascade.
|
||||
const { harness } = await openTasks(storage, [Dormant]);
|
||||
try {
|
||||
harness.resume();
|
||||
await eventually(async () => (await harness.getTask(leaf, context))?.abortReason === "restart");
|
||||
await eventually(async () => {
|
||||
const inspected = (await harness.inspect(context)).tasks.find((task) => task.record.id === leaf);
|
||||
return inspected?.state.kind === "blocked";
|
||||
});
|
||||
expect((await harness.getTask(leaf, context))?.state.status).not.toBe("terminal");
|
||||
// A request on the top task reaches the leaf two levels down, which is then orphaned.
|
||||
await harness.abortTask(top, context);
|
||||
for (const [id, status] of [
|
||||
[leaf, "orphaned"],
|
||||
[middle, "aborted"],
|
||||
[top, "aborted"],
|
||||
] as const) {
|
||||
expect((await harness.waitForTask(id, context)).state.outcome.status).toBe(status);
|
||||
}
|
||||
} finally {
|
||||
await harness.close(context);
|
||||
}
|
||||
});
|
||||
|
||||
it("marks a flagged task that holds its outcome, aborting its owned work, and keeps the outcome", async () => {
|
||||
const storage = new MemoryStorage();
|
||||
const session = createSession(storage);
|
||||
const conversation = await session.commit(
|
||||
(tx) => tx.createConversation({ ownership: { kind: "ownerless" } }),
|
||||
context,
|
||||
);
|
||||
const holder = await session.commit(
|
||||
(tx) =>
|
||||
tx.createTask(Dormant, null, {
|
||||
conversationId: conversation.id,
|
||||
ownership: { kind: "conversation" },
|
||||
abandonOnRestart: true,
|
||||
}),
|
||||
context,
|
||||
);
|
||||
const child = await session.commit(
|
||||
(tx) => tx.createTask(Dormant, null, { ownership: { kind: "task", taskId: holder } }),
|
||||
context,
|
||||
);
|
||||
const record = (await storage.task(holder, context))!;
|
||||
const completing = { status: "completing", outcome: { status: "completed", result: null } } as const;
|
||||
await storage.commit([{ type: "task", value: { ...record, state: completing } as typeof record }], context);
|
||||
const { harness } = await openTasks(storage, [Dormant]);
|
||||
try {
|
||||
harness.resume();
|
||||
expect((await harness.waitForTask(child, context)).state.outcome.status).toBe("aborted");
|
||||
const settled = await harness.waitForTask(holder, context);
|
||||
expect(settled).toMatchObject({ abortReason: "restart", state: { outcome: { status: "completed" } } });
|
||||
} finally {
|
||||
await harness.close(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user