Skip to content
Closed
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
240 changes: 240 additions & 0 deletions studio/src/lib/harness/client-actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { StreamEvent } from "@/features/agent/types";
import {
cancelHarnessSteer,
compactHarnessSession,
fetchHarnessSessionDetail,
listHarnessAgents,
retryHarnessRun,
steerHarnessRun,
} from "./client";

/**
* Pins the request/response contracts of the chat-resilience client calls:
* manual compaction (ADR 0244), the strict multimodal steer + the
* cancel-steer route naming (ADR 0252), and the GET-session resolved-model
* echo the context meter reads (B1).
*/

type Captured = { url: string; init?: RequestInit };

function stubFetch(status: number, body: unknown): Captured {
const captured: Captured = { url: "" };
vi.stubGlobal("fetch", async (url: RequestInfo | URL, init?: RequestInit) => {
captured.url = String(url);
captured.init = init;
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
});
return captured;
}

afterEach(() => {
vi.unstubAllGlobals();
});

describe("compactHarnessSession", () => {
it("POSTs bodyless and reads the compacted bool", async () => {
const captured = stubFetch(200, { compacted: true });
await expect(compactHarnessSession("s1")).resolves.toBe(true);
expect(captured.url).toBe("/api/mecatl/v1/sessions/s1/compact");
expect(captured.init?.method).toBe("POST");
expect(captured.init?.body).toBeUndefined();
});

it("reads an empty answer as nothing-to-compact (the live daemon omits false)", async () => {
stubFetch(200, {});
await expect(compactHarnessSession("s1")).resolves.toBe(false);
});

it("throws the typed error on a refusal", async () => {
stubFetch(412, { code: "session_active", error: "session is running" });
await expect(compactHarnessSession("s1")).rejects.toMatchObject({
name: "HarnessApiError",
status: 412,
code: "session_active",
});
});
});

describe("steerHarnessRun", () => {
it("sends the strict body: text, message_id, parts, and expected_run_id (ADR 0252)", async () => {
const captured = stubFetch(200, {
outcome: "accepted",
message_id: "m-1",
});
const result = await steerHarnessRun("s1", "focus", "m-1", {
expectedRunId: "run-9",
parts: [{ kind: "image", mime_type: "image/png", data: "aGk=" }],
});
expect(result).toEqual({ outcome: "accepted", messageId: "m-1" });
expect(captured.url).toBe("/api/mecatl/v1/sessions/s1/steer");
expect(JSON.parse(String(captured.init?.body))).toEqual({
text: "focus",
message_id: "m-1",
expected_run_id: "run-9",
parts: [{ kind: "image", mime_type: "image/png", data: "aGk=" }],
});
});

it("omits parts and expected_run_id when the caller has none (legacy shape)", async () => {
const captured = stubFetch(200, { outcome: "appended" });
await steerHarnessRun("s1", "focus", "m-2");
expect(JSON.parse(String(captured.init?.body))).toEqual({
text: "focus",
message_id: "m-2",
});
});

it("surfaces the strict 409 as the typed stale_run_control error", async () => {
stubFetch(409, {
code: "stale_run_control",
error: "the named run already ended",
});
await expect(
steerHarnessRun("s1", "focus", "m-3", { expectedRunId: "run-old" }),
).rejects.toMatchObject({
name: "HarnessApiError",
status: 409,
code: "stale_run_control",
});
});
});

describe("cancelHarnessSteer", () => {
it("POSTs the ADR-0252 cancel-steer route (steer-cancel is the deprecated alias)", async () => {
const captured = stubFetch(200, { outcome: "retracted" });
await expect(cancelHarnessSteer("s1")).resolves.toBe("retracted");
expect(captured.url).toBe("/api/mecatl/v1/sessions/s1/cancel-steer");
expect(captured.init?.method).toBe("POST");
});
});

describe("retryHarnessRun", () => {
it("POSTs the retry route bodyless and relays the SSE exactly like a prompt (B2.2)", async () => {
const sse = [
'data: {"type":"model.retry","model_retry":{"retry_disposition":2}}',
"",
'data: {"type":"message.delta","text":"resumed"}',
"",
'data: {"type":"result","result":{"stop":"end_turn","text":"done"}}',
"",
"",
].join("\n");
const captured: Captured = { url: "" };
vi.stubGlobal(
"fetch",
async (url: RequestInfo | URL, init?: RequestInit) => {
captured.url = String(url);
captured.init = init;
return new Response(sse, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
},
);
const events: StreamEvent[] = [];
await retryHarnessRun("s1", (event) => events.push(event));
expect(captured.url).toBe("/api/mecatl/v1/sessions/s1/retry");
expect(captured.init?.method).toBe("POST");
expect(captured.init?.body).toBeUndefined();
expect(events).toEqual([
{ type: "notice", text: "Retrying the failed step…" },
{ type: "token", text: "resumed" },
{
type: "run_result",
stop: "end_turn",
text: "done",
errorText: "",
permanent: false,
},
]);
});

it("surfaces the 409 ineligibility as the typed code", async () => {
stubFetch(409, {
code: "failed_step_retry_ineligible",
error: "retry is not eligible",
});
await expect(retryHarnessRun("s1", () => {})).rejects.toMatchObject({
name: "HarnessApiError",
status: 409,
code: "failed_step_retry_ineligible",
});
});
});

describe("fetchHarnessSessionDetail", () => {
it("decodes the resolved_model echo and the capabilities object (B1)", async () => {
stubFetch(200, {
session_id: "s1",
resolved_model: {
provider_id: "openrouter",
model_id: "openai/gpt-5",
context_window: 400000,
},
capabilities: { manual_compaction: true },
});
await expect(fetchHarnessSessionDetail("s1")).resolves.toEqual({
resolvedModel: {
providerId: "openrouter",
modelId: "openai/gpt-5",
contextWindow: 400000,
},
capabilities: { manual_compaction: true },
});
});

it("tolerates a daemon that echoes neither field", async () => {
stubFetch(200, { session_id: "s1", mode: "default" });
await expect(fetchHarnessSessionDetail("s1")).resolves.toEqual({
resolvedModel: null,
capabilities: {},
});
});
});

describe("listHarnessAgents", () => {
it("decodes the full AgentInfo row: model, tools, permission_mode, color (D2.2)", async () => {
stubFetch(200, {
agents: [
{
name: "reviewer",
description: "Reviews diffs",
model: "anthropic/claude-opus-4.6",
tools: ["Read", "Grep", 7, "Glob"],
permission_mode: "plan",
color: "cyan",
},
// A minimal def: the live daemon omits every empty field.
{ name: "explorer" },
],
});
await expect(listHarnessAgents()).resolves.toEqual([
{
name: "reviewer",
description: "Reviews diffs",
model: "anthropic/claude-opus-4.6",
// Non-string entries are dropped, never rendered.
tools: ["Read", "Grep", "Glob"],
permissionMode: "plan",
color: "cyan",
},
{
name: "explorer",
description: "",
model: "",
tools: [],
permissionMode: "",
color: "",
},
]);
});

it("reads an empty roster off the live daemon's bare {} response", async () => {
stubFetch(200, {});
await expect(listHarnessAgents()).resolves.toEqual([]);
});
});
94 changes: 94 additions & 0 deletions studio/src/lib/harness/client-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { fetchHarnessCompatibility, HarnessApiError } from "./client";

/**
* Pins the ADR-0248 client contract: errors are typed on the stable machine
* `code` (problem-details body), with the legacy `error` prose as the
* message; named codes get plainer framing; compatibility decoding tolerates
* older daemons (404 → null).
*/

function jsonResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/problem+json" },
});
}

describe("HarnessApiError via fetchHarnessCompatibility", () => {
it("carries the stable code and the server's message", async () => {
const original = globalThis.fetch;
globalThis.fetch = async () =>
jsonResponse(409, {
type: "urn:mecatl:error:stale_run_control",
code: "stale_run_control",
error: "the named run already ended",
status: 409,
});
try {
await expect(fetchHarnessCompatibility()).rejects.toMatchObject({
name: "HarnessApiError",
status: 409,
code: "stale_run_control",
message: "the named run already ended",
});
} finally {
globalThis.fetch = original;
}
});

it("frames the named codes in plain language", async () => {
const original = globalThis.fetch;
globalThis.fetch = async () =>
jsonResponse(503, { code: "draining", error: "server draining" });
try {
const error = await fetchHarnessCompatibility().catch((e) => e);
expect(error).toBeInstanceOf(HarnessApiError);
expect((error as HarnessApiError).code).toBe("draining");
expect((error as HarnessApiError).message).toMatch(/restarting/);
} finally {
globalThis.fetch = original;
}
});

it("degrades to status text against a non-JSON error body", async () => {
const original = globalThis.fetch;
globalThis.fetch = async () =>
new Response("nope", { status: 500, statusText: "Internal Error" });
try {
const error = await fetchHarnessCompatibility().catch((e) => e);
expect((error as HarnessApiError).code).toBe("");
expect((error as HarnessApiError).message).toBe("500 Internal Error");
} finally {
globalThis.fetch = original;
}
});

it("decodes the compatibility document and tolerates a 404 daemon", async () => {
const original = globalThis.fetch;
globalThis.fetch = async () =>
jsonResponse(200, {
api_major: 1,
features: ["watch_session_events", 7, "server_info"],
capabilities: { steer: true },
deployment: "lab-1",
});
try {
const doc = await fetchHarnessCompatibility();
expect(doc).toEqual({
apiMajor: 1,
features: ["watch_session_events", "server_info"],
capabilities: { steer: true },
deployment: "lab-1",
});
} finally {
globalThis.fetch = original;
}
globalThis.fetch = async () => new Response("", { status: 404 });
try {
expect(await fetchHarnessCompatibility()).toBeNull();
} finally {
globalThis.fetch = original;
}
});
});
Loading
Loading