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
113 changes: 99 additions & 14 deletions apps/server/src/orchestration-v2/Adapters/PiAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import {
CheckpointId,
EnvironmentId,
NodeId,
ProviderInstanceId,
ProviderSessionId,
Expand All @@ -23,9 +24,10 @@ import * as Queue from "effect/Queue";
import * as Schema from "effect/Schema";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import { ChildProcessSpawner } from "effect/unstable/process";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { ServerConfig } from "../../config.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import { IdAllocatorV2, layer as idAllocatorLayer } from "../IdAllocator.ts";
import {
ProviderAdapterV2RuntimePolicy,
Expand Down Expand Up @@ -75,6 +77,10 @@ interface FakePi {
readonly queueEntries: (data: unknown) => void;
/** Make the next `switch_session` ack report an extension veto. */
readonly vetoNextSwitch: () => void;
readonly lastSpawn: () => {
readonly args: ReadonlyArray<string>;
readonly env: NodeJS.ProcessEnv;
};
}

/**
Expand Down Expand Up @@ -144,9 +150,19 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
}
});

const spawner = ChildProcessSpawner.make(() =>
Effect.succeed(
ChildProcessSpawner.makeHandle({
let lastSpawn: { readonly args: ReadonlyArray<string>; readonly env: NodeJS.ProcessEnv } = {
args: [],
env: {},
};
const spawner = ChildProcessSpawner.make((command) =>
Effect.sync(() => {
if (ChildProcess.isStandardCommand(command)) {
lastSpawn = {
args: command.args,
env: command.options.env ?? {},
};
}
return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(FAKE_PID),
exitCode: Effect.never,
isRunning: Effect.succeed(true),
Expand All @@ -158,8 +174,8 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
}),
),
});
}),
);

const takeRequest = (type: string): Effect.Effect<PiRpcRecord> =>
Expand All @@ -178,6 +194,7 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () {
vetoNextSwitch: () => {
vetoSwitch = true;
},
lastSpawn: () => lastSpawn,
} satisfies FakePi;
});

Expand Down Expand Up @@ -288,10 +305,39 @@ describe("PiAdapterV2", () => {
assert.isFalse(PiProviderCapabilitiesV2.turns.supportsSteeringByInterruptRestart);
assert.equal(PiProviderCapabilitiesV2.turns.terminalStatusQuality, "strong");
assert.isFalse(PiProviderCapabilitiesV2.approvals.supportsCommandApproval);
assert.isFalse(PiProviderCapabilitiesV2.tools.supportsMcpTools);
assert.isTrue(PiProviderCapabilitiesV2.tools.supportsMcpTools);
assert.isTrue(PiProviderCapabilitiesV2.subagents.exposesSubagentThreadIds);
assert.equal(PiProviderCapabilitiesV2.identity.nativeThreadIds, "strong");
});

it.effect("injects the T3 MCP extension and bearer when a session exists", () =>
Effect.gen(function* () {
McpProviderSession.setMcpProviderSession({
environmentId: EnvironmentId.make("environment-pi-mcp"),
threadId: THREAD_ID,
providerSessionId: "mcp-session-pi",
providerInstanceId: PI_INSTANCE_ID,
endpoint: "http://127.0.0.1:43123/mcp",
authorizationHeader: "Bearer secret-pi-token",
});
const fake = yield* makeFakePi;
yield* openRuntime(fake);
const spawn = fake.lastSpawn();
assert.isTrue(spawn.args.includes("--extension"));
const extensions = spawn.args.flatMap((arg, index) =>
arg === "--extension" ? [spawn.args[index + 1]] : [],
);
assert.isTrue(extensions.some((path) => path?.endsWith("pi-t3-subagent-extension.ts")));
assert.isTrue(extensions.some((path) => path?.endsWith("pi-t3-mcp-extension.ts")));
assert.equal(spawn.env.T3_MCP_URL, "http://127.0.0.1:43123/mcp");
assert.equal(spawn.env.T3_MCP_BEARER_TOKEN, "secret-pi-token");
}).pipe(
Effect.ensuring(Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID))),
Effect.scoped,
Effect.provide(testLayer),
),
);

it.effect("registers the thread from get_state and resumes via switch_session", () =>
Effect.gen(function* () {
const fake = yield* makeFakePi;
Expand All @@ -303,6 +349,13 @@ describe("PiAdapterV2", () => {
});
assert.equal(providerThread.nativeThreadRef?.nativeId, FAKE_SESSION_FILE);
assert.equal(providerThread.driver, PI_PROVIDER);
const spawn = fake.lastSpawn();
assert.isTrue(
spawn.args.some(
(arg, index) =>
arg === "--extension" && spawn.args[index + 1]?.endsWith("pi-t3-subagent-extension.ts"),
),
);

yield* runtime.resumeThread({ providerThread });
const switchRequest = yield* fake.takeRequest("switch_session");
Expand Down Expand Up @@ -673,6 +726,7 @@ describe("PiAdapterV2", () => {
task: "map the repo",
exitCode: 0,
stderr: "",
sessionFile: "/tmp/pi-children/scout.jsonl",
messages: [
{ role: "assistant", content: [{ type: "text", text: "scanning files" }] },
],
Expand All @@ -681,14 +735,32 @@ describe("PiAdapterV2", () => {
},
},
});
const childThread = yield* takeEvent((event) => event.type === "app_thread.created");
if (childThread.type !== "app_thread.created") {
assert.fail("expected app_thread.created");
return;
}
const childThreadId = childThread.appThread.id;
const childProviderThread = yield* takeEvent(
(event) =>
event.type === "provider_thread.updated" &&
event.providerThread.appThreadId === childThreadId,
);
assert.isTrue(
childProviderThread.type === "provider_thread.updated" &&
childProviderThread.providerThread.nativeThreadRef?.nativeId ===
"/tmp/pi-children/scout.jsonl" &&
childProviderThread.providerThread.providerSessionId === null,
);
const running = yield* takeEvent(
(event) => event.type === "subagent.updated" && event.subagent.status === "running",
);
assert.isTrue(
running.type === "subagent.updated" &&
running.subagent.title === "scout" &&
running.subagent.prompt === "map the repo" &&
running.subagent.progress === "scanning files",
running.subagent.progress === "scanning files" &&
running.subagent.childThreadId === childThreadId,
);
yield* fake.emit({
type: "tool_execution_end",
Expand All @@ -706,6 +778,7 @@ describe("PiAdapterV2", () => {
exitCode: 0,
stopReason: "stop",
stderr: "",
sessionFile: "/tmp/pi-children/scout.jsonl",
messages: [
{ role: "assistant", content: [{ type: "text", text: "repo has one file" }] },
],
Expand All @@ -725,20 +798,32 @@ describe("PiAdapterV2", () => {
(event) => event.type === "subagent.updated" && event.subagent.status === "completed",
);
assert.isTrue(
doneCard.type === "subagent.updated" && doneCard.subagent.result === "repo has one file",
doneCard.type === "subagent.updated" &&
doneCard.subagent.result === "repo has one file" &&
doneCard.subagent.childThreadId === childThreadId,
);
// Completed turn_item is emitted immediately after the completed card;
// waiting for the failed card first would consume it.
const subagentItem = yield* takeEvent(
(event) =>
event.type === "turn_item.updated" &&
event.turnItem.type === "subagent" &&
event.turnItem.status === "completed",
);
assert.isTrue(
subagentItem.type === "turn_item.updated" &&
subagentItem.turnItem.type === "subagent" &&
subagentItem.turnItem.childThreadId === childThreadId,
);
const failedCard = yield* takeEvent(
(event) => event.type === "subagent.updated" && event.subagent.status === "failed",
);
assert.isTrue(
failedCard.type === "subagent.updated" &&
failedCard.subagent.title === "worker" &&
failedCard.subagent.result === "boom",
);
const subagentItem = yield* takeEvent(
(event) => event.type === "turn_item.updated" && event.turnItem.type === "subagent",
failedCard.subagent.result === "boom" &&
failedCard.subagent.childThreadId === null,
);
assert.equal(subagentItem.type, "turn_item.updated");
}).pipe(Effect.scoped, Effect.provide(testLayer)),
);

Expand Down
Loading
Loading