From f22ebca62ace0b970671d94c3b8f97e6335dfe3b Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Tue, 25 Aug 2026 05:38:46 +0000 Subject: [PATCH] test(FN-WF): treat a revoked merge gate as the deferral it is, and stop the engine before clearing globals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two harness correctness fixes, both found chasing the S05 flake. REVOKED GATE. `MergeGateRevokedError` escaped the graph dispatch and failed the scenario. It is a DEFERRAL, not a failure — FN-180's own contract, and the reason it carries a dedicated error type so callers cannot convert a gate lost mid-merge into a retry or a failed park. It fires when a merge admitted on an earlier turn reaches its ref-advance fence after a REVISE has already returned the card to in-progress: the engine refusing correctly while the driver races it. The dispatch now swallows ONLY that type, by NAME rather than by import, because importing merger-errors.js would break the pre-FN-180 differential run the harness self-test enforces. TEARDOWN ORDER. `dispose` reset the shared mock registry and cleared `activeSessionRegistry` BEFORE stopping the engine, stranding in-flight sessions on default scripts mid-teardown and hiding them from the liveness checks the stop path consults. Both are process-global, so the damage landed on whichever file ran next. Together these took S05 on builtin:coding-ideas-v2 from failing 3 runs out of 3 to 2 out of 6 — a real reduction, and not zero, so it is still not shipped. S05 stays on its original workflows, where the lane is green four consecutive full runs: 122.4s, 126.6s, 124.0s, 116.9s of the 150s budget. builtin:coding-ideas-v2 remains at 18 of 19 scenarios plus the multi-repository workspace drive. --- .../pipeline-smoke/_pipeline-harness.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/__tests__/pipeline-smoke/_pipeline-harness.ts b/packages/engine/src/__tests__/pipeline-smoke/_pipeline-harness.ts index 9b98e9134a..81e34051ae 100644 --- a/packages/engine/src/__tests__/pipeline-smoke/_pipeline-harness.ts +++ b/packages/engine/src/__tests__/pipeline-smoke/_pipeline-harness.ts @@ -354,11 +354,18 @@ export class PipelineSmokeHarness { async dispose(): Promise { try { + /* + FNXC:PipelineSmoke 2026-08-24-23:10: + Stop the engine BEFORE clearing the shared registries. Resetting the mock registry while + sessions are still in flight strands them on default scripts mid-teardown, and clearing + `activeSessionRegistry` first hides those sessions from the liveness checks the stop path + consults. Both are process-global, so the damage lands on whichever file runs next. + */ + await this.engine.stop(); activeSessionRegistry.clear(); this.mockScriptStates.clear(); this.scriptedBranches.clear(); resetMockScripts(); - await this.engine.stop(); this.guard.restore(); await this.centralCore.close(); // The shared PG harness owns taskStore.asyncLayer and closes it after this file. @@ -1218,7 +1225,21 @@ export class PipelineSmokeHarness { throw new Error("Pipeline smoke did not resolve the executor's authoritative workflow seams."); } this.authoritativeSeamsObserved = true; - await executor.executeAuthoritativeGraph(task); + /* + FNXC:PipelineSmoke 2026-08-24-23:10: + A revoked merge gate is a DEFERRAL, not a failure. That is FN-180's own contract and the reason + it carries a dedicated error type, so callers cannot convert a gate lost mid-merge into a retry + or a failed park. It surfaces here when a merge admitted on an earlier turn reaches its + ref-advance fence after a REVISE has already returned the card to in-progress: the engine is + refusing correctly, and the driver must simply take another turn. Swallowing ONLY this type + keeps every other merge failure fatal to the scenario. + */ + try { + await executor.executeAuthoritativeGraph(task); + } catch (error) { + /* Detected by NAME: importing merger-errors.js would break the pre-FN-180 differential run. */ + if ((error as { name?: string } | undefined)?.name !== "MergeGateRevokedError") throw error; + } const after = await this.freshTask(taskId); if (after.mergeDetails?.mergeConfirmed === true) this.manualHoldTaskIds.delete(taskId); return after;