A Claude Code or Codex spawn now asks for the structured view unless the operator says otherwise: the two spawn forms open with the box ticked and `maestro spawn` takes `--terminal` to opt out. Once the operator works the toggle, a command change no longer re-ticks it under them. The support predicate moves to `maestro_protocol::display` so all three clients gate on the same resolution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { createSpawnForm, type SpawnFormHandle } from "./spawnForm.js";
|
|
|
|
function stubApi(): RequestInit[] {
|
|
const sessionRequests: RequestInit[] = [];
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
if (url.includes("/api/sessions") && init?.method === "POST") {
|
|
sessionRequests.push(init);
|
|
return new Response(JSON.stringify({ id: "s1", command: "claude", cwd: "/srv/work" }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
if (url.includes("/api/info")) {
|
|
return new Response(
|
|
JSON.stringify({ hostname: "h", version: "0", home_dir: "/home/user" }),
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
if (url.includes("/api/names")) {
|
|
return new Response(JSON.stringify({ names: ["amber-fox"] }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify([]), { status: 200 });
|
|
}),
|
|
);
|
|
return sessionRequests;
|
|
}
|
|
|
|
describe("spawn form structured view", () => {
|
|
let handle: SpawnFormHandle | null = null;
|
|
|
|
async function drain(): Promise<void> {
|
|
for (let i = 0; i < 20; i++) await Promise.resolve();
|
|
}
|
|
|
|
afterEach(async () => {
|
|
handle?.destroy();
|
|
handle = null;
|
|
await drain();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("is default-on and shown for resolved Claude Code and Codex commands", async () => {
|
|
stubApi();
|
|
handle = createSpawnForm({ onSuccess() {} });
|
|
document.body.appendChild(handle.el);
|
|
await drain();
|
|
|
|
const wrap = handle.el.querySelector<HTMLElement>(".structured-view")!;
|
|
const checkbox = wrap.querySelector<HTMLInputElement>("input")!;
|
|
const disclosure = handle.el.querySelector<HTMLElement>(".wt-collapsible")!;
|
|
expect(disclosure.hidden).toBe(true);
|
|
handle.el.querySelector<HTMLButtonElement>(".wt-toggle")!.click();
|
|
expect(disclosure.hidden).toBe(false);
|
|
expect(wrap.hidden).toBe(false);
|
|
expect(checkbox.checked).toBe(true);
|
|
|
|
const custom = handle.el.querySelector<HTMLButtonElement>('[data-id="custom"]')!;
|
|
custom.click();
|
|
const command = handle.el.querySelector<HTMLInputElement>(".cmd-raw")!;
|
|
command.value = "FOO=1 /usr/local/bin/codex";
|
|
command.dispatchEvent(new Event("input"));
|
|
expect(wrap.hidden).toBe(false);
|
|
expect(checkbox.checked).toBe(true);
|
|
|
|
command.value = "opencode";
|
|
command.dispatchEvent(new Event("input"));
|
|
expect(wrap.hidden).toBe(true);
|
|
expect(checkbox.checked).toBe(false);
|
|
});
|
|
|
|
it("keeps an unchecked box unchecked across a command change", async () => {
|
|
stubApi();
|
|
handle = createSpawnForm({ onSuccess() {} });
|
|
document.body.appendChild(handle.el);
|
|
await drain();
|
|
|
|
const checkbox = handle.el.querySelector<HTMLInputElement>("input[name=structured_view]")!;
|
|
expect(checkbox.checked).toBe(true);
|
|
checkbox.click();
|
|
expect(checkbox.checked).toBe(false);
|
|
|
|
handle.el.querySelector<HTMLButtonElement>('[data-id="custom"]')!.click();
|
|
const command = handle.el.querySelector<HTMLInputElement>(".cmd-raw")!;
|
|
command.value = "codex";
|
|
command.dispatchEvent(new Event("input"));
|
|
|
|
expect(checkbox.checked).toBe(false);
|
|
});
|
|
|
|
it("clears the checked default when Codex changes to an unsupported command", async () => {
|
|
stubApi();
|
|
handle = createSpawnForm({ onSuccess() {} });
|
|
document.body.appendChild(handle.el);
|
|
await drain();
|
|
|
|
handle.el.querySelector<HTMLButtonElement>('[data-id="custom"]')!.click();
|
|
const command = handle.el.querySelector<HTMLInputElement>(".cmd-raw")!;
|
|
command.value = "env -i codex";
|
|
command.dispatchEvent(new Event("input"));
|
|
|
|
const checkbox = handle.el.querySelector<HTMLInputElement>("input[name=structured_view]")!;
|
|
expect(checkbox.checked).toBe(true);
|
|
|
|
command.value = "opencode";
|
|
command.dispatchEvent(new Event("input"));
|
|
|
|
expect(checkbox.closest<HTMLElement>(".structured-view")!.hidden).toBe(true);
|
|
expect(checkbox.checked).toBe(false);
|
|
});
|
|
|
|
it.each(["claude", "codex"])("emits Structured for a %s command by default", async (tool) => {
|
|
const requests = stubApi();
|
|
handle = createSpawnForm({
|
|
seed: { cwd: "/srv/work", cwdRawVisible: true, worktreeEnabled: false },
|
|
onSuccess() {},
|
|
});
|
|
document.body.appendChild(handle.el);
|
|
await drain();
|
|
|
|
if (tool === "codex") {
|
|
handle.el.querySelector<HTMLButtonElement>('[data-id="custom"]')!.click();
|
|
const command = handle.el.querySelector<HTMLInputElement>(".cmd-raw")!;
|
|
command.value = "codex";
|
|
command.dispatchEvent(new Event("input"));
|
|
}
|
|
|
|
handle.el.requestSubmit();
|
|
await drain();
|
|
|
|
expect(requests).toHaveLength(1);
|
|
expect(JSON.parse(String(requests[0].body))).toMatchObject({
|
|
command: tool,
|
|
view: "Structured",
|
|
});
|
|
});
|
|
|
|
it("emits no view once the operator unticks the box", async () => {
|
|
const requests = stubApi();
|
|
handle = createSpawnForm({
|
|
seed: { cwd: "/srv/work", cwdRawVisible: true, worktreeEnabled: false },
|
|
onSuccess() {},
|
|
});
|
|
document.body.appendChild(handle.el);
|
|
await drain();
|
|
|
|
handle.el.querySelector<HTMLInputElement>("input[name=structured_view]")!.click();
|
|
handle.el.requestSubmit();
|
|
await drain();
|
|
|
|
expect(requests).toHaveLength(1);
|
|
expect(JSON.parse(String(requests[0].body)).view).toBeUndefined();
|
|
});
|
|
});
|