feat(FN-4948): complete Steps 7-8 — finalize verification and release artifacts
Fusion-Task-Id: FN-4948 Fusion-Task-Lineage: dc622643-4c3e-4217-9219-a6a6e5424427
This commit is contained in:
committed by
gsxdsm
parent
34b03120aa
commit
c161e626e2
5
.changeset/fn-4948-precommit-branch-identity-guard.md
Normal file
5
.changeset/fn-4948-precommit-branch-identity-guard.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Prevent cross-branch commit contamination at the source: every task worktree now installs a pre-commit hook that refuses commits when HEAD does not match the worktree's owning task branch (with an allowlist for parallel-step branches). As defense-in-depth, contamination auto-recovery now drops `obviously-misrouted` foreign commits whose task-id attribution and changed-path namespace unambiguously belong to another task (initial heuristic: `.changeset/fn-<that-id>-*`), instead of escalating them to human adjudication.
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
import {
|
||||
autoRecoverCrossContamination,
|
||||
classifyForeignCommits,
|
||||
classifyMisroutedForeignCommit,
|
||||
} from "../../branch-conflicts.js";
|
||||
|
||||
function git(dir: string, cmd: string): string {
|
||||
return execSync(cmd, { cwd: dir, stdio: "pipe" }).toString().trim();
|
||||
}
|
||||
|
||||
describe("reliability interaction: misrouted foreign commit recovery (real git)", () => {
|
||||
it("drops misrouted changeset-only foreign commit", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "fn-4948-ri-misrouted-"));
|
||||
try {
|
||||
git(dir, "git init -b main");
|
||||
git(dir, 'git config user.email "test@example.com"');
|
||||
git(dir, 'git config user.name "Test"');
|
||||
writeFileSync(join(dir, "README.md"), "init\n");
|
||||
git(dir, "git add README.md && git commit -m 'init'");
|
||||
const baseSha = git(dir, "git rev-parse HEAD");
|
||||
|
||||
git(dir, "git checkout -b fusion/fn-1000");
|
||||
writeFileSync(join(dir, "owned.txt"), "owned\n");
|
||||
git(dir, "git add owned.txt && git commit -m 'feat(FN-1000): own change' -m 'Fusion-Task-Id: FN-1000'");
|
||||
|
||||
mkdirSync(join(dir, ".changeset"), { recursive: true });
|
||||
writeFileSync(join(dir, ".changeset", "fn-2000-fix.md"), "patch\n");
|
||||
git(dir, "git add .changeset/fn-2000-fix.md && git commit -m 'fix(FN-2000): foreign changeset only' -m 'Fusion-Task-Id: FN-2000'");
|
||||
const foreignSha = git(dir, "git rev-parse HEAD");
|
||||
|
||||
const classified = await classifyForeignCommits({
|
||||
repoDir: dir,
|
||||
branchName: "fusion/fn-1000",
|
||||
baseSha,
|
||||
foreignCommits: [{ sha: foreignSha, subject: "fix(FN-2000): foreign changeset only", foreignTaskId: "FN-2000" }],
|
||||
});
|
||||
expect(classified.unique).toHaveLength(1);
|
||||
|
||||
const misrouted = await classifyMisroutedForeignCommit({
|
||||
repoDir: dir,
|
||||
sha: foreignSha,
|
||||
commitSubject: "fix(FN-2000): foreign changeset only",
|
||||
commitBody: "Fusion-Task-Id: FN-2000",
|
||||
currentTaskId: "FN-1000",
|
||||
});
|
||||
expect(misrouted.misrouted).toBe(true);
|
||||
|
||||
const recovered = await autoRecoverCrossContamination({
|
||||
repoDir: dir,
|
||||
branchName: "fusion/fn-1000",
|
||||
baseSha,
|
||||
taskId: "FN-1000",
|
||||
shasToDrop: [foreignSha],
|
||||
});
|
||||
expect(recovered.droppedShas).toContain(foreignSha);
|
||||
|
||||
const history = git(dir, "git log --format=%s fusion/fn-1000");
|
||||
expect(history).not.toContain("foreign changeset only");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not classify mixed-path foreign commit as misrouted", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "fn-4948-ri-shared-"));
|
||||
try {
|
||||
git(dir, "git init -b main");
|
||||
git(dir, 'git config user.email "test@example.com"');
|
||||
git(dir, 'git config user.name "Test"');
|
||||
writeFileSync(join(dir, "README.md"), "init\n");
|
||||
git(dir, "git add README.md && git commit -m 'init'");
|
||||
const baseSha = git(dir, "git rev-parse HEAD");
|
||||
|
||||
git(dir, "git checkout -b fusion/fn-1000");
|
||||
mkdirSync(join(dir, ".changeset"), { recursive: true });
|
||||
mkdirSync(join(dir, "packages", "engine", "src"), { recursive: true });
|
||||
writeFileSync(join(dir, ".changeset", "fn-2000-fix.md"), "patch\n");
|
||||
writeFileSync(join(dir, "packages", "engine", "src", "executor.ts"), "x\n");
|
||||
git(dir, "git add .changeset/fn-2000-fix.md packages/engine/src/executor.ts");
|
||||
git(dir, "git commit -m 'fix(FN-2000): mixed' -m 'Fusion-Task-Id: FN-2000'");
|
||||
const foreignSha = git(dir, "git rev-parse HEAD");
|
||||
|
||||
const misrouted = await classifyMisroutedForeignCommit({
|
||||
repoDir: dir,
|
||||
sha: foreignSha,
|
||||
commitSubject: "fix(FN-2000): mixed",
|
||||
commitBody: "Fusion-Task-Id: FN-2000",
|
||||
currentTaskId: "FN-1000",
|
||||
});
|
||||
expect(misrouted.misrouted).toBe(false);
|
||||
|
||||
const shouldAutoRecover = false;
|
||||
expect(shouldAutoRecover).toBe(false);
|
||||
expect(git(dir, `git rev-parse fusion/fn-1000`)).not.toBe(baseSha);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8428,6 +8428,19 @@ Backward compat fallback: if JSON is unavailable, you may still begin output wit
|
||||
// and confuse every tool that walks git state.
|
||||
await this.assertWorktreePathNotNested(path, taskId);
|
||||
|
||||
const installGuardOrCleanup = async () => {
|
||||
try {
|
||||
await installTaskWorktreeIdentityGuard({ worktreePath: path, taskId });
|
||||
} catch (error) {
|
||||
try {
|
||||
await execAsync(`rm -rf "${path}"`, { cwd: this.rootDir });
|
||||
} catch {
|
||||
executorLog.log(`Warning: failed to remove worktree after identity-guard install failure: ${path}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// If directory exists but is not a registered worktree, remove it first
|
||||
if (existsSync(path)) {
|
||||
const isRegistered = await this.isRegisteredWorktree(path);
|
||||
@@ -8484,19 +8497,6 @@ Backward compat fallback: if JSON is unavailable, you may still begin output wit
|
||||
}
|
||||
};
|
||||
|
||||
const installGuardOrCleanup = async () => {
|
||||
try {
|
||||
await installTaskWorktreeIdentityGuard({ worktreePath: path, taskId });
|
||||
} catch (error) {
|
||||
try {
|
||||
await execAsync(`rm -rf "${path}"`, { cwd: this.rootDir });
|
||||
} catch {
|
||||
executorLog.log(`Warning: failed to remove worktree after identity-guard install failure: ${path}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
let staleLockRecoveryAttempted = false;
|
||||
try {
|
||||
await createWithBranch(branch);
|
||||
|
||||
Reference in New Issue
Block a user