Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions packages/provider-pi/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,71 @@ const FAKE_SESSION_FILE = "/fake/.pi/agent/sessions/--workspace--/0001_abc.jsonl
/** Deliberately outside the valid pid range so a group-kill can never land. */
const FAKE_PID = 999_999_999;

const at = (seconds: number) => DateTime.makeUnsafe(seconds * 1000);

const sessionMessage = (
id: string,
parentId: string | null,
role: "user" | "assistant",
seconds: number,
) => ({
type: "message",
id,
parentId,
timestamp: DateTime.formatIso(at(seconds)),
message: { role, content: [] },
});

const TURN_ONE_ENTRIES = [
sessionMessage("u1", null, "user", 10),
sessionMessage("a1", "u1", "assistant", 20),
sessionMessage("steer1", "a1", "user", 30),
sessionMessage("a1b", "steer1", "assistant", 40),
];
const UNCAPTURED_TURN_TREE = {
entries: [
...TURN_ONE_ENTRIES,
sessionMessage("u2", "a1b", "user", 110),
sessionMessage("a2", "u2", "assistant", 120),
sessionMessage("steer2", "a2", "user", 130),
sessionMessage("a2b", "steer2", "assistant", 140),
],
leafId: "a2b",
};

function uncapturedTurns(
providerThreadId: ProviderThreadId,
window = { startedAt: 150, completedAt: 200 },
): ReadonlyArray<OrchestrationV2ProviderTurn> {
const turn = (
ordinal: number,
nativeTurnRef: OrchestrationV2ProviderTurn["nativeTurnRef"],
status: OrchestrationV2ProviderTurn["status"],
startedAt: number,
completedAt: number,
): OrchestrationV2ProviderTurn => ({
id: ProviderTurnId.make(`turn-${ordinal}`),
providerThreadId,
nodeId: NodeId.make(`node-${ordinal}`),
runAttemptId: null,
nativeTurnRef,
ordinal,
status,
startedAt: at(startedAt),
completedAt: at(completedAt),
});
return [
turn(1, { driver: PI_PROVIDER, nativeId: "u1", strength: "strong" }, "completed", 0, 50),
turn(
2,
{ driver: PI_PROVIDER, nativeId: "synthetic-2", strength: "weak" },
"cancelled",
window.startedAt,
window.completedAt,
),
];
}

const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
Expand Down Expand Up @@ -1902,6 +1967,50 @@ describe("PiAdapterV2", () => {
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("natively forks before a turn whose start entry was never captured", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
const forkFake = yield* makeFakePi;
const forkFile = "/fake/forked.jsonl";
const { runtime } = yield* openRuntime(fake, "default", THREAD_ID, SESSION_ID, forkFake);
const source = yield* runtime.ensureThread({
threadId: THREAD_ID,
modelSelection: modelSelection("default"),
runtimePolicy,
});
const forkAtFirst = (turns: ReadonlyArray<OrchestrationV2ProviderTurn>) =>
runtime.forkThread({
sourceProviderThread: source,
sourceProviderTurns: turns,
providerTurnId: turns[0]!.id,
targetThreadId: ThreadId.make("fork-target"),
});
const forkedEntries = () =>
forkFake
.allRequests()
.filter((request) => request.type === "fork")
.map((request) => request.entryId);

// A later prompt cannot stand in for a cancelled turn that ended before it landed.
forkFake.queueEntries(UNCAPTURED_TURN_TREE);
const unresolved = yield* forkAtFirst(
uncapturedTurns(source.id, { startedAt: 60, completedAt: 100 }),
).pipe(Effect.result);
assert.deepEqual(forkedEntries(), []);
assert.equal(unresolved._tag, "Failure");

forkFake.queueEntries(UNCAPTURED_TURN_TREE);
forkFake.queueState({ sessionFile: forkFile });
fake.queueState({ sessionFile: forkFile });
const forked = yield* forkAtFirst(
uncapturedTurns(source.id, { startedAt: 100, completedAt: 200 }),
);
assert.equal(forked.nativeThreadRef?.nativeId, forkFile);
assert.deepEqual(forkedEntries(), ["u2"]);
assert.isFalse(fake.allRequests().some((request) => request.type === "fork"));
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("presents explicitly namespaced MCP extension tools", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
Expand Down Expand Up @@ -2697,6 +2806,106 @@ describe("PiAdapterV2", () => {
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("rolls back past a turn whose start entry was never captured", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
const { runtime } = yield* openRuntime(fake);
const providerThread = yield* runtime.ensureThread({
threadId: THREAD_ID,
modelSelection: modelSelection("default"),
runtimePolicy,
});
const rollBackToFirst = (turns: ReadonlyArray<OrchestrationV2ProviderTurn>) =>
runtime.rollbackThread({
providerThread,
target: {
type: "provider_turn",
checkpointId: CheckpointId.make("checkpoint-1"),
appRunOrdinal: 1,
providerTurn: turns[0]!,
},
providerThreadTurns: turns,
});
const forkedEntries = () =>
fake
.allRequests()
.filter((request) => request.type === "fork")
.map((request) => request.entryId);

fake.queueEntries(UNCAPTURED_TURN_TREE);
const unresolved = yield* rollBackToFirst(
uncapturedTurns(providerThread.id, { startedAt: 60, completedAt: 100 }),
).pipe(Effect.result);
assert.deepEqual(forkedEntries(), []);
assert.equal(unresolved._tag, "Failure");

fake.queueEntries({ entries: TURN_ONE_ENTRIES, leafId: "a1b" });
const missing = yield* rollBackToFirst(uncapturedTurns(providerThread.id)).pipe(
Effect.result,
);
assert.deepEqual(forkedEntries(), []);
assert.equal(missing._tag, "Failure");

// An adopted wake precedes its T3 turn, but follows turn 1 and its steer.
fake.queueEntries(UNCAPTURED_TURN_TREE);
yield* rollBackToFirst(uncapturedTurns(providerThread.id));
assert.deepEqual(forkedEntries(), ["u2"]);
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("preserves inherited history when rolling back an uncaptured first turn", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
const { runtime } = yield* openRuntime(fake);
const providerThread = yield* runtime.ensureThread({
threadId: THREAD_ID,
modelSelection: modelSelection("default"),
runtimePolicy,
});
const rollBackToStart = (startedAt: number) =>
runtime.rollbackThread({
providerThread: { ...providerThread, createdAt: at(50) },
target: {
type: "thread_start",
checkpointId: CheckpointId.make("checkpoint-start"),
appRunOrdinal: 0,
},
providerThreadTurns: [
{
...uncapturedTurns(providerThread.id)[1]!,
ordinal: 1,
startedAt: at(startedAt),
},
],
});
const forkedEntries = () =>
fake
.allRequests()
.filter((request) => request.type === "fork")
.map((request) => request.entryId);

const entries = {
entries: [
...TURN_ONE_ENTRIES,
sessionMessage("u2", "a1b", "user", 100),
sessionMessage("a2", "u2", "assistant", 120),
sessionMessage("u3", "a2", "user", 160),
sessionMessage("a3", "u3", "assistant", 180),
],
leafId: "a3",
};

// The adopted wake precedes T3's first turn; its steer must be discarded with it.
fake.queueEntries(entries);
yield* rollBackToStart(150);
assert.deepEqual(forkedEntries(), ["u2"]);

fake.queueEntries(entries);
yield* rollBackToStart(100);
assert.deepEqual(forkedEntries(), ["u2", "u2"]);
}).pipe(Effect.scoped, Effect.provide(layerTest)),
);

it.effect("settles a command-only prompt from its deferred ack and idle probe", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
Expand Down
Loading
Loading