Files
paseo/plugins/antigravity-provider/server/provider.test.ts
T
Mohamed Boudra 7b0cace991 Restore Antigravity with Full access and session warnings (#5795)
* Revert "Remove the unreleased Antigravity provider plugin (#5781)"

This reverts commit 9d9d9af993.

* fix(providers): commit plugin notices emitted during session opening

* feat(providers): restore Antigravity with full access and session warnings

* fix(providers): warn once when opening Antigravity sessions

Driver respawn retains the live transcript and unchanged permission posture. Emit the warning only when opening a session, including resume after a daemon restart.
2026-09-30 23:18:05 +01:00

776 lines
28 KiB
TypeScript

import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import {
PROVIDER_CAPABILITIES,
ProviderEventSchema,
type ProviderConnection,
type ProviderContent,
type ProviderEvent,
type ProviderInput,
type ProviderLaunch,
type ProviderPersistence,
} from "@getpaseo/plugin/server/provider";
import { signalPlan } from "./internal/signals.js";
import { createAntigravityProvider } from "./provider.js";
const temporary: string[] = [];
const connections: ProviderConnection[] = [];
const fake = fileURLToPath(new URL("../test/agy.mjs", import.meta.url));
const provider = createAntigravityProvider();
it("publishes a monochrome, theme-aware provider icon", async () => {
const icon = await readFile(new URL("../icon.svg", import.meta.url), "utf8");
expect(icon).toContain('viewBox="0 0 24 24"');
expect(icon).toContain('fill="currentColor"');
expect(icon).not.toMatch(/#[\da-f]{3,8}\b|rgba?\(|hsla?\(|url\(/i);
expect(icon).not.toMatch(/<(?:style|mask|defs|linearGradient|radialGradient)\b|\bid=/i);
expect([...icon.matchAll(/\b(?:fill|stroke)="([^"]+)"/g)].map((match) => match[1])).toEqual([
"currentColor",
]);
});
afterEach(async () => {
await Promise.all(connections.splice(0).map((connection) => connection.close()));
await Promise.all(
temporary.splice(0).map((directory) => rm(directory, { recursive: true, force: true })),
);
});
async function harness(env: Record<string, string> = {}) {
const cwd = await mkdtemp(path.join(os.tmpdir(), "antigravity-test-"));
temporary.push(cwd);
const log = path.join(cwd, "argv.ndjson");
const launch: ProviderLaunch = {
command: process.execPath,
args: [fake, "--launch-prefix"],
env: { AGY_TEST_LOG: log, ...env },
};
const connection = await provider.connect({
versions: [1],
capabilities: PROVIDER_CAPABILITIES,
launch,
});
connections.push(connection);
const events: ProviderEvent[] = [];
connection.onEvent((event) => {
ProviderEventSchema.parse(event);
events.push(event);
});
async function wait(predicate: (event: ProviderEvent) => boolean) {
await expect.poll(() => events.some(predicate), { timeout: 5000, interval: 10 }).toBe(true);
const event = events.find(predicate);
if (!event) throw new Error("Expected event missing");
return event;
}
async function request(input: ProviderInput) {
await connection.send(input);
if ("requestId" in input)
await wait(
(event) =>
"requestId" in event &&
event.requestId === input.requestId &&
(event.type === "request.completed" ||
event.type === "request.failed" ||
event.type === "session.ready" ||
event.type === "catalog"),
);
}
async function open(restored?: ProviderPersistence, sessionId = "s") {
await request({
type: "session.open",
requestId: `open-${events.length}`,
sessionId,
persistence: restored,
history: "replay",
config: {
cwd,
env: { AGY_SESSION_VALUE: "session-overlay" },
settings: {},
mcpServers: {},
persist: true,
systemPrompt: "SYSTEM PREFIX",
},
});
}
async function message(content: ProviderContent[], sessionId = "s") {
const clientMessageId = `prompt-${events.length}`;
await connection.send({
type: "session.prompt",
sessionId,
prompt: {
clientMessageId,
delivery: "auto",
input: { type: "message", content },
},
});
await wait(
(event) =>
event.type === "session.prompt_result" && event.clientMessageId === clientMessageId,
);
return clientMessageId;
}
function prompt(text: string) {
return message([{ type: "text", text }]);
}
async function completed(clientMessageId: string) {
const result = events.find(
(event) =>
event.type === "session.prompt_result" && event.clientMessageId === clientMessageId,
);
if (!result || result.type !== "session.prompt_result" || result.result.type !== "turn")
throw new Error("Prompt was not admitted");
const turnId = result.result.turnId;
return wait(
(event) =>
event.type === "session.turn" && event.turnId === turnId && event.state !== "started",
);
}
async function records() {
return (await readFile(log, "utf8"))
.trim()
.split("\n")
.map((line) => JSON.parse(line));
}
function persistence() {
const opened = events.find((event) => event.type === "session.opened");
if (!opened || opened.type !== "session.opened" || !opened.persistence)
throw new Error("Missing persistence");
return opened.persistence;
}
return {
connection,
events,
launch,
cwd,
wait,
request,
open,
prompt,
message,
completed,
records,
persistence,
};
}
it("streams complete text snapshots, prefixes the system prompt only once and preserves launch argv/env", async () => {
const h = await harness();
expect(h.connection.capabilities).toEqual([
"prompt.message",
"prompt.image",
"session.configure",
"session.persistence",
]);
await h.open();
await h.completed(await h.prompt("HELLO"));
await h.completed(await h.prompt("HELLO AGAIN"));
const text = h.events.filter(
(event) => event.type === "timeline.item" && event.item.type === "assistant_message",
);
expect(
text.map((event) =>
event.type === "timeline.item" && event.item.type === "assistant_message"
? event.item.text
: "",
),
).toEqual(["PHASE0_HELLO", "PHASE0_HELLO\n", "PHASE0_HELLO", "PHASE0_HELLO\n"]);
const records = await h.records();
expect(records.find((entry) => entry.args.includes("--input-format")).args).toEqual([
"--launch-prefix",
"--input-format",
"stream-json",
"--output-format",
"stream-json",
"--add-dir",
h.cwd,
"--disable-slash-commands",
"--dangerously-skip-permissions",
]);
expect(records.find((entry) => entry.args.includes("--input-format")).env).toBe(
"session-overlay",
);
expect(
records.filter((entry) => entry.input).map((entry) => entry.input.message.content),
).toEqual(["SYSTEM PREFIX\n\nHELLO", "HELLO AGAIN"]);
});
it("maps tools and emits one denial notice for a SUCCESS result", async () => {
const h = await harness();
await h.open();
// This capture contains three distinct native results, one for each submitted prompt.
await h.completed(await h.prompt("TOOLS"));
await h.completed(await h.prompt("TOOLS"));
const result = await h.completed(await h.prompt("TOOLS"));
expect(result).toMatchObject({ type: "session.turn", state: "completed" });
const tools = h.events.filter(
(event) => event.type === "timeline.item" && event.item.type === "tool_call",
);
expect(tools).toEqual(
expect.arrayContaining([
expect.objectContaining({
item: expect.objectContaining({
name: "view_file",
status: "completed",
detail: expect.objectContaining({ type: "read" }),
}),
}),
expect.objectContaining({
item: expect.objectContaining({
name: "write_to_file",
status: "completed",
detail: expect.objectContaining({ type: "write" }),
}),
}),
expect.objectContaining({
item: expect.objectContaining({
name: "run_command",
status: "failed",
detail: expect.objectContaining({ type: "shell", command: "printf PHASE0_SHELL" }),
}),
}),
]),
);
const notices = h.events.filter(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
);
expect(notices).toHaveLength(1);
expect(notices[0]).toMatchObject({
notice: {
title: "Shell command denied",
description: "Antigravity's own policy denied it.",
},
});
});
it("reports only new process denials across replayed turns and resets after respawn", async () => {
const h = await harness();
await h.open();
await h.completed(await h.prompt("DENIAL_REPLAY"));
expect(
h.events.filter(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
),
).toHaveLength(1);
const followupStart = h.events.length;
await h.completed(await h.prompt("DENIAL_REPLAY"));
const followup = h.events.slice(followupStart);
expect(
followup.some(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
),
).toBe(false);
expect(followup).toContainEqual(
expect.objectContaining({
type: "timeline.item",
item: expect.objectContaining({ type: "assistant_message", text: "FOLLOWUP_OK\n" }),
}),
);
await h.request({
type: "session.configure",
sessionId: "s",
requestId: "mode",
changes: { model: "gemini-3.8-flash-low" },
});
await h.completed(await h.prompt("DENIAL_REPLAY"));
expect(
h.events.filter(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
),
).toHaveLength(2);
expect(
(await h.records()).filter((entry) => entry.args?.includes("--input-format")),
).toHaveLength(2);
});
it("counts repeated native action denials without exposing tool identifiers", async () => {
const h = await harness({ AGY_TEST_DENIAL_COUNTS: "2,2,3" });
await h.open();
await h.completed(await h.prompt("DENIAL_REPLAY"));
await h.completed(await h.prompt("DENIAL_REPLAY"));
const notices = h.events.filter(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
);
expect(notices).toHaveLength(1);
expect(notices[0]).toMatchObject({
notice: {
title: "2 actions denied",
description: "Antigravity's own policy denied it.",
},
});
await h.completed(await h.prompt("DENIAL_REPLAY"));
expect(
h.events.filter(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
),
).toHaveLength(2);
expect(
h.events.findLast(
(event) => event.type === "session.notice" && event.notice.id !== "s:full-access",
),
).toMatchObject({
notice: { title: "Shell command denied" },
});
});
it("accounts DONE generation usage once without adding cumulative results, including resume", async () => {
const h = await harness();
await h.open();
await h.completed(await h.prompt("MULTI"));
await h.completed(await h.prompt("MULTI"));
const usages = h.events.filter((event) => event.type === "session.usage");
expect(
usages.map((event) => (event.type === "session.usage" ? event.usage.inputTokens : null)),
).toEqual([11263, 11604]);
const persistence = h.persistence();
await h.request({ type: "session.close", sessionId: "s", requestId: "close" });
await h.open(persistence);
await h.completed(await h.prompt("RESUME"));
expect(h.events.filter((event) => event.type === "session.usage")).toHaveLength(3);
expect(h.events.findLast((event) => event.type === "session.usage")).toMatchObject({
usage: { inputTokens: 11815 },
});
});
it("maps subagent steps to a tool carrying the child conversation ID", async () => {
const h = await harness();
await h.open();
await h.completed(await h.prompt("SUBAGENT"));
expect(h.events).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "timeline.item",
item: expect.objectContaining({
name: "invoke_subagent",
status: "completed",
detail: expect.objectContaining({
type: "sub_agent",
childSessionId: "b6f02329-7415-49a7-a9a9-1cd09e898d26",
}),
}),
}),
]),
);
});
it.each(["HANG", "TOOL_HANG"])(
"interrupts %s, settles canceled, and respawns the same conversation",
async (text) => {
const h = await harness();
await h.open();
const message = await h.prompt(text);
await h.wait(
(event) =>
event.type === "timeline.item" &&
(event.item.type === "assistant_message" || event.item.type === "tool_call"),
);
await h.request({ type: "session.interrupt", requestId: "interrupt", sessionId: "s" });
expect(await h.completed(message)).toMatchObject({ state: "canceled" });
await h.completed(await h.prompt("HELLO"));
const records = await h.records();
const launches = records.filter((entry) => entry.args && entry.args.includes("--input-format"));
expect(launches).toHaveLength(2);
expect(launches[1].args.slice(-2)).toEqual([
"--conversation",
"e5cf1e2d-c715-4328-8e67-4a85c8dc3cda",
]);
// Windows taskkill terminates the tree without invoking the CLI's signal handlers.
expect(records.filter((entry) => entry.signal)).toEqual(
process.platform === "win32" ? [] : [{ signal: "SIGINT" }],
);
const canceled = h.events.filter(
(event) => event.type === "session.turn" && event.state === "canceled",
);
expect(canceled).toHaveLength(1);
},
);
it("stops an unresponsive driver and still respawns the conversation", async () => {
const h = await harness({ AGY_TEST_IGNORE_SIGNALS: "yes" });
await h.open();
const message = await h.prompt("HANG");
await h.wait(
(event) => event.type === "timeline.item" && event.item.type === "assistant_message",
);
await h.request({ type: "session.interrupt", requestId: "interrupt-timeout", sessionId: "s" });
expect(await h.completed(message)).toMatchObject({ state: "canceled" });
expect((await h.records()).filter((entry) => entry.signal)).toEqual(
process.platform === "win32" ? [] : [{ signal: "SIGINT" }, { signal: "SIGTERM" }],
);
await h.completed(await h.prompt("HELLO"));
});
it("rejects plan and keeps the full-access selection without respawning", async () => {
const h = await harness();
await h.open();
await h.request({
type: "session.configure",
requestId: "plan",
sessionId: "s",
changes: { mode: "plan" },
});
expect(h.events).toContainEqual(
expect.objectContaining({
type: "request.failed",
requestId: "plan",
error: { code: "INVALID_MODE", message: "Unknown Antigravity mode: plan" },
}),
);
await h.request({
type: "session.configure",
requestId: "full",
sessionId: "s",
changes: { mode: "full-access" },
});
await h.completed(await h.prompt("HELLO"));
expect(
(await h.records()).filter((entry) => entry.args?.includes("--input-format")),
).toHaveLength(1);
});
it("emits one full-access warning per open, none on respawn, and one again on resume", async () => {
const h = await harness();
await h.open();
const warning = {
type: "session.notice",
sessionId: "s",
notice: {
id: "s:full-access",
severity: "warning",
title: "Antigravity is running with full access",
description:
"Antigravity's CLI cannot ask for permission when another app drives it, so Paseo starts it with --dangerously-skip-permissions. Every tool call, including shell commands, runs without asking.",
},
};
const opened = h.events.findIndex((event) => event.type === "session.opened");
expect(h.events[opened + 1]).toEqual(warning);
expect(h.events.slice(0, opened).some((event) => event.type === "session.notice")).toBe(false);
await h.completed(await h.prompt("HELLO"));
await h.request({
type: "session.configure",
requestId: "model",
sessionId: "s",
changes: { model: "gemini-3.8-flash-low" },
});
await h.completed(await h.prompt("HELLO"));
expect(h.events.filter((event) => event.type === "session.notice")).toEqual([warning]);
const launches = (await h.records()).filter((entry) => entry.args?.includes("--input-format"));
expect(launches).toHaveLength(2);
for (const launch of launches) {
expect(launch.args).toContain("--dangerously-skip-permissions");
expect(launch.args).not.toContain("--mode");
}
const persistence = h.persistence();
await h.connection.close();
const resumed = await harness();
await resumed.open(persistence);
await resumed.completed(await resumed.prompt("RESUME"));
expect(resumed.events.filter((event) => event.type === "session.notice")).toEqual([warning]);
});
it("defers model and mode changes until the next prompt, then respawns with validated flags", async () => {
const h = await harness();
await h.open();
await h.request({
type: "session.configure",
requestId: "change",
sessionId: "s",
changes: { model: "gemini-3.8-flash-low", mode: "full-access" },
});
expect(
(await h.records()).filter((entry) => entry.args && entry.args.includes("--input-format")),
).toHaveLength(1);
await h.completed(await h.prompt("HELLO"));
const launches = (await h.records()).filter(
(entry) => entry.args && entry.args.includes("--input-format"),
);
expect(launches).toHaveLength(2);
expect(launches[1].args.slice(-4)).toEqual([
"--model",
"gemini-3.8-flash-low",
"--conversation",
"e5cf1e2d-c715-4328-8e67-4a85c8dc3cda",
]);
await h.request({
type: "session.configure",
requestId: "invalid",
sessionId: "s",
changes: { model: "unknown" },
});
expect(h.events).toContainEqual(
expect.objectContaining({
type: "request.failed",
requestId: "invalid",
error: expect.objectContaining({ code: "INVALID_MODEL" }),
}),
);
});
it("resumes after connection close without replay or another system prefix", async () => {
const first = await harness();
await first.open();
await first.completed(await first.prompt("HELLO"));
const persistence = first.persistence();
await first.connection.close();
const second = await harness();
await second.open(persistence);
expect(second.events.filter((event) => event.type === "timeline.item")).toEqual([]);
await second.completed(await second.prompt("RESUME"));
expect(
(await second.records())
.filter((entry) => entry.input)
.map((entry) => entry.input.message.content),
).toEqual(["RESUME"]);
});
it.each(["auth", "exit", "stderr"])(
"reports startup %s without waiting for init forever",
async (startup) => {
const h = await harness({ AGY_TEST_STARTUP: startup });
await h.open();
expect(h.events.filter((event) => event.type === "session.ready")).toEqual([]);
const failure = h.events.find((event) => event.type === "request.failed");
expect(failure).toMatchObject({
type: "request.failed",
error: {
message: expect.stringMatching(startup === "exit" ? /exited/ : /Run `agy` and sign in/),
},
});
},
);
it("rejects mid-turn steering and reports a busy message without queueing a second native turn", async () => {
const h = await harness();
await h.open();
await h.prompt("HANG");
const second = await h.prompt("SECOND");
expect(
h.events.find(
(event) => event.type === "session.prompt_result" && event.clientMessageId === second,
),
).toMatchObject({
type: "session.prompt_result",
result: { type: "failed", error: { code: "TURN_ACTIVE" } },
});
await expect(
h.connection.send({
type: "session.prompt",
sessionId: "s",
prompt: {
clientMessageId: "steer",
delivery: "steer",
input: { type: "message", content: [{ type: "text", text: "x" }] },
},
}),
).rejects.toThrow(/prompt.steer/);
});
describe("discovery", () => {
it("reuses one discovered catalog for two opens and configure, and refreshes explicit catalog requests", async () => {
const h = await harness();
await h.open();
await h.open(undefined, "second");
await h.request({
type: "session.configure",
requestId: "configure-cached",
sessionId: "second",
changes: { model: "gemini-3.8-flash-low" },
});
expect(
(await h.records()).filter((entry) => entry.args && entry.args.includes("models")),
).toHaveLength(1);
await h.request({ type: "catalog", requestId: "refresh" });
expect(
(await h.records()).filter((entry) => entry.args && entry.args.includes("models")),
).toHaveLength(2);
});
it.each(["1.1.15", "1.2.12", "1.2.13"])("accepts driver-capable version %s", async (version) => {
const h = await harness({ AGY_TEST_VERSION: version });
expect(await provider.status?.({ launch: h.launch })).toEqual({ available: true });
});
it("parses tab-separated catalog and exposes only full access without thinking options", async () => {
const h = await harness();
await h.request({ type: "catalog", requestId: "catalog" });
const event = h.events[0];
expect(event).toMatchObject({
type: "catalog",
catalog: {
models: expect.arrayContaining([
{ id: "gemini-3.8-flash-low", label: "Gemini 3.8 Flash (Low)" },
]),
thinkingOptions: [],
defaultMode: "full-access",
},
});
if (event.type !== "catalog") throw new Error("Missing catalog");
expect(event.catalog.models).toHaveLength(11);
expect(event.catalog.modes).toEqual([
{
id: "full-access",
label: "Full access",
icon: "ShieldOff",
colorTier: "dangerous",
isUnattended: true,
description:
"Antigravity cannot ask for permission when another app drives it. Paseo starts it with --dangerously-skip-permissions.",
},
]);
});
it("reports missing, old, unauthenticated and available launches", async () => {
expect(await provider.status?.({})).toMatchObject({
available: false,
diagnostic: expect.stringContaining("agy not found"),
});
const missing = await harness();
expect(
await provider.status?.({
launch: { ...missing.launch, command: path.join(missing.cwd, "missing-agy") },
}),
).toMatchObject({ available: false, diagnostic: expect.stringContaining("ENOENT") });
const old = await harness({ AGY_TEST_VERSION: "1.1.14" });
expect(await provider.status?.({ launch: old.launch })).toMatchObject({
available: false,
diagnostic: expect.stringContaining("1.1.15"),
});
const unauthenticated = await harness({ AGY_TEST_AUTH: "missing" });
expect(await provider.status?.({ launch: unauthenticated.launch })).toEqual({
available: false,
diagnostic: "Antigravity authentication failed. Run `agy` and sign in, then retry.",
});
const authenticated = await harness();
expect(await provider.status?.({ launch: authenticated.launch })).toEqual({ available: true });
});
it("keys catalog caching by the complete effective launch without exposing env values", async () => {
const h = await harness();
const key = await provider.getCatalogCacheKey?.({ scope: "global", launch: h.launch });
expect(key).toMatch(/^[a-f0-9]{64}$/);
expect(
await provider.getCatalogCacheKey?.({
scope: "workspace",
cwd: h.cwd,
force: true,
launch: h.launch,
}),
).toBe(key);
expect(
await provider.getCatalogCacheKey?.({
scope: "global",
launch: { ...h.launch, args: [...h.launch.args, "changed"] },
}),
).not.toBe(key);
expect(
await provider.getCatalogCacheKey?.({
scope: "global",
launch: { ...h.launch, env: { ...h.launch.env, HOME: "different" } },
}),
).not.toBe(key);
});
});
describe("process signal plans", () => {
it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)("kills the Windows tree for %s", (signal) => {
expect(signalPlan({ platform: "win32", pid: 123, signal })).toEqual({
type: "tree",
command: "taskkill",
args: ["/PID", "123", "/T", "/F"],
});
});
it.each(["SIGINT", "SIGTERM", "SIGKILL"] as const)(
"preserves POSIX group signal %s",
(signal) => {
expect(signalPlan({ platform: "linux", pid: 123, signal })).toEqual({
type: "group",
pid: -123,
signal,
});
},
);
});
describe("image prompts", () => {
it.each(["session", "connection"])(
"encodes mixed content as private session files and cleans up on %s close",
async (close) => {
const h = await harness();
await h.open();
const png = await readFile(
new URL("../test/fixtures/image-file-external.png", import.meta.url),
);
const jpeg = Buffer.from("image file bytes");
await h.completed(
await h.message([
{ type: "text", text: "Describe these images." },
{ type: "image", data: png.toString("base64"), mimeType: "image/png" },
{ type: "text", text: "Compare them." },
{ type: "image", data: jpeg.toString("base64"), mimeType: "image/jpeg" },
]),
);
const sent = (await h.records()).find((entry) => entry.input).input.message.content;
const lines = sent.split("\n");
const pngPath = lines[3].slice("Attached image: ".length);
const jpegPath = lines[5].slice("Attached image: ".length);
expect(sent).toBe(
`SYSTEM PREFIX\n\nDescribe these images.\nAttached image: ${pngPath}\nCompare them.\nAttached image: ${jpegPath}`,
);
expect(path.isAbsolute(pngPath)).toBe(true);
expect(path.extname(pngPath)).toBe(".png");
expect(path.extname(jpegPath)).toBe(".jpg");
expect(path.dirname(pngPath)).toBe(path.dirname(jpegPath));
expect(path.dirname(pngPath)).not.toBe(h.cwd);
expect(await readFile(pngPath)).toEqual(png);
expect(await readFile(jpegPath)).toEqual(jpeg);
if (process.platform !== "win32") {
expect((await stat(path.dirname(pngPath))).mode & 0o777).toBe(0o700);
expect((await stat(pngPath)).mode & 0o777).toBe(0o600);
expect((await stat(jpegPath)).mode & 0o777).toBe(0o600);
}
const args = (await h.records()).find((entry) => entry.args?.includes("--input-format")).args;
expect(args.filter((arg: string) => arg === "--add-dir")).toHaveLength(1);
if (close === "connection") await h.connection.close();
else await h.request({ type: "session.close", requestId: "close-images", sessionId: "s" });
await expect(stat(path.dirname(pngPath))).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("keeps image-only prompts in distinct session directories until each session closes", async () => {
const h = await harness();
await h.open();
await h.open(undefined, "other");
const content: ProviderContent[] = [
{ type: "image", data: "aW1hZ2U=", mimeType: "image/webp" },
];
await h.completed(await h.message(content));
await h.completed(await h.message(content));
await h.completed(await h.message(content, "other"));
const paths = (await h.records())
.filter((entry) => entry.input)
.map((entry) => entry.input.message.content.split("Attached image: ")[1]);
expect(new Set(paths).size).toBe(3);
expect(path.dirname(paths[0])).toBe(path.dirname(paths[1]));
expect(path.dirname(paths[0])).not.toBe(path.dirname(paths[2]));
expect(paths.every((file: string) => path.extname(file) === ".webp")).toBe(true);
await h.request({ type: "session.close", requestId: "close-first", sessionId: "s" });
await expect(stat(path.dirname(paths[0]))).rejects.toMatchObject({ code: "ENOENT" });
expect(await readFile(paths[2], "utf8")).toBe("image");
await h.connection.close();
await expect(stat(path.dirname(paths[2]))).rejects.toMatchObject({ code: "ENOENT" });
});
it("rejects a non-image MIME type before writing to native stdin", async () => {
const h = await harness();
await h.open();
const id = await h.message([{ type: "image", data: "dGV4dA==", mimeType: "text/plain" }]);
expect(
h.events.find(
(event) => event.type === "session.prompt_result" && event.clientMessageId === id,
),
).toMatchObject({
result: { type: "failed", error: { message: expect.stringContaining("image MIME type") } },
});
expect((await h.records()).filter((entry) => entry.input)).toHaveLength(0);
});
});