feat(maestro): web spawn form stops naming sessions
The name field starts empty and the reroll is gone; the worktree branch keeps a default by borrowing one handle from /api/names. A card whose name the daemon generated renders dimmed until a first prompt settles it. Refs #131
This commit is contained in:
@@ -4,10 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSpawnForm, type SpawnFormHandle } from "./spawnForm.js";
|
||||
|
||||
const BATCH_A = ["cello", "fugue", "lyre"];
|
||||
const BATCH_B = ["oboe", "tuba", "harp"];
|
||||
|
||||
/** Stub the three fetches the form's initial load makes. Records every
|
||||
* /api/names URL so tests can assert how many batches were fetched. */
|
||||
/** Stub the fetches the form's initial load makes. Records every /api/names
|
||||
* URL so tests can assert how many handles were asked for. */
|
||||
function stubFetch(batches: string[][]): string[] {
|
||||
let batch = 0;
|
||||
const nameUrls: string[] = [];
|
||||
@@ -30,7 +29,7 @@ function stubFetch(batches: string[][]): string[] {
|
||||
return nameUrls;
|
||||
}
|
||||
|
||||
describe("spawnForm name batch (#53)", () => {
|
||||
describe("spawnForm naming (#131)", () => {
|
||||
let handle: SpawnFormHandle | null = null;
|
||||
let nameUrls: string[];
|
||||
|
||||
@@ -46,7 +45,7 @@ describe("spawnForm name batch (#53)", () => {
|
||||
});
|
||||
|
||||
async function mount(seed?: Parameters<typeof createSpawnForm>[0]["seed"]) {
|
||||
nameUrls = stubFetch([BATCH_A, BATCH_B]);
|
||||
nameUrls = stubFetch([BATCH_A]);
|
||||
handle = createSpawnForm({ seed, onSuccess() {} });
|
||||
document.body.appendChild(handle.el);
|
||||
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||
@@ -55,75 +54,49 @@ describe("spawnForm name batch (#53)", () => {
|
||||
|
||||
const nameOf = (form: HTMLFormElement) =>
|
||||
form.querySelector<HTMLInputElement>("input[name=name]")!.value;
|
||||
const branchOf = (form: HTMLFormElement) =>
|
||||
form.querySelector<HTMLInputElement>("input[name=worktree_branch]")!.value;
|
||||
|
||||
function reroll(form: HTMLFormElement): void {
|
||||
form.querySelector<HTMLButtonElement>(".reroll")!.click();
|
||||
}
|
||||
|
||||
async function drain(): Promise<void> {
|
||||
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||
}
|
||||
|
||||
it("seeds its name from a fetched batch when no seed provides one", async () => {
|
||||
it("leaves the name field empty so the daemon names the session", async () => {
|
||||
const form = await mount();
|
||||
expect(BATCH_A).toContain(nameOf(form));
|
||||
expect(nameUrls).toHaveLength(1);
|
||||
expect(nameUrls[0]).toContain("/api/names");
|
||||
expect(nameUrls[0]).toContain("count=");
|
||||
expect(nameOf(form)).toBe("");
|
||||
expect(form.querySelector(".reroll")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a seeded name and defers fetching until the first reroll", async () => {
|
||||
it("still seeds the worktree branch with one generated handle", async () => {
|
||||
const form = await mount();
|
||||
expect(BATCH_A).toContain(branchOf(form));
|
||||
expect(nameUrls).toHaveLength(1);
|
||||
expect(nameUrls[0]).toContain("count=1");
|
||||
});
|
||||
|
||||
it("keeps a name a clone prefilled, and seeds the branch from it", async () => {
|
||||
const form = await mount({ name: "my-session" });
|
||||
expect(nameOf(form)).toBe("my-session");
|
||||
expect(nameUrls ?? []).toHaveLength(0);
|
||||
|
||||
reroll(form);
|
||||
await drain();
|
||||
expect(nameOf(form)).not.toBe("my-session");
|
||||
expect(nameUrls).toHaveLength(1);
|
||||
expect(branchOf(form)).toBe("my-session");
|
||||
});
|
||||
|
||||
it("rerolls locally from the batch with no extra round trips", async () => {
|
||||
it("seeds the branch from a typed name, overriding the fetched handle", async () => {
|
||||
const form = await mount();
|
||||
expect(nameUrls).toHaveLength(1);
|
||||
const seen = new Set([nameOf(form)]);
|
||||
const input = form.querySelector<HTMLInputElement>("input[name=name]")!;
|
||||
input.value = "hand-typed";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
|
||||
for (let i = 0; i < BATCH_A.length; i++) {
|
||||
reroll(form);
|
||||
seen.add(nameOf(form));
|
||||
}
|
||||
await drain();
|
||||
|
||||
// Every value came from the one fetched batch.
|
||||
expect(seen.size).toBeGreaterThanOrEqual(2);
|
||||
for (const value of seen) {
|
||||
expect([...BATCH_A, ...BATCH_B]).toContain(value);
|
||||
}
|
||||
expect(nameUrls).toHaveLength(2); // refetch fired when the batch ran out
|
||||
expect(nameUrls[1]).toContain("/api/names");
|
||||
expect(nameOf(form)).toBe("hand-typed");
|
||||
expect(branchOf(form)).toBe("hand-typed");
|
||||
});
|
||||
|
||||
it("refetches only once even if rerolls outpace the network", async () => {
|
||||
const form = await mount();
|
||||
// Drain the batch without awaiting between rerolls.
|
||||
for (let i = 0; i < BATCH_A.length + 1; i++) reroll(form);
|
||||
await drain();
|
||||
// One refill in flight covers the whole run — no fetch storm.
|
||||
expect(nameUrls.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("does not clobber an operator-typed name when the batch arrives late", async () => {
|
||||
it("never overwrites a branch the operator typed", async () => {
|
||||
nameUrls = stubFetch([BATCH_A]);
|
||||
handle = createSpawnForm({ onSuccess() {} });
|
||||
const form = handle.el;
|
||||
document.body.appendChild(form);
|
||||
|
||||
// The operator types before the fetch resolves.
|
||||
const input = form.querySelector<HTMLInputElement>("input[name=name]")!;
|
||||
input.value = "hand-typed";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
const branch = form.querySelector<HTMLInputElement>("input[name=worktree_branch]")!;
|
||||
branch.value = "feat/mine";
|
||||
branch.dispatchEvent(new Event("input"));
|
||||
for (let i = 0; i < 40; i++) await Promise.resolve();
|
||||
|
||||
expect(nameOf(form)).toBe("hand-typed");
|
||||
expect(branchOf(form)).toBe("feat/mine");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user