FN-7840: suppress advisory merger/pull-request await-confirmation interventions
Stops decidePlannerRecovery from recording noisy advisory confirmation interventions for merger/pull-request stages that never actually block progress when auto-merge will proceed unattended. - decidePlannerRecovery now returns action "none" (no pending confirmation, no steering comment, no overseer:intervention entry) for merger/pull-request stages when autoMergeWillProceed === true, since this checkpoint is purely advisory in that case - Genuine human-approval blocks (autoMergeWillProceed === false) and the neutral pure-function default (undefined) keep the await_confirmation decision intact - Updated planner-recovery.test.ts to assert the new "none" outcome for the advisory case - Simplified planner-overseer-intervention-wiring.test.ts to match the reduced intervention surface - Added changeset documenting the fix as a patch-level bug fix Files changed: .changeset/fn-7840-advisory-merger-confirmations.md | 7 ++ packages/core/src/__tests__/planner-recovery.test.ts | 32 ++--- packages/core/src/planner-recovery.ts | 47 ++++--- packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts | 135 +++++---------------- 4 files changed, 79 insertions(+), 142 deletions(-) Fusion-Task-Id: FN-7840 Fusion-Task-Lineage: 610a9003-f229-4e78-9948-ee0bb85193bc Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7840-advisory-merger-confirmations.md
Normal file
7
.changeset/fn-7840-advisory-merger-confirmations.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Stop recording advisory "merger awaiting-confirmation" planner interventions that never block auto-merge.
|
||||||
|
category: fix
|
||||||
|
dev: decidePlannerRecovery now returns action "none" for merger/pull-request stages when autoMergeWillProceed === true; the genuine human-approval (false) and neutral (undefined) confirmation paths are unchanged (FN-7840).
|
||||||
@@ -103,26 +103,22 @@ describe("decidePlannerRecovery", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// FN-7692: the `reason` copy must accurately reflect whether the active
|
// FNXC:PlannerOversight 2026-07-11-00:00:
|
||||||
// auto-merge policy will advance the merge/PR unattended (advisory,
|
// FN-7840 changes `autoMergeWillProceed: true` from a messaging-only advisory checkpoint into a true suppression path: no await_confirmation decision, no pending confirmation, no steering comment, and no intervention entry. The false/undefined paths remain the safety valve.
|
||||||
// non-blocking wording) or genuinely requires a human approval (blocking
|
it("suppresses the merger/pull-request confirmation when auto-merge will proceed unattended", () => {
|
||||||
// wording) — never the old unconditional "requires explicit confirmation
|
|
||||||
// before ... may run" claim, which was false whenever auto-merge would
|
|
||||||
// proceed on its own (observed on FN-7689). `action`/`requiresConfirmation`/
|
|
||||||
// `sideEffectClass` must stay byte-for-byte identical across all three
|
|
||||||
// `autoMergeWillProceed` states — this is messaging-only.
|
|
||||||
it("produces accurate advisory copy when auto-merge will proceed unattended, for both merger and pull-request stages", () => {
|
|
||||||
for (const stage of ["merger", "pull-request"] as const) {
|
for (const stage of ["merger", "pull-request"] as const) {
|
||||||
const decision = decidePlannerRecovery({
|
const decision = decidePlannerRecovery({
|
||||||
snapshot: observation({ stage, signal: "failed" }),
|
snapshot: observation({ stage, signal: "failed" }),
|
||||||
autoMergeWillProceed: true,
|
autoMergeWillProceed: true,
|
||||||
});
|
});
|
||||||
expect(decision.action, `stage=${stage}`).toBe("await_confirmation");
|
expect(decision.action, `stage=${stage}`).toBe("none");
|
||||||
expect(decision.requiresConfirmation, `stage=${stage}`).toBe(true);
|
expect(decision.requiresConfirmation, `stage=${stage}`).toBe(false);
|
||||||
expect(decision.sideEffectClass, `stage=${stage}`).toBe("merge_pr");
|
expect(decision.sideEffectClass, `stage=${stage}`).toBe("merge_pr");
|
||||||
|
expect(decision.proposedAction, `stage=${stage}`).toBeUndefined();
|
||||||
expect(decision.reason, `stage=${stage}`).not.toMatch(/requires explicit confirmation before .* may run/);
|
expect(decision.reason, `stage=${stage}`).not.toMatch(/requires explicit confirmation before .* may run/);
|
||||||
|
expect(decision.reason, `stage=${stage}`).not.toMatch(/await|advisory|does not block progress/i);
|
||||||
expect(decision.reason, `stage=${stage}`).toMatch(/automatically/i);
|
expect(decision.reason, `stage=${stage}`).toMatch(/automatically/i);
|
||||||
expect(decision.reason, `stage=${stage}`).toMatch(/advisory/i);
|
expect(decision.reason, `stage=${stage}`).toMatch(/no confirmation checkpoint recorded/i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,13 +139,15 @@ describe("decidePlannerRecovery", () => {
|
|||||||
it("uses neutral, non-overclaiming copy when the auto-merge policy is unknown to the caller", () => {
|
it("uses neutral, non-overclaiming copy when the auto-merge policy is unknown to the caller", () => {
|
||||||
for (const stage of ["merger", "pull-request"] as const) {
|
for (const stage of ["merger", "pull-request"] as const) {
|
||||||
const decision = decidePlannerRecovery({ snapshot: observation({ stage, signal: "failed" }) });
|
const decision = decidePlannerRecovery({ snapshot: observation({ stage, signal: "failed" }) });
|
||||||
|
expect(decision.action, `stage=${stage}`).toBe("await_confirmation");
|
||||||
|
expect(decision.requiresConfirmation, `stage=${stage}`).toBe(true);
|
||||||
expect(decision.reason, `stage=${stage}`).not.toMatch(/requires explicit confirmation before .* may run/);
|
expect(decision.reason, `stage=${stage}`).not.toMatch(/requires explicit confirmation before .* may run/);
|
||||||
expect(decision.reason, `stage=${stage}`).not.toMatch(/automatically/i);
|
expect(decision.reason, `stage=${stage}`).not.toMatch(/automatically/i);
|
||||||
expect(decision.reason, `stage=${stage}`).not.toMatch(/will not .* until a human explicitly approves/);
|
expect(decision.reason, `stage=${stage}`).not.toMatch(/will not .* until a human explicitly approves/);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps action/requiresConfirmation/sideEffectClass byte-for-byte unchanged across autoMergeWillProceed states (messaging-only guard)", () => {
|
it("diverges only the advisory autoMergeWillProceed=true case while preserving blocking and neutral confirmations", () => {
|
||||||
for (const stage of ["merger", "pull-request"] as const) {
|
for (const stage of ["merger", "pull-request"] as const) {
|
||||||
const base = decidePlannerRecovery({ snapshot: observation({ stage, signal: "failed" }) });
|
const base = decidePlannerRecovery({ snapshot: observation({ stage, signal: "failed" }) });
|
||||||
const proceeds = decidePlannerRecovery({
|
const proceeds = decidePlannerRecovery({
|
||||||
@@ -160,12 +158,18 @@ describe("decidePlannerRecovery", () => {
|
|||||||
snapshot: observation({ stage, signal: "failed" }),
|
snapshot: observation({ stage, signal: "failed" }),
|
||||||
autoMergeWillProceed: false,
|
autoMergeWillProceed: false,
|
||||||
});
|
});
|
||||||
for (const decision of [base, proceeds, blocks]) {
|
|
||||||
|
// FNXC:PlannerOversight 2026-07-11-00:00: FN-7840 intentionally breaks the old messaging-only invariant only for autoMergeWillProceed === true; false and undefined keep the confirmation safety valve.
|
||||||
|
for (const decision of [base, blocks]) {
|
||||||
expect(decision.action, `stage=${stage}`).toBe("await_confirmation");
|
expect(decision.action, `stage=${stage}`).toBe("await_confirmation");
|
||||||
expect(decision.requiresConfirmation, `stage=${stage}`).toBe(true);
|
expect(decision.requiresConfirmation, `stage=${stage}`).toBe(true);
|
||||||
expect(decision.sideEffectClass, `stage=${stage}`).toBe("merge_pr");
|
expect(decision.sideEffectClass, `stage=${stage}`).toBe("merge_pr");
|
||||||
expect(decision.proposedAction, `stage=${stage}`).toBe(base.proposedAction);
|
expect(decision.proposedAction, `stage=${stage}`).toBe(base.proposedAction);
|
||||||
}
|
}
|
||||||
|
expect(proceeds.action, `stage=${stage}`).toBe("none");
|
||||||
|
expect(proceeds.requiresConfirmation, `stage=${stage}`).toBe(false);
|
||||||
|
expect(proceeds.sideEffectClass, `stage=${stage}`).toBe("merge_pr");
|
||||||
|
expect(proceeds.proposedAction, `stage=${stage}`).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -223,30 +223,37 @@ export function decidePlannerRecovery(input: DecidePlannerRecoveryInput): Planne
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// FNXC:PlannerOversight 2026-07-04-13:00: merger / pull-request stage
|
/*
|
||||||
// actions beyond guidance/retry now surface as a confirmation-required
|
FNXC:PlannerOversight 2026-07-04-13:00:
|
||||||
// `"await_confirmation"` decision (FN-7513) instead of the FN-7512
|
Merger / pull-request stage actions beyond guidance/retry surface as a confirmation-required `"await_confirmation"` decision (FN-7513) instead of the FN-7512 `"none"` deferral — the recovery layer identifies what WOULD run on approval, but never dispatches it itself.
|
||||||
// `"none"` deferral — the recovery layer identifies what WOULD run on
|
|
||||||
// approval, but never dispatches it itself.
|
FNXC:PlannerOversight 2026-07-08-00:00:
|
||||||
|
FN-7692 fix: the merger/pull-request `reason` string previously read "requires explicit confirmation before ... may run" UNCONDITIONALLY, which is false when the active auto-merge policy will advance the merge/PR unattended (observed on FN-7689: the Intervention Timeline claimed a hard block that the merge sailed past ~4 minutes later with no human approval). `input.autoMergeWillProceed` (threaded by the engine controller from the existing `allowsAutoMergeProcessing` predicate) selects accurate wording per policy state.
|
||||||
|
|
||||||
|
FNXC:PlannerOversight 2026-07-11-00:00:
|
||||||
|
FN-7840: the merger/pull-request confirmation checkpoint is purely advisory whenever the active auto-merge policy will advance the merge unattended (autoMergeWillProceed === true) — in real tick() wiring this is the ONLY reachable state, because evaluateOverseerHumanControl withholds all oversight when allowsAutoMergeProcessing === false (the same predicate). Recording it produced a stream of "advisory and does not block progress" interventions the operator saw as pure noise. Suppress it: return action "none" (no pending confirmation, no steering comment, no overseer:intervention entry) for the advisory case. Genuine human-approval blocks (autoMergeWillProceed === false) and the neutral pure-function default (undefined) keep await_confirmation intact.
|
||||||
|
*/
|
||||||
if (snapshot.stage === "merger" || snapshot.stage === "pull-request") {
|
if (snapshot.stage === "merger" || snapshot.stage === "pull-request") {
|
||||||
const proposedAction = snapshot.stage === "merger" ? "advance_merge" : "advance_pull_request";
|
const proposedAction = snapshot.stage === "merger" ? "advance_merge" : "advance_pull_request";
|
||||||
const sideEffectClass = classifyPlannerActionSideEffect({ watchedStage: snapshot.stage, proposedAction });
|
const sideEffectClass = classifyPlannerActionSideEffect({ watchedStage: snapshot.stage, proposedAction });
|
||||||
const actionPhrase = proposedAction.replace(/_/g, " ");
|
const actionPhrase = proposedAction.replace(/_/g, " ");
|
||||||
// FNXC:PlannerOversight 2026-07-08-00:00:
|
if (input.autoMergeWillProceed === true) {
|
||||||
// FN-7692 fix: this `reason` string previously read "requires explicit
|
return {
|
||||||
// confirmation before ... may run" UNCONDITIONALLY, which is false when
|
action: "none",
|
||||||
// the active auto-merge policy will advance the merge/PR unattended
|
reason: `Stage "${snapshot.stage}" will ${actionPhrase} automatically under the active auto-merge policy — no confirmation checkpoint recorded`,
|
||||||
// (observed on FN-7689: the Intervention Timeline claimed a hard block
|
attemptCount,
|
||||||
// that the merge sailed past ~4 minutes later with no human approval).
|
attemptLimit,
|
||||||
// `input.autoMergeWillProceed` (threaded by the engine controller from
|
exhausted: false,
|
||||||
// the existing `allowsAutoMergeProcessing` predicate) selects accurate
|
watchedStage,
|
||||||
// wording per policy state; it is messaging-only and does not change
|
sourceLinks,
|
||||||
// `action`/`requiresConfirmation`/`sideEffectClass` below.
|
requiresConfirmation: false,
|
||||||
const reason = input.autoMergeWillProceed === true
|
sideEffectClass,
|
||||||
? `Stage "${snapshot.stage}" will ${actionPhrase} automatically under the active auto-merge policy — this confirmation checkpoint is advisory and does not block progress`
|
proposedAction: undefined,
|
||||||
: input.autoMergeWillProceed === false
|
};
|
||||||
? `Stage "${snapshot.stage}" will not ${actionPhrase} until a human explicitly approves — auto-merge is not enabled for this task`
|
}
|
||||||
: `Stage "${snapshot.stage}" is awaiting confirmation before ${actionPhrase} may run`;
|
const reason = input.autoMergeWillProceed === false
|
||||||
|
? `Stage "${snapshot.stage}" will not ${actionPhrase} until a human explicitly approves — auto-merge is not enabled for this task`
|
||||||
|
: `Stage "${snapshot.stage}" is awaiting confirmation before ${actionPhrase} may run`;
|
||||||
return {
|
return {
|
||||||
action: "await_confirmation",
|
action: "await_confirmation",
|
||||||
reason,
|
reason,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* FNXC:PlannerOversight 2026-07-04-19:45:
|
* FNXC:PlannerOversight 2026-07-04-19:45:
|
||||||
* FN-7551 engine-level end-to-end test: proves real overseer decision points
|
* FN-7551 engine-level end-to-end test: proves real overseer decision points
|
||||||
* — observation, retry, targeted-fix, steering (reviewer), confirmation
|
* — observation, retry, targeted-fix, steering (reviewer), advisory
|
||||||
* request, confirmation resolution, and bounded-recovery escalation —
|
* confirmation suppression, and bounded-recovery escalation — populate the
|
||||||
* populate the `overseer:intervention` run-audit timeline via the ACTUAL
|
* `overseer:intervention` run-audit timeline via the ACTUAL
|
||||||
* production wiring in `project-engine.ts` (`PlannerOverseerMonitor#onObservation`
|
* production wiring in `project-engine.ts` (`PlannerOverseerMonitor#onObservation`
|
||||||
* → the private `emitOverseerObservationDeduped`, the private
|
* → the private `emitOverseerObservationDeduped`, the private
|
||||||
* `buildPlannerRecoveryHandlers`, the private `emitOverseerEscalationDeduped`),
|
* `buildPlannerRecoveryHandlers`, the private `emitOverseerEscalationDeduped`),
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
* initialized by class-field initializers the constructor never runs here)
|
* initialized by class-field initializers the constructor never runs here)
|
||||||
* — this exercises the exact same code that runs inside
|
* — this exercises the exact same code that runs inside
|
||||||
* `pollPlannerOverseer`/`start()` in production, not a reimplementation.
|
* `pollPlannerOverseer`/`start()` in production, not a reimplementation.
|
||||||
|
*
|
||||||
|
* FNXC:PlannerOversight 2026-07-11-00:00:
|
||||||
|
* FN-7840 removes the only real-tick producer of advisory merger awaiting-confirmation interventions, so this live-wiring harness now proves suppression at the source rather than request/resolution emission for an unreachable advisory pending-confirmation path.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
@@ -200,81 +203,36 @@ describe("FN-7551 — overseer decision points populate the intervention timelin
|
|||||||
expect(steeringEntry?.stage).toBe("reviewer");
|
expect(steeringEntry?.stage).toBe("reviewer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("merger/pull-request confirmation-required decision emits a request-confirmation entry; approving it emits a resolution entry with 'succeeded'", async () => {
|
// FNXC:PlannerOversight 2026-07-11-00:00:
|
||||||
|
// FN-7840 regression coverage: real tick() wiring reaches merger/pull-request oversight only when the same `allowsAutoMergeProcessing` predicate says auto-merge will proceed unattended. That advisory state must be silent — no pending confirmation, no request-confirmation intervention, and no merge-checkpoint steering comment/badge source.
|
||||||
|
it("suppresses advisory merger confirmations under active auto-merge policy", async () => {
|
||||||
const task = await seedTask("in-review");
|
const task = await seedTask("in-review");
|
||||||
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
||||||
await monitor.observeTask(task, "autonomous"); // merger stage (plain in-review, no PR/reviewState)
|
await monitor.observeTask(task, "autonomous"); // merger stage (plain in-review, no PR/reviewState)
|
||||||
|
|
||||||
const decision = await controller.tick(task);
|
const firstDecision = await controller.tick(task, { settings: { autoMerge: true } });
|
||||||
expect(decision?.requiresConfirmation).toBe(true);
|
expect(firstDecision?.action).toBe("none");
|
||||||
|
expect(firstDecision?.requiresConfirmation).toBe(false);
|
||||||
|
expect(firstDecision?.reason).toMatch(/automatically/i);
|
||||||
|
|
||||||
let timeline = getPlannerInterventionTimeline(store, task.id);
|
const secondDecision = await controller.tick(task, { settings: { autoMerge: true } });
|
||||||
const requestEntry = timeline.find((e) => e.action === "request-confirmation");
|
expect(secondDecision?.action).toBe("none");
|
||||||
expect(requestEntry).toBeTruthy();
|
expect(secondDecision?.requiresConfirmation).toBe(false);
|
||||||
expect(requestEntry?.outcome).toBe("awaiting-confirmation");
|
|
||||||
|
|
||||||
const pending = controller.getPendingConfirmations(task.id);
|
|
||||||
expect(pending).toHaveLength(1);
|
|
||||||
|
|
||||||
await controller.resolveConfirmation(task.id, pending[0].requestId, "approved", "test-user");
|
|
||||||
|
|
||||||
timeline = getPlannerInterventionTimeline(store, task.id);
|
|
||||||
const confirmationEntries = timeline.filter((e) => e.action === "request-confirmation");
|
|
||||||
expect(confirmationEntries.length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(confirmationEntries.some((e) => e.outcome === "succeeded")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FN-7692: the recorded `overseer:intervention` reason for a merger/
|
|
||||||
// pull-request confirmation must accurately reflect whether auto-merge will
|
|
||||||
// proceed unattended (advisory copy) or genuinely requires a human approval
|
|
||||||
// (blocking copy) — reproducing the FN-7689 scenario where the timeline
|
|
||||||
// claimed a hard block that the merge sailed past unattended. A pending
|
|
||||||
// confirmation must still be recorded either way (no dispatch change).
|
|
||||||
it("records accurate advisory copy (not a false hard-block claim) when ctx.settings.autoMerge is truthy for an in-review merger task", async () => {
|
|
||||||
const task = await seedTask("in-review");
|
|
||||||
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
|
||||||
await monitor.observeTask(task, "autonomous"); // merger stage (plain in-review, no PR/reviewState)
|
|
||||||
|
|
||||||
const decision = await controller.tick(task, { settings: { autoMerge: true } });
|
|
||||||
expect(decision?.requiresConfirmation).toBe(true);
|
|
||||||
expect(decision?.action).toBe("await_confirmation");
|
|
||||||
|
|
||||||
const timeline = getPlannerInterventionTimeline(store, task.id);
|
const timeline = getPlannerInterventionTimeline(store, task.id);
|
||||||
const requestEntry = timeline.find((e) => e.action === "request-confirmation");
|
expect(timeline.filter((e) => e.action === "request-confirmation")).toHaveLength(0);
|
||||||
expect(requestEntry).toBeTruthy();
|
expect(controller.getPendingConfirmations(task.id)).toHaveLength(0);
|
||||||
expect(requestEntry?.outcome).toBe("awaiting-confirmation");
|
|
||||||
expect(requestEntry?.reason).not.toMatch(/requires explicit confirmation before .* may run/);
|
|
||||||
expect(requestEntry?.reason).toMatch(/automatically/i);
|
|
||||||
|
|
||||||
// A pending confirmation is still recorded — no dispatch, no behavior change.
|
const refreshedTask = await store.getTask(task.id);
|
||||||
expect(controller.getPendingConfirmations(task.id)).toHaveLength(1);
|
const allCommentText = [
|
||||||
|
...(refreshedTask?.comments ?? []).map((comment) => comment.text),
|
||||||
|
...(refreshedTask?.steeringComments ?? []).map((comment) => comment.text),
|
||||||
|
];
|
||||||
|
expect(allCommentText.some((text) => text.includes("[planner-oversight] merge checkpoint"))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: a genuinely-blocking `autoMergeWillProceed: false` state is
|
// FNXC:PlannerOversight 2026-07-11-00:00:
|
||||||
// exercised directly against the pure `decidePlannerRecovery` in
|
// FN-7840 makes the old request/approve/deny confirmation-resolution tests unreachable through the public real tick() seam: auto-merge true returns `none`, while auto-merge false is withheld by `evaluateOverseerHumanControl` before `decidePlannerRecovery`. The pure false/undefined safety-valve contract remains covered in @fusion/core; this engine harness documents the production reachability instead of injecting private pending-confirmation state.
|
||||||
// `planner-recovery.test.ts` (@fusion/core). It is NOT independently
|
|
||||||
// reachable through this engine's real `tick()` wiring: `allowsAutoMerge
|
|
||||||
// Processing(task, settings) === false` is exactly the condition
|
|
||||||
// `evaluateOverseerHumanControl` uses to withhold ALL oversight action
|
|
||||||
// (including confirmation recording) BEFORE `decidePlannerRecovery` is ever
|
|
||||||
// called — so a real merger/pull-request confirmation entry can only ever
|
|
||||||
// be recorded when auto-merge WILL proceed. This is documented here rather
|
|
||||||
// than asserted redundantly to avoid a test that can never legitimately fail.
|
|
||||||
|
|
||||||
it("denying a confirmation resolution emits a 'skipped' outcome entry", async () => {
|
|
||||||
const task = await seedTask("in-review");
|
|
||||||
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
|
||||||
await monitor.observeTask(task, "autonomous");
|
|
||||||
await controller.tick(task);
|
|
||||||
const pending = controller.getPendingConfirmations(task.id);
|
|
||||||
expect(pending).toHaveLength(1);
|
|
||||||
|
|
||||||
await controller.resolveConfirmation(task.id, pending[0].requestId, "denied");
|
|
||||||
|
|
||||||
const timeline = getPlannerInterventionTimeline(store, task.id);
|
|
||||||
const confirmationEntries = timeline.filter((e) => e.action === "request-confirmation");
|
|
||||||
expect(confirmationEntries.some((e) => e.outcome === "skipped")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("bounded-recovery exhaustion emits exactly one escalate entry across repeated polls of the same exhausted stage", async () => {
|
it("bounded-recovery exhaustion emits exactly one escalate entry across repeated polls of the same exhausted stage", async () => {
|
||||||
const task = await seedTask("in-progress");
|
const task = await seedTask("in-progress");
|
||||||
@@ -297,45 +255,6 @@ describe("FN-7551 — overseer decision points populate the intervention timelin
|
|||||||
expect(escalations[0].outcome).toBe("failed");
|
expect(escalations[0].outcome).toBe("failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exhaustion actually reached through real tick()s (three denials) emits escalate exactly once thereafter", async () => {
|
|
||||||
const task = await seedTask("in-review");
|
|
||||||
// FNXC:PlannerOversight 2026-07-07-08:50:
|
|
||||||
// FN-7577 (2026-07-05) made PlannerRecoveryController.tick() drop the
|
|
||||||
// bounded-recovery attempt budget whenever a watched stage reports a
|
|
||||||
// HEALTHY/human-wait signal (progressing/complete/awaiting-human). A plain
|
|
||||||
// in-review task derives a "progressing" merger signal, so denials never
|
|
||||||
// accumulate through the real monitor wiring and exhaustion can't be
|
|
||||||
// reached — the 4th tick kept returning await_confirmation instead of the
|
|
||||||
// exhausted "none". This test's invariant is escalation DEDUP after
|
|
||||||
// exhaustion reached via real tick()s, so wire the controller to a PROBLEM
|
|
||||||
// (failed) merger snapshot (controllerWithSnapshot — the documented seam for
|
|
||||||
// branches the monitor's own signal-derivation cannot produce) whose signal
|
|
||||||
// holds the attempt budget, then drive three real denials to reach genuine
|
|
||||||
// exhaustion. The merger stage still surfaces await_confirmation regardless
|
|
||||||
// of signal (decidePlannerRecovery), so requiresConfirmation stays asserted.
|
|
||||||
const { controllerWithSnapshot, emitEscalation } = wireRealEngineOverseer(store);
|
|
||||||
const controller = controllerWithSnapshot(observation({ taskId: task.id, stage: "merger", signal: "failed" }));
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i += 1) {
|
|
||||||
const decision = await controller.tick(task);
|
|
||||||
expect(decision?.requiresConfirmation).toBe(true);
|
|
||||||
const pending = controller.getPendingConfirmations(task.id);
|
|
||||||
await controller.resolveConfirmation(task.id, pending[0].requestId, "denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalDecision = await controller.tick(task);
|
|
||||||
expect(finalDecision?.action).toBe("none");
|
|
||||||
expect(finalDecision?.exhausted).toBe(true);
|
|
||||||
|
|
||||||
// The poll wires escalation emission itself (project-engine.ts), so drive
|
|
||||||
// it explicitly here with the real decision object, twice, to prove the dedup.
|
|
||||||
emitEscalation(task.id, finalDecision!);
|
|
||||||
emitEscalation(task.id, finalDecision!);
|
|
||||||
|
|
||||||
const timeline = getPlannerInterventionTimeline(store, task.id);
|
|
||||||
expect(timeline.filter((e) => e.action === "escalate")).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("oversight level 'off' and a human-control-withheld (userPaused) task never emit any steering/retry/fix/confirmation/escalation entry", async () => {
|
it("oversight level 'off' and a human-control-withheld (userPaused) task never emit any steering/retry/fix/confirmation/escalation entry", async () => {
|
||||||
const task = await seedTask("in-review");
|
const task = await seedTask("in-review");
|
||||||
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
const { monitor, controllerFromMonitor: controller } = wireRealEngineOverseer(store);
|
||||||
|
|||||||
Reference in New Issue
Block a user