FN-5783: enforce branch-group autoMerge precedence for shared promotions
Honor group-level autoMerge precedence when promoting shared branch groups. - add core/store helpers to preserve branch-group autoMerge overrides and resolve effective precedence against task/project settings - gate branch-group promotion eligibility in engine merge coordination and emit promotion-gated audit metadata - update dashboard planning/subtask flows for shared mission triage routing and add reliability/unit coverage - include docs and changeset updates for branch-group autoMerge precedence behavior Files changed: .../fn-5783-branch-group-automerge-precedence.md | 5 + AGENTS.md | 1 + docs/settings-reference.md | 2 +- docs/workflow-steps.md | 2 + .../core/src/__tests__/branch-group-store.test.ts | 18 ++++ packages/core/src/__tests__/task-merge.test.ts | 18 ++++ packages/core/src/index.ts | 1 + packages/core/src/mission-store.ts | 13 +++ packages/core/src/store.ts | 17 ++++ packages/core/src/task-merge.ts | 8 ++ .../dashboard/src/__tests__/mission-e2e.test.ts | 98 ++++++++++++++++-- .../src/__tests__/routes-planning.test.ts | 109 ++++++++++++++++++++- packages/dashboard/src/planning.ts | 1 + .../src/routes/register-planning-subtask-routes.ts | 28 ++++++ packages/dashboard/src/subtask-breakdown.ts | 1 + .../branch-group-automerge-precedence.test.ts | 102 +++++++++++++++++++ packages/engine/src/group-merge-coordinator.ts | 31 +++++- packages/engine/src/merger.ts | 30 ++++-- 18 files changed, 466 insertions(+), 19 deletions(-) Fusion-Task-Id: FN-5783 Fusion-Task-Lineage: 74327984-b14f-445c-81cd-5295ee562382
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { type TaskStore } from "@fusion/core";
|
||||
import { aiMergeTask } from "../../merger.js";
|
||||
import { git, hasGit, makeReliabilityFixture } from "./_helpers.js";
|
||||
|
||||
async function stageMergeBranch(store: TaskStore, rootDir: string, taskId: string, fileName: string): Promise<void> {
|
||||
const task = await store.getTask(taskId);
|
||||
const branch = `fusion/${taskId.toLowerCase()}`;
|
||||
const worktreePath = join(`${rootDir}-worktrees`, taskId.toLowerCase());
|
||||
await store.updateTask(taskId, {
|
||||
baseBranch: "",
|
||||
branch,
|
||||
column: "in-review",
|
||||
worktree: worktreePath,
|
||||
steps: (task?.steps ?? []).map((step) => ({ ...step, status: "done" as const })),
|
||||
currentStep: (task?.steps ?? []).length ?? 0,
|
||||
} as any);
|
||||
git(rootDir, `git checkout -b ${branch}`);
|
||||
await mkdir(join(rootDir, "packages/engine/src"), { recursive: true });
|
||||
git(rootDir, `sh -c 'printf ${JSON.stringify(`export const ${fileName} = true;\n`)} > ${JSON.stringify(`packages/engine/src/${fileName}.ts`)}'`);
|
||||
git(rootDir, `git add ${JSON.stringify(`packages/engine/src/${fileName}.ts`)}`);
|
||||
git(rootDir, `git commit -m ${JSON.stringify(`feat: add ${fileName}`)}`);
|
||||
git(rootDir, "git checkout main");
|
||||
store.enqueueMergeQueue(taskId);
|
||||
}
|
||||
|
||||
function listAuditEvents(store: TaskStore) {
|
||||
const persisted = store.getRunAuditEvents();
|
||||
const transient = Array.isArray((store as any).__audits) ? (store as any).__audits : [];
|
||||
return [...transient, ...persisted] as Array<{ mutationType?: string; metadata?: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
function findGateEvent(store: TaskStore, groupId: string) {
|
||||
return listAuditEvents(store).find((event) =>
|
||||
event.mutationType === "merge:branch-group-promotion-gated" && (event.metadata as any)?.groupId === groupId,
|
||||
);
|
||||
}
|
||||
|
||||
describe("FN-5783 reliability interactions: branch group automerge precedence", () => {
|
||||
it.skipIf(!hasGit)("records eligible when group autoMerge=true even if task autoMerge=false", async () => {
|
||||
const fixture = await makeReliabilityFixture({ settings: { autoMerge: true, testMode: true } as any });
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
await stageMergeBranch(store, rootDir, task.id, "fn5783Eligible");
|
||||
const group = store.createBranchGroup({ sourceType: "planning", sourceId: "PS-5783", branchName: "fusion/groups/fn-5783", autoMerge: true });
|
||||
await store.setTaskBranchGroup(task.id, group.id);
|
||||
await store.updateTask(task.id, {
|
||||
autoMerge: false,
|
||||
branchContext: { groupId: group.id, source: "planning", assignmentMode: "shared" },
|
||||
} as any);
|
||||
await aiMergeTask(store, rootDir, task.id);
|
||||
expect(findGateEvent(store, group.id)?.metadata).toMatchObject({ groupId: group.id, groupAutoMerge: true, effectiveEligible: true, reason: "eligible" });
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.skipIf(!hasGit)("records disabled when group autoMerge=false even if task autoMerge=true", async () => {
|
||||
const fixture = await makeReliabilityFixture({ settings: { autoMerge: true, testMode: true } as any });
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
await stageMergeBranch(store, rootDir, task.id, "fn5783Disabled");
|
||||
const group = store.createBranchGroup({ sourceType: "mission", sourceId: "M-5783", branchName: "fusion/groups/fn-5783-disabled", autoMerge: false });
|
||||
await store.setTaskBranchGroup(task.id, group.id);
|
||||
await store.updateTask(task.id, {
|
||||
autoMerge: true,
|
||||
branchContext: { groupId: group.id, source: "mission", assignmentMode: "shared" },
|
||||
} as any);
|
||||
await aiMergeTask(store, rootDir, task.id);
|
||||
expect(findGateEvent(store, group.id)?.metadata).toMatchObject({ groupId: group.id, groupAutoMerge: false, effectiveEligible: false, reason: "group-automerge-disabled" });
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.skipIf(!hasGit)("applies pause and settings overrides", async () => {
|
||||
const scenarios = [
|
||||
{ settings: { autoMerge: true, globalPause: true }, reason: "global-pause" },
|
||||
{ settings: { autoMerge: true, enginePaused: true }, reason: "engine-paused" },
|
||||
{ settings: { autoMerge: false, globalPause: false, enginePaused: false }, reason: "settings-automerge-disabled" },
|
||||
] as const;
|
||||
for (const [index, scenario] of scenarios.entries()) {
|
||||
const fixture = await makeReliabilityFixture({ settings: { ...scenario.settings, testMode: true } as any });
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
await stageMergeBranch(store, rootDir, task.id, `fn5783Gate${index}`);
|
||||
const group = store.createBranchGroup({ sourceType: "planning", sourceId: `PS-5783-${index}`, branchName: `fusion/groups/fn-5783-gate-${index}`, autoMerge: true });
|
||||
await store.setTaskBranchGroup(task.id, group.id);
|
||||
await store.updateTask(task.id, {
|
||||
branchContext: { groupId: group.id, source: "planning", assignmentMode: "shared" },
|
||||
} as any);
|
||||
await aiMergeTask(store, rootDir, task.id);
|
||||
expect(findGateEvent(store, group.id)?.metadata).toMatchObject({ groupId: group.id, groupAutoMerge: true, effectiveEligible: false, reason: scenario.reason });
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
Reference in New Issue
Block a user