feat(FN-4461): add transition evidence backfill script and tests
Adds a backfill script and its tests for FN-4441 transition evidence, covering two new files totaling 204 lines. Fusion-Task-Id: FN-4461
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { tsImport } from "tsx/esm/api";
|
||||
import { composeTransitionEvidence } from "../backfill-fn-4441-transition-evidence.mjs";
|
||||
|
||||
async function loadTaskStore() {
|
||||
const moduleUrl = new globalThis.URL("../../packages/core/src/store.ts", import.meta.url).href;
|
||||
const mod = await tsImport(moduleUrl, import.meta.url);
|
||||
return mod.TaskStore;
|
||||
}
|
||||
|
||||
test("composeTransitionEvidence includes required evidence fields", () => {
|
||||
const content = composeTransitionEvidence({
|
||||
mergeRetries: undefined,
|
||||
branchTip: {
|
||||
short: "f57f70165",
|
||||
full: "f57f70165ac154f1ca79e68510990bc61916660e",
|
||||
},
|
||||
mainSHAs: [
|
||||
{
|
||||
sha: "00c8739d5",
|
||||
title: "feat(FN-4441): complete Step 1 — add node_modules cache layer",
|
||||
ancestorStatus: 0,
|
||||
ancestorOutput: "ancestor",
|
||||
},
|
||||
{
|
||||
sha: "41070475b",
|
||||
title: "feat(FN-4441): complete Step 2 — verify cache-hit integrity",
|
||||
ancestorStatus: 0,
|
||||
ancestorOutput: "ancestor",
|
||||
},
|
||||
],
|
||||
lastMergeError: "The previous cherry-pick is now empty",
|
||||
resolutionTaskId: "FN-4450",
|
||||
postState: {
|
||||
column: "done",
|
||||
branchDisposition: "deleted",
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(content, /f57f70165/);
|
||||
assert.match(content, /00c8739d5/);
|
||||
assert.match(content, /41070475b/);
|
||||
assert.match(content, /FN-4450/);
|
||||
assert.match(content, /mergeRetries:/i);
|
||||
assert.match(content, /last merge error:/i);
|
||||
});
|
||||
|
||||
test("TaskStore upsertTaskDocument increments revision and round-trips latest content", async () => {
|
||||
const TaskStore = await loadTaskStore();
|
||||
const store = new TaskStore(process.cwd(), undefined, { inMemoryDb: true });
|
||||
|
||||
try {
|
||||
await store.createTaskWithReservedId({ description: "seed" }, { taskId: "FN-4441" });
|
||||
|
||||
const first = await store.upsertTaskDocument("FN-4441", {
|
||||
key: "transition-evidence",
|
||||
content: "first",
|
||||
author: "test",
|
||||
});
|
||||
const second = await store.upsertTaskDocument("FN-4441", {
|
||||
key: "transition-evidence",
|
||||
content: "second",
|
||||
author: "test",
|
||||
});
|
||||
|
||||
assert.equal(first.revision, 1);
|
||||
assert.equal(second.revision, 2);
|
||||
|
||||
const doc = await store.getTaskDocument("FN-4441", "transition-evidence");
|
||||
assert.equal(doc?.revision, 2);
|
||||
assert.equal(doc?.content, "second");
|
||||
} finally {
|
||||
await store.close();
|
||||
}
|
||||
});
|
||||
128
scripts/backfill-fn-4441-transition-evidence.mjs
Normal file
128
scripts/backfill-fn-4441-transition-evidence.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tsImport } from "tsx/esm/api";
|
||||
|
||||
export function composeTransitionEvidence({
|
||||
mergeRetries,
|
||||
branchTip,
|
||||
mainSHAs,
|
||||
lastMergeError,
|
||||
resolutionTaskId,
|
||||
postState,
|
||||
}) {
|
||||
const mergeRetriesLine =
|
||||
typeof mergeRetries === "number" ? String(mergeRetries) : "not captured at the time";
|
||||
|
||||
const mainLandingLines = mainSHAs
|
||||
.map(
|
||||
({ sha, title, ancestorStatus, ancestorOutput }) =>
|
||||
`- \`${sha}\` — ${title}\n - verify: \`git merge-base --is-ancestor ${sha} origin/main\` => exit ${ancestorStatus}${ancestorOutput ? ` (${ancestorOutput})` : ""}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return `# FN-4441 transition evidence backfill\n\n## Pre-state\n- mergeRetries: ${mergeRetriesLine}\n- Branch tip SHA: \`${branchTip.short}\` (full: \`${branchTip.full}\`)\n- last merge error: ${lastMergeError}\n\n## Canonical main landings\n${mainLandingLines}\n\n## Resolution pointer\n- Resolving task: ${resolutionTaskId}\n- Summary: Branch tip \`${branchTip.short}\` was out-of-scope FN-4393 contamination that cherry-picked empty; FN-4441's File Scope deliverable (\`.github/actions/setup-node-pnpm/action.yml\`) was already on main via \`00c8739d5\` + \`41070475b\`.\n\n## Post-state\n- FN-4441 column at time of write: ${postState.column}\n- Branch \`fusion/fn-4441\` disposition: ${postState.branchDisposition}\n`;
|
||||
}
|
||||
|
||||
function getAncestorStatus(sha) {
|
||||
try {
|
||||
execSync(`git merge-base --is-ancestor ${sha} origin/main`, { stdio: "pipe" });
|
||||
return { status: 0, output: "ancestor" };
|
||||
} catch (error) {
|
||||
const status = typeof error?.status === "number" ? error.status : 1;
|
||||
const output = String(error?.stderr || error?.stdout || "not ancestor").trim();
|
||||
return { status, output: output || "not ancestor" };
|
||||
}
|
||||
}
|
||||
|
||||
function getBranchDisposition() {
|
||||
const local = execSync("git branch --list fusion/fn-4441", { stdio: "pipe" }).toString().trim();
|
||||
const remote = execSync("git branch -r --list origin/fusion/fn-4441", {
|
||||
stdio: "pipe",
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
if (local || remote) {
|
||||
return `still present (${[local || null, remote || null].filter(Boolean).join(", ")})`;
|
||||
}
|
||||
return "not present locally or on origin refs";
|
||||
}
|
||||
|
||||
async function loadTaskStore() {
|
||||
const moduleUrl = new globalThis.URL("../packages/core/src/store.ts", import.meta.url).href;
|
||||
const mod = await tsImport(moduleUrl, import.meta.url);
|
||||
return mod.TaskStore;
|
||||
}
|
||||
|
||||
export async function runBackfill() {
|
||||
const TaskStore = await loadTaskStore();
|
||||
const store = new TaskStore(process.cwd());
|
||||
try {
|
||||
const targetTask = await store.getTask("FN-4441");
|
||||
const preResolution = await store.getTaskDocument("FN-4450", "resolution");
|
||||
const retriesMatch = preResolution?.content?.match(/mergeRetries\s*:\s*(\d+)/i);
|
||||
const mergeRetries = retriesMatch ? Number(retriesMatch[1]) : undefined;
|
||||
|
||||
const mainStep1 = getAncestorStatus("00c8739d5");
|
||||
const mainStep2 = getAncestorStatus("41070475b");
|
||||
|
||||
const content = composeTransitionEvidence({
|
||||
mergeRetries,
|
||||
branchTip: {
|
||||
short: "f57f70165",
|
||||
full: "f57f70165ac154f1ca79e68510990bc61916660e",
|
||||
},
|
||||
mainSHAs: [
|
||||
{
|
||||
sha: "00c8739d5",
|
||||
title: "feat(FN-4441): complete Step 1 — add node_modules cache layer",
|
||||
ancestorStatus: mainStep1.status,
|
||||
ancestorOutput: mainStep1.output,
|
||||
},
|
||||
{
|
||||
sha: "41070475b",
|
||||
title: "feat(FN-4441): complete Step 2 — verify cache-hit integrity",
|
||||
ancestorStatus: mainStep2.status,
|
||||
ancestorOutput: mainStep2.output,
|
||||
},
|
||||
],
|
||||
lastMergeError:
|
||||
"`git cherry-pick -X ours f57f70165...` returned 'The previous cherry-pick is now empty, possibly due to conflict resolution.' and retries exhausted.",
|
||||
resolutionTaskId: "FN-4450",
|
||||
postState: {
|
||||
column: targetTask?.column ?? "unknown",
|
||||
branchDisposition: getBranchDisposition(),
|
||||
},
|
||||
});
|
||||
|
||||
const writeResult = await store.upsertTaskDocument("FN-4441", {
|
||||
key: "transition-evidence",
|
||||
content,
|
||||
author: "FN-4461",
|
||||
});
|
||||
|
||||
const readBack = await store.getTaskDocument("FN-4441", "transition-evidence");
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
revision: writeResult.revision,
|
||||
key: readBack?.key ?? "transition-evidence",
|
||||
contentLength: readBack?.content.length ?? 0,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
return { writeResult, readBack };
|
||||
} finally {
|
||||
await store.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
runBackfill().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user