Files
Mohamed Boudra 9760292650 fix(providers): launch plugin CLIs on Windows and tolerate Muse startup (#6151)
* fix(providers): launch plugin CLIs on Windows and tolerate Muse startup

Plugin providers bypassed the core Windows launcher and the plugin host exposed an empty server SDK module. Share the launcher through the SDK and bind it in the host. Give Muse initialization its own deadline because native startup traverses session history before answering MSP.

* fix(providers): terminate Windows launcher process trees

A command-script launcher leaves the CLI behind cmd.exe. Expose forced process-tree termination in the server SDK, use it for Muse and SDK ACP teardown, and bound Muse closing so inherited streams cannot leave callers waiting forever.

* test(muse): keep platform setup outside the teardown assertion

* refactor(plugin): expose process capabilities without quoting helpers

* fix(providers): close process trees on uncorrelated protocol errors
2026-10-05 17:10:02 +01:00

792 lines
28 KiB
TypeScript

import { mkdtemp, readFile, rm, stat, writeFile } 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);
});
});
it.runIf(process.platform === "win32")(
"launches an Antigravity command shim for probes and conversation startup",
async () => {
const h = await harness();
const shim = path.join(h.cwd, "agy launcher.cmd");
await writeFile(shim, `@echo off\r\n"${process.execPath}" "${fake}" %*\r\n`);
h.launch.command = shim;
h.launch.args = ["--launch-prefix"];
expect(await provider.status!({ launch: h.launch })).toEqual({ available: true });
await h.open();
expect(await h.wait((event) => event.type === "session.opened")).toMatchObject({
sessionId: "s",
});
},
);