FN-7486: fix no-diff merge recovery ownership checks

Fix no-op task branch recovery by recognizing canonical branches with no unique diff before rejecting inherited foreign trailers.

- Add no-diff ownership classification for already-merged detection when canonical task branches inherit another task's landed commit.
- Teach self-healing and branch-misbound recovery to ignore foreign branch-tip trailers only for branches proven to have no unique task diff.
- Skip synthetic verify:fast typechecks for JavaScript alias packages without tsconfig files and cover the behavior with tests.
- Add regression coverage and a patch changeset for the recovery fix.

Files changed:
 .changeset/fn-7486-merge-recovery-noop-ownership.md       |  7 ++
 .../already-merged-detector.real-git.test.ts       | 68 +++++++++++++++++
 .../self-healing-already-merged.real-git.test.ts   | 89 ++++++++++++++++++++--
 packages/engine/src/already-merged-detector.ts     | 88 +++++++++++++++------
 packages/engine/src/self-healing.ts                | 61 +++++++++++++--
 scripts/__tests__/verify-fast.test.mjs             | 16 +++-
 scripts/verify-fast.mjs                            | 15 +++-
 7 files changed, 303 insertions(+), 41 deletions(-)

Fusion-Task-Id: FN-7486

Fusion-Task-Lineage: 4185c6ed-9731-4ffc-b033-34bf0c3a83ad

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-03 22:04:45 -07:00
parent b42ba9f515
commit 20184acdfd
7 changed files with 303 additions and 41 deletions

View File

@@ -110,7 +110,7 @@ test("buildVerifyPlan: typecheck for all eligible, then builds, then boot smoke
test("buildVerifyPlan: a package without a build script gets a typecheck step but no build step", () => {
const packageMeta = new Map([
["@fusion/engine", { hasTypecheck: true, hasBuild: true }],
["@fusion/test-only", { hasTypecheck: false, hasBuild: false }],
["@fusion/test-only", { hasTypecheck: false, hasTsconfig: true, hasBuild: false }],
]);
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/test-only"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
@@ -126,6 +126,20 @@ test("buildVerifyPlan: a package without a build script gets a typecheck step bu
assert.deepEqual(tc.args, ["--filter", "@fusion/test-only", "exec", "tsc", "--noEmit", "-p", "."]);
});
test("buildVerifyPlan: skips synthetic typecheck for JavaScript alias packages with no tsconfig", () => {
const packageMeta = new Map([
["runfusion.ai", { hasTypecheck: false, hasTsconfig: false, hasBuild: false }],
["@runfusion/fusion", { hasTypecheck: true, hasTsconfig: true, hasBuild: true }],
]);
const plan = buildVerifyPlan({ packages: ["runfusion.ai", "@runfusion/fusion"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@runfusion/fusion",
"build:@runfusion/fusion",
"boot-smoke",
]);
});
test("buildVerifyPlan: desktop/mobile are excluded from scoped steps but boot smoke still runs", () => {
const packageMeta = new Map([
["@fusion/engine", { hasTypecheck: true, hasBuild: true }],

View File

@@ -32,7 +32,7 @@ of blocking forever, and we exit nonzero on the first failing step.
*/
import path from "node:path";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
@@ -135,7 +135,7 @@ export function buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin = proces
*
* @param {object} opts
* @param {string[]} [opts.packages] affected package names
* @param {Map<string, { dir?: string, hasTypecheck?: boolean, hasBuild?: boolean }>} [opts.packageMeta]
* @param {Map<string, { dir?: string, hasTypecheck?: boolean, hasTsconfig?: boolean, hasBuild?: boolean }>} [opts.packageMeta]
* @param {string} opts.bootSmokeScriptPath
* @param {string} [opts.artifactBootstrapScriptPath]
* @param {string} [opts.nodeBin]
@@ -147,7 +147,13 @@ export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSm
const steps = [buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin)];
for (const pkg of eligiblePackages) {
steps.push(buildTypecheckStep(pkg, packageMeta.get(pkg) ?? {}));
const meta = packageMeta.get(pkg) ?? {};
/*
FNXC:TestInfrastructure 2026-07-03-21:54:
Workspace alias packages such as `runfusion.ai` are publishable JavaScript shims with no tsconfig. verify:fast should not synthesize a `tsc -p .` fallback for those packages; their executable behavior is covered by the required CLI build and boot smoke.
*/
if (meta.hasTypecheck === false && meta.hasTsconfig === false) continue;
steps.push(buildTypecheckStep(pkg, meta));
}
const builtPackages = new Set();
@@ -181,7 +187,7 @@ export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSm
* @param {string[]} packages
* @param {Map<string, string>} packageDirByName pkg name → repo-relative dir
* @param {string} [root]
* @returns {Map<string, { dir: string, hasTypecheck: boolean, hasBuild: boolean }>}
* @returns {Map<string, { dir: string, hasTypecheck: boolean, hasTsconfig: boolean, hasBuild: boolean }>}
*/
export function readPackageMeta(packages, packageDirByName, root = repoRoot) {
const meta = new Map();
@@ -199,6 +205,7 @@ export function readPackageMeta(packages, packageDirByName, root = repoRoot) {
meta.set(pkg, {
dir: dir ?? null,
hasTypecheck: typeof scripts.typecheck === "string",
hasTsconfig: dir ? existsSync(path.join(root, dir, "tsconfig.json")) : true,
hasBuild: typeof scripts.build === "string",
});
}