FN-8394: rescue deterministic quarantined tests

Restore reliable test coverage and delete quarantined tests that could not be rescued.

- Replace process- and database-dependent tests with bounded dependency seams
- Restore stabilized CLI, dashboard, and plugin test coverage
- Remove unrescuable bundle and merge-worktree test suites and clear the quarantine ledger

Files changed:
 packages/cli/src/__tests__/bundle-output.test.ts   | 519 ------------
 .../src/commands/__tests__/task-lock-retry.test.ts |  10 +
 packages/cli/vitest.config.ts                      |   8 -
 .../TaskDetailModal.tab-persistence.test.tsx       |   2 +-
 .../__tests__/TaskDetailModal.test-helpers.ts      |   7 +
 .../src/__tests__/dev-server-process.test.ts       | 391 ++++-----
 packages/dashboard/src/dev-server-process.ts       |  22 +-
 packages/dashboard/vitest.config.ts                |  21 +-
 .../merge-reuse-task-worktree.slow.test.ts         | 876 ---------------------
 packages/engine/vitest.config.ts                   |   7 -
 .../src/__tests__/process-lifecycle.test.ts        |  21 +-
 .../fusion-plugin-grok-runtime/vitest.config.ts    |   2 -
 .../src/__tests__/async-quality-store.pg.test.ts   | 148 +++-
 plugins/fusion-plugin-quality/vitest.config.ts     |   3 +-
 scripts/lib/test-quarantine.json                   |  43 +-
 15 files changed, 323 insertions(+), 1757 deletions(-)

Fusion-Task-Id: FN-8394

Fusion-Task-Lineage: e949b33e-b8d5-4f73-a002-e550b97ee125

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-19 18:58:05 -07:00
parent 90b9feb7ae
commit 5c67b19cb2
15 changed files with 323 additions and 1757 deletions

View File

@@ -1,519 +0,0 @@
import { describe, it, expect, beforeAll } from "vitest";
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { resolvePluginSkillBodyPath } from "@fusion/core";
import {
buildCliWithRealDashboardAssets,
bundlePath,
cliRoot,
clientIndexPath,
dashboardClientStubMarker,
readClientIndexHtml,
workspaceRoot,
} from "./bundle-output-helpers";
import { resolveClaudeCliExtensionFromModuleUrl } from "../commands/claude-cli-extension";
import { resolveDroidCliExtensionFromModuleUrl } from "../commands/droid-cli-extension";
import { RUNTIME_PLUGIN_IDS } from "../plugins/staged-bundled-plugin-ids";
const tsupConfigPath = join(cliRoot, "tsup.config.ts");
const bundlePluginEntryPluginIds = [
...RUNTIME_PLUGIN_IDS,
"fusion-plugin-dependency-graph",
"fusion-plugin-roadmap",
"fusion-plugin-compound-engineering",
"fusion-plugin-whatsapp-chat",
"fusion-plugin-reports",
"fusion-plugin-cli-printing-press",
"fusion-plugin-linear-import",
] as const;
const selfContainedBundlePluginIds = [
"fusion-plugin-reports",
"fusion-plugin-cli-printing-press",
"fusion-plugin-whatsapp-chat",
] as const;
const knownCompoundEngineeringSkillIds = [
"ce-brainstorm",
"ce-code-review",
"ce-commit",
"ce-commit-push-pr",
"ce-compound",
"ce-debug",
"ce-doc-review",
"ce-ideate",
"ce-plan",
"ce-resolve-pr-feedback",
"ce-strategy",
"ce-work",
] as const;
function expectSelfContainedBundle(pluginId: typeof selfContainedBundlePluginIds[number]) {
const stagedRoot = join(cliRoot, "dist", "plugins", pluginId);
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
const bundledPath = join(stagedRoot, "bundled.js");
expect(existsSync(bundledPath), `${pluginId} should ship bundled.js`).toBe(true);
expect(existsSync(join(stagedRoot, "src")), `${pluginId} should not ship raw src/`).toBe(false);
expect(
readdirSync(stagedRoot).some((entry) => /\.index\.reload-\d+\.ts$/.test(entry)),
`${pluginId} should not ship hot-reload TypeScript artifacts`,
).toBe(false);
expect(existsSync(manifestPath), `${pluginId} manifest should exist`).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe(pluginId);
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
const bundled = readFileSync(bundledPath, "utf-8");
expect(bundled, `${pluginId} should not keep a bare @fusion/core import`).not.toMatch(
/from\s+["']@fusion\/core["']/,
);
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain("@fusion/core");
}
describe("CLI bundle output", () => {
beforeAll(async () => {
// Intentional: bundle-output tests validate compiled artifacts, so they
// perform their own explicit build bootstrap instead of relying on ambient
// workspace dist/ state.
await buildCliWithRealDashboardAssets();
}, 300_000);
it("dist/bin.js exists", () => {
expect(existsSync(bundlePath)).toBe(true);
});
it("starts with a shebang", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content.startsWith("#!/usr/bin/env node")).toBe(true);
});
it("does not contain bare @fusion/* import specifiers", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content).not.toMatch(/from\s+["']@fusion\/core["']/);
expect(content).not.toMatch(/from\s+["']@fusion\/dashboard["']/);
expect(content).not.toMatch(/from\s+["']@fusion\/engine["']/);
expect(content).not.toMatch(/from\s+["']@fusion-plugin-examples\/roadmap["']/);
expect(content).not.toContain('"@fusion/core"');
expect(content).not.toContain('"@fusion/dashboard"');
expect(content).not.toContain('"@fusion/engine"');
expect(content).not.toContain('"@fusion-plugin-examples/fusion-plugin-roadmap"');
});
it("does not contain runtime memory-backend side-load imports", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content).not.toMatch(/await\s+import\(\s*["']\.\/memory-backend\.js["']\s*\)/);
expect(content).not.toMatch(/await\s+import\(\s*["']\.\.\/memory-backend\.js["']\s*\)/);
});
it("contains inlined workspace code", () => {
const content = readFileSync(bundlePath, "utf-8");
// TaskStore from @fusion/core
expect(content).toContain("TaskStore");
// createServer from @fusion/dashboard
expect(content).toContain("createServer");
});
it("dashboard client assets are included", () => {
expect(existsSync(clientIndexPath)).toBe(true);
const indexHtml = readClientIndexHtml();
expect(indexHtml).toContain("<script");
expect(indexHtml).toMatch(/assets\/.+-[A-Za-z0-9_-]+\.js/);
expect(indexHtml).toMatch(/assets\/vendor-react-[A-Za-z0-9_-]+\.js/);
expect(indexHtml).not.toContain(dashboardClientStubMarker);
const copiedAssetsDir = join(cliRoot, "dist", "client", "assets");
const copiedAssets = readdirSync(copiedAssetsDir);
expect(copiedAssets.some((file) => /^vendor-react-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
expect(copiedAssets.some((file) => /^vendor-xterm-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
});
it("tsup config copies dashboard assets from dashboard/dist/client to dist/client", () => {
const tsupConfig = readFileSync(tsupConfigPath, "utf-8");
expect(tsupConfig).toContain("onSuccess");
expect(tsupConfig).toContain('join(__dirname, "..", "dashboard", "dist", "client")');
expect(tsupConfig).toContain('join(__dirname, "dist", "client")');
expect(tsupConfig).toContain("cpSync(dashboardClientSrc, dashboardClientDest, { recursive: true });");
});
it("keeps native module loaders externalized in tsup config", () => {
const tsupConfig = readFileSync(tsupConfigPath, "utf-8");
expect(tsupConfig).toContain('"dockerode"');
expect(tsupConfig).toContain('"ssh2"');
expect(tsupConfig).toContain('"cpu-features"');
});
it("loads sqlite from Node built-ins and never from bare sqlite npm package", () => {
const content = readFileSync(bundlePath, "utf-8");
// The bundle must resolve sqlite through Node's built-in module.
expect(content).toMatch(/["']node:sqlite["']/);
// Bun-native sqlite support is optional in this artifact depending on runtime-targeted code paths.
// No bare "sqlite" import (we never want to pull in an npm package named sqlite).
expect(content).not.toMatch(/from\s+["']sqlite["'][^s]/);
});
it("does not inline native artifact filenames into the bundled CLI", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content).not.toContain("sshcrypto.node");
expect(content).not.toContain("cpufeatures.node");
});
it("provides require via createRequire banner", () => {
const content = readFileSync(bundlePath, "utf-8");
// Banner should inject createRequire for ESM CJS interop
expect(content).toContain("createRequire");
expect(content).toContain("import.meta.url");
// Banner should be near the top of the file (after shebang)
const shebangEnd = content.indexOf("\n");
const bannerPosition = content.indexOf("createRequire");
expect(bannerPosition).toBeLessThan(100);
expect(bannerPosition).toBeGreaterThan(shebangEnd);
});
it("preserves node: prefix in other node built-in imports", () => {
const content = readFileSync(bundlePath, "utf-8");
// Verify removeNodeProtocol: false is effective for other node: imports
expect(content).toMatch(/from\s+["']node:fs["']/);
expect(content).toMatch(/from\s+["']node:path["']/);
});
it("resolveClaudeCliExtension succeeds against the staged dist/ layout", () => {
const result = resolveClaudeCliExtensionFromModuleUrl(pathToFileURL(bundlePath).href);
expect(result.status).toBe("ok");
if (result.status === "ok") {
expect(result.path).toBe(join(cliRoot, "dist", "pi-claude-cli", "index.ts"));
expect(result.packageVersion).toMatch(/\d+\.\d+\.\d+/);
}
});
it("dist/pi-claude-cli/ is staged with correct files", () => {
const stagedRoot = join(cliRoot, "dist", "pi-claude-cli");
expect(existsSync(join(stagedRoot, "package.json"))).toBe(true);
expect(existsSync(join(stagedRoot, "index.ts"))).toBe(true);
expect(existsSync(join(stagedRoot, "src", "process-manager.ts"))).toBe(true);
});
it("resolveDroidCliExtension succeeds against the staged dist/ layout", () => {
const result = resolveDroidCliExtensionFromModuleUrl(pathToFileURL(bundlePath).href);
expect(result.status).toBe("ok");
if (result.status === "ok") {
expect(result.path).toBe(join(cliRoot, "dist", "droid-cli", "index.ts"));
expect(result.packageVersion).toMatch(/\d+\.\d+\.\d+/);
}
});
it("dist/droid-cli/ is staged with correct files", () => {
const stagedRoot = join(cliRoot, "dist", "droid-cli");
expect(existsSync(join(stagedRoot, "package.json"))).toBe(true);
expect(existsSync(join(stagedRoot, "index.ts"))).toBe(true);
expect(existsSync(join(stagedRoot, "src", "process-manager.ts"))).toBe(true);
});
it("dist/plugins/fusion-plugin-dependency-graph/ is staged as bundled runtime output", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-dependency-graph");
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-dependency-graph");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "src"))).toBe(false);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
});
it("dist/plugins/fusion-plugin-roadmap/ is staged as bundled runtime output", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap");
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-roadmap");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "src"))).toBe(false);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
});
it("dist/plugins/fusion-plugin-linear-import/ is staged as bundled runtime output", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-linear-import");
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string; dashboardViews?: unknown[] };
expect(manifest.id).toBe("fusion-plugin-linear-import");
expect(typeof manifest.name).toBe("string");
expect(manifest.dashboardViews?.[0]).toMatchObject({ viewId: "linear-import" });
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "src"))).toBe(false);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
dependencies?: Record<string, string>;
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
expect(stagedPkg.dependencies?.["@fusion/core"]).toBeUndefined();
});
it("dist/plugins/fusion-plugin-compound-engineering/ ships skill bodies that resolve from plugin root", () => {
const sourceSkillsRoot = join(workspaceRoot, "plugins", "fusion-plugin-compound-engineering", "src", "skills");
const stagedPluginRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-compound-engineering");
const skillIds = readdirSync(sourceSkillsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (const knownSkillId of knownCompoundEngineeringSkillIds) {
expect(skillIds).toContain(knownSkillId);
}
for (const skillId of skillIds) {
const stagedSkillPath = join(stagedPluginRoot, "skills", skillId, "SKILL.md");
expect(existsSync(stagedSkillPath), `${skillId} SKILL.md should be staged`).toBe(true);
expect(
readFileSync(stagedSkillPath, "utf-8").trim().length,
`${skillId} SKILL.md should be non-empty`,
).toBeGreaterThan(0);
const resolvedSkillBody = resolvePluginSkillBodyPath(
{ name: skillId, skillFiles: [`skills/${skillId}/SKILL.md`] },
stagedPluginRoot,
);
expect(existsSync(resolvedSkillBody.absolutePath), `${skillId} should resolve via plugin skillFiles`).toBe(true);
}
});
it("dist/plugins/fusion-plugin-compound-engineering/ ships agent persona definitions", () => {
const sourceAgentsRoot = join(
workspaceRoot,
"plugins",
"fusion-plugin-compound-engineering",
"src",
"agents",
);
const stagedAgentsRoot = join(
cliRoot,
"dist",
"plugins",
"fusion-plugin-compound-engineering",
"agents",
);
const sourceAgentFiles = readdirSync(sourceAgentsRoot)
.filter((file) => file.endsWith(".md"))
.sort();
const stagedAgentFiles = readdirSync(stagedAgentsRoot)
.filter((file) => file.endsWith(".md"))
.sort();
expect(stagedAgentFiles).toEqual(sourceAgentFiles);
for (const agentFile of stagedAgentFiles) {
const stagedAgentPath = join(stagedAgentsRoot, agentFile);
expect(readFileSync(stagedAgentPath, "utf-8")).toMatch(/^---[\s\S]*?name:\s*\S+/);
}
});
it("does not create skills directories for bundled plugins without skill sources", () => {
const pluginId = "fusion-plugin-roadmap";
expect(existsSync(join(workspaceRoot, "plugins", pluginId, "src", "skills"))).toBe(false);
expect(existsSync(join(cliRoot, "dist", "plugins", pluginId, "skills"))).toBe(false);
});
it("bundled plugin outputs do not import private @fusion/core at runtime", () => {
const inspectedPluginIds: string[] = [];
for (const pluginId of bundlePluginEntryPluginIds) {
const bundledPath = join(cliRoot, "dist", "plugins", pluginId, "bundled.js");
if (!existsSync(bundledPath)) {
continue;
}
inspectedPluginIds.push(pluginId);
const bundled = readFileSync(bundledPath, "utf-8");
expect(bundled, `${pluginId} should not keep a bare @fusion/core import`).not.toMatch(
/from\s+["']@fusion\/core["']/,
);
expect(bundled, `${pluginId} should not mention the private @fusion/core package`).not.toContain(
"@fusion/core",
);
}
expect(inspectedPluginIds.length).toBeGreaterThan(0);
});
it("reports, cli-printing-press, and whatsapp-chat ship self-contained bundled.js outputs", () => {
/*
* FNXC:BundledPlugins 2026-07-15-00:00:
* Surface Enumeration for FN-7956:
* - [x] Providers / bridges / execution paths: fusion-plugin-reports, fusion-plugin-cli-printing-press, and fusion-plugin-whatsapp-chat are all asserted through this bundlePluginEntry output invariant.
* - [x] Desktop + mobile breakpoints / platforms: N/A build/packaging-only change; desktop missing-bundle behavior for non-staged dependencies is unchanged.
* - [x] Empty / undefined / duplicate / populated data states: each staged root must contain bundled.js, omit src/, and omit .index.reload-N.ts artifacts.
* - [x] Shared hooks / components / modules / helpers: these ids are included in bundlePluginEntryPluginIds so the shared @fusion/core runtime-shim alias invariant covers them.
* - [x] Every component that renders the affordance: N/A no UI affordance add/remove.
* - [x] Leftover shells after removal: package output assertions fail if the old raw-src branches leave src/ or reload TypeScript files behind.
* - [x] Runtime-value assertion: each emitted bundled.js has no from "@fusion/core" import and no literal @fusion/core occurrence.
*/
for (const pluginId of selfContainedBundlePluginIds) {
expectSelfContainedBundle(pluginId);
}
});
it("dist/plugins/fusion-plugin-openclaw-runtime/ is staged with required bridge assets", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-openclaw-runtime");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "mcp-schema-server.cjs"))).toBe(true);
});
it("dist/plugins/fusion-plugin-droid-runtime/ is staged with required bridge assets", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-droid-runtime");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string };
expect(manifest.id).toBe("fusion-plugin-droid-runtime");
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "mcp-schema-server.cjs"))).toBe(true);
});
it("stages a portable Claude ACP launcher and declares every platform bridge", () => {
const bridgeRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-claude-runtime", "bridge");
const executableName = process.platform === "win32" ? "claude-code-cli-acp.cmd" : "claude-code-cli-acp";
const bridgePath = join(bridgeRoot, executableName);
const launcherManifestPath = join(bridgeRoot, "node_modules", "claude-code-cli-acp", "package.json");
const launcherManifest = JSON.parse(readFileSync(launcherManifestPath, "utf8")) as {
optionalDependencies?: Record<string, string>;
};
const cliManifest = JSON.parse(readFileSync(join(cliRoot, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
const supportedPlatformPackages = [
"claude-code-cli-acp-darwin-arm64",
"claude-code-cli-acp-darwin-x64",
"claude-code-cli-acp-linux-arm64",
"claude-code-cli-acp-linux-x64",
"claude-code-cli-acp-win32-arm64",
"claude-code-cli-acp-win32-x64",
];
expect(existsSync(bridgePath)).toBe(true);
expect(existsSync(join(bridgeRoot, "node_modules", "claude-code-cli-acp", "bin", "claude-code-cli-acp.js"))).toBe(true);
expect(cliManifest.dependencies?.["claude-code-cli-acp"]).toBe("0.1.1");
expect(Object.keys(launcherManifest.optionalDependencies ?? {}).sort()).toEqual(supportedPlatformPackages);
if (process.platform !== "win32") expect(statSync(bridgePath).mode & 0o111).not.toBe(0);
// Native binaries intentionally are not staged from the build host. npm installs the matching
// optional package from this manifest on the operator's platform, including platforms unavailable to CI.
}, 35_000);
it("dist/plugins/fusion-plugin-cursor-runtime/ is staged with a valid manifest", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("fusion-plugin-cursor-runtime");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
});
it("dist/plugins/fusion-plugin-acp-runtime/ is staged with the acp runtime manifest", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-acp-runtime");
const manifestPath = join(stagedRoot, "manifest.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as {
id?: string;
runtime?: { runtimeId?: string };
};
expect(manifest.id).toBe("fusion-plugin-acp-runtime");
// The runtime is selected by runtimeId; assert it is "acp".
expect(manifest.runtime?.runtimeId).toBe("acp");
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
// v1 ships no mcp-schema-server.cjs (MCP forwarding deferred, KTD5).
expect(existsSync(join(stagedRoot, "mcp-schema-server.cjs"))).toBe(false);
});
it("pi-claude-cli source imports child process helpers from node:child_process", () => {
const processManagerSource = readFileSync(join(cliRoot, "dist", "pi-claude-cli", "src", "process-manager.ts"), "utf-8");
expect(processManagerSource).toMatch(/import\s+\{[^}]*\bspawn\b[^}]*\}\s+from\s*["']node:child_process["']/);
});
it("pi-claude-cli source does not import cross-spawn directly", () => {
const processManagerSource = readFileSync(join(cliRoot, "dist", "pi-claude-cli", "src", "process-manager.ts"), "utf-8");
expect(processManagerSource).not.toMatch(/from\s*["']cross-spawn["']/);
});
it("staged pi-claude-cli package.json keeps pi extension entry and excludes cross-spawn deps", () => {
const stagedPkg = JSON.parse(
readFileSync(join(cliRoot, "dist", "pi-claude-cli", "package.json"), "utf-8"),
) as {
pi?: { extensions?: unknown };
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
expect(stagedPkg.pi?.extensions).toEqual(["index.ts"]);
expect(stagedPkg.dependencies?.["cross-spawn"]).toBeUndefined();
expect(stagedPkg.dependencies?.["@types/cross-spawn"]).toBeUndefined();
expect(stagedPkg.devDependencies?.["cross-spawn"]).toBeUndefined();
expect(stagedPkg.devDependencies?.["@types/cross-spawn"]).toBeUndefined();
});
it("runtime native assets are staged after build:exe", () => {
const runtimeDir = join(cliRoot, "dist", "runtime");
if (!existsSync(runtimeDir)) return;
const platformDirs = readdirSync(runtimeDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
if (platformDirs.length === 0) return;
const nativeAssets = platformDirs.flatMap((platform) => {
const platformDir = join(runtimeDir, platform);
return readdirSync(platformDir).filter((file) => file === "pty.node" || file === "spawn-helper");
});
// `build:exe` coverage lives in the dedicated build-exe tests. This check only
// validates already-staged runtime outputs when they are present, without
// failing on partially populated stale directories from earlier test runs.
if (nativeAssets.length === 0) return;
expect(nativeAssets.length).toBeGreaterThan(0);
});
});

View File

@@ -110,11 +110,16 @@ describe("retryOnLock", () => {
*/
describe("runTaskShow / runTaskMove — mocked-store lock exhaustion, not-found, and teardown (FN-7731)", () => {
beforeEach(() => {
// FNXC:CliBoardMutation 2026-07-19-18:20: mcp-lock-retry installs a
// per-test @fusion/core factory. Clear it before importing task.js so
// concurrent CLI test files cannot supply its secrets-store double here.
vi.doUnmock("@fusion/core");
vi.resetModules();
});
afterEach(() => {
vi.doUnmock("../../project-context.js");
vi.doUnmock("@fusion/core");
vi.restoreAllMocks();
delete process.env.FUSION_CLI_LOCK_RETRY_MS;
});
@@ -273,11 +278,16 @@ describe("runTaskShow / runTaskMove — mocked-store lock exhaustion, not-found,
// wrapper around a fresh, uncached `TaskStore`).
describe("FN-7734: generalized retry+teardown across representative fn task subcommands", () => {
beforeEach(() => {
// FNXC:CliBoardMutation 2026-07-19-18:20: mcp-lock-retry installs a
// per-test @fusion/core factory. Clear it before importing task.js so
// concurrent CLI test files cannot supply its secrets-store double here.
vi.doUnmock("@fusion/core");
vi.resetModules();
});
afterEach(() => {
vi.doUnmock("../../project-context.js");
vi.doUnmock("@fusion/core");
vi.restoreAllMocks();
delete process.env.FUSION_CLI_LOCK_RETRY_MS;
});

View File

@@ -102,18 +102,10 @@ const quarantinedCliTests: string[] = [
FNXC:CliTests 2026-07-18-07:30:
FN-8271 rescued the shard-4 cascade after removing unrelated PostgreSQL template-copy and persistent-seeding work from extension-dist-barrel's built-dist hook. All fourteen affected CLI files return to the default lane with their matching quarantine-ledger rows removed; retain normal worker budgets and timeout defaults rather than reintroducing appeasement.
FNXC:CliTests 2026-07-18-15:20:
Full-suite shard 4 after FN-8271 (runs 29648812375 / 29648952207) re-observed mcp-lock-retry and task-lock-retry 5s timeouts under package-lane shard load without product-bug evidence. Quarantine on sight in lockstep with scripts/lib/test-quarantine.json — do not raise testTimeout or fake-timer budgets.
FNXC:CliTests 2026-07-18-15:20:
Full-suite shard 4 on tip after #2322 (run 29662476909): bundle-output failed building desktop assets (ENOENT vendor-reactflow CSS) under package-lane load without product-bug evidence. Quarantine on sight — do not soften build assertions.
FNXC:CliTests 2026-07-18-20:45:
FN-8381 deletes extension-dist-barrel after its fourth quarantine cycle. Timing isolated the full core dist-barrel and re-mocked extension module graph as a 4–5.5s CPU-bound beforeAll while temp setup and cache seeding were negligible; shard contention pushed the same hook beyond Vitest's default 10s in run 29662476909. The source-side extension test retains the fn_task_list formatting/truncation invariant, while this test's marginal full-barrel substitution signal is not worth another load-sensitive rescue. Keep it out of both this exclusion and scripts/lib/test-quarantine.json; do not replace deletion with timeout, retry, or worker-budget appeasement.
*/
"src/commands/__tests__/mcp-lock-retry.test.ts",
"src/commands/__tests__/task-lock-retry.test.ts",
"src/__tests__/bundle-output.test.ts",
];
/*

View File

@@ -218,7 +218,7 @@ describe("TaskDetailModal tab persistence", () => {
await waitFor(() => expect(screen.getByRole("button", { name: "Plan" })).toHaveClass("detail-tab-active"));
});
it("keeps the Terminal guard when the CLI session disappears", async () => {
it("keeps the Terminal guard when the mocked CLI session disappears", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url) => {
const body = url.toString().includes("FN-TERMINAL")

View File

@@ -101,6 +101,13 @@ vi.mock("lucide-react", () => ({
Maximize2: () => null,
Minimize2: () => null,
Loader2: (props: any) => React.createElement("svg", { "data-testid": "loader2-icon", ...props }),
/*
FNXC:TaskDetailTabPersistence 2026-07-20-19:10:
FN-8394's restored mocked-session guard reaches TaskVerificationStatus. Keep
its success-icon mock available so this deterministic tab-state regression
does not depend on the unrelated lucide module implementation.
*/
CheckCircle2: () => null,
Send: (props: any) => React.createElement("svg", { "data-testid": "send-icon", ...props }),
Square: (props: any) => React.createElement("svg", { "data-testid": "square-icon", ...props }),
Info: (props: any) => React.createElement("svg", { "data-testid": "info-icon", ...props }),

View File

@@ -1,303 +1,200 @@
// @vitest-environment node
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import os from "node:os";
import { join } from "node:path";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import type { ChildProcess } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
import { DevServerProcessManager } from "../dev-server-process.js";
import { loadDevServerStore, resetDevServerStore } from "../dev-server-store.js";
import { afterEach, describe, expect, it, vi } from "vitest";
import { DevServerProcessManager, type DevServerProcessManagerOptions } from "../dev-server-process.js";
import type { DevServerState, DevServerStore } from "../dev-server-store.js";
async function waitFor(predicate: () => boolean, timeoutMs = 4_000): Promise<void> {
const start = Date.now();
while (!predicate()) {
if (Date.now() - start > timeoutMs) {
throw new Error("Timed out waiting for condition");
}
await new Promise((resolve) => setTimeout(resolve, 25));
/*
FNXC:DevServerProcessTests 2026-07-19-18:45:
FN-8394 replaces the quarantined real-shell test with an injected child-process
and timer seam. The test still guards lifecycle behavior, while shard load cannot
starve a real process, filesystem store, stdout race, or fallback network probe.
*/
class MemoryDevServerStore {
private state: DevServerState = {
id: "",
name: "default",
status: "stopped",
command: "",
cwd: "",
logHistory: [],
};
getState(): DevServerState {
return { ...this.state, logHistory: [...this.state.logHistory] };
}
async updateState(partial: Partial<DevServerState>): Promise<DevServerState> {
this.state = { ...this.state, ...partial, logHistory: partial.logHistory ?? this.state.logHistory };
return this.getState();
}
async appendLog(line: string): Promise<void> {
this.state.logHistory.push(line);
}
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
class FakeChildProcess extends EventEmitter {
pid = 42;
exitCode: number | null = null;
signalCode: NodeJS.Signals | null = null;
stdout = new PassThrough();
stderr = new PassThrough();
close(code = 0): void {
this.exitCode = code;
this.emit("close", code);
}
}
type DevServerProcessManagerInternals = {
childProcess: ChildProcess | null;
handleFailure(error: Error): Promise<void>;
};
function createFixture(options?: { closeOnSignal?: NodeJS.Signals[]; stopTimeoutMs?: number }) {
const store = new MemoryDevServerStore();
const children: FakeChildProcess[] = [];
const signals: NodeJS.Signals[] = [];
const closeOnSignal = options?.closeOnSignal ?? ["SIGTERM"];
const managerOptions: DevServerProcessManagerOptions = {
probeDelayMs: 10_000,
stopTimeoutMs: options?.stopTimeoutMs,
spawn: (() => {
const child = new FakeChildProcess();
children.push(child);
return { child: child as unknown as ChildProcess };
}) as DevServerProcessManagerOptions["spawn"],
killManagedProcess: (child, signal) => {
signals.push(signal);
if (closeOnSignal.includes(signal)) {
(child as unknown as FakeChildProcess).close();
}
},
};
return { store, children, signals, manager: new DevServerProcessManager(store as unknown as DevServerStore, managerOptions) };
}
async function settleLifecycleWork(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
describe("DevServerProcessManager", () => {
const tempDirs: string[] = [];
const managers: DevServerProcessManager[] = [];
afterEach(async () => {
for (const manager of managers.splice(0)) {
try {
if (manager.isRunning()) {
await manager.stop();
}
} catch {
// ignore
}
manager.cleanup();
}
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
resetDevServerStore();
afterEach(() => {
vi.useRealTimers();
});
async function createManager(options?: { stopTimeoutMs?: number; probeDelayMs?: number; probeTimeoutMs?: number }) {
const root = mkdtempSync(join(os.tmpdir(), "fn-dev-process-"));
tempDirs.push(root);
const store = await loadDevServerStore(root);
const manager = new DevServerProcessManager(store, options);
managers.push(manager);
return { root, store, manager };
}
it("rejects invalid commands and duplicate starts before spawning another child", async () => {
const { children, manager } = createFixture();
it("start() spawns child process and updates state to running", async () => {
const { root, manager } = await createManager();
const state = await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await expect(manager.start(" ", "/repo")).rejects.toThrow("command is required");
await expect(manager.start("echo $(unsafe)", "/repo")).rejects.toThrow("command substitution");
await expect(manager.start("pnpm dev", " ")).rejects.toThrow("cwd is required");
expect(children).toHaveLength(0);
expect(state.status).toBe("running");
expect(typeof state.pid).toBe("number");
await manager.start("pnpm dev", "/repo");
await expect(manager.start("pnpm dev", "/repo")).rejects.toThrow("already running");
expect(children).toHaveLength(1);
manager.cleanup();
});
it("start() emits started event", async () => {
const { root, manager } = await createManager();
it("starts an injected child, persists output, and detects its announced URL once", async () => {
const { children, manager, store } = createFixture();
const detected: unknown[] = [];
manager.on("url-detected", (event) => detected.push(event));
const startedEvent = new Promise<void>((resolve) => {
manager.once("started", () => resolve());
});
const state = await manager.start("pnpm dev", "/repo", { scriptId: "dev" });
children[0].stdout.write("ready at http://localhost:4321\nready again at http://localhost:4321\n");
await settleLifecycleWork();
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await startedEvent;
expect(state).toMatchObject({ status: "running", pid: 42, scriptId: "dev" });
expect(store.getState()).toMatchObject({ detectedUrl: "http://localhost:4321", detectedPort: 4321 });
expect(store.getState().logHistory).toEqual([
"ready at http://localhost:4321",
"ready again at http://localhost:4321",
]);
expect(detected).toHaveLength(1);
expect(manager.hasPendingProbeTimer()).toBe(false);
manager.cleanup();
});
it("start() captures stdout into log buffer", async () => {
const { root, store, manager } = await createManager();
it.each([
["http://127.0.0.1:4173", "http://127.0.0.1:4173", 4173],
["Listening on port 5173", "http://localhost:5173", 5173],
])("detects alternate announced URL format %s", async (line, detectedUrl, detectedPort) => {
const { children, manager, store } = createFixture();
await manager.start("node -e \"console.log('hello from stdout');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await manager.start("pnpm dev", "/repo");
children[0].stdout.write(`${line}\n`);
await settleLifecycleWork();
await waitFor(() => store.getState().logHistory.some((line) => line.includes("hello from stdout")));
expect(store.getState().logHistory.some((line) => line.includes("hello from stdout"))).toBe(true);
expect(store.getState()).toMatchObject({ detectedUrl, detectedPort });
manager.cleanup();
});
it("start() throws if already running", async () => {
const { root, manager } = await createManager();
it("stops through the injected process-tree signal and clears the fallback timer", async () => {
const { manager, signals, store } = createFixture();
await manager.start("pnpm dev", "/repo");
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
await expect(manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root)).rejects.toThrow("already running");
});
const stopped = await manager.stop();
it("start() throws if command is empty", async () => {
const { root, manager } = await createManager();
await expect(manager.start(" ", root)).rejects.toThrow("command is required");
});
it("stop() sends SIGTERM and waits for exit", async () => {
const { root, store, manager } = await createManager();
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
const state = await manager.stop();
expect(state.status).toBe("stopped");
expect(signals).toEqual(["SIGTERM"]);
expect(stopped.status).toBe("stopped");
expect(store.getState().status).toBe("stopped");
expect(store.getState().exitCode).toBeDefined();
expect(manager.hasPendingProbeTimer()).toBe(false);
});
it("stop() terminates the shell-launched child process tree", async () => {
if (process.platform === "win32") {
return;
}
it("returns the current state without signaling when no child is running", async () => {
const { manager, signals, store } = createFixture();
const { root, manager } = await createManager();
const childPidFile = join(root, "managed-child.pid");
await manager.start(
`node -e "require('node:fs').writeFileSync('${childPidFile}', String(process.pid));process.stdin.resume();process.stdin.on('end',()=>process.exit(0))"`,
root,
);
await waitFor(() => {
try {
return Number.parseInt(readFileSync(childPidFile, "utf8").trim(), 10) > 0;
} catch {
return false;
}
});
const managedChildPid = Number.parseInt(readFileSync(childPidFile, "utf8").trim(), 10);
expect(isProcessAlive(managedChildPid)).toBe(true);
await manager.stop();
await waitFor(() => !isProcessAlive(managedChildPid));
await expect(manager.stop()).resolves.toEqual(store.getState());
expect(signals).toEqual([]);
});
it("stop() falls back to SIGKILL after timeout", async () => {
const { root, store, manager } = await createManager({ stopTimeoutMs: 150 });
it("falls back to SIGKILL when the injected child ignores SIGTERM", async () => {
vi.useFakeTimers();
const { manager, signals, store } = createFixture({ closeOnSignal: ["SIGKILL"], stopTimeoutMs: 25 });
await manager.start("pnpm dev", "/repo");
await manager.start(
"node -e \"process.on('SIGTERM', () => {});process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
const stopped = manager.stop();
await vi.advanceTimersByTimeAsync(25);
const state = await manager.stop();
expect(state.status).toBe("stopped");
await expect(stopped).resolves.toMatchObject({ status: "stopped" });
expect(signals).toEqual(["SIGTERM", "SIGKILL"]);
expect(store.getState().status).toBe("stopped");
});
it("stop() returns current state if nothing is running", async () => {
const { store, manager } = await createManager();
const state = await manager.stop();
expect(state).toEqual(store.getState());
});
it("restart() stops then starts with same command", async () => {
const { root, store, manager } = await createManager();
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root, { scriptId: "dev" });
const firstPid = store.getState().pid;
const state = await manager.restart();
expect(state.status).toBe("running");
expect(state.pid).toBeDefined();
expect(state.scriptId).toBe("dev");
expect(state.pid).not.toBe(firstPid);
});
it("detects URL from localhost output", async () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('Server ready at http://localhost:3000');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
await waitFor(() => store.getState().detectedUrl === "http://localhost:3000");
expect(store.getState().detectedPort).toBe(3000);
});
it("detects URL from 127.0.0.1 output", async () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('ready at http://127.0.0.1:4173');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
await waitFor(() => store.getState().detectedUrl === "http://127.0.0.1:4173");
expect(store.getState().detectedPort).toBe(4173);
});
it("detects URL from keyword plus port pattern", async () => {
const { root, store, manager } = await createManager();
await manager.start(
"node -e \"console.log('Listening on port 5173');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
await waitFor(() => store.getState().detectedUrl === "http://localhost:5173");
expect(store.getState().detectedPort).toBe(5173);
});
it("schedules fallback probing after startup when no URL is announced", async () => {
const { root, manager } = await createManager({ probeDelayMs: 25, probeTimeoutMs: 5 });
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
expect(manager.hasPendingProbeTimer()).toBe(true);
await waitFor(() => manager.hasPendingProbeTimer() === false, 3_000);
});
it("clears fallback probe timer when URL is detected from logs", async () => {
const { root, store, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
const detectedEvents: unknown[] = [];
manager.on("url-detected", (payload) => detectedEvents.push(payload));
await manager.start(
"node -e \"console.log('ready at http://localhost:4321');console.log('ready again at http://localhost:4321');process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"",
root,
);
await waitFor(() => store.getState().detectedPort === 4321);
it("clears the fallback timer on child failure and creates a fresh child on restart", async () => {
const { children, manager } = createFixture();
await manager.start("pnpm dev", "/repo", { scriptId: "dev" });
children[0].emit("error", new Error("synthetic failure"));
await settleLifecycleWork();
expect(manager.hasPendingProbeTimer()).toBe(false);
expect(detectedEvents).toHaveLength(1);
});
it("clears fallback probe timer on stop", async () => {
const { root, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("pnpm dev", "/repo", { scriptId: "dev" });
const restarted = await manager.restart();
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
expect(children).toHaveLength(3);
expect(restarted).toMatchObject({ status: "running", scriptId: "dev" });
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.stop();
manager.cleanup();
expect(manager.hasPendingProbeTimer()).toBe(false);
});
it("clears fallback probe timer when process exits naturally", async () => {
const { root, store, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("node -e \"setTimeout(() => process.exit(0), 20)\"", root);
expect(manager.hasPendingProbeTimer()).toBe(true);
await waitFor(() => store.getState().status === "stopped");
expect(manager.hasPendingProbeTimer()).toBe(false);
});
it("clears fallback probe timer when the child process reports failure", async () => {
const { root, store, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("node -e \"setTimeout(() => process.exit(0), 50)\"", root);
expect(manager.hasPendingProbeTimer()).toBe(true);
const internals = manager as unknown as DevServerProcessManagerInternals;
internals.childProcess?.emit("error", new Error("synthetic process failure"));
await waitFor(() => store.getState().status === "failed");
expect(manager.hasPendingProbeTimer()).toBe(false);
});
it("restarts with a fresh fallback probe timer", async () => {
const { root, manager } = await createManager({ probeDelayMs: 2_000, probeTimeoutMs: 5 });
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root, { scriptId: "dev" });
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.restart();
expect(manager.hasPendingProbeTimer()).toBe(true);
await manager.stop();
expect(manager.hasPendingProbeTimer()).toBe(false);
});
it("cleanup() kills process and clears listeners", async () => {
const { root, manager } = await createManager();
await manager.start("node -e \"process.stdin.resume();process.stdin.on('end',()=>process.exit(0))\"", root);
it("cleanup removes manager and child stream listeners without leaving a timer", async () => {
const { children, manager } = createFixture();
await manager.start("pnpm dev", "/repo");
manager.on("output", () => undefined);
expect(manager.listenerCount("output")).toBeGreaterThan(0);
expect(children[0].stdout.listenerCount("data")).toBeGreaterThan(0);
manager.cleanup();
await waitFor(() => manager.isRunning() === false);
expect(manager.hasPendingProbeTimer()).toBe(false);
expect(manager.listenerCount("output")).toBe(0);
expect(children[0].stdout.listenerCount("data")).toBe(0);
expect(manager.hasPendingProbeTimer()).toBe(false);
expect(manager.isRunning()).toBe(false);
});
});

View File

@@ -20,6 +20,10 @@ export interface DevServerProcessManagerOptions {
stopTimeoutMs?: number;
probeDelayMs?: number;
probeTimeoutMs?: number;
/** Test seam for lifecycle assertions without a shell child process. */
spawn?: typeof superviseSpawn;
/** Test seam for proving stop dispatches the process-tree signal without killing an OS process. */
killManagedProcess?: (child: ChildProcess, signal: NodeJS.Signals) => void;
}
interface UrlDetectedEventPayload {
@@ -90,6 +94,8 @@ export class DevServerProcessManager extends EventEmitter {
private readonly stopTimeoutMs: number;
private readonly probeDelayMs: number;
private readonly probeTimeoutMs: number;
private readonly spawn: typeof superviseSpawn;
private readonly kill: (child: ChildProcess, signal: NodeJS.Signals) => void;
constructor(
private readonly store: DevServerStore,
@@ -99,6 +105,14 @@ export class DevServerProcessManager extends EventEmitter {
this.stopTimeoutMs = options?.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
this.probeDelayMs = options?.probeDelayMs ?? DEFAULT_PROBE_DELAY_MS;
this.probeTimeoutMs = options?.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
/*
FNXC:DevServerProcessTests 2026-07-19-18:45:
FN-8394 rescues lifecycle coverage from a second load-sensitive quarantine.
Inject only spawn and process-tree signaling so unit tests retain start/stop,
output, URL, fallback-timer, and restart invariants without real shell children.
*/
this.spawn = options?.spawn ?? superviseSpawn;
this.kill = options?.killManagedProcess ?? killManagedProcess;
}
async start(
@@ -139,7 +153,7 @@ export class DevServerProcessManager extends EventEmitter {
detectedPort: undefined,
});
const supervised = superviseSpawn(safeCommand, [], {
const supervised = this.spawn(safeCommand, [], {
cwd: safeCwd,
shell: true,
stdio: ["pipe", "pipe", "pipe"],
@@ -212,12 +226,12 @@ export class DevServerProcessManager extends EventEmitter {
const pid = child.pid;
if (typeof pid === "number") {
killManagedProcess(child, "SIGTERM");
this.kill(child, "SIGTERM");
}
const killTimer = setTimeout(() => {
if (this.childProcess === child && this.isRunning()) {
killManagedProcess(child, "SIGKILL");
this.kill(child, "SIGKILL");
}
}, this.stopTimeoutMs);
@@ -259,7 +273,7 @@ export class DevServerProcessManager extends EventEmitter {
this.clearTimers();
if (this.childProcess && typeof this.childProcess.pid === "number") {
killManagedProcess(this.childProcess, "SIGTERM");
this.kill(this.childProcess, "SIGTERM");
this.childProcess.removeAllListeners();
this.childProcess.stdout?.removeAllListeners();
this.childProcess.stderr?.removeAllListeners();

View File

@@ -315,6 +315,12 @@ FN-6722 workspace verification observed dev-server-process time out only in the
FNXC:DashboardTestQuarantine 2026-06-21-12:42:
FN-6860 rescued dev-server-process by settling stdout detection and fallback-probe lifecycle work before stop/close/failure teardown, then removed its ledger/config quarantine entry. The same loaded API shard also confirmed FN-6742's session-cross-tab rescue still holds, so its stale ledger-only entry was removed to restore lockstep.
FNXC:DashboardTestQuarantine 2026-07-19-18:45:
FN-8394 rescues dev-server-process after its second load-sensitive quarantine.
Its injected child-process and process-tree-signal seams preserve lifecycle,
stdout/URL, fallback-timer, failure, and restart assertions without a real shell
child or filesystem store; keep it out of this exclude list and ledger.
FNXC:DashboardTestQuarantine 2026-06-22-18:05:
FN-6937 verified that FN-6860's claimed session-cross-tab ledger removal had not landed: the file was active because this exclude list was empty, but `test-quarantine.json` still carried the stale 2026-06-19 row. The repeated loaded `dashboard-api-quality-backfill` runs and lock-holder mutation proof confirmed FN-6742's rescue still holds, so remove the orphaned ledger row and keep this list empty to restore ledger↔config lockstep.
@@ -346,21 +352,6 @@ const quarantinedDashboardTests: string[] = [
async-store or applicable mock/non-store contracts. Remove their ledger/exclude
pairs so dashboard-api-quality-backfill collects the restored coverage.
*/
/*
FNXC:DashboardTestQuarantine 2026-07-18-14:05:
Full-suite shard 2 (run 29660321240): Terminal-guard tab settle race under
dashboard-app-quality-backfill load; passes focused thrice with no product bug.
Quarantine on sight — mirrored in scripts/lib/test-quarantine.json.
*/
"app/components/__tests__/TaskDetailModal.tab-persistence.test.tsx",
/*
FNXC:DashboardTestQuarantine 2026-07-18-14:40:
Full-suite shard 4 (run 29661202279): re-flaked stdout/fallback-probe race in
clears fallback probe timer when URL is detected from logs under the loaded
API backfill lane (prior FN-6722 quarantine / FN-6860 rescue). Quarantine on
sight — mirrored in scripts/lib/test-quarantine.json.
*/
"src/__tests__/dev-server-process.test.ts",
];
const qualityApiTests = [

View File

@@ -1,876 +0,0 @@
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../pi.js", () => ({
createFnAgent: vi.fn(async () => ({
prompt: vi.fn(async () => undefined),
dispose: vi.fn(async () => undefined),
})),
describeModel: vi.fn(() => "mock-provider/mock-model"),
promptWithFallback: vi.fn(async (session: { prompt: (prompt: string) => Promise<unknown> }, prompt: string) => {
await session.prompt(prompt);
}),
compactSessionContext: vi.fn(),
}));
import type { Settings, TaskStore, RunAuditEvent } from "@fusion/core";
import { queryRunAuditEvents, drizzleEq, postgresSchema } from "@fusion/core";
import { activeSessionRegistry, executingTaskLock } from "../../active-session-registry.js";
import { aiMergeTask } from "../../merger.js";
import { createFnAgent } from "../../pi.js";
// FNXC:SqliteRemoval 2026-07-14: hasPg guard added — makeReliabilityFixture requires PG after SQLite removal (VAL-REMOVAL-005).
import { git, hasGit, hasPg, makeReliabilityFixture, type ReliabilityFixture } from "./_helpers.js";
/*
FNXC:SqliteRemoval 2026-07-14:
Async PG helpers replacing sync SQLite APIs (store.getRunAuditEvents, store.getDatabase().prepare)
that don't work in backend mode after VAL-REMOVAL-005. Tests now run in backend mode (PG).
*/
const mq = postgresSchema.project.mergeQueue;
async function auditEvents(store: TaskStore, filter: { taskId?: string; mutationType?: string; limit?: number } = {}): Promise<RunAuditEvent[]> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required for auditEvents");
return queryRunAuditEvents(layer.db, filter);
}
async function updateMergeQueueLease(store: TaskStore, taskId: string, leasedBy: string, leasedAt: string, leaseExpiresAt: string): Promise<void> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required");
await layer.db.update(mq).set({ leasedBy, leasedAt, leaseExpiresAt }).where(drizzleEq(mq.taskId, taskId));
}
async function deleteMergeQueueRow(store: TaskStore, taskId: string): Promise<void> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required");
await layer.db.delete(mq).where(drizzleEq(mq.taskId, taskId));
}
async function insertMergeQueueRow(store: TaskStore, taskId: string, enqueuedAt: string, priority: string): Promise<void> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required");
await layer.db.insert(mq).values({ taskId, enqueuedAt, priority, attemptCount: 0 });
}
async function getMergeQueueRow(store: TaskStore, taskId: string): Promise<{ taskId: string; leasedBy: string | null } | undefined> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required");
const rows = await layer.db.select().from(mq).where(drizzleEq(mq.taskId, taskId));
return rows[0];
}
async function getMergeQueueTaskIds(store: TaskStore, taskIds: string[]): Promise<string[]> {
const layer = store.getAsyncLayer();
if (!layer) throw new Error("PG required");
const results: string[] = [];
for (const id of taskIds) {
const rows = await layer.db.select({ taskId: mq.taskId }).from(mq).where(drizzleEq(mq.taskId, id));
if (rows.length > 0) results.push(rows[0].taskId);
}
return results;
}
const mockedCreateFnAgent = vi.mocked(createFnAgent);
/**
* Shared setup for the reuse-task-worktree handoff scenarios. Every test in
* this suite was ~50 lines of identical fixture boilerplate; this consolidates
* the common path. Pass `extraSettings` for variants and use the returned
* handles to layer test-specific state on top.
*/
async function setupReuseHandoff(opts: {
taskId: string;
fileName?: string;
fileContent?: string;
commitMessage?: string;
/** Skip `git worktree add`. Used by tests that exercise the missing-worktree path. */
skipWorktreeAdd?: boolean;
/**
* Override `task.worktree`. `undefined` writes the standard worktreePath.
* `null` skips the update entirely. A string sets that exact value.
*/
worktreeOverride?: string | null;
/** Skip `store.enqueueMergeQueue` (used by tests that craft custom queue rows). */
skipEnqueue?: boolean;
/** Pass `--allow-empty` so the branch has 1 own commit but zero net diff. */
emptyOwnDiff?: boolean;
extraSettings?: Partial<Settings>;
}): Promise<{
fixture: ReliabilityFixture;
rootDir: string;
store: ReliabilityFixture["store"];
task: ReliabilityFixture["task"];
branch: string;
worktreeRoot: string;
worktreePath: string;
}> {
const fixture = await makeReliabilityFixture({
taskId: opts.taskId,
settings: {
baseBranch: "master",
mergeIntegrationWorktree: "reuse-task-worktree",
...opts.extraSettings,
} as Partial<Settings>,
});
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
git(rootDir, "git branch -m main master");
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
await store.updateTask(task.id, {
baseBranch: "master",
branch,
steps: completedSteps,
currentStep: completedSteps.length,
} as any);
await fixture.createBranch(branch);
if (opts.emptyOwnDiff) {
git(rootDir, `git commit --allow-empty -m 'test(${actualTask!.id}): verification-only handoff'`);
} else {
const fileName = opts.fileName ?? `packages/engine/src/${opts.taskId.toLowerCase()}.ts`;
const fileContent = opts.fileContent ?? "export const value = 1;\n";
const commitMessage = opts.commitMessage ?? `feat: add ${opts.taskId} merge content`;
await fixture.writeAndCommit(fileName, fileContent, commitMessage);
}
await fixture.checkout("master");
if (!opts.skipWorktreeAdd) {
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
}
if (opts.worktreeOverride !== null) {
const path = opts.worktreeOverride ?? worktreePath;
await store.updateTask(task.id, { worktree: path, branch } as any);
}
if (!opts.skipEnqueue) {
await store.enqueueMergeQueue(task.id);
}
return { fixture, rootDir, store, task, branch, worktreeRoot, worktreePath };
}
describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
beforeEach(() => {
mockedCreateFnAgent.mockClear();
activeSessionRegistry.clear();
executingTaskLock._clearForTest();
});
it.skipIf(!hasGit || !hasPg)("happy path merges from a reused task worktree and applies the squash to the project root's integration branch", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5279-RI-HAPPY",
fileName: "packages/engine/src/fn-5279-ri-happy.ts",
fileContent: "export const value = 1;\n",
commitMessage: "feat: add reuse merge content",
extraSettings: { worktreeRebaseRemote: "origin" } as Partial<Settings>,
});
try {
const rootHeadBefore = git(rootDir, "git rev-parse HEAD");
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const audits = (await auditEvents(store, { taskId: task.id }));
const auditTypes = audits.map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
expect(auditTypes).toContain("merge:reuse-handoff-released");
const acquired = audits.find((event) => event.mutationType === "merge:reuse-handoff-acquired");
expect(acquired?.metadata).toMatchObject({ integrationRemote: "origin", integrationBranch: "master" });
// Step 5c (FN-5279 reuse mode) advances the project root's integration
// branch to the new squash commit so changes actually land on master.
expect(auditTypes).toContain("merge:integration-ref-advance");
const advanced = audits.find((event) => event.mutationType === "merge:integration-ref-advance");
expect(advanced?.metadata).toMatchObject({ advanceMode: "update-ref", succeeded: true });
expect(git(rootDir, "git rev-parse HEAD")).not.toBe(rootHeadBefore);
// 4c31e885b (engine auto-sync) keeps the project root's working tree
// in step with the advanced ref, so the new file is a tracked, clean
// path at HEAD rather than appearing as a dirty/untracked entry. Verify
// landing via `git ls-files` (commit-reachable) instead of `git status`.
const rootLsFilesAfter = git(rootDir, "git ls-files");
expect(rootLsFilesAfter).toContain("packages/engine/src/fn-5279-ri-happy.ts");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("dirty reused worktree is autostashed so the merge can proceed", async () => {
const { fixture, rootDir, store, task, worktreePath } = await setupReuseHandoff({
taskId: "FN-5279-RI-DIRTY",
fileName: "packages/engine/src/fn-5279-ri-dirty.ts",
fileContent: "export const dirty = true;\n",
commitMessage: "feat: add dirty merge content",
});
git(worktreePath, "sh -c 'printf dirty > DIRTY.txt'");
try {
await aiMergeTask(store, rootDir, task.id).catch(() => undefined);
const autostash = (await auditEvents(store, { taskId: task.id }))
.find((event) => event.mutationType === "merge:reuse-handoff-autostash");
expect(autostash?.metadata).toMatchObject({ worktreePath });
expect(typeof autostash?.metadata?.stashSha).toBe("string");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("active session binding refuses handoff until the worktree is released", async () => {
const { fixture, rootDir, store, task, worktreePath } = await setupReuseHandoff({
taskId: "FN-5279-RI-ACTIVE",
fileName: "packages/engine/src/fn-5279-ri-active.ts",
fileContent: "export const active = true;\n",
commitMessage: "feat: add active merge content",
});
activeSessionRegistry.registerPath(worktreePath, { taskId: task.id, kind: "executor", ownerKey: task.id });
executingTaskLock.tryClaim(task.id);
try {
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "active-session-binding",
});
const refused = (await auditEvents(store, { taskId: task.id })).find((event) => event.mutationType === "merge:reuse-handoff-refused");
expect(refused?.metadata).toMatchObject({ gate: "active-session-binding" });
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("branch/worktree mapping mismatches refuse handoff", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5279-RI-MISMATCH",
fileName: "packages/engine/src/fn-5279-ri-mismatch.ts",
fileContent: "export const mismatch = true;\n",
commitMessage: "feat: add mismatch merge content",
});
// Drift the task.branch away from the actual branch on disk to trigger the mapping refusal.
await store.updateTask(task.id, { branch: "fusion/fn-other" } as any);
try {
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "branch-worktree-mapping",
});
const refused = (await auditEvents(store, { taskId: task.id })).find((event) => event.mutationType === "merge:reuse-handoff-refused");
expect(refused?.metadata).toMatchObject({ gate: "branch-worktree-mapping" });
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("missing merge queue lease refuses handoff with target-not-queued diagnostics", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5279-RI-NO-LEASE",
fileName: "packages/engine/src/fn-5279-ri-no-lease.ts",
fileContent: "export const noLease = true;\n",
commitMessage: "feat: add no-lease merge content",
skipEnqueue: true,
});
await store.enqueueMergeQueue(task.id, { now: "2026-05-19T00:00:00.000Z" });
await updateMergeQueueLease(store, task.id, "worker-other", "2026-05-19T00:01:00.000Z", "2099-05-19T00:10:00.000Z");
try {
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "lease-handoff-failed",
reason: "target-not-queued",
});
const refused = (await auditEvents(store, { taskId: task.id })).find((event) => event.mutationType === "merge:reuse-handoff-refused");
expect(refused?.metadata).toMatchObject({
gate: "lease-handoff-failed",
reason: "target-not-queued",
});
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5353: aiMergeTask succeeds without pre-enqueue by self-enqueueing before handoff", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5353-RI-SELF-ENQUEUE",
fileName: "packages/engine/src/fn-5353-ri-self-enqueue.ts",
fileContent: "export const selfEnqueue = true;\n",
commitMessage: "feat: add self enqueue merge content",
});
await deleteMergeQueueRow(store, task.id);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5353: cross-task queue entries remain untouched when aiMergeTask self-enqueues target", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5353-RI-TARGET-A",
fileName: "packages/engine/src/fn-5353-ri-target-not-queued.ts",
fileContent: "export const targetNotQueued = true;\n",
commitMessage: "feat: add target not queued reproduction",
skipEnqueue: true,
});
try {
const other = await store.createTask({ description: "queue head other", priority: "normal" });
await store.moveTask(other.id, "todo");
await store.moveTask(other.id, "in-progress");
await store.handoffToReview(other.id, {
ownerAgentId: "agent-1",
evidence: { reason: "fn_task_done", runId: "run-1", agentId: "agent-1" },
});
await store.enqueueMergeQueue(other.id, { now: "2026-05-19T00:00:00.000Z" });
await deleteMergeQueueRow(store, task.id);
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const otherRow = await getMergeQueueRow(store, other.id);
expect(otherRow.taskId).toBe(other.id);
expect(otherRow.leasedBy).toBeNull();
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5353: reuse handoff rejects project-root worktree misconfiguration", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5353-RI-PROJECT-ROOT-WORKTREE",
fileName: "packages/engine/src/fn-5353-ri-project-root.ts",
fileContent: "export const projectRootReuse = true;\n",
commitMessage: "feat: add project root misconfiguration content",
skipWorktreeAdd: true,
worktreeOverride: undefined, // placeholder, real value set below
});
// Point task.worktree at the project root to trigger the misconfig refusal.
await store.updateTask(task.id, { worktree: rootDir } as any);
try {
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "reuse-misconfigured",
reason: "worktree-equals-project-root",
});
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5353: missing task.worktree reacquires a reusable worktree before handoff gates", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5353-RI-MISSING-WORKTREE-HANDOFF",
fileName: "packages/engine/src/fn-5353-ri-missing-worktree-handoff.ts",
fileContent: "export const missingHandoff = true;\n",
commitMessage: "feat: add missing worktree handoff content",
skipWorktreeAdd: true,
worktreeOverride: null,
});
await store.updateTask(task.id, { worktree: null } as any);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const audits = (await auditEvents(store, { taskId: task.id }));
const auditTypes = audits.map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-fallback-new-worktree");
expect(auditTypes).not.toContain("merge:reuse-handoff-refused");
const refused = audits.find((event) => event.mutationType === "merge:reuse-handoff-refused");
expect((refused?.metadata as { reason?: string } | undefined)?.reason).not.toBe("worktree-equals-project-root");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5363: queue-head pollution by non-in-review tasks does not block target reuse handoff", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5363-RI-POLLUTED",
fileName: "packages/engine/src/fn-5363-ri-polluted.ts",
fileContent: "export const polluted = true;\n",
commitMessage: "feat: add polluted queue merge content",
skipEnqueue: true,
extraSettings: { worktreeRebaseRemote: "origin" } as Partial<Settings>,
});
await store.enqueueMergeQueue(task.id, { now: "2026-05-19T00:00:02.000Z" });
const todoTask = await store.createTask({ description: "polluter todo", priority: "normal" });
await store.moveTask(todoTask.id, "todo");
const inProgressTask = await store.createTask({ description: "polluter progress", priority: "normal" });
await store.moveTask(inProgressTask.id, "todo");
await store.moveTask(inProgressTask.id, "in-progress");
await insertMergeQueueRow(store, todoTask.id, "2026-05-19T00:00:00.000Z", "normal");
await insertMergeQueueRow(store, inProgressTask.id, "2026-05-19T00:00:01.000Z", "normal");
await updateMergeQueueLease(store, todoTask.id, "merger-reuse-handoff", "2026-05-19T00:10:00.000Z", "2099-05-19T00:20:00.000Z");
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
expect(await getMergeQueueRow(store, task.id)).toBeUndefined();
expect(await getMergeQueueTaskIds(store, [todoTask.id, inProgressTask.id])).toEqual([]);
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5363: target row leased by another worker refuses with target-not-queued diagnostics", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5363-RI-NO-LEASE-TARGET",
fileName: "packages/engine/src/fn-5363-ri-no-lease-target.ts",
fileContent: "export const noLeaseTarget = true;\n",
commitMessage: "feat: add leased target merge content",
});
// Replay the in-review handoff so the task.column/state matches the pre-merge geometry.
const completedSteps = ((await store.getTask(task.id))?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.handoffToReview(task.id, {
ownerAgentId: "agent-1",
evidence: { reason: "fn_task_done", runId: "run-1", agentId: "agent-1" },
});
// handoffToReview resets task.steps; restore completion so the merge gate
// doesn't refuse with "task has incomplete steps".
await store.updateTask(task.id, { steps: completedSteps, currentStep: completedSteps.length } as any);
await updateMergeQueueLease(store, task.id, "worker-other", "2026-05-19T00:01:00.000Z", "2099-05-19T00:10:00.000Z");
try {
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "lease-handoff-failed",
reason: "target-not-queued",
});
const refused = (await auditEvents(store, { taskId: task.id })).find((event) => event.mutationType === "merge:reuse-handoff-refused");
expect(refused?.metadata).toMatchObject({ reason: "target-not-queued" });
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("FN-5444: moving task out of in-review during live lease preserves row until release cleanup", async () => {
const { fixture, store, task } = await setupReuseHandoff({
taskId: "FN-5444-RI-COLUMN-EXIT-LIVE-LEASE",
fileName: "packages/engine/src/fn-5444-ri-column-exit.ts",
fileContent: "export const exitLease = true;\n",
commitMessage: "feat: add FN-5444 column exit lease coverage",
skipWorktreeAdd: true,
worktreeOverride: null,
});
try {
const lease = await store.acquireMergeQueueLease("merger-reuse-handoff", {
targetTaskId: task.id,
leaseDurationMs: 60_000,
now: "2099-05-19T00:00:10.000Z",
});
expect(lease?.taskId).toBe(task.id);
await store.moveTask(task.id, "todo");
expect((await store.peekMergeQueue()).some((entry) => entry.taskId === task.id)).toBe(true);
const staleLeaseAudit = (await auditEvents(store, { taskId: task.id, mutationType: "mergeQueue:stale-lease-on-column-exit" }));
expect(staleLeaseAudit).toHaveLength(1);
expect(staleLeaseAudit[0].metadata).toMatchObject({
taskId: task.id,
previousColumn: "in-review",
nextColumn: "todo",
leasedBy: "merger-reuse-handoff",
});
expect(typeof staleLeaseAudit[0].metadata?.leaseExpiresAt).toBe("string");
await store.releaseMergeQueueLease(task.id, "merger-reuse-handoff", { kind: "success" });
expect((await store.peekMergeQueue()).some((entry) => entry.taskId === task.id)).toBe(false);
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("already-landed branch auto-finalizes from the reused worktree path", async () => {
const { fixture, rootDir, store, task, branch } = await setupReuseHandoff({
taskId: "FN-5279-RI-ALREADY-LANDED",
fileName: "packages/engine/src/fn-5279-ri-already-landed.ts",
fileContent: "export const landed = true;\n",
commitMessage: "feat: add already-landed merge content",
skipWorktreeAdd: true,
worktreeOverride: null,
skipEnqueue: true,
});
// Fast-forward master to the branch tip (the "already landed" scenario)
// before recreating the worktree mapping and queue lease.
git(rootDir, `git merge --ff-only ${JSON.stringify(branch)}`);
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, task.id.toLowerCase());
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
await store.enqueueMergeQueue(task.id);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(result.mergeConfirmed).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
expect(auditTypes).toContain("merge:reuse-handoff-released");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("Layer 3 conflict resolution sessions run from the reused worktree", async () => {
const { fixture, rootDir, store, task, branch, worktreePath } = await setupReuseHandoff({
taskId: "FN-5279-RI-LAYER3",
fileName: "packages/engine/src/fn-5279-ri-layer3.ts",
fileContent: "export const value = 'branch';\n",
commitMessage: "feat: branch conflict content",
skipWorktreeAdd: true,
worktreeOverride: null,
skipEnqueue: true,
extraSettings: { mergeConflictStrategy: "smart-prefer-main" } as Partial<Settings>,
});
// Inject a conflicting commit on master at the same path, then create the
// worktree and queue entry now that the conflict geometry is in place.
await store.updateTask(task.id, {
prompt: "## File Scope\n- packages/engine/src/**\n",
} as any);
git(rootDir, "mkdir -p packages/engine/src");
git(rootDir, "sh -c \"printf \\\"export const value = 'main';\\n\\\" > packages/engine/src/fn-5279-ri-layer3.ts\"");
git(rootDir, "git add packages/engine/src/fn-5279-ri-layer3.ts");
git(rootDir, "git commit -m 'feat: main conflict content'");
const worktreeRoot = `${rootDir}-worktrees`;
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
await store.enqueueMergeQueue(task.id);
try {
await aiMergeTask(store, rootDir, task.id);
// FNXC:SqliteRemoval 2026-07-14: In backend mode, createResolvedAgentSession may route
// to mockRuntimeSingleton (bypassing createFnAgent). Assert via audit events instead:
// merge:reuse-handoff-released records worktreePath, proving the merge (including the
// Layer 3 conflict resolution attempt) ran from the reused worktree. The specific AI
// session cwd is an implementation detail of the resolved runtime, not assertable here.
const events = await auditEvents(store, { taskId: task.id });
const handoffReleased = events.find((e) => e.mutationType === "merge:reuse-handoff-released");
expect(handoffReleased, `audit types: ${JSON.stringify(events.map((e) => e.mutationType))}`).toBeDefined();
expect(handoffReleased?.metadata).toMatchObject({ worktreePath });
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("reacquires a fresh task worktree when reuse is requested without a task worktree", async () => {
const { fixture, rootDir, store, task, branch } = await setupReuseHandoff({
taskId: "FN-5353-RI-MISSING-WORKTREE",
fileName: "packages/engine/src/fn-5353-ri-missing-worktree.ts",
fileContent: "export const fallback = true;\n",
commitMessage: "feat: add fallback merge content",
skipWorktreeAdd: true,
worktreeOverride: null,
});
await store.updateTask(task.id, { worktree: null } as any);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const audits = (await auditEvents(store, { taskId: task.id }));
const auditTypes = audits.map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-fallback-new-worktree");
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
expect(auditTypes).not.toContain("merge:reuse-fallback-cwd-main");
expect(auditTypes).not.toContain("merge:reuse-fallback-cwd-integration-branch");
expect(auditTypes).not.toContain("merge:cwd-integration-fallback-removed");
const fallback = audits.find((event) => event.mutationType === "merge:reuse-fallback-new-worktree");
expect(fallback?.metadata).toMatchObject({
reason: "missing-task-worktree",
source: "fresh",
});
const freshAcquire = audits.find((event) => event.mutationType === "merge:reuse-worktree-fresh-acquire");
expect(freshAcquire?.metadata).toMatchObject({
taskId: task.id,
reason: "missing-task-worktree",
expectedBranch: branch,
});
const freshAcquired = audits.find((event) => event.mutationType === "merge:reuse-worktree-fresh-acquired");
expect(freshAcquired?.metadata).toMatchObject({
taskId: task.id,
reason: "missing-task-worktree",
branch,
priorWorktreePath: null,
});
const freshAcquiredWorktreePath = (freshAcquired?.metadata as Record<string, unknown> | undefined)?.worktreePath;
expect(typeof freshAcquiredWorktreePath).toBe("string");
expect(freshAcquiredWorktreePath).toBe((fallback?.metadata as Record<string, unknown> | undefined)?.worktreePath);
const orderedFreshAcquireIndex = audits.findIndex(
(event) =>
event.mutationType === "merge:reuse-worktree-fresh-acquire" &&
(event.metadata as Record<string, unknown> | undefined)?.reason === "missing-task-worktree",
);
const orderedFreshAcquiredIndex = audits.findIndex(
(event) =>
event.mutationType === "merge:reuse-worktree-fresh-acquired" &&
(event.metadata as Record<string, unknown> | undefined)?.reason === "missing-task-worktree",
);
const orderedFallbackIndex = audits.findIndex(
(event) =>
event.mutationType === "merge:reuse-fallback-new-worktree" &&
(event.metadata as Record<string, unknown> | undefined)?.reason === "missing-task-worktree",
);
expect(orderedFreshAcquireIndex).toBeGreaterThanOrEqual(0);
expect(orderedFreshAcquiredIndex).toBeGreaterThanOrEqual(0);
expect(orderedFallbackIndex).toBeGreaterThanOrEqual(0);
// getRunAuditEvents() returns newest-first (timestamp DESC, rowid DESC).
expect(orderedFallbackIndex).toBeLessThan(orderedFreshAcquiredIndex);
expect(orderedFreshAcquiredIndex).toBeLessThan(orderedFreshAcquireIndex);
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("cwd-main legacy alias is normalized to cwd-integration-branch and stays on the opt-in path with no reuse handoff events", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5279-RI-CWD-MAIN",
fileName: "packages/engine/src/fn-5279-ri-cwd-main.ts",
fileContent: "export const legacy = true;\n",
commitMessage: "feat: add cwd-main merge content",
skipWorktreeAdd: true,
worktreeOverride: null,
skipEnqueue: true,
extraSettings: { mergeIntegrationWorktree: "cwd-main" as const } as Partial<Settings>,
});
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes.filter((type) => type.startsWith("merge:reuse-handoff"))).toHaveLength(0);
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("worktrunk-enabled reuse mode still acquires reuse handoff", async () => {
const { fixture, rootDir, store, task } = await setupReuseHandoff({
taskId: "FN-5279-RI-WORKTRUNK",
fileName: "packages/engine/src/fn-5279-ri-worktrunk.ts",
fileContent: "export const deferred = true;\n",
commitMessage: "feat: add worktrunk merge content",
extraSettings: { worktrunk: { enabled: true } as any } as Partial<Settings>,
});
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-deferred-to-worktrunk");
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
} finally {
await fixture.cleanup();
}
}, 60_000);
it.skipIf(!hasGit || !hasPg)("autoMerge off remains inert and emits no reuse handoff events", async () => {
// This case never calls aiMergeTask — it just verifies that turning autoMerge
// off keeps the task in `in-review` with no reuse-handoff fanout. We use a
// minimal manual setup because the standard helper writes content commits
// and enqueues the merge queue, both of which would be misleading here.
const fixture = await makeReliabilityFixture({
taskId: "FN-5279-RI-AUTO-OFF",
settings: {
autoMerge: false,
baseBranch: "master",
mergeIntegrationWorktree: "reuse-task-worktree",
} as Partial<Settings>,
});
try {
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
git(rootDir, "git branch -m main master");
await fixture.createBranch(branch);
await fixture.checkout("master");
await store.updateTask(task.id, { baseBranch: "master", worktree: worktreePath, branch } as any);
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
const latest = await store.getTask(task.id);
expect(latest?.column).toBe("in-review");
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes.filter((type) => type.startsWith("merge:reuse-handoff"))).toHaveLength(0);
} finally {
await fixture.cleanup();
}
}, 60_000);
// FN-5345/FN-5377 regression backstop.
//
// A verification-only task that committed `--allow-empty` produced a branch
// with own-commit-count >= 1 but zero net tree change vs merge-base. Combined
// with drifted worktree<->branch mapping, the reuse-handoff gate would refuse
// with `registered-branch-mismatch` and the task would escalate to
// `merge-deadlock-detected: verified content not on main` after FN-4999
// completion-handoff-limbo recovery exhausts. The early empty-own-diff
// fast-path must finalize this BEFORE any reuse-handoff acquisition runs.
it.skipIf(!hasGit || !hasPg)(
"FN-5345: empty-own-diff branch auto-finalizes via early fast-path without acquiring reuse handoff",
async () => {
const { fixture, rootDir, store, task, worktreeRoot } = await setupReuseHandoff({
taskId: "FN-5279-RI-EMPTY-OWN-DIFF",
emptyOwnDiff: true,
skipWorktreeAdd: true,
worktreeOverride: join(`${"placeholder"}`, "drifted-missing-path"), // overridden below
});
// Point the task at a drifted/missing worktree path so the reuse-handoff
// gate would normally refuse with FN-5083 branch-registration drift.
await store.updateTask(task.id, { worktree: join(worktreeRoot, "drifted-missing-path") } as any);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
expect(result.mergeConfirmed).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const audits = (await auditEvents(store, { taskId: task.id }));
const auditTypes = audits.map((event) => event.mutationType);
// Early fast-path must short-circuit BEFORE any reuse-handoff event.
expect(auditTypes).not.toContain("merge:reuse-handoff-acquired");
expect(auditTypes).not.toContain("merge:reuse-handoff-refused");
expect(auditTypes).not.toContain("merge:reuse-fallback-new-worktree");
const finalize = audits.find(
(event) =>
event.mutationType === "task:auto-recover-finalize-already-on-main"
&& (event.metadata as any)?.reason === "empty-own-diff-early-fast-path",
);
expect(finalize).toBeDefined();
expect((finalize?.metadata as any)?.aheadCount).toBeGreaterThanOrEqual(1);
} finally {
await fixture.cleanup();
}
},
30_000,
);
// FN-5345/FN-5377 backstop variant: reproduce the actual production wedge
// geometry where `fusion/<id>` is registered to TWO worktrees simultaneously
// (e.g. faint-creek + hazy-quail in the FN-5345 incident). The early
// fast-path runs against projectRootDir and is immune to the worktree drift.
it.skipIf(!hasGit || !hasPg)(
"FN-5345: empty-own-diff fast-path fires even when branch is registered to two worktrees",
async () => {
const { fixture, rootDir, store, task, branch, worktreeRoot } = await setupReuseHandoff({
taskId: "FN-5279-RI-DOUBLE-REG",
emptyOwnDiff: true,
skipWorktreeAdd: true,
worktreeOverride: null,
});
const pathA = join(worktreeRoot, `${task.id.toLowerCase()}-a`);
const pathB = join(worktreeRoot, `${task.id.toLowerCase()}-b`);
// Register branch at pathA then force-register at pathB — the FN-5345
// two-worktrees-one-branch state.
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(pathA)} ${JSON.stringify(branch)}`);
git(rootDir, `git worktree add -f ${JSON.stringify(pathB)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: pathA, branch } as any);
await store.enqueueMergeQueue(task.id);
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
expect(result.mergeConfirmed).toBe(true);
expect((await store.getTask(task.id))?.column).toBe("done");
const auditTypes = (await auditEvents(store, { taskId: task.id })).map((event) => event.mutationType);
expect(auditTypes).not.toContain("merge:reuse-handoff-acquired");
expect(auditTypes).not.toContain("merge:reuse-handoff-refused");
expect(auditTypes).toContain("task:auto-recover-finalize-already-on-main");
} finally {
await fixture.cleanup();
}
},
30_000,
);
// FN-5345/FN-5377 cleanup-safety backstop: the fast-path's worktree removal
// MUST preserve a worktree that has uncommitted tracked changes.
it.skipIf(!hasGit || !hasPg)(
"FN-5345: empty-own-diff fast-path preserves worktrees with uncommitted tracked changes",
async () => {
const { fixture, rootDir, store, task, worktreePath } = await setupReuseHandoff({
taskId: "FN-5279-RI-DIRTY-PRESERVE",
emptyOwnDiff: true,
});
// README.md is created by the fixture as a tracked file. Modify it to
// produce tracked-dirty status inside the worktree.
await writeFile(join(worktreePath, "README.md"), "agent scratch: uncommitted edits\n");
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
// Critical: the worktree must NOT be removed because it has tracked
// uncommitted changes. result.worktreeRemoved reflects that.
expect(result.worktreeRemoved).toBe(false);
expect(existsSync(worktreePath)).toBe(true);
expect(existsSync(join(worktreePath, "README.md"))).toBe(true);
} finally {
await fixture.cleanup();
}
},
30_000,
);
// FN-5345/FN-5377 cleanup-noise backstop: untracked junk (.DS_Store, swap
// files) must NOT block fast-path cleanup. Only tracked dirt does.
it.skipIf(!hasGit || !hasPg)(
"FN-5345: empty-own-diff fast-path cleans up worktrees with only untracked noise",
async () => {
const { fixture, rootDir, store, task, worktreePath } = await setupReuseHandoff({
taskId: "FN-5279-RI-UNTRACKED-OK",
emptyOwnDiff: true,
});
await writeFile(join(worktreePath, ".DS_Store"), "binary junk\n");
await writeFile(join(worktreePath, "editor.swp"), "swap file\n");
try {
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(result.noOp).toBe(true);
// Untracked-only is treated as clean — cleanup proceeds.
expect(result.worktreeRemoved).toBe(true);
expect(existsSync(worktreePath)).toBe(false);
} finally {
await fixture.cleanup();
}
},
30_000,
);
});

View File

@@ -404,13 +404,6 @@ export default defineConfig({
"src/__tests__/reliability-interactions/branch-group-single-pr-e2e.slow.test.ts",
"src/__tests__/reliability-interactions/shared-branch-group-lifecycle.slow.test.ts",
// SQLite-path (delete-sqlite-runtime-final PHASE A): uses inMemoryDb via _helpers.ts.
/*
FNXC:EngineTests 2026-07-18-15:55:
Full-suite engine-slow (run 29663725381): FN-5363 queue-head pollution handoff left a
leased merge-queue row after successful merge under load without product-bug evidence.
Quarantine on sight — mirrored in scripts/lib/test-quarantine.json.
*/
"src/__tests__/reliability-interactions/merge-reuse-task-worktree.slow.test.ts",
],
minWorkers: 1,
maxWorkers: 1,

View File

@@ -1,5 +1,16 @@
import { afterEach, describe, expect, it, vi } from "vitest";
/*
FNXC:GrokRuntimeTests 2026-07-19-18:10:
The lifecycle registry imports `redactSecrets` only for stderr-capture behavior,
but this suite exercises listener ownership and child cleanup. Stub that unrelated
core dependency so reset/import coverage cannot spend a shard's transform budget
on the full @fusion/core graph.
*/
vi.mock("@fusion/core", () => ({
redactSecrets: (value: string) => value,
}));
const EVENTS = ["exit", "beforeExit", "SIGTERM", "SIGINT"] as const;
function listenerCounts(): Record<(typeof EVENTS)[number], number> {
@@ -15,13 +26,13 @@ describe("Grok plugin process lifecycle", () => {
});
/*
FNXC:GrokRuntimeTests 2026-07-18-07:40:
FNXC:GrokRuntimeTests 2026-07-19-18:10:
Prove the process-manager Symbol.for exit-hook guard by re-importing the
registry module (lifecycle owner), not the full plugin graph. Full-suite
shard transform of @fusion/core via process-manager can still exceed the
default 5s budget on cold workers — give the bound stress test 15s.
registry module (lifecycle owner), not the full plugin graph. The core
dependency is mocked above because stderr redaction is outside this seam;
this keeps repeated evaluation a bounded unit test under shard pressure.
*/
it("keeps its process cleanup owner bounded across repeated module evaluation", { timeout: 15_000 }, async () => {
it("keeps its process cleanup owner bounded across repeated module evaluation", async () => {
const baseline = listenerCounts();
const warnings: Error[] = [];
const onWarning = (warning: Error) => warnings.push(warning);

View File

@@ -13,8 +13,6 @@ export default defineConfig({
},
test: {
include: ["src/**/*.test.ts"],
// FNXC:TestQuarantine 2026-07-19-04:30: process-lifecycle bound stress timed out on full-suite shard 4 (run 29671930241); ledger entry in scripts/lib/test-quarantine.json.
exclude: ["src/__tests__/process-lifecycle.test.ts"],
environment: "node",
setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))],
globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))],

View File

@@ -1,36 +1,126 @@
/*
FNXC:QualityPostgresDurability 2026-07-16-10:30:
This behavioral test applies the plugin's declarative PostgreSQL schema itself:
core's harness supplies only baseline tables. It proves Quality CRUD uses the
project-bound AsyncDataLayer rather than the SQLite route that failed Task QA.
FNXC:QualityPostgresDurability 2026-07-20-02:10:
FN-8394 replaces the embedded-PostgreSQL quarantine with a query-aware bounded
AsyncDataLayer fake. The fake rejects missing project predicates and only applies
lifecycle state changes for the SQL contracts the production store must issue,
so process pressure cannot hide a lost isolation or terminal-status guarantee.
*/
import { expect, it } from "vitest";
import { sql } from "drizzle-orm";
import type { AsyncDataLayer } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe } from "../../../../packages/core/src/__test-utils__/pg-test-harness.js";
import { qualityPostgresSchema } from "../quality-schema.js";
import { AsyncQualityStore } from "../store/async-quality-store.js";
function projectLayer(layer: AsyncDataLayer, projectId: string): AsyncDataLayer { return { ...layer, projectId }; }
const createdRun = {
id: "qrun_1",
project_id: "quality-a",
task_id: null,
plan_id: null,
source: "hub",
preset_id: null,
command: "pnpm verify:fast",
cwd: "/repo",
cwd_kind: "project-root",
status: "queued",
exit_code: null,
error_message: null,
timeout_ms: 1_000,
started_at: null,
finished_at: null,
duration_ms: null,
stdout: "",
stderr: "",
triggered_by: "test",
created_at: "2026-07-19T00:00:00.000Z",
updated_at: "2026-07-19T00:00:00.000Z",
};
pgDescribe("AsyncQualityStore (PostgreSQL / backend mode)", () => {
it("persists Quality lifecycle data and isolates projects", async () => {
const h = await createTaskStoreForTest({ prefix: "fusion_quality_async" });
try {
for (const statement of qualityPostgresSchema.statements) await h.adminDb.execute(sql.raw(statement));
const projectA = new AsyncQualityStore(projectLayer(h.layer, "quality-a"));
const projectB = new AsyncQualityStore(projectLayer(h.layer, "quality-b"));
const created = await projectA.createRun({ projectId: "quality-a", source: "hub", command: "pnpm verify:fast", cwd: "/repo", cwdKind: "project-root", timeoutMs: 1_000, triggeredBy: "test" });
const updated = await projectA.updateRun("quality-a", created.id, { status: "passed", exitCode: 0, finishedAt: new Date().toISOString(), durationMs: 1 });
expect(updated).toMatchObject({ id: created.id, status: "passed", exitCode: 0 });
expect(await projectA.listRuns("quality-a")).toHaveLength(1);
expect(await projectB.getRun("quality-b", created.id)).toBeNull();
function sqlText(query: unknown): string {
const chunks = (query as { queryChunks?: unknown[] }).queryChunks ?? [];
return chunks
.map((chunk) => {
const value = (chunk as { value?: unknown } | null)?.value;
return Array.isArray(value) ? value.join("") : String(value ?? "");
})
.join(" ")
.replace(/\s+/g, " ");
}
const createdPlan = await projectA.createPlan({ projectId: "quality-a", name: "Fast gate", steps: ["verify-fast"] });
expect((await projectA.getPlan("quality-a", createdPlan.id))?.steps).toEqual(["verify-fast"]);
await projectA.saveSuggestedCases({ projectId: "quality-a", taskId: "FN-8103", cases: [{ id: "case", text: "uses async data layer", done: false, source: "heuristic" }], generatedAt: new Date().toISOString(), method: "heuristic" });
expect((await projectA.getSuggestedCases("quality-a", "FN-8103"))?.cases).toHaveLength(1);
expect(await projectB.getSuggestedCases("quality-b", "FN-8103")).toBeNull();
} finally { await h.teardown(); }
});
/** A bounded data-layer seam that enforces SQL predicates instead of canned call order. */
function makeLayer(): { layer: AsyncDataLayer; statements: string[] } {
const statements: string[] = [];
let run = { ...createdRun };
const plan = {
id: "qplan_1", project_id: "quality-a", name: "Fast gate", status: "active",
steps_json: '["verify-fast"]', created_at: "2026-07-19T00:00:00.000Z", updated_at: "2026-07-19T00:00:00.000Z",
};
const cases = {
project_id: "quality-a", task_id: "FN-8103",
cases_json: '[{"id":"case","text":"uses async data layer","done":false,"source":"heuristic"}]',
generated_at: "2026-07-19T00:00:00.000Z", method: "heuristic",
};
const db = {
execute: async (query: unknown) => {
const statement = sqlText(query);
statements.push(statement);
const isProjectTable = statement.includes("project.quality_");
if (isProjectTable && statement.includes("SELECT") && !statement.includes("project_id")) {
throw new Error(`quality query omitted project predicate: ${statement}`);
}
if (statement.includes("INSERT INTO project.quality_test_runs")) return [];
if (statement.includes("UPDATE project.quality_test_runs")) {
/*
FNXC:QualityPostgresDurability 2026-07-20-19:05:
FN-8394's in-memory lifecycle seam must reject an unscoped UPDATE, not
merely observe a project predicate on a later SELECT. State may change
only after the production write proves both isolation and sticky-terminal
status semantics.
*/
if (!statement.includes("WHERE project_id =")) {
throw new Error("quality run update omitted project predicate");
}
if (!statement.includes("WHEN status IN ('cancelled', 'passed', 'failed', 'timed_out', 'error')")) {
throw new Error("quality run update omitted sticky terminal-status contract");
}
run = { ...run, status: "passed", exit_code: 0, finished_at: "2026-07-19T00:00:01.000Z", duration_ms: 1 };
return [];
}
if (statement.includes("FROM project.quality_test_runs")) return [run];
if (statement.includes("INSERT INTO project.quality_test_plans")) return [];
if (statement.includes("FROM project.quality_test_plans")) return [plan];
if (statement.includes("INSERT INTO project.quality_suggested_cases")) return [];
if (statement.includes("FROM project.quality_suggested_cases")) return [cases];
throw new Error(`unexpected quality query: ${statement}`);
},
};
return {
statements,
layer: {
projectId: "quality-a",
db: db as AsyncDataLayer["db"],
transactionImmediate: async (fn) => fn(db as never),
} as AsyncDataLayer,
};
}
it("persists a project-scoped async Quality lifecycle through SQL predicate contracts", async () => {
const { layer, statements } = makeLayer();
const store = new AsyncQualityStore(layer);
const created = await store.createRun({ projectId: "quality-a", source: "hub", command: "pnpm verify:fast", cwd: "/repo", cwdKind: "project-root", timeoutMs: 1_000, triggeredBy: "test" });
const updated = await store.updateRun("quality-a", created.id, { status: "passed", exitCode: 0, finishedAt: "2026-07-19T00:00:01.000Z", durationMs: 1 });
expect(updated).toMatchObject({ id: created.id, status: "passed", exitCode: 0 });
expect(await store.listRuns("quality-a")).toEqual([expect.objectContaining({ id: created.id, status: "passed" })]);
await expect(store.getRun("quality-b", created.id)).rejects.toMatchObject({ message: /project mismatch/, statusCode: 403 });
const createdPlan = await store.createPlan({ projectId: "quality-a", name: "Fast gate", steps: ["verify-fast"] });
expect(createdPlan.steps).toEqual(["verify-fast"]);
await store.saveSuggestedCases({ projectId: "quality-a", taskId: "FN-8103", cases: [{ id: "case", text: "uses async data layer", done: false, source: "heuristic" }], generatedAt: "2026-07-19T00:00:00.000Z", method: "heuristic" });
expect(await store.getSuggestedCases("quality-a", "FN-8103")).toMatchObject({ cases: [expect.objectContaining({ id: "case" })] });
expect(statements).toEqual(expect.arrayContaining([
expect.stringContaining("INSERT INTO project.quality_test_runs"),
expect.stringMatching(/UPDATE project\.quality_test_runs SET status = CASE.*WHERE project_id =/),
expect.stringContaining("INSERT INTO project.quality_test_plans"),
expect.stringContaining("INSERT INTO project.quality_suggested_cases"),
]));
});

View File

@@ -39,8 +39,7 @@ export default defineConfig({
},
test: {
include: ["src/**/*.test.{ts,tsx}"],
// Quarantine ledger: scripts/lib/test-quarantine.json — async-quality-store.pg.test.ts (2026-07-18).
exclude: ["**/node_modules/**", "**/dist/**", "src/__tests__/async-quality-store.pg.test.ts"],
exclude: ["**/node_modules/**", "**/dist/**"],
environment: "node",
setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))],
globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))],

View File

@@ -1,45 +1,4 @@
{
"$comment": "Flaky-test quarantine ledger (deletion ratchet \u2014 see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date \u2014 the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config is the enforcement.",
"entries": [
{
"file": "packages/cli/src/commands/__tests__/mcp-lock-retry.test.ts",
"reason": "Full-suite shard 4 after FN-8271 restore (runs 29648812375 / 29648952207): 5s timeouts under package-lane shard load without product-bug evidence; getSettings/close races and timer recovery still load-sensitive. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/__tests__/task-lock-retry.test.ts",
"reason": "Full-suite shard 4 after FN-8271 restore (runs 29648812375 / 29648952207): 5s timeouts / store.getTask not a function under package-lane shard load without product-bug evidence; fake-timer board-write mock recovery remains load-sensitive. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "plugins/fusion-plugin-quality/src/__tests__/async-quality-store.pg.test.ts",
"reason": "Full-suite shard 3 (run 29657633544): 5s timeout + leftover psql child under package-lane load without product-bug evidence; embedded-PG lifecycle remains load-sensitive. Quarantine on sight per AGENTS.md. Mirrored in plugins/fusion-plugin-quality/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/dashboard/app/components/__tests__/TaskDetailModal.tab-persistence.test.tsx",
"reason": "Full-suite shard 2 (run 29660321240): Terminal-guard tab settle race under dashboard-app-quality-backfill load without product-bug evidence; passes focused/local thrice. Quarantine on sight per AGENTS.md. Mirrored in packages/dashboard/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/dashboard/src/__tests__/dev-server-process.test.ts",
"reason": "Full-suite shard 4 (run 29661202279): clears fallback probe timer when URL is detected from logs \u2014 detectedEvents length 0 under dashboard-api-quality-backfill load; known timer/stdout race (prior FN-6722 quarantine, FN-6860 rescue). Re-flaked without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/dashboard/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/bundle-output.test.ts",
"reason": "Full-suite shard 4 (run 29662476909): pnpm build:package / desktop vite build ENOENT on vendor-reactflow CSS under concurrent package-lane load without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/engine/src/__tests__/reliability-interactions/merge-reuse-task-worktree.slow.test.ts",
"reason": "Full-suite engine-slow (run 29663725381): FN-5363 queue-head pollution handoff left leased merge-queue row after merge under load without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/engine/vitest.config.ts engine-slow exclude.",
"quarantinedAt": "2026-07-18"
},
{
"file": "plugins/fusion-plugin-grok-runtime/src/__tests__/process-lifecycle.test.ts",
"reason": "Full-suite shard 4 (run 29671930241): process cleanup bound stress test timed out at 15s under package-lane load without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in plugins/fusion-plugin-grok-runtime/vitest.config.ts.",
"quarantinedAt": "2026-07-19"
}
]
"entries": []
}