The cleanup guard plus deterministic paths had created a trap: one untracked file an agent left behind — a log, a .env, a stray artifact — made the guard refuse removal, the directory stayed, and every later spawn of that branch hit PathExists. The branch was burned permanently and the product offered no way to clear it. A leftover tree registered for the same branch is now adopted, so the operator lands back in their own unsaved work. A tree a *live* session holds is still rejected, since two sessions must not share a checkout. Also: - Fast-forward still awaited /api/info, /api/projects and /api/names inline, the exact freeze the draft loaders were restructured to avoid. It posts to the same channel now. - Draft updates were routed by tab index, which shifts when a tab closes: an in-flight load could be dropped or land on the wrong pane. Drafts carry an id and are found by it. - extend_names left the name it had just displayed at the head of the queue, so the next reroll produced it again and the key looked dead. - The fast-forward fallback discarded info.home_dir, so its draft showed unabbreviated paths and seeded "Other directory…" with / — disagreeing with the normal draft entry point. - Web fast-forward mounted via openSession, which replaces the focused pane: pressing it while attached evicted that session from the layout. It reuses an empty pane or splits, as the TUI does. - branchEdited was inferred from the seeded branch value, but in "new" mode that value is the auto-synced session name — so any form rebuild froze the branch against later rerolls. - resolveBaseRepo ignored the worktree checkbox, rebuilding the fieldset expanded under an unticked box and probing gitroot for a worktree the operator had turned off. - short_hash used DefaultHasher, whose algorithm is not stable across Rust releases, in a directory name — a toolchain bump would orphan every worktree under the override root. Now FNV-1a.
100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { buildSpawnRequest, type SpawnFormValues } from "./spawnForm.js";
|
|
|
|
const base: SpawnFormValues = {
|
|
command: "claude",
|
|
cwd: "/home/user/project",
|
|
name: "amber-fox",
|
|
worktreeEnabled: false,
|
|
worktreeBranch: "",
|
|
worktreeBranchMode: "new",
|
|
worktreeBranchEdited: false,
|
|
};
|
|
|
|
describe("buildSpawnRequest", () => {
|
|
it("builds a plain request with the picked cwd and command", () => {
|
|
const res = buildSpawnRequest(base, null);
|
|
expect(res).toEqual({
|
|
req: { cwd: "/home/user/project", command: "claude", metadata: { name: "amber-fox" } },
|
|
});
|
|
});
|
|
|
|
it("omits metadata.name when the name is blank", () => {
|
|
const res = buildSpawnRequest({ ...base, name: " " }, null);
|
|
expect(res).toEqual({ req: { cwd: base.cwd, command: base.command, metadata: {} } });
|
|
});
|
|
|
|
it("attaches a trimmed branch when worktree is enabled, and always cleans up", () => {
|
|
const res = buildSpawnRequest(
|
|
{ ...base, worktreeEnabled: true, worktreeBranch: " feat/foo " },
|
|
"/home/user/project",
|
|
);
|
|
expect(res).toEqual({
|
|
req: {
|
|
cwd: base.cwd,
|
|
command: base.command,
|
|
metadata: { name: "amber-fox" },
|
|
worktree: {
|
|
base_repo: "/home/user/project",
|
|
branch: "feat/foo",
|
|
cleanup_on_kill: true,
|
|
create_branch: true,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("checks out an existing branch when the mode is existing (#61)", () => {
|
|
const res = buildSpawnRequest(
|
|
{
|
|
...base,
|
|
worktreeEnabled: true,
|
|
worktreeBranch: "spawn-v2",
|
|
worktreeBranchMode: "existing",
|
|
},
|
|
"/home/user/project",
|
|
);
|
|
expect(res).toEqual({
|
|
req: {
|
|
cwd: base.cwd,
|
|
command: base.command,
|
|
metadata: { name: "amber-fox" },
|
|
worktree: {
|
|
base_repo: "/home/user/project",
|
|
branch: "spawn-v2",
|
|
cleanup_on_kill: true,
|
|
create_branch: false,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects an existing-mode worktree with no branch picked", () => {
|
|
expect(
|
|
buildSpawnRequest(
|
|
{ ...base, worktreeEnabled: true, worktreeBranch: "", worktreeBranchMode: "existing" },
|
|
"/repo",
|
|
),
|
|
).toHaveProperty("error");
|
|
});
|
|
|
|
it("rejects a missing working directory", () => {
|
|
expect(buildSpawnRequest({ ...base, cwd: "" }, null)).toHaveProperty("error");
|
|
});
|
|
|
|
it("rejects a missing command", () => {
|
|
expect(buildSpawnRequest({ ...base, command: "" }, null)).toHaveProperty("error");
|
|
});
|
|
|
|
it("rejects a worktree without an inferred base repo", () => {
|
|
expect(buildSpawnRequest({ ...base, worktreeEnabled: true, worktreeBranch: "b" }, null))
|
|
.toHaveProperty("error");
|
|
});
|
|
|
|
it("rejects a worktree without a branch name", () => {
|
|
expect(buildSpawnRequest({ ...base, worktreeEnabled: true, worktreeBranch: " " }, "/repo"))
|
|
.toHaveProperty("error");
|
|
});
|
|
});
|