## Summary Adds a bundled **Quality** plugin (`fusion-plugin-quality`) that makes task QA easier and more visual: - **Task QA tab** (action-first): preview/test server for the task worktree, allowlisted test runs, report viewer, screenshots CTA, suggested test cases, CI handoff - **Quality hub** (left sidebar): project-wide run history and preset launches - Host **task-detail slot context** (`taskId`, worktree, `projectId`) so plugin tabs can scope correctly - `superviseSpawn` re-exported on the plugin packaging shim for published plugins - Plan: `docs/plans/2026-07-14-001-feat-quality-plugin-plan.md` ## Design constraints - Does **not** replace the merge gate — advisory orchestration only - Composes Dev Server process patterns and artifact registry (no second browser stack) - Never free-form shell; never port 4040 - Full-suite requires explicit confirm ## Test plan - [x] `pnpm --filter @fusion-plugin-examples/quality test` (15 tests) - [x] PluginSlot unit tests still pass - [ ] Enable Quality plugin in dashboard Settings → Built-in Plugins - [ ] Open Task Detail → **QA** tab with a worktree; start preview, run verify:fast, generate suggestions - [ ] Open left sidebar **Quality** hub and list runs - [ ] Confirm merge gate / PR checks unchanged ## Residual / follow-up (same plan, later units) - Deeper hub CI (host route) - Full browser-verification toggle UX + agent QA sessions (U7/U9/U10) - Richer screenshots gallery wiring to live artifacts API - Test plans CRUD polish <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the Quality plugin with a project Quality hub and task-focused QA tab. * Added test runs, reports, preview server controls, suggested test cases, and run history. * Added configurable test presets, cancellation, status tracking, and safe command execution. * Added experimental-feature controls for enabling Quality functionality. * Bundled Quality with the CLI and made it available through the plugin manager. * **Documentation** * Added Quality plugin guidance, terminology, configuration details, and implementation planning documentation. * **Bug Fixes** * Improved process supervision so command failures and shutdown timers are handled safely. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
import type { QualityPresetId } from "../store/quality-types.js";
|
|
|
|
/*
|
|
FNXC:Quality 2026-07-14-21:45:
|
|
Allowlisted preset id → command mapping. Server resolves only; clients never supply command/argv/cwd.
|
|
full-suite requires explicit confirmFullSuite. file-scoped builds from server-side path list.
|
|
*/
|
|
|
|
export const QUALITY_PRESET_IDS: readonly QualityPresetId[] = [
|
|
"project-test",
|
|
"test-gate",
|
|
"verify-fast",
|
|
"file-scoped",
|
|
"full-suite",
|
|
] as const;
|
|
|
|
export function isQualityPresetId(value: unknown): value is QualityPresetId {
|
|
return typeof value === "string" && (QUALITY_PRESET_IDS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export interface ResolvePresetInput {
|
|
preset: QualityPresetId;
|
|
/** Project settings.testCommand when set */
|
|
testCommand?: string | null;
|
|
/** Absolute project root */
|
|
projectRoot: string;
|
|
/** File paths relative to project/worktree for file-scoped preset */
|
|
filePaths?: string[];
|
|
confirmFullSuite?: boolean;
|
|
}
|
|
|
|
export type ResolvePresetResult =
|
|
| { ok: true; command: string; label: string }
|
|
| { ok: false; reason: string; code: "unknown_preset" | "disabled" | "confirm_required" | "empty_files" };
|
|
|
|
/**
|
|
* Reject path tokens that could escape the worktree or inject shell metacharacters.
|
|
*/
|
|
export function isSafeFilePathToken(path: string): boolean {
|
|
if (!path || typeof path !== "string") return false;
|
|
if (path.includes("\0") || path.includes("\n") || path.includes("\r")) return false;
|
|
if (path.startsWith("/") || path.includes("..")) return false;
|
|
// Disallow shell metacharacters when paths are joined into a shell command string.
|
|
if (/[;&|`$<>\\]/.test(path)) return false;
|
|
return true;
|
|
}
|
|
|
|
export function resolvePresetCommand(input: ResolvePresetInput): ResolvePresetResult {
|
|
switch (input.preset) {
|
|
case "project-test": {
|
|
const cmd = (input.testCommand ?? "").trim();
|
|
if (!cmd) {
|
|
return {
|
|
ok: false,
|
|
reason: "Project testCommand is not configured",
|
|
code: "disabled",
|
|
};
|
|
}
|
|
return { ok: true, command: cmd, label: "Project test" };
|
|
}
|
|
case "test-gate":
|
|
return { ok: true, command: "pnpm test:gate", label: "Merge gate tests" };
|
|
case "verify-fast":
|
|
return { ok: true, command: "pnpm verify:fast", label: "Verify fast (test-free)" };
|
|
case "file-scoped": {
|
|
const paths = (input.filePaths ?? []).map((p) => p.trim()).filter(Boolean);
|
|
if (paths.length === 0) {
|
|
return { ok: false, reason: "No changed files for file-scoped run", code: "empty_files" };
|
|
}
|
|
const safe = paths.filter(isSafeFilePathToken);
|
|
if (safe.length === 0) {
|
|
return { ok: false, reason: "No safe file paths for file-scoped run", code: "empty_files" };
|
|
}
|
|
// Prefer vitest path list; keep command server-built.
|
|
const joined = safe.map((p) => JSON.stringify(p)).join(" ");
|
|
return {
|
|
ok: true,
|
|
command: `pnpm exec vitest run ${joined}`,
|
|
label: "File-scoped tests",
|
|
};
|
|
}
|
|
case "full-suite": {
|
|
if (!input.confirmFullSuite) {
|
|
return {
|
|
ok: false,
|
|
reason: "full-suite requires confirmFullSuite: true",
|
|
code: "confirm_required",
|
|
};
|
|
}
|
|
return { ok: true, command: "pnpm test:full", label: "Full suite (opt-in)" };
|
|
}
|
|
default:
|
|
return { ok: false, reason: "Unknown preset", code: "unknown_preset" };
|
|
}
|
|
}
|
|
|
|
export function listPresetCatalog(): Array<{ id: QualityPresetId; label: string; needsConfirm?: boolean }> {
|
|
return [
|
|
{ id: "project-test", label: "Project test" },
|
|
{ id: "test-gate", label: "Merge gate (test:gate)" },
|
|
{ id: "verify-fast", label: "Verify fast" },
|
|
{ id: "file-scoped", label: "File-scoped" },
|
|
{ id: "full-suite", label: "Full suite", needsConfirm: true },
|
|
];
|
|
}
|