FN-8880: align shared-member recovery consent tests
Align recovery expectations with the shared-member consent policy. - Hold non-opted-in shared members when project auto-merge is off. - Cover explicit per-task auto-merge opt-in during startup and self-healing recovery. - Document the project-off consent rule for branch-group members. Files changed: AGENTS.md | 2 +- docs/architecture.md | 4 +- packages/engine/src/__tests__/project-engine.test.ts | 41 +++++++++++------ packages/engine/src/__tests__/self-healing.test.ts | 51 +++++++++++++++++++--- 4 files changed, 75 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-8880 Fusion-Task-Lineage: 3118064d-59ca-4c2e-89a8-442ee8876cf8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -267,7 +267,7 @@ Per-task opt-out exists: `task.scopeOverride = true` (log the reason).
|
||||
|
||||
When `settings.autoMerge: false`, `in-review` is terminal-until-merged by a human. Lifecycle-mutating self-healing must not move these tasks backward, pause/fail them, or re-enqueue them for execution.
|
||||
|
||||
Scoped exception (FN-5819): shared-branch-group members (`branchContext.assignmentMode === "shared"`) still run the member→shared-branch local integration step while auto-merge is off. This exception is only for assembling `branch_groups.branchName`; shared-branch → default-branch promotion remains gated by group/global auto-merge.
|
||||
Scoped exception (FN-5819/FN-8823): while project auto-merge is On, shared-branch-group members (`branchContext.assignmentMode === "shared"`) still run the member→shared-branch local integration step subject to the user-Off hold. Under project auto-merge Off, every member is held unless its task explicitly sets `autoMerge: true`; shared-branch → default-branch promotion remains separately gated by group/global auto-merge.
|
||||
|
||||
### Mock provider (test mode)
|
||||
|
||||
|
||||
@@ -2112,7 +2112,7 @@ The GitHub tracking state listener now attaches to every registered project stor
|
||||
### Merge strategies
|
||||
- Setting type: `MergeStrategy = "direct" | "pull-request"` (`types.ts`)
|
||||
- `aiMergeTask()` in `merger.ts` performs merge flow
|
||||
- FN-5782 wires branch-group routing into merge target resolution: tasks with `branchContext.assignmentMode === "shared"` and a resolvable `branch_groups` row merge onto `branch_groups.branchName` (`mergeTarget.source = "branch-group-integration"`) instead of the project default branch; ungrouped and `per-task-derived` tasks keep the existing direct-to-default path unchanged. Merge emits `merge:branch-group-routed` audit telemetry for routed members. FN-5846 extends the same contract to deterministic/self-healing finalize paths (`recoverAlreadyMergedReviewTasks`, interrupted/deadlock/misbound finalizers, and the `mergeConfirmed` fast path): a resolvable shared member is re-routed to the group branch before reachability checks, `mergeTargetSource`/`mergeTargetBranch` are stamped by the finalizer, `recordBranchGroupMemberLanded` is called, and a defensive audit event is emitted if a path would otherwise evaluate the member against the project default branch. FN-8811 narrows the member fast-path exception: only `autoMerge === false` with `autoMergeProvenance === "user"` enters the pre-integration manual hold. Mission policy (`"mission"`), legacy stamps, and inherited false values keep flowing for a live intermediate group; stale/default-branch groups use the normal standalone policy, and group→default promotion remains independently gated. FN-5788 adds a callable promotion-decision hook (`evaluateBranchGroupPromotion`) and `merge:branch-group-promotion-gated` telemetry; FN-5830 lands the completion gate + promotion machinery via `evaluateBranchGroupCompletion` and idempotent `promoteBranchGroup` (single shared→default merge/PR with finalized status and PR tracking persistence).
|
||||
- FN-5782 wires branch-group routing into merge target resolution: tasks with `branchContext.assignmentMode === "shared"` and a resolvable `branch_groups` row merge onto `branch_groups.branchName` (`mergeTarget.source = "branch-group-integration"`) instead of the project default branch; ungrouped and `per-task-derived` tasks keep the existing direct-to-default path unchanged. Merge emits `merge:branch-group-routed` audit telemetry for routed members. FN-5846 extends the same contract to deterministic/self-healing finalize paths (`recoverAlreadyMergedReviewTasks`, interrupted/deadlock/misbound finalizers, and the `mergeConfirmed` fast path): a resolvable shared member is re-routed to the group branch before reachability checks, `mergeTargetSource`/`mergeTargetBranch` are stamped by the finalizer, `recordBranchGroupMemberLanded` is called, and a defensive audit event is emitted if a path would otherwise evaluate the member against the project default branch. With project auto-merge On, FN-8811 narrows the member fast-path exception: only `autoMerge === false` with `autoMergeProvenance === "user"` enters the pre-integration manual hold. FN-8823 adds that with project auto-merge Off, `hasSharedBranchMemberAutoMergeHold` holds every member unless the task explicitly sets `autoMerge: true`; stale/default-branch groups use the normal standalone policy, and group→default promotion remains independently gated. FN-5788 adds a callable promotion-decision hook (`evaluateBranchGroupPromotion`) and `merge:branch-group-promotion-gated` telemetry; FN-5830 lands the completion gate + promotion machinery via `evaluateBranchGroupCompletion` and idempotent `promoteBranchGroup` (single shared→default merge/PR with finalized status and PR tracking persistence).
|
||||
- FN-5279 adds `mergeIntegrationWorktree` for auto-merge only. Default `reuse-task-worktree` hands merger ownership from executor to the merger inside the task worktree after five gates (clean tree, expected branch, no live executor session, canonical branch/worktree binding, lease handoff). Refusals emit `merge:reuse-handoff-refused`, leave the task in `in-review`, and do **not** silently fall back to project-root merge mode. `cwd-integration-branch` is the explicit opt-in project-root path; `cwd-main` is a deprecated alias normalized to `cwd-integration-branch`. Integration-branch defaults across merger and self-healing flows are resolved dynamically via `resolveIntegrationBranch(rootDir, settings)` (`integrationBranch` → `baseBranch` → `origin/HEAD` → `main`). When `worktrunk.enabled=true`, worktrunk-managed merge/worktree behavior still wins and the handoff path emits a defer event instead of taking over. FN-5363 tightens this path: `acquireMergeQueueLease({ targetTaskId })` is strict (no queue-head fallback), merge queue rows are enqueue/lease-gated to `in-review` tasks, and stale non-review rows are auto-cleaned (including on `in-review` column exit when leases are absent or expired). FN-5353 extends the same contract: merger self-enqueues the target before strict target leasing, null target leases are surfaced as `merge:reuse-handoff-refused` with `reason: "target-not-queued"`, `acquireReuseHandoff` hard-refuses `reason: "worktree-equals-project-root"`, and `resolveMergeIntegrationRoot` returns a missing-worktree sentinel (`rootDir: ""`) so reacquire executes before any reuse gate can misroute against project root. FN-6278 adds a stable cwd preflight before root-derived git spawns: in `reuse-task-worktree` mode, an empty, missing, incomplete, or de-registered `task.worktree` is repaired/reacquired before the first spawn, while `cwd-integration-branch` remains a no-op project-root path. FN-5351 adds a production verification trail for integration-branch invariants: `merge:integration-worktree-state`, `merge:cwd-integration-fallback-refused`, and `merge:integration-ref-advance`.
|
||||
- `merger.ts` also exposes a test-only `__test__` helper object for internal merger unit/integration coverage (for example autostash orphan cleanup behavior)
|
||||
- Supports workflow-step execution after merge (post-merge phase)
|
||||
@@ -2292,7 +2292,7 @@ This section preserves the detailed lifecycle/self-healing contracts that were f
|
||||
- **Empty-commit refusal + early empty-own-diff finalize (FN-5345/FN-5377)**: Fusion task worktrees install a `prepare-commit-msg` hook that refuses `git commit --allow-empty` and other zero-staged-diff commits, preventing verification-only tasks from manufacturing empty handoff commits that defeat the merger's no-op classifier. The hook allows legitimate empty-tree paths (amend, merge, squash, cherry-pick, revert, rebase). Amend detection tokenizes the parent process command line (`ps -o args=` with `/proc/$PPID/cmdline` fallback for Alpine/busybox) and stops at the first message-supplying flag (`-m`/`-F`/`--message`/`--file`) so a commit message containing the substring `--amend` cannot bypass the guard. In `aiMergeTask`, an early empty-own-diff fast-path runs BEFORE any reuse-handoff acquisition: when integration mode is `reuse-task-worktree`, the branch exists, `git rev-list --count <mergeTarget>..<branch>` is > 0, and `git diff --quiet <mergeBase>..<branch>` exits 0, the task auto-finalizes as no-op with `mergeDetails.noOpMerge: true` and emits `task:auto-recover-finalize-already-on-main` with `reason: "empty-own-diff-early-fast-path"`. The fast-path best-effort removes the stranded worktree (FN-4811 same-task/foreign-owner guard) and deletes the `fusion/<id>` branch so empty-own-diff residuals do not accumulate. This unsticks tasks where a stale empty handoff commit combined with drifted worktree↔branch mapping would otherwise wedge the handoff gate with `registered-branch-mismatch`. The explicit `cwd-integration-branch` mode is unchanged (`cwd-main` remains a deprecated alias normalized to it). `classifyOwnedLandedEvidence` also detects empty-own-diff (aheadCount > 0, zero net diff) and returns `proven-no-op` so downstream self-healing and post-handoff finalize paths benefit too. Additionally, merger's reuse-fallback path now consults `git worktree list --porcelain` before creating a new worktree: extant usable registrations of `fusion/<id>` are reused directly (rather than blindly `git worktree add -f` producing a duplicate registration), and stale registrations are pruned first. The direct-reuse shortcut is guarded by FN-4811 (refuses paths owned by a different task in `activeSessionRegistry`) and FN-4954 (skipped when `recycleWorktrees=true` with a pool attached, so `WorktreePool.acquire` lease bookkeeping stays consistent). Two audit subtypes — `merge:reuse-fallback-pruned-stale-registration` and `merge:reuse-fallback-reused-existing-registration` — replace the prior overloading of `merge:reuse-fallback-new-worktree` for these cases.
|
||||
- **Verified no-op/duplicate executor completion (FN-6275/FN-7488)**: explicit `fn_task_done` may complete with zero branch commits only when the summary starts with a recognized sentinel (`PREMISE STALE:`, `NO-OP:`, `NOOP:`, `DUPLICATE: FN-NNNN ...`, or `REDUNDANT:`), the task already carries a no-commit contract, or the PROMPT declares a source-free gitignored task-artifact delivery. The source-free path is intentionally narrow: File Scope must be populated and limited to board/task artifacts such as `.fusion/tasks/...`, task documents/logs, or attachments; the prompt must forbid force-adding ignored `.fusion/` artifacts and fabricating empty commits or equivalently state that source-free/gitignored task artifacts are the only deliverables; and any tracked source/docs/config/test/changeset scope keeps the `no_commits` refusal active (even if `.fusion/` artifacts are also listed). These exemptions only relax the `no_commits` invariant; `wrong_toplevel`, `wrong_branch`, pending-step/review refusals, and scope-leak guards still run. Accepted sentinel completions persist `noCommitsExpected: true`, write task-log audit details with marker kind/reason/raw summary/run/agent IDs, and add a task timeline activity so the no-code terminal path remains explainable. Prompt-derived source-free completions log `prompt-derived source-free task-artifact contract` for operator audit. Ordinary zero-commit implementation completions without one of these contracts are still refused.
|
||||
- **In-review branch-binding self-heal (FN-5083/FN-6695)**: `reconcile-in-review-branch-rebind` runs after `reconcile-task-worktree-metadata` and before `reclaim-stale-active-branches`. It restores `task.branch` (and clears `task.worktree` for fresh acquisition) for `in-review` tasks when exactly one case-insensitive `fusion/<id>` candidate branch has unique commits versus the integration base. Ambiguous candidates emit `task:auto-rebind-skipped` (`reason: "ambiguous-candidates"`) and are never auto-resolved. Unsafe metadata repair is also skipped with `task:auto-rebind-skipped`: `userPaused` preserves authoritative user intent, and `checkedOutBy` preserves live agent checkout ownership. Branch construction across executor/worktree-pool/worktree-acquisition/merger/self-healing canonicalizes to lowercase via `canonicalFusionBranchName`; `fn_task_done` wrong-branch checks now auto-canonicalize case-only mismatches and emit `branch:auto-canonicalize-case`.
|
||||
- **In-review is terminal-until-merged under `autoMerge: false` (FN-5147)**: when a project sets `settings.autoMerge: false`, `in-review` is the intended resting state until a human merges the PR. No lifecycle-mutating self-healing sweep (`reclaimSelfOwnedBranchConflicts`, `recoverGhostReviewTasks`, `recoverStaleIncompleteReviewTasks`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`, `recoverMissingWorktreeReviewFailures`, `recoverPartialProgressNoTaskDoneFailures`, `recoverCompletionHandoffLimbo`, `recoverPostDoneNonContinuableWedge`, `recoverMergeableReviewTasks`, `recoverMergedReviewTasks`, `recoverAlreadyMergedReviewTasks`, `recoverOrphanOnlyScopeViolations`, `recoverForeignOnlyContaminatedInReviewTasks`, `recoverReviewTasksWithFailedPreMergeSteps`, `finalizeNoOpReviewTasks`, `surfaceInReviewStalls`, `surfaceInReviewStalled`) may move the task out of `in-review`, mark it `paused`/`failed`, or re-enqueue it for execution. That includes merge-active unusable-worktree recovery: automation emits `task:reconcile-missing-worktree-merge-active-no-action` with `reason:"auto-merge-off"` instead of moving the row, while explicit operator retry remains supported because it is a manual reset request. Explicit per-task overrides are distinguished by `task.autoMergeProvenance: "user"`; ambiguous legacy rows stamped `autoMerge: true` by the pre-FN-6245 review-entry path are marked `"legacy-stamp"` once and surfaced in run-audit/logs, but are only cleared by the operator-driven `reconcileLegacyAutoMergeStamps({ apply: true })` action. Scoped FN-5819/FN-8811 exception: shared-group members (`branchContext.assignmentMode === "shared"`) are still allowed through the member→`branch_groups.branchName` integration step while `autoMerge` is off only when the group is open and its normalized branch differs from the resolved project default branch. An operator-authored task `autoMerge:false` paired with `autoMergeProvenance:"user"` is the sole opt-in exception: it parks at the manual hold before member integration, while mission policy, inherited, and legacy false values continue flowing. This is a soft pre-integration only and does not permit shared-branch → default-branch promotion; a default-branch collision follows the normal manual-release path. FN-7182 applies the same human-gated treatment to an open `PrInfo.manual` PR created or linked from the dashboard **Create PR** action: automatic merge queues and self-healing stand down until the PR is closed/merged or handled manually, while pipeline-created PRs without `manual` remain auto-merge eligible. RECONCILE-ONLY sweeps (branch rebind, blocker fan-out, stale-status clears, contamination metadata cleanup, attribution restore, PR refresh, misclassified-failure error clearing) continue to run.
|
||||
- **In-review is terminal-until-merged under `autoMerge: false` (FN-5147)**: when a project sets `settings.autoMerge: false`, `in-review` is the intended resting state until a human merges the PR. No lifecycle-mutating self-healing sweep (`reclaimSelfOwnedBranchConflicts`, `recoverGhostReviewTasks`, `recoverStaleIncompleteReviewTasks`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`, `recoverMissingWorktreeReviewFailures`, `recoverPartialProgressNoTaskDoneFailures`, `recoverCompletionHandoffLimbo`, `recoverPostDoneNonContinuableWedge`, `recoverMergeableReviewTasks`, `recoverMergedReviewTasks`, `recoverAlreadyMergedReviewTasks`, `recoverOrphanOnlyScopeViolations`, `recoverForeignOnlyContaminatedInReviewTasks`, `recoverReviewTasksWithFailedPreMergeSteps`, `finalizeNoOpReviewTasks`, `surfaceInReviewStalls`, `surfaceInReviewStalled`) may move the task out of `in-review`, mark it `paused`/`failed`, or re-enqueue it for execution. That includes merge-active unusable-worktree recovery: automation emits `task:reconcile-missing-worktree-merge-active-no-action` with `reason:"auto-merge-off"` instead of moving the row, while explicit operator retry remains supported because it is a manual reset request. Explicit per-task overrides are distinguished by `task.autoMergeProvenance: "user"`; ambiguous legacy rows stamped `autoMerge: true` by the pre-FN-6245 review-entry path are marked `"legacy-stamp"` once and surfaced in run-audit/logs, but are only cleared by the operator-driven `reconcileLegacyAutoMergeStamps({ apply: true })` action. Scoped FN-5819/FN-8811/FN-8823 exception: while project auto-merge is On, shared-group members (`branchContext.assignmentMode === "shared"`) are allowed through the member→`branch_groups.branchName` integration step only when the group is open and its normalized branch differs from the resolved project default branch; an operator-authored task `autoMerge:false` paired with `autoMergeProvenance:"user"` parks at the manual hold. Under project auto-merge Off, `hasSharedBranchMemberAutoMergeHold` instead holds every member unless its task explicitly sets `autoMerge:true`, before group liveness can admit it. This is a soft pre-integration only and does not permit shared-branch → default-branch promotion; a default-branch collision follows the normal manual-release path. FN-7182 applies the same human-gated treatment to an open `PrInfo.manual` PR created or linked from the dashboard **Create PR** action: automatic merge queues and self-healing stand down until the PR is closed/merged or handled manually, while pipeline-created PRs without `manual` remain auto-merge eligible. RECONCILE-ONLY sweeps (branch rebind, blocker fan-out, stale-status clears, contamination metadata cleanup, attribution restore, PR refresh, misclassified-failure error clearing) continue to run.
|
||||
- **Auto-merge integration-root default (FN-5279)**: direct auto-merge now defaults `mergeIntegrationWorktree` to `reuse-task-worktree`; merger must pass the reuse handoff gates or emit `merge:reuse-handoff-refused` and leave the task in `in-review` without silently falling back to `cwd-integration-branch` (`cwd-main` remains a deprecated alias normalized to that mode).
|
||||
- **Orphaned execution sweep is observation-only (FN-5337)**: `recoverOrphanedExecutions` only annotates stale in-progress candidates with `task:orphan-detected-no-action` and `[orphan-detected] ... no action (operator-decides)` logs. It must never move `in-progress`/`in-review` backward to `todo` or mutate lease/worktree metadata. Proof-based backward recovery remains exclusively in `recoverInProgressLimbo` (FN-5219), `RestartRecoveryCoordinator`, `recoverMissingWorktreeReviewFailures`, and explicit executor/merger failure paths. Reintroducing lifecycle mutation here requires hard git/session proof gating plus CEO+CTO+PM sign-off.
|
||||
- **Self-owned reclaim resume-limbo escalation (FN-5704)**: `reclaimSelfOwnedBranchConflicts` tracks `resumeLimboCount`, `resumeLimboTipSha`, and `resumeLimboStepSignature` for in-progress reclaim/unpause loops. If reclaim finds no progress (same tip, same step-status signature, and no active-session signal) for `MAX_NO_PROGRESS_RESUME_ATTEMPTS` consecutive sweeps, self-healing escalates by moving the task to `todo` with `preserveWorktree: true`, `preserveProgress: true`, and `preserveResumeState: true` instead of endlessly re-arming resume. Escalation emits `task:resume-limbo-escalated` run-audit metadata (`frozenTipSha`, `idleMs`, `resumeAttemptCount`, `currentStep`) and resets the limbo counter.
|
||||
|
||||
@@ -2747,7 +2747,13 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
|
||||
await engine.stop();
|
||||
});
|
||||
|
||||
it("startup merge sweep enqueues shared-group members when autoMerge is false", async () => {
|
||||
/*
|
||||
FNXC:SharedBranchMemberHold 2026-08-09-09:09:
|
||||
FN-8823 applies the project-Off consent rule to startup merge recovery as well
|
||||
as direct admission. An explicit per-task On remains eligible, but group
|
||||
liveness cannot re-admit a non-opted-in shared member.
|
||||
*/
|
||||
it("startup merge sweep holds non-opted-in shared-group members when autoMerge is false", async () => {
|
||||
const mockStore = createMockStore({ ...baseSettings, autoMerge: false });
|
||||
mockStore.store.getBranchGroup.mockReturnValue({ id: "BG-5819", status: "open", branchName: "fusion/groups/bg-5819" });
|
||||
const inReviewTasks = [
|
||||
@@ -2759,6 +2765,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
|
||||
status: null,
|
||||
branchContext: { assignmentMode: "shared", groupId: "BG-5819", source: "planning" },
|
||||
},
|
||||
{ id: "FN-opted-in", column: "in-review", paused: false, mergeRetries: 0, status: null, autoMerge: true, branchContext: { assignmentMode: "shared", groupId: "BG-5819", source: "planning" } },
|
||||
{ id: "FN-plain", column: "in-review", paused: false, mergeRetries: 0, status: null },
|
||||
];
|
||||
// Critical stale-status cleanup reads first; deferred startup then evaluates eligibility.
|
||||
@@ -2769,9 +2776,10 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
|
||||
const enqueueSpy = vi.spyOn(privateEngine, "internalEnqueueMerge");
|
||||
|
||||
await engine.start();
|
||||
await vi.waitFor(() => expect(enqueueSpy).toHaveBeenCalledWith("FN-shared"));
|
||||
await vi.waitFor(() => expect(enqueueSpy).toHaveBeenCalledWith("FN-opted-in"));
|
||||
|
||||
expect(enqueueSpy).toHaveBeenCalledWith("FN-shared");
|
||||
expect(enqueueSpy).toHaveBeenCalledWith("FN-opted-in");
|
||||
expect(enqueueSpy).not.toHaveBeenCalledWith("FN-shared");
|
||||
expect(enqueueSpy).not.toHaveBeenCalledWith("FN-plain");
|
||||
|
||||
await engine.stop();
|
||||
@@ -3689,23 +3697,30 @@ describe("allowInReviewMergeProcessing per-task autoMerge override", () => {
|
||||
await expect(gate({ autoMerge: false }, { autoMerge: true })).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("still exempts live shared-branch-group member integration on an intermediate branch when the global setting is off", async () => {
|
||||
await expect(gate(
|
||||
{ branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] },
|
||||
{ autoMerge: false, integrationBranch: "main" },
|
||||
{ status: "open", branchName: "mission/M-3324" },
|
||||
)).resolves.toBe(true);
|
||||
/*
|
||||
FNXC:SharedBranchMemberHold 2026-08-09-09:09:
|
||||
FN-8823 supersedes the FN-5819 live-member exemption when project auto-merge
|
||||
is Off. Every non-opted-in member is held before liveness is considered; an
|
||||
explicit task-level On is the sole consent path through this requester.
|
||||
*/
|
||||
it("holds live shared-branch-group member integration on an intermediate branch when the global setting is off", async () => {
|
||||
const shared = { branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] };
|
||||
const settings = { autoMerge: false, integrationBranch: "main" };
|
||||
const group = { status: "open" as const, branchName: "mission/M-3324" };
|
||||
|
||||
await expect(gate(shared, settings, group)).resolves.toBe(false);
|
||||
await expect(gate({ ...shared, autoMerge: true }, settings, group)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("holds only an operator-authored false override before live member integration", async () => {
|
||||
it("holds every non-opted-in provenance before live member integration when global auto-merge is off", async () => {
|
||||
const shared = { branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] };
|
||||
const settings = { autoMerge: false, integrationBranch: "main" };
|
||||
const group = { status: "open" as const, branchName: "mission/M-3324" };
|
||||
|
||||
await expect(gate({ ...shared, autoMerge: false, autoMergeProvenance: "user" }, settings, group)).resolves.toBe(false);
|
||||
await expect(gate({ ...shared, autoMerge: false, autoMergeProvenance: "mission" }, settings, group)).resolves.toBe(true);
|
||||
await expect(gate({ ...shared, autoMerge: false, autoMergeProvenance: "legacy-stamp" }, settings, group)).resolves.toBe(true);
|
||||
await expect(gate({ ...shared, autoMerge: false }, settings, group)).resolves.toBe(true);
|
||||
await expect(gate({ ...shared, autoMerge: false, autoMergeProvenance: "mission" }, settings, group)).resolves.toBe(false);
|
||||
await expect(gate({ ...shared, autoMerge: false, autoMergeProvenance: "legacy-stamp" }, settings, group)).resolves.toBe(false);
|
||||
await expect(gate({ ...shared, autoMerge: false }, settings, group)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("keeps live shared-branch-group member integration on the default branch behind the manual gate", async () => {
|
||||
|
||||
@@ -5113,12 +5113,12 @@ describe("SelfHealingManager", () => {
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:SharedBranchMemberHold 2026-08-05-23:14:
|
||||
A self-healing sweep is a production merge requester. It must preserve the
|
||||
intentional mission member fast path under global Off, but never bypass an
|
||||
operator-authored task Off hold while doing recovery admission.
|
||||
FNXC:SharedBranchMemberHold 2026-08-09-09:09:
|
||||
FN-8823 supersedes the mission-policy fast path under project Off. A
|
||||
self-healing merge requester must treat project Off as withheld consent for
|
||||
every non-opted-in shared member, preserving the FN-8811 user hold as a subset.
|
||||
*/
|
||||
it("re-enqueues a mission-policy shared member under global auto-merge off but preserves a user hold", async () => {
|
||||
it("holds every non-opted-in shared member under global auto-merge off, including mission policy", async () => {
|
||||
const enqueueMerge = vi.fn().mockReturnValue(true);
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
rootDir: "/tmp/test-project",
|
||||
@@ -5165,10 +5165,47 @@ describe("SelfHealingManager", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await managerWithRecovery.recoverMergeableReviewTasks()).toBe(0);
|
||||
expect(enqueueMerge).not.toHaveBeenCalledWith("FN-8811-MISSION");
|
||||
expect(enqueueMerge).not.toHaveBeenCalledWith("FN-8811-USER");
|
||||
expect(store.mergeTask).not.toHaveBeenCalled();
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("recovers an explicitly opted-in shared member under global auto-merge off", async () => {
|
||||
const enqueueMerge = vi.fn().mockReturnValue(true);
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
rootDir: "/tmp/test-project",
|
||||
enqueueMerge,
|
||||
});
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
autoMerge: false,
|
||||
integrationBranch: "main",
|
||||
globalPause: false,
|
||||
enginePaused: false,
|
||||
});
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
status: "open",
|
||||
branchName: "mission/M-8811",
|
||||
});
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([{
|
||||
id: "FN-8811-OPTED-IN",
|
||||
column: "in-review",
|
||||
paused: false,
|
||||
status: null,
|
||||
error: null,
|
||||
worktree: "/tmp/test-project/.worktrees/fn-8811-opted-in",
|
||||
steps: [{ name: "Ship it", status: "done" }],
|
||||
workflowStepResults: [{ id: "ws-opted-in", status: "passed", phase: "pre-merge" }],
|
||||
autoMerge: true,
|
||||
branchContext: { assignmentMode: "shared", groupId: "BG-8811", source: "mission" },
|
||||
log: [],
|
||||
}]);
|
||||
|
||||
expect(await managerWithRecovery.recoverMergeableReviewTasks()).toBe(1);
|
||||
expect(enqueueMerge).toHaveBeenCalledTimes(1);
|
||||
expect(enqueueMerge).toHaveBeenCalledWith("FN-8811-MISSION");
|
||||
expect(enqueueMerge).not.toHaveBeenCalledWith("FN-8811-USER");
|
||||
expect(enqueueMerge).toHaveBeenCalledWith("FN-8811-OPTED-IN");
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user