git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
442 lines
15 KiB
TypeScript
442 lines
15 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { DEFAULT_COMPACTION_SETTINGS } from "../../../src/harness/compaction/compaction.ts";
|
|
import { BACKGROUND_CONTEXT } from "../../../src/harness/context.ts";
|
|
import { restoreLane, restoreSession } from "../../../src/harness/runtime/restore.ts";
|
|
import { MemorySessionRepo, MemoryStorage } from "../../../src/harness/session/memory.ts";
|
|
import type {
|
|
CheckpointOperation,
|
|
LaneConfiguration,
|
|
OperationMeta,
|
|
OperationScope,
|
|
OperationState,
|
|
ResultBoundary,
|
|
Session,
|
|
} from "../../../src/harness/session/types.ts";
|
|
import * as storedValues from "../../../src/harness/session/values.ts";
|
|
|
|
const repos: MemorySessionRepo[] = [];
|
|
const configuration: LaneConfiguration = {
|
|
model: { provider: "test", modelId: "model" },
|
|
thinkingLevel: "off",
|
|
activeToolNames: [],
|
|
};
|
|
|
|
async function createSession(): Promise<Session> {
|
|
const repo = new MemorySessionRepo();
|
|
repos.push(repo);
|
|
const session = await repo.create({}, BACKGROUND_CONTEXT);
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.branchTip("main"), null),
|
|
storedValues.setValue(storedValues.laneConfig("main"), configuration),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: null,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
return session;
|
|
}
|
|
|
|
function operationScope(): OperationScope {
|
|
return {
|
|
control: { status: "running" },
|
|
settings: {
|
|
compaction: DEFAULT_COMPACTION_SETTINGS,
|
|
steeringMode: "all",
|
|
followUpMode: "all",
|
|
toolExecution: "parallel",
|
|
},
|
|
latestAssistantEntryId: null,
|
|
};
|
|
}
|
|
|
|
function runState(triggerEntryId: string): CheckpointOperation {
|
|
return {
|
|
...operationScope(),
|
|
at: "checkpoint",
|
|
continuation: { kind: "need_assistant", overflowRecoveryUsed: false },
|
|
triggerEntryId,
|
|
};
|
|
}
|
|
|
|
function summaryState(boundary: ResultBoundary): OperationState {
|
|
return {
|
|
...operationScope(),
|
|
at: "summary.deciding",
|
|
task: {
|
|
taskId: "task",
|
|
...(boundary.kind === "finish"
|
|
? { reason: "manual" as const }
|
|
: boundary.kind === "resume_checkpoint"
|
|
? { reason: "threshold" as const }
|
|
: {}),
|
|
boundary,
|
|
},
|
|
};
|
|
}
|
|
|
|
afterEach(async () => {
|
|
for (const repo of repos.splice(0)) await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
describe("runtime lane restore", () => {
|
|
it("restores an idle lane's latest operation id without reading its result", async () => {
|
|
const session = await createSession();
|
|
const result = {
|
|
operationId: session.idGenerator.next(),
|
|
kind: "navigation" as const,
|
|
status: "completed" as const,
|
|
fromTipId: null,
|
|
tipId: null,
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
};
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.operationResult(result.operationId), result),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: null,
|
|
lastOperationId: result.operationId,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
|
|
const state = await restoreLane(session, "main", BACKGROUND_CONTEXT);
|
|
|
|
expect(state).toEqual({
|
|
tipId: null,
|
|
configuration,
|
|
inbox: [],
|
|
lastOperationId: result.operationId,
|
|
operation: null,
|
|
});
|
|
});
|
|
|
|
it("restores an open operation without interpreting its referenced payloads", async () => {
|
|
const session = await createSession();
|
|
const operationId = session.idGenerator.next();
|
|
const missingTriggerId = session.idGenerator.next();
|
|
const meta: OperationMeta = {
|
|
operationId,
|
|
lane: "main",
|
|
sourceTipId: null,
|
|
startedAt: 1,
|
|
intent: { kind: "run", promptEntryIds: [] },
|
|
};
|
|
const state = runState(missingTriggerId);
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.operationMeta(operationId), meta),
|
|
storedValues.setValue(storedValues.operationState(operationId), state),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: operationId,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
|
|
const restored = await restoreLane(session, "main", BACKGROUND_CONTEXT);
|
|
|
|
expect(restored.operation).toEqual({ meta, state });
|
|
});
|
|
|
|
it("validates current-operation identity, lane ownership, and intent/state compatibility", async () => {
|
|
for (const corruption of ["identity", "lane", "kind"] as const) {
|
|
const session = await createSession();
|
|
const operationId = session.idGenerator.next();
|
|
const base: OperationMeta = {
|
|
operationId,
|
|
lane: "main",
|
|
sourceTipId: null,
|
|
startedAt: 1,
|
|
intent: { kind: "run", promptEntryIds: [] },
|
|
};
|
|
const meta: OperationMeta =
|
|
corruption === "identity"
|
|
? { ...base, operationId: "different-operation" }
|
|
: corruption === "lane"
|
|
? { ...base, lane: "worker" }
|
|
: { ...base, intent: { kind: "navigation", targetId: null, summarize: false } };
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.operationMeta(operationId), meta),
|
|
storedValues.setValue(storedValues.operationState(operationId), runState("trigger")),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: operationId,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
|
|
await expect(restoreLane(session, "main", BACKGROUND_CONTEXT)).rejects.toBeInstanceOf(Error);
|
|
}
|
|
});
|
|
|
|
it("accepts exactly the family-neutral state reachability matrix", async () => {
|
|
const resume = { kind: "resume_checkpoint", resumeAfter: runState("trigger") } satisfies ResultBoundary;
|
|
const finish = { kind: "finish" } satisfies ResultBoundary;
|
|
const navigation = { kind: "commit_navigation", targetId: "target" } satisfies ResultBoundary;
|
|
const cases: { intent: OperationMeta["intent"]; state: OperationState; accepted: boolean }[] = [
|
|
{ intent: { kind: "run", promptEntryIds: [] }, state: runState("trigger"), accepted: true },
|
|
{ intent: { kind: "run", promptEntryIds: [] }, state: summaryState(resume), accepted: true },
|
|
{ intent: { kind: "run", promptEntryIds: [] }, state: summaryState(finish), accepted: false },
|
|
{ intent: { kind: "run", promptEntryIds: [] }, state: summaryState(navigation), accepted: false },
|
|
{ intent: { kind: "compaction" }, state: summaryState(finish), accepted: true },
|
|
{ intent: { kind: "compaction" }, state: summaryState(resume), accepted: false },
|
|
{ intent: { kind: "compaction" }, state: summaryState(navigation), accepted: false },
|
|
{ intent: { kind: "compaction" }, state: runState("trigger"), accepted: false },
|
|
{
|
|
intent: { kind: "navigation", targetId: null, summarize: false },
|
|
state: { ...operationScope(), at: "navigation.ready_to_commit", targetId: null },
|
|
accepted: true,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: false },
|
|
state: { ...operationScope(), at: "navigation.ready_to_commit", targetId: "different" },
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: false },
|
|
state: summaryState(navigation),
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: true },
|
|
state: summaryState(navigation),
|
|
accepted: true,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "different", summarize: true },
|
|
state: summaryState(navigation),
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: true },
|
|
state: { ...operationScope(), at: "navigation.ready_to_commit", targetId: "target" },
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: true },
|
|
state: summaryState(finish),
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: true },
|
|
state: summaryState(resume),
|
|
accepted: false,
|
|
},
|
|
{
|
|
intent: { kind: "navigation", targetId: "target", summarize: false },
|
|
state: runState("trigger"),
|
|
accepted: false,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const session = await createSession();
|
|
const operationId = session.idGenerator.next();
|
|
const meta: OperationMeta = {
|
|
operationId,
|
|
lane: "main",
|
|
sourceTipId: null,
|
|
startedAt: 1,
|
|
intent: testCase.intent,
|
|
};
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.operationMeta(operationId), meta),
|
|
storedValues.setValue(storedValues.operationState(operationId), testCase.state),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: operationId,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
const restored = restoreLane(session, "main", BACKGROUND_CONTEXT);
|
|
if (testCase.accepted)
|
|
await expect(restored).resolves.toMatchObject({ operation: { meta, state: testCase.state } });
|
|
else await expect(restored).rejects.toThrow("does not match state");
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
[storedValues.branchTip("main").namespace, storedValues.deleteValue(storedValues.branchTip("main"))],
|
|
[storedValues.laneConfig("main").namespace, storedValues.deleteValue(storedValues.laneConfig("main"))],
|
|
[storedValues.laneState("main").namespace, storedValues.deleteValue(storedValues.laneState("main"))],
|
|
] as const)("requires %s", async (namespace, write) => {
|
|
const session = await createSession();
|
|
await session.mutate((mutator) => mutator.commit([write], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
|
|
|
|
await expect(restoreLane(session, "main", BACKGROUND_CONTEXT)).rejects.toThrow(`missing ${namespace.slice(3)}`);
|
|
});
|
|
|
|
it.each([storedValues.operationMeta("").namespace, storedValues.operationState("").namespace] as const)(
|
|
"requires %s for the current operation",
|
|
async (namespace) => {
|
|
const session = await createSession();
|
|
const operationId = session.idGenerator.next();
|
|
const meta: OperationMeta = {
|
|
operationId,
|
|
lane: "main",
|
|
sourceTipId: null,
|
|
startedAt: 1,
|
|
intent: { kind: "run", promptEntryIds: [] },
|
|
};
|
|
const state = runState(session.idGenerator.next());
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
...(namespace === storedValues.operationMeta(operationId).namespace
|
|
? []
|
|
: [storedValues.setValue(storedValues.operationMeta(operationId), meta)]),
|
|
...(namespace === storedValues.operationState(operationId).namespace
|
|
? []
|
|
: [storedValues.setValue(storedValues.operationState(operationId), state)]),
|
|
storedValues.setValue(storedValues.laneState("main"), {
|
|
currentOperationId: operationId,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
|
|
await expect(restoreLane(session, "main", BACKGROUND_CONTEXT)).rejects.toThrow(
|
|
`missing ${namespace.slice(3)}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("restores every configured lane exactly once without writing", async () => {
|
|
const session = await createSession();
|
|
const workerConfiguration: LaneConfiguration = {
|
|
model: { provider: "test", modelId: "worker" },
|
|
thinkingLevel: "high",
|
|
activeToolNames: ["read"],
|
|
};
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.branchTip("worker"), null),
|
|
storedValues.setValue(storedValues.laneConfig("worker"), workerConfiguration),
|
|
storedValues.setValue(storedValues.laneState("worker"), {
|
|
currentOperationId: null,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
const operationId = session.idGenerator.next();
|
|
const meta: OperationMeta = {
|
|
operationId,
|
|
lane: "worker",
|
|
sourceTipId: null,
|
|
startedAt: 1,
|
|
intent: { kind: "run", promptEntryIds: [] },
|
|
};
|
|
const state = runState(session.idGenerator.next());
|
|
await session.mutate(
|
|
(mutator) =>
|
|
mutator.commit(
|
|
[
|
|
storedValues.setValue(storedValues.operationMeta(operationId), meta),
|
|
storedValues.setValue(storedValues.operationState(operationId), state),
|
|
storedValues.setValue(storedValues.laneState("worker"), {
|
|
currentOperationId: operationId,
|
|
lastOperationId: null,
|
|
inbox: [],
|
|
}),
|
|
],
|
|
BACKGROUND_CONTEXT,
|
|
),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
const before = {
|
|
leaves: await session.scanValues(storedValues.branchTipInventoryPrefix(), BACKGROUND_CONTEXT),
|
|
mainConfiguration: await session.getValue(storedValues.laneConfig("main"), BACKGROUND_CONTEXT),
|
|
workerConfiguration: await session.getValue(storedValues.laneConfig("worker"), BACKGROUND_CONTEXT),
|
|
mainState: await session.getValue(storedValues.laneState("main"), BACKGROUND_CONTEXT),
|
|
workerState: await session.getValue(storedValues.laneState("worker"), BACKGROUND_CONTEXT),
|
|
};
|
|
const mutate = vi.spyOn(session, "mutate");
|
|
const getValue = vi.spyOn(MemoryStorage.prototype, "getValue");
|
|
const readList = vi.spyOn(MemoryStorage.prototype, "readList");
|
|
|
|
const lanes = await restoreSession(session, BACKGROUND_CONTEXT);
|
|
|
|
expect([...lanes.keys()].sort()).toEqual(["main", "worker"]);
|
|
expect(lanes.get("main")?.configuration).toEqual(configuration);
|
|
expect(lanes.get("worker")).toMatchObject({
|
|
configuration: workerConfiguration,
|
|
operation: { meta, state },
|
|
});
|
|
expect(mutate).toHaveBeenCalledTimes(1);
|
|
expect(getValue.mock.calls.map(([address]) => ({ namespace: address.namespace, key: address.key }))).toEqual([
|
|
{ namespace: storedValues.operationMeta("").namespace, key: operationId },
|
|
{ namespace: storedValues.operationState("").namespace, key: operationId },
|
|
]);
|
|
expect(readList).not.toHaveBeenCalled();
|
|
getValue.mockRestore();
|
|
expect({
|
|
leaves: await session.scanValues(storedValues.branchTipInventoryPrefix(), BACKGROUND_CONTEXT),
|
|
mainConfiguration: await session.getValue(storedValues.laneConfig("main"), BACKGROUND_CONTEXT),
|
|
workerConfiguration: await session.getValue(storedValues.laneConfig("worker"), BACKGROUND_CONTEXT),
|
|
mainState: await session.getValue(storedValues.laneState("main"), BACKGROUND_CONTEXT),
|
|
workerState: await session.getValue(storedValues.laneState("worker"), BACKGROUND_CONTEXT),
|
|
}).toEqual(before);
|
|
readList.mockRestore();
|
|
});
|
|
|
|
it("allows an empty inventory but rejects lane values without a Branch", async () => {
|
|
const repo = new MemorySessionRepo();
|
|
repos.push(repo);
|
|
const empty = await repo.create({}, BACKGROUND_CONTEXT);
|
|
await expect(restoreSession(empty, BACKGROUND_CONTEXT)).resolves.toEqual(new Map());
|
|
|
|
const session = await createSession();
|
|
await session.mutate(
|
|
(mutator) => mutator.commit([storedValues.deleteValue(storedValues.branchTip("main"))], BACKGROUND_CONTEXT),
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
await expect(restoreSession(session, BACKGROUND_CONTEXT)).rejects.toThrow('Lane "main" is missing branch.tip');
|
|
});
|
|
});
|