test(agent-sdk): mirror backend snake_case wire format and new status enums in mock proxy

Mock proxy now serializes responses using snake_case keys (agent_id,
created_at, system_prompt, run_id, etc.) and reads request bodies as
snake_case so it matches the real backend that the SDK now talks to via
the new transform layer. Also update the status string literals to the
new SessionStatus and RunStatus values, and read the followup body as
{prompt: {text}}.
This commit is contained in:
Ishaan Jaffer 2026-05-06 16:10:35 -07:00
parent b65d494c39
commit f68bd10230
No known key found for this signature in database

View file

@ -23,18 +23,23 @@ interface AgentRecord {
interface SessionRecord {
id: string;
agentId: string;
status: "pending" | "running" | "idle" | "terminated" | "failed";
status: "provisioning" | "ready" | "busy" | "error" | "terminated";
vmId: string;
createdAt: string;
runs: Map<string, RunRecord>;
followups: string[];
conversation: { role: string; content: string; runId?: string; createdAt: string }[];
conversation: {
role: string;
content: string;
runId?: string;
createdAt: string;
}[];
}
interface RunRecord {
id: string;
sessionId: string;
status: "queued" | "running" | "completed" | "failed" | "cancelled";
status: "queued" | "running" | "finished" | "cancelled" | "error";
result: string | null;
events: { seq: number; type: string; data: unknown }[];
startedAt: string | null;
@ -68,7 +73,9 @@ export class MockProxy {
}
async start(): Promise<string> {
await new Promise<void>((resolve) => this.server.listen(0, "127.0.0.1", resolve));
await new Promise<void>((resolve) =>
this.server.listen(0, "127.0.0.1", resolve),
);
const addr = this.server.address() as AddressInfo;
return `http://127.0.0.1:${addr.port}`;
}
@ -84,7 +91,7 @@ export class MockProxy {
}
}
await new Promise<void>((resolve, reject) =>
this.server.close((err) => (err ? reject(err) : resolve()))
this.server.close((err) => (err ? reject(err) : resolve())),
);
}
@ -124,7 +131,7 @@ export class MockProxy {
complete(runId: string, result: string): void {
const run = this.findRun(runId);
if (!run) return;
run.status = "completed";
run.status = "finished";
run.result = result;
run.completedAt = new Date().toISOString();
this.emit(runId, "done", { result });
@ -137,18 +144,28 @@ export class MockProxy {
return `${prefix}_${this.idCounter}`;
}
private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
private async handle(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
if (this.options.failFirstRequest && !this.firstRequestServed) {
this.firstRequestServed = true;
res.writeHead(503, { "retry-after": "0", "content-type": "application/json" });
res.end(JSON.stringify({ error: { code: "transient", message: "boom" } }));
res.writeHead(503, {
"retry-after": "0",
"content-type": "application/json",
});
res.end(
JSON.stringify({ error: { code: "transient", message: "boom" } }),
);
return;
}
this.firstRequestServed = true;
if (req.headers.authorization !== "Bearer test-key") {
res.writeHead(401, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { code: "unauthorized", message: "bad key" } }));
res.end(
JSON.stringify({ error: { code: "unauthorized", message: "bad key" } }),
);
return;
}
@ -174,7 +191,11 @@ export class MockProxy {
return ok(res, { ok: true });
}
}
if (segments.length === 4 && segments[1] === "agents" && segments[3] === "sessions") {
if (
segments.length === 4 &&
segments[1] === "agents" &&
segments[3] === "sessions"
) {
const agent = this.agents.get(segments[2]);
if (!agent) return notFound(res);
if (method === "POST") return this.createSession(agent, req, res);
@ -183,7 +204,11 @@ export class MockProxy {
items: [...agent.sessions.values()].map(serializeSession),
});
}
if (segments.length === 5 && segments[1] === "agents" && segments[3] === "sessions") {
if (
segments.length === 5 &&
segments[1] === "agents" &&
segments[3] === "sessions"
) {
const agent = this.agents.get(segments[2]);
const session = agent?.sessions.get(segments[4]);
if (!session) return notFound(res);
@ -200,43 +225,78 @@ export class MockProxy {
return ok(res, { ok: true });
}
}
if (segments.length === 4 && segments[3] === "runs" && method === "POST") {
if (
segments.length === 4 &&
segments[3] === "runs" &&
method === "POST"
) {
return this.createRun(session, req, res);
}
if (segments.length === 4 && segments[3] === "runs" && method === "GET") {
if (
segments.length === 4 &&
segments[3] === "runs" &&
method === "GET"
) {
return ok(res, {
items: [...session.runs.values()].map(serializeRun),
});
}
if (segments.length === 4 && segments[3] === "followup" && method === "POST") {
if (
segments.length === 4 &&
segments[3] === "followup" &&
method === "POST"
) {
const body = await readJson(req);
session.followups.push(body.message);
// Wire format: {prompt: {text: "..."}} per FollowupCreate.
const text = body?.prompt?.text ?? "";
session.followups.push(text);
session.conversation.push({
role: "user",
content: body.message,
content: text,
createdAt: new Date().toISOString(),
});
return ok(res, { ok: true });
}
if (segments.length === 4 && segments[3] === "conversation" && method === "GET") {
return ok(res, { turns: session.conversation });
if (
segments.length === 4 &&
segments[3] === "conversation" &&
method === "GET"
) {
return ok(res, { turns: session.conversation.map(serializeTurn) });
}
if (segments.length === 5 && segments[3] === "runs") {
const run = session.runs.get(segments[4]);
if (!run) return notFound(res);
if (method === "GET") return ok(res, serializeRun(run));
}
if (segments.length === 6 && segments[3] === "runs" && segments[5] === "events") {
if (
segments.length === 6 &&
segments[3] === "runs" &&
segments[5] === "events"
) {
const run = session.runs.get(segments[4]);
if (!run) return notFound(res);
return this.streamEvents(run, req, res, url);
}
if (segments.length === 6 && segments[3] === "runs" && segments[5] === "conversation") {
if (
segments.length === 6 &&
segments[3] === "runs" &&
segments[5] === "conversation"
) {
const run = session.runs.get(segments[4]);
if (!run) return notFound(res);
return ok(res, { turns: session.conversation.filter((t) => !t.runId || t.runId === run.id) });
return ok(res, {
turns: session.conversation
.filter((t) => !t.runId || t.runId === run.id)
.map(serializeTurn),
});
}
if (segments.length === 6 && segments[3] === "runs" && segments[5] === "cancel" && method === "POST") {
if (
segments.length === 6 &&
segments[3] === "runs" &&
segments[5] === "cancel" &&
method === "POST"
) {
const run = session.runs.get(segments[4]);
if (!run) return notFound(res);
run.status = "cancelled";
@ -248,19 +308,26 @@ export class MockProxy {
} catch (e) {
res.writeHead(500, { "content-type": "application/json" });
res.end(
JSON.stringify({ error: { code: "internal", message: (e as Error).message } })
JSON.stringify({
error: { code: "internal", message: (e as Error).message },
}),
);
}
}
private async createAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
private async createAgent(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
const body = await readJson(req);
const id = this.nextId("agt");
const agent: AgentRecord = {
id,
name: body.name ?? "unnamed",
model: body.model ?? { id: "test-model" },
systemPrompt: body.systemPrompt,
// Wire format is snake_case (Python idiom); SDK transforms camelCase
// public API to snake_case before sending.
systemPrompt: body.system_prompt,
metadata: body.metadata ?? {},
createdAt: new Date().toISOString(),
sessions: new Map(),
@ -278,7 +345,7 @@ export class MockProxy {
private async createSession(
agent: AgentRecord,
req: IncomingMessage,
res: ServerResponse
res: ServerResponse,
): Promise<void> {
await readJson(req);
const id = this.nextId("ses");
@ -286,7 +353,7 @@ export class MockProxy {
const session: SessionRecord = {
id,
agentId: agent.id,
status: "idle",
status: "ready",
vmId,
createdAt: new Date().toISOString(),
runs: new Map(),
@ -300,7 +367,7 @@ export class MockProxy {
private async createRun(
session: SessionRecord,
req: IncomingMessage,
res: ServerResponse
res: ServerResponse,
): Promise<void> {
const body = await readJson(req);
// 409 if any non-terminal run exists.
@ -308,7 +375,9 @@ export class MockProxy {
if (r.status === "queued" || r.status === "running") {
res.writeHead(409, { "content-type": "application/json" });
res.end(
JSON.stringify({ error: { code: "session_busy", message: "run in flight" } })
JSON.stringify({
error: { code: "session_busy", message: "run in flight" },
}),
);
return;
}
@ -338,7 +407,7 @@ export class MockProxy {
run: RunRecord,
req: IncomingMessage,
res: ServerResponse,
url: URL
url: URL,
): void {
const startingSeq = Number(url.searchParams.get("starting_seq") ?? -1);
res.writeHead(200, {
@ -348,13 +417,18 @@ export class MockProxy {
});
// Replay events from startingSeq forward.
const replayFrom = Number.isFinite(startingSeq) && startingSeq >= 0 ? startingSeq : 0;
const replayFrom =
Number.isFinite(startingSeq) && startingSeq >= 0 ? startingSeq : 0;
for (const event of run.events) {
if (event.seq < replayFrom) continue;
res.write(`id: ${event.seq}\ndata: ${JSON.stringify(event)}\n\n`);
}
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
if (
run.status === "finished" ||
run.status === "error" ||
run.status === "cancelled"
) {
res.end();
return;
}
@ -375,11 +449,12 @@ export class MockProxy {
}
private serializeAgent(agent: AgentRecord) {
// Wire format is snake_case; SDK transforms back to camelCase on receive.
return {
id: agent.id,
name: agent.name,
model: agent.model,
createdAt: agent.createdAt,
created_at: agent.createdAt,
};
}
}
@ -387,20 +462,34 @@ export class MockProxy {
function serializeSession(s: SessionRecord) {
return {
id: s.id,
agentId: s.agentId,
agent_id: s.agentId,
status: s.status,
createdAt: s.createdAt,
created_at: s.createdAt,
};
}
function serializeRun(r: RunRecord) {
return {
id: r.id,
sessionId: r.sessionId,
session_id: r.sessionId,
status: r.status,
result: r.result,
startedAt: r.startedAt,
completedAt: r.completedAt,
started_at: r.startedAt,
completed_at: r.completedAt,
};
}
function serializeTurn(t: {
role: string;
content: string;
runId?: string;
createdAt: string;
}) {
return {
role: t.role,
content: t.content,
run_id: t.runId,
created_at: t.createdAt,
};
}
@ -411,7 +500,9 @@ function ok(res: ServerResponse, body: unknown): void {
function notFound(res: ServerResponse): void {
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { code: "not_found", message: "not found" } }));
res.end(
JSON.stringify({ error: { code: "not_found", message: "not found" } }),
);
}
function readJson(req: IncomingMessage): Promise<any> {