## Summary
Follow-up on **#2127** (Quality plugin already on `main`). This PR only
lands the remaining Quality deltas that were not merged:
- **Experimental gate fix** — `TaskStore.getSettings()` is async; the
gate now awaits merged settings so enabling
`experimentalFeatures.qualityPlugin` actually works, and status-bearing
errors return structured `{ status, body }` instead of collapsing to
hard failures
- **Done-task QA worktrees** — when a task has no live worktree (typical
after land), preview/task runs create a disposable checkout under
`.fusion/quality-qa/` at the task branch or merge commit so processes
run the **done task’s code**, not project root
- **Hub layout** — shared `ViewHeader` + dashboard spacing/typography so
Quality matches Insights / Compound Engineering / Goals
Scoped to `plugins/fusion-plugin-quality/**` only (rebased onto current
`main`; duplicate plugin-landing commits dropped).
## Test plan
- [ ] Enable **Settings → Experimental → Quality Plugin**, restart if
routes were cold
- [ ] Quality hub: header matches other views; refresh + presets work
- [ ] Done task → QA tab → Start preview uses QA worktree at
branch/merge commit (not project root)
- [ ] Active task with live worktree still uses that worktree
- [ ] Flag off: clear experimental-disabled error (not generic empty
failure)
- [ ] `pnpm --filter @fusion-plugin-examples/quality test` (32 tests)
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { resolveTaskCodeCwd } from "../preview/task-code-worktree.js";
|
|
|
|
function git(cwd: string, args: string[]): string {
|
|
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
|
}
|
|
|
|
describe("resolveTaskCodeCwd", () => {
|
|
const temps: string[] = [];
|
|
afterEach(() => {
|
|
for (const dir of temps.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("uses a live worktree when present on disk", async () => {
|
|
const worktree = mkdtempSync(join(tmpdir(), "quality-live-wt-"));
|
|
temps.push(worktree);
|
|
const result = await resolveTaskCodeCwd({
|
|
projectRoot: "/project",
|
|
task: { id: "FN-1", worktree },
|
|
});
|
|
expect(result).toMatchObject({ cwd: worktree, cwdKind: "worktree", created: false });
|
|
});
|
|
|
|
it("creates a disposable QA worktree at the task merge commit for done tasks", async () => {
|
|
const repo = mkdtempSync(join(tmpdir(), "quality-repo-"));
|
|
temps.push(repo);
|
|
git(repo, ["init"]);
|
|
git(repo, ["config", "user.email", "test@example.com"]);
|
|
git(repo, ["config", "user.name", "Test"]);
|
|
writeFileSync(join(repo, "readme.md"), "main\n");
|
|
git(repo, ["add", "readme.md"]);
|
|
git(repo, ["commit", "-m", "init"]);
|
|
git(repo, ["checkout", "-b", "fusion/fn-42"]);
|
|
writeFileSync(join(repo, "feature.md"), "task code\n");
|
|
git(repo, ["add", "feature.md"]);
|
|
git(repo, ["commit", "-m", "task"]);
|
|
const mergeSha = git(repo, ["rev-parse", "HEAD"]);
|
|
git(repo, ["checkout", "-"]);
|
|
// Done-task cleanup often deletes fusion/<id>; fall back to merge commit.
|
|
git(repo, ["branch", "-D", "fusion/fn-42"]);
|
|
|
|
const result = await resolveTaskCodeCwd({
|
|
projectRoot: repo,
|
|
task: {
|
|
id: "FN-42",
|
|
worktree: undefined,
|
|
branch: undefined,
|
|
mergeDetails: { commitSha: mergeSha },
|
|
},
|
|
});
|
|
|
|
expect(result.cwdKind).toBe("qa-worktree");
|
|
expect(result.created).toBe(true);
|
|
expect(result.ref).toBe(mergeSha);
|
|
expect(result.cwd).toContain(".fusion/quality-qa");
|
|
expect(git(result.cwd, ["rev-parse", "HEAD"])).toBe(mergeSha);
|
|
// Task file exists only on the task commit, not on main
|
|
expect(() => git(result.cwd, ["cat-file", "-e", "HEAD:feature.md"])).not.toThrow();
|
|
});
|
|
|
|
it("reuses an existing QA worktree path without failing", async () => {
|
|
const repo = mkdtempSync(join(tmpdir(), "quality-repo-reuse-"));
|
|
temps.push(repo);
|
|
git(repo, ["init"]);
|
|
git(repo, ["config", "user.email", "test@example.com"]);
|
|
git(repo, ["config", "user.name", "Test"]);
|
|
writeFileSync(join(repo, "a.txt"), "a\n");
|
|
git(repo, ["add", "a.txt"]);
|
|
git(repo, ["commit", "-m", "init"]);
|
|
const sha = git(repo, ["rev-parse", "HEAD"]);
|
|
|
|
const first = await resolveTaskCodeCwd({
|
|
projectRoot: repo,
|
|
task: { id: "FN-7", mergeDetails: { commitSha: sha } },
|
|
});
|
|
const second = await resolveTaskCodeCwd({
|
|
projectRoot: repo,
|
|
task: { id: "FN-7", mergeDetails: { commitSha: sha } },
|
|
});
|
|
expect(second.cwd).toBe(first.cwd);
|
|
expect(second.created).toBe(false);
|
|
});
|
|
|
|
it("errors when no worktree and no reachable ref (no project-root fallback)", async () => {
|
|
const repo = mkdtempSync(join(tmpdir(), "quality-repo-empty-"));
|
|
temps.push(repo);
|
|
git(repo, ["init"]);
|
|
await expect(
|
|
resolveTaskCodeCwd({
|
|
projectRoot: repo,
|
|
task: { id: "FN-missing" },
|
|
}),
|
|
).rejects.toThrow(/no live worktree and no reachable branch\/merge commit/i);
|
|
});
|
|
});
|