From 0b187b9f62f1e1a7a67a74d1abb5a9fb34e19dc2 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 20:12:00 +0200 Subject: [PATCH] fix(headless): remove legacy v1 fallback path --- src/headless.ts | 38 +++++++++++-------------- src/tests/headless-v2-migration.test.ts | 22 +++----------- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/headless.ts b/src/headless.ts index 2a069e0e6..dc924b79a 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -105,7 +105,6 @@ import { } from "./resources/extensions/sf/trace-collector.js"; const HEADLESS_HEARTBEAT_INTERVAL_MS = 60_000; -const HEADLESS_LEGACY_FALLBACK_ENV = "SF_HEADLESS_ALLOW_V1_FALLBACK"; interface HeadlessTimeoutSolverEvalRecord { runId: string; @@ -1983,27 +1982,22 @@ async function runHeadlessOnce( process.exit(1); } - // v2 protocol negotiation — attempt init for structured completion events - let _v2Enabled = false; - try { - await client.init({ clientId: "sf-headless" }); - _v2Enabled = true; - } catch (initErr) { - const reason = initErr instanceof Error ? initErr.message : String(initErr); - if (process.env[HEADLESS_LEGACY_FALLBACK_ENV] !== "1") { - process.stderr.write( - `[headless] v2 init failed (${reason}); refusing legacy v1 string-matching fallback. Set ${HEADLESS_LEGACY_FALLBACK_ENV}=1 only for explicit recovery.\n`, - ); - await client.stop().catch(() => {}); - if (timeoutTimer) clearTimeout(timeoutTimer); - if (idleTimer) clearTimeout(idleTimer); - if (heartbeatTimer) clearInterval(heartbeatTimer); - process.exit(EXIT_ERROR); - } - process.stderr.write( - `[headless] Warning: v2 init failed (${reason}), falling back to v1 string-matching because ${HEADLESS_LEGACY_FALLBACK_ENV}=1\n`, - ); - } + // v2 protocol negotiation — attempt init for structured completion events + let _v2Enabled = false; + try { + await client.init({ clientId: "sf-headless" }); + _v2Enabled = true; + } catch (initErr) { + const reason = initErr instanceof Error ? initErr.message : String(initErr); + process.stderr.write( + `[headless] v2 init failed (${reason}); refusing legacy v1 string-matching fallback.\n`, + ); + await client.stop().catch(() => {}); + if (timeoutTimer) clearTimeout(timeoutTimer); + if (idleTimer) clearTimeout(idleTimer); + if (heartbeatTimer) clearInterval(heartbeatTimer); + process.exit(EXIT_ERROR); + } clientStarted = true; diff --git a/src/tests/headless-v2-migration.test.ts b/src/tests/headless-v2-migration.test.ts index 76a6d8db8..45e9a29a3 100644 --- a/src/tests/headless-v2-migration.test.ts +++ b/src/tests/headless-v2-migration.test.ts @@ -97,17 +97,13 @@ class MockRpcClient { async function negotiateV2ForTest( client: MockRpcClient, - allowLegacyFallback = false, -): Promise<"v2" | "legacy" | "fatal"> { +): Promise<"v2" | "fatal"> { try { await client.init({ clientId: "sf-headless" }); return "v2"; } catch { - if (!allowLegacyFallback) { - await client.stop(); - return "fatal"; - } - return "legacy"; + await client.stop(); + return "fatal"; } } @@ -526,7 +522,7 @@ test("v2 init success sets v2Enabled", async () => { await client.init({ clientId: "sf-headless" }); v2Enabled = true; } catch { - // fall back to v1 + await client.stop(); } assert.equal(client.initCalled, true); @@ -543,16 +539,6 @@ test("v2 init failure is fatal by default", async () => { assert.equal(mode, "fatal"); }); -test("v2 init failure uses v1 only with explicit legacy fallback opt-in", async () => { - const client = new MockRpcClient(); - client.initShouldFail = true; - const mode = await negotiateV2ForTest(client, true); - - assert.equal(client.initCalled, true); - assert.equal(client.stopCalled, false); - assert.equal(mode, "legacy"); -}); - // ─── injector adapter ─────────────────────────────────────────────────────── test("injector adapter parses serialized JSONL and calls sendUIResponse", () => {