From a3f699f623734247762475a4e42872e7c46431c0 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Mon, 21 Sep 2026 20:50:57 +0900
Subject: [PATCH 01/17] docs: harden branch content classification against
renames
---
devlog/_fin/260904_repo_hygiene_campaign/010_method.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/010_method.md b/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
index 773b6db6494..de498d0c0fb 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
@@ -7,7 +7,7 @@ A local branch is deletable when at least one holds, and no guard fires.
```
T1 ancestry git merge-base --is-ancestor
origin/dev
T2 patch-equiv git cherry origin/dev
-> no '+' lines
-T3 content paths = git diff --name-only origin/dev...
+T3 content paths = git diff --no-renames --name-only origin/dev...
git diff --name-only origin/dev
-- -> empty
T4 scratch branch name encodes a PR number whose state is MERGED or CLOSED
AND the name matches the scratch prefix set
@@ -22,6 +22,13 @@ report "unmerged" for work that is fully shipped. T3 asks the only question that
is actually load-bearing — is there any difference left in the files this branch
claims to change.
+Rename detection must be disabled while collecting that path set. Otherwise a
+rename contributes only its destination: if `dev` independently contains the
+same destination but retains the source, the restricted second diff is empty
+even though the complete tip trees differ. `--no-renames` emits both the deleted
+source and added destination, so the source-side difference prevents a false
+LANDED verdict.
+
T4 is deliberately narrow. It fires only for throwaway prefixes
(`pr*`, `rb-`, `jrb-`, `mtp/`, `big-`, `cf-`, `ocx-`, `wip/`, `backup/`,
`candidate`, `cursor-`, `midstream`) created by earlier review and rebase runs,
From a4d343c246037595ac1032a540834bb5ce2ef453 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Tue, 22 Sep 2026 06:25:49 +0900
Subject: [PATCH 02/17] docs: date the no-renames correction and align sibling
commands
---
.../260904_repo_hygiene_campaign/000_plan.md | 8 ++++----
.../260904_repo_hygiene_campaign/010_method.md | 18 +++++++++++-------
.../100_pr_verdicts.md | 7 ++++---
3 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md b/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
index f7f030084a9..338920eed06 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
@@ -22,10 +22,10 @@ Every branch was scored on four independent axes rather than by name:
1. `git merge-base --is-ancestor
origin/dev` — plain ancestry.
2. `git cherry origin/dev
` — patch-equivalence, which catches rebases.
3. Content landing — the files the branch touches
- (`git diff --name-only origin/dev...
`) are compared two-dot against
- `origin/dev` restricted to exactly those paths. Zero remaining difference
- means the branch's content is already on `dev` even though a squash merge
- destroyed its commit identity.
+ (`git diff --no-renames --name-only origin/dev...
`) are compared two-dot
+ against `origin/dev` restricted to exactly those paths. Zero remaining
+ difference means the branch's content is already on `dev` even though a
+ squash merge destroyed its commit identity.
4. Exact reference matching against live GitHub state: open-PR head refs,
worktree-backing refs, and the PR number a scratch branch was cut for.
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/010_method.md b/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
index de498d0c0fb..c8e98858bc5 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/010_method.md
@@ -8,7 +8,7 @@ A local branch is deletable when at least one holds, and no guard fires.
T1 ancestry git merge-base --is-ancestor
origin/dev
T2 patch-equiv git cherry origin/dev
-> no '+' lines
T3 content paths = git diff --no-renames --name-only origin/dev...
- git diff --name-only origin/dev
-- -> empty
+ git diff --no-renames --name-only origin/dev
-- -> empty
T4 scratch branch name encodes a PR number whose state is MERGED or CLOSED
AND the name matches the scratch prefix set
AND the number is a WHOLE numeric token of the branch name
@@ -22,12 +22,16 @@ report "unmerged" for work that is fully shipped. T3 asks the only question that
is actually load-bearing — is there any difference left in the files this branch
claims to change.
-Rename detection must be disabled while collecting that path set. Otherwise a
-rename contributes only its destination: if `dev` independently contains the
-same destination but retains the source, the restricted second diff is empty
-even though the complete tip trees differ. `--no-renames` emits both the deleted
-source and added destination, so the source-side difference prevents a false
-LANDED verdict.
+Correction, 2026-09-21: the 71 deletions recorded below ran the listing command
+without `--no-renames`. Rename detection must be disabled while collecting that
+path set, and any rerun after this date should use the form shown above.
+Otherwise a rename contributes only its destination: if `dev` independently
+contains the same destination but retains the source, the restricted second
+diff is empty even though the complete tip trees differ. `--no-renames` emits
+both the deleted source and added destination, so the source-side difference
+prevents a false LANDED verdict. No wrongly-LANDED branch has been identified
+from the earlier run; this is a preventive correction for the next sweep, not a
+measured incident.
T4 is deliberately narrow. It fires only for throwaway prefixes
(`pr*`, `rb-`, `jrb-`, `mtp/`, `big-`, `cf-`, `ocx-`, `wip/`, `backup/`,
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md b/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
index bf8e5d701c0..8990abe5a3e 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
@@ -2,9 +2,10 @@
Full classification of the 53 pull requests open when the campaign started.
Method: fetch each PR head, take the files it touches
-(`git diff --name-only origin/dev...`), then compare those exact paths
-two-dot against `origin/dev`. Remaining differences mean the work has not
-landed.
+(`git diff --no-renames --name-only origin/dev...`), then compare those
+exact paths two-dot against `origin/dev`. Remaining differences mean the work
+has not landed. (2026-09-21: `--no-renames` added so a rename cannot hide the
+deleted source side from the path set; see 010_method.md.)
## Closed
From 9128ba311ed849767900a634ed0c46da9677cc3e Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:27:49 +0900
Subject: [PATCH 03/17] test(oauth): exercise configured generic pool
validators
---
.../account-pool-management-api.test.ts | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/tests/server/account-pool-management-api.test.ts b/tests/server/account-pool-management-api.test.ts
index 1c66163c87c..2bb6f1b832d 100644
--- a/tests/server/account-pool-management-api.test.ts
+++ b/tests/server/account-pool-management-api.test.ts
@@ -615,6 +615,17 @@ describe("legacy pool contract goldens (#wp5)", () => {
const resp = await handleCodexAuthAPI(req, new URL(req.url), makeCodexConfig());
return resp!.status;
};
+ const previousHome = process.env.OPENCODEX_HOME;
+ const testDir = mkdtempSync(join(tmpdir(), "ocx-pool-validator-"));
+ process.env.OPENCODEX_HOME = testDir;
+ saveConfig({
+ port: 0,
+ hostname: "127.0.0.1",
+ defaultProvider: "google-antigravity",
+ providers: {
+ "google-antigravity": { adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authMode: "oauth" },
+ },
+ } as OcxConfig);
const server = startServer(0);
try {
const oauth = async (payload: Record) => {
@@ -623,11 +634,14 @@ describe("legacy pool contract goldens (#wp5)", () => {
});
return res.status;
};
- for (const strategy of ["weighted", "", 3, null]) {
+ for (const strategy of ["weighted", "", 3]) {
expect(await codex({ strategy })).toBe(400);
expect(await oauth({ provider: "anthropic", strategy })).toBe(400);
expect(await oauth({ provider: "google-antigravity", strategy })).toBe(400);
}
+ expect(await codex({ strategy: null })).toBe(400);
+ expect(await oauth({ provider: "anthropic", strategy: null })).toBe(400);
+ expect(await oauth({ provider: "google-antigravity", strategy: null })).toBe(200);
// 0 and 101 sit just outside the shared bound; 1 and 100 are the edges that must pass.
for (const stickyLimit of [0, 101, 1.5]) {
expect(await codex({ stickyLimit })).toBe(400);
@@ -639,6 +653,9 @@ describe("legacy pool contract goldens (#wp5)", () => {
}
} finally {
await server.stop(true);
+ if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
+ else process.env.OPENCODEX_HOME = previousHome;
+ removeTreeWithRetry(testDir);
}
});
From f8dfca4b553b2e6a81491b646f027b43d8995891 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Tue, 22 Sep 2026 06:50:16 +0900
Subject: [PATCH 04/17] test(oauth): prove the generic null-strategy clear and
harden test teardown
---
.../account-pool-management-api.test.ts | 42 +++++++++++++------
1 file changed, 29 insertions(+), 13 deletions(-)
diff --git a/tests/server/account-pool-management-api.test.ts b/tests/server/account-pool-management-api.test.ts
index 2bb6f1b832d..1dfb3728239 100644
--- a/tests/server/account-pool-management-api.test.ts
+++ b/tests/server/account-pool-management-api.test.ts
@@ -603,11 +603,15 @@ describe("legacy pool contract goldens (#wp5)", () => {
}
});
- test("a bad strategy and a bad stickyLimit are rejected identically on every kind", async () => {
+ test("a bad strategy and a bad stickyLimit are rejected identically on every kind, with one null exception", async () => {
// One validator, three adapters. The kinds keep their own request and response shapes --
// that is what the goldens above pin -- but the VALUE rules are now a single implementation,
// so "quota, round-robin, fill-first" and the 1..100 sticky bound cannot drift apart per
// kind. Before this, the generic kind carried a private copy of both.
+ // ONE deliberate exception: `strategy: null` is not a bad value on the legacy generic
+ // endpoint -- it clears the saved strategy and answers 200. Codex, Anthropic, and the
+ // unified /api/pool/settings route all reject the same null with 400. A future reader
+ // who sees the 200 must not "fix" it back without deciding that contract first.
const codex = async (payload: Record) => {
const req = new Request("http://localhost/api/codex-auth/pool-strategy", {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
@@ -617,23 +621,30 @@ describe("legacy pool contract goldens (#wp5)", () => {
};
const previousHome = process.env.OPENCODEX_HOME;
const testDir = mkdtempSync(join(tmpdir(), "ocx-pool-validator-"));
- process.env.OPENCODEX_HOME = testDir;
- saveConfig({
- port: 0,
- hostname: "127.0.0.1",
- defaultProvider: "google-antigravity",
- providers: {
- "google-antigravity": { adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authMode: "oauth" },
- },
- } as OcxConfig);
- const server = startServer(0);
+ let server: ReturnType | undefined;
try {
+ process.env.OPENCODEX_HOME = testDir;
+ saveConfig({
+ port: 0,
+ hostname: "127.0.0.1",
+ defaultProvider: "google-antigravity",
+ providers: {
+ "google-antigravity": { adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authMode: "oauth" },
+ },
+ } as OcxConfig);
+ server = startServer(0);
const oauth = async (payload: Record) => {
const res = await fetch(new URL("/api/oauth/accounts/pool", server.url), {
method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(payload),
});
return res.status;
};
+ const oauthJson = async (payload: Record) => {
+ const res = await fetch(new URL("/api/oauth/accounts/pool", server.url), {
+ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(payload),
+ });
+ return { status: res.status, body: await res.json() as { strategy?: unknown } };
+ };
for (const strategy of ["weighted", "", 3]) {
expect(await codex({ strategy })).toBe(400);
expect(await oauth({ provider: "anthropic", strategy })).toBe(400);
@@ -641,7 +652,12 @@ describe("legacy pool contract goldens (#wp5)", () => {
}
expect(await codex({ strategy: null })).toBe(400);
expect(await oauth({ provider: "anthropic", strategy: null })).toBe(400);
- expect(await oauth({ provider: "google-antigravity", strategy: null })).toBe(200);
+ // The generic legacy contract: null clears the saved strategy. Prove the clear actually
+ // happened -- a 200 that left the old strategy in place would be a silent no-op.
+ expect(await oauth({ provider: "google-antigravity", strategy: "round-robin" })).toBe(200);
+ const cleared = await oauthJson({ provider: "google-antigravity", strategy: null });
+ expect(cleared.status).toBe(200);
+ expect(cleared.body.strategy ?? null).toBeNull();
// 0 and 101 sit just outside the shared bound; 1 and 100 are the edges that must pass.
for (const stickyLimit of [0, 101, 1.5]) {
expect(await codex({ stickyLimit })).toBe(400);
@@ -652,7 +668,7 @@ describe("legacy pool contract goldens (#wp5)", () => {
expect(await codex({ stickyLimit })).toBe(200);
}
} finally {
- await server.stop(true);
+ await server?.stop(true);
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
removeTreeWithRetry(testDir);
From 0cc9ab566cd396357a823ffa92f7709889827f04 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Tue, 22 Sep 2026 13:56:15 +0900
Subject: [PATCH 05/17] test(oauth): require the strategy property in the
cleared response
---
tests/server/account-pool-management-api.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/server/account-pool-management-api.test.ts b/tests/server/account-pool-management-api.test.ts
index 1dfb3728239..fa52f8e076f 100644
--- a/tests/server/account-pool-management-api.test.ts
+++ b/tests/server/account-pool-management-api.test.ts
@@ -657,7 +657,7 @@ describe("legacy pool contract goldens (#wp5)", () => {
expect(await oauth({ provider: "google-antigravity", strategy: "round-robin" })).toBe(200);
const cleared = await oauthJson({ provider: "google-antigravity", strategy: null });
expect(cleared.status).toBe(200);
- expect(cleared.body.strategy ?? null).toBeNull();
+ expect(cleared.body).toHaveProperty("strategy", null);
// 0 and 101 sit just outside the shared bound; 1 and 100 are the edges that must pass.
for (const stickyLimit of [0, 101, 1.5]) {
expect(await codex({ stickyLimit })).toBe(400);
From 48a8725d5360fff96a32424dee690afe57220a18 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 20 Sep 2026 12:21:39 +0900
Subject: [PATCH 06/17] fix(integrations): harden Raycast defaults probe
---
src/integrations/raycast-detect.ts | 21 +++++++++++++++++----
tests/clients/raycast-detect.test.ts | 21 ++++++++++++++++++++-
2 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/src/integrations/raycast-detect.ts b/src/integrations/raycast-detect.ts
index 7ae70edf461..1fafd992ff9 100644
--- a/src/integrations/raycast-detect.ts
+++ b/src/integrations/raycast-detect.ts
@@ -44,10 +44,19 @@ export interface RaycastDetectDeps {
*/
const RAYCAST_DEFAULTS_DOMAIN = "com.raycast.macos.v1";
const RAYCAST_SUBSCRIPTION_KEY = "subscriptions_active";
+const DEFAULTS_PATH = "/usr/bin/defaults";
+const DEFAULTS_TIMEOUT_MS = 2_000;
-export function realRaycastDetectDeps(): RaycastDetectDeps {
+interface RealRaycastDetectRuntime {
+ platform?: string;
+ spawnSync?: typeof Bun.spawnSync;
+}
+
+export function realRaycastDetectDeps(runtime: RealRaycastDetectRuntime = {}): RaycastDetectDeps {
+ const platform = runtime.platform ?? process.platform;
+ const spawnSync = runtime.spawnSync ?? Bun.spawnSync;
return {
- platform: process.platform,
+ platform,
homedir: homedir(),
env: process.env,
exists: path => {
@@ -59,9 +68,13 @@ export function realRaycastDetectDeps(): RaycastDetectDeps {
},
readDefault: (domain, key) => {
// `defaults` is macOS-only; elsewhere the plan is simply unknown.
- if (process.platform !== "darwin") return null;
+ if (platform !== "darwin") return null;
try {
- const result = Bun.spawnSync(["defaults", "read", domain, key], { stdout: "pipe", stderr: "pipe" });
+ const result = spawnSync([DEFAULTS_PATH, "read", domain, key], {
+ stdout: "pipe",
+ stderr: "pipe",
+ timeout: DEFAULTS_TIMEOUT_MS,
+ });
if (result.exitCode !== 0) return null;
return result.stdout.toString().trim();
} catch {
diff --git a/tests/clients/raycast-detect.test.ts b/tests/clients/raycast-detect.test.ts
index 4e4268b29b3..c2865a5fa76 100644
--- a/tests/clients/raycast-detect.test.ts
+++ b/tests/clients/raycast-detect.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, test } from "bun:test";
-import { detectRaycast, type RaycastDetectDeps } from "../../src/integrations/raycast-detect";
+import {
+ detectRaycast,
+ realRaycastDetectDeps,
+ type RaycastDetectDeps,
+} from "../../src/integrations/raycast-detect";
/**
* Stubbed deps only. The real detector spawns `defaults` and reads the
@@ -29,6 +33,21 @@ function fakeDeps(
}
describe("detectRaycast", () => {
+ test("the macOS preference probe uses the system binary with a bounded runtime", () => {
+ let invocation: { command: string[]; timeout?: number } | undefined;
+ const spawnSync = ((command: string[], options: { timeout?: number }) => {
+ invocation = { command, timeout: options.timeout };
+ return { exitCode: 0, stdout: Buffer.from("1") };
+ }) as typeof Bun.spawnSync;
+
+ const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
+ expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBe("1");
+ expect(invocation).toEqual({
+ command: ["/usr/bin/defaults", "read", "com.raycast.macos.v1", "subscriptions_active"],
+ timeout: 2_000,
+ });
+ });
+
test("darwin: a Pro subscription, the app bundle and the revealed ai folder", () => {
const deps = fakeDeps("darwin", ["/Applications/Raycast.app", "/home/u/.config/raycast/ai"], { defaultValue: "1" });
expect(detectRaycast(deps)).toEqual({
From 3ea46f08188061b6e49c9ec102eebe6cfdf7e014 Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Sun, 20 Sep 2026 14:55:02 +0900
Subject: [PATCH 07/17] test(integrations): cover killed defaults probe in
Raycast detection
---
src/integrations/raycast-detect.ts | 2 ++
tests/clients/raycast-detect.test.ts | 8 ++++++++
2 files changed, 10 insertions(+)
diff --git a/src/integrations/raycast-detect.ts b/src/integrations/raycast-detect.ts
index 1fafd992ff9..e0ef3191f6d 100644
--- a/src/integrations/raycast-detect.ts
+++ b/src/integrations/raycast-detect.ts
@@ -75,6 +75,8 @@ export function realRaycastDetectDeps(runtime: RealRaycastDetectRuntime = {}): R
stderr: "pipe",
timeout: DEFAULTS_TIMEOUT_MS,
});
+ // A timed-out or signal-killed probe reports exitCode === null; that
+ // and any non-zero exit mean the preference was not read.
if (result.exitCode !== 0) return null;
return result.stdout.toString().trim();
} catch {
diff --git a/tests/clients/raycast-detect.test.ts b/tests/clients/raycast-detect.test.ts
index c2865a5fa76..afdc3dbeb4f 100644
--- a/tests/clients/raycast-detect.test.ts
+++ b/tests/clients/raycast-detect.test.ts
@@ -74,6 +74,14 @@ describe("detectRaycast", () => {
expect(detectRaycast(fakeDeps("darwin", [], { defaultValue: "" })).plan).toBe("unknown");
});
+ test("darwin: a timed-out or killed defaults probe is unknown, not a false positive", () => {
+ // Bun reports exitCode === null when the 2s timeout kills the process.
+ const spawnSync = (() => ({ exitCode: null, stdout: Buffer.from("") })) as typeof Bun.spawnSync;
+ const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
+ expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBeNull();
+ expect(detectRaycast(deps).plan).toBe("unknown");
+ });
+
test("win32: LOCALAPPDATA\\Programs\\Raycast is the install path and the plan is unknown", () => {
const local = "C:\\Users\\u\\AppData\\Local";
const deps = fakeDeps("win32", [`${local}\\Programs\\Raycast`, "C:\\Users\\u\\.config\\raycast\\ai"], {
From 88953124e1bedba63fbc7e3bfecebd606765156a Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Mon, 21 Sep 2026 17:10:19 +0900
Subject: [PATCH 08/17] fix(reauth): stop polling terminal unknown flows
---
gui/src/components/use-main-device-reauth.ts | 6 ++++++
gui/tests/main-device-reauth-ownership.test.tsx | 11 +++++++++++
2 files changed, 17 insertions(+)
diff --git a/gui/src/components/use-main-device-reauth.ts b/gui/src/components/use-main-device-reauth.ts
index 626e004a5c1..44d18b9645f 100644
--- a/gui/src/components/use-main-device-reauth.ts
+++ b/gui/src/components/use-main-device-reauth.ts
@@ -181,6 +181,12 @@ export function useMainDeviceReauth(apiBase: string, onCompleted: () => void) {
const dto = await res.json().catch(() => ({})) as FlowDto;
if (!isCurrent() || flowRef.current !== flowId) return;
if (!res.ok) {
+ if (res.status === 404 && dto.code === "unknown_flow") {
+ stopPolling();
+ flowRef.current = null;
+ setState({ phase: "failed", code: "request_failed" });
+ return;
+ }
// Ownership continues from the Cancel click, including while DELETE
// is unresolved. Never offer a replacement POST during that window,
// and keep the existing poll cadence so a later terminal status remains observable.
diff --git a/gui/tests/main-device-reauth-ownership.test.tsx b/gui/tests/main-device-reauth-ownership.test.tsx
index 917cb5f80d1..e90315560b3 100644
--- a/gui/tests/main-device-reauth-ownership.test.tsx
+++ b/gui/tests/main-device-reauth-ownership.test.tsx
@@ -315,6 +315,17 @@ for (const failure of ["network", "http", "nonterminal"] as const) {
});
}
+test("unknown flow status stops polling after a failed cancellation", async () => {
+ await mount();
+ await beginFlow("A");
+ await invoke(() => hook.cancel());
+ await reply(take("DELETE", "A"), { code: "unavailable" }, 503);
+ await act(async () => { for (const wake of sleepers.splice(0)) wake(); });
+ await reply(take("GET", "A"), { code: "unknown_flow" }, 404);
+ expect(hook.state).toEqual({ phase: "failed", code: "request_failed" });
+ expect(sleepers).toHaveLength(0);
+});
+
test("two successful cancellation replies complete the same flow only once", async () => {
await mount();
await beginFlow("A");
From f6504dfde229499665103d004ee1c2984cb1efdb Mon Sep 17 00:00:00 2001
From: luvs01 <27862058+luvs01@users.noreply.github.com>
Date: Mon, 21 Sep 2026 03:58:22 +0900
Subject: [PATCH 09/17] fix(qoder): preserve offsets in scaffold scanning
---
src/adapters/qoder/scaffold-guard.ts | 45 ++++++++++++++------
tests/providers/qoder-scaffold-guard.test.ts | 27 ++++++++++++
2 files changed, 59 insertions(+), 13 deletions(-)
diff --git a/src/adapters/qoder/scaffold-guard.ts b/src/adapters/qoder/scaffold-guard.ts
index 8a1b6df6206..abc1a4613ef 100644
--- a/src/adapters/qoder/scaffold-guard.ts
+++ b/src/adapters/qoder/scaffold-guard.ts
@@ -54,11 +54,31 @@ const MAX_MARKER_LENGTH = Math.max(...ALL_MARKERS.map(marker => marker.length));
* refuse the turn. A stem running to the end of the buffer still counts: more text may be
* arriving, and reading it as prose is the one reading that could release the block body.
*/
-function reminderOpensHere(lowered: string, at: number): boolean {
- const after = lowered[at + REMINDER_OPEN.length];
+function reminderOpensHere(text: string, at: number): boolean {
+ const after = text[at + REMINDER_OPEN.length];
return after === undefined || /[\s/>]/.test(after);
}
+/**
+ * Find an ASCII marker without transforming `text`.
+ *
+ * Marker offsets must remain offsets into the original string. Unicode lowercasing can expand
+ * one code unit into several (for example, `İ` becomes `i` plus a combining dot), so an index
+ * obtained from `text.toLowerCase()` is unsafe to reuse with `text.slice()`.
+ */
+function indexOfMarker(text: string, marker: string, from = 0): number {
+ const last = text.length - marker.length;
+ outer: for (let at = Math.max(0, from); at <= last; at++) {
+ for (let offset = 0; offset < marker.length; offset++) {
+ const code = text.charCodeAt(at + offset);
+ const folded = code >= 65 && code <= 90 ? code + 32 : code;
+ if (folded !== marker.charCodeAt(offset)) continue outer;
+ }
+ return at;
+ }
+ return -1;
+}
+
/**
* Ceiling on a suppressed block before it is treated as unterminated.
*
@@ -80,9 +100,10 @@ export interface ScaffoldFilterResult {
function heldSuffixLength(text: string): number {
const limit = Math.min(MAX_MARKER_LENGTH - 1, text.length);
for (let length = limit; length > 0; length--) {
- const suffix = text.slice(text.length - length).toLowerCase();
for (const marker of ALL_MARKERS) {
- if (marker.length > length && marker.startsWith(suffix)) return length;
+ if (marker.length > length && indexOfMarker(text, marker.slice(0, length), text.length - length) >= 0) {
+ return length;
+ }
}
}
return 0;
@@ -114,7 +135,6 @@ export class QoderScaffoldFilter {
for (;;) {
if (this.mode === "suppress") {
const scan = this.suppressedTail + buffer;
- const scanned = scan.toLowerCase();
// Unwind nesting rather than ending at the first closer. A reminder containing another
// reminder would otherwise hand the outer block's remaining body — the MCP server list
// in the reported leak — to the client as the model's answer, with a successful
@@ -122,11 +142,11 @@ export class QoderScaffoldFilter {
let cursor = 0;
let close = -1;
for (;;) {
- const nextClose = scanned.indexOf(REMINDER_CLOSE, cursor);
+ const nextClose = indexOfMarker(scan, REMINDER_CLOSE, cursor);
if (nextClose < 0) break;
- let nextOpen = scanned.indexOf(REMINDER_OPEN, cursor);
- while (nextOpen >= 0 && !reminderOpensHere(scanned, nextOpen)) {
- nextOpen = scanned.indexOf(REMINDER_OPEN, nextOpen + 1);
+ let nextOpen = indexOfMarker(scan, REMINDER_OPEN, cursor);
+ while (nextOpen >= 0 && !reminderOpensHere(scan, nextOpen)) {
+ nextOpen = indexOfMarker(scan, REMINDER_OPEN, nextOpen + 1);
}
if (nextOpen >= 0 && nextOpen < nextClose) {
this.suppressDepth += 1;
@@ -159,11 +179,10 @@ export class QoderScaffoldFilter {
let earliest = -1;
let found = "";
- const lowered = buffer.toLowerCase();
for (const marker of ALL_MARKERS) {
- let at = lowered.indexOf(marker);
- while (at >= 0 && marker === REMINDER_OPEN && !reminderOpensHere(lowered, at)) {
- at = lowered.indexOf(marker, at + 1);
+ let at = indexOfMarker(buffer, marker);
+ while (at >= 0 && marker === REMINDER_OPEN && !reminderOpensHere(buffer, at)) {
+ at = indexOfMarker(buffer, marker, at + 1);
}
if (at < 0) continue;
// A closer sitting exactly where an opener starts cannot happen, so ties are impossible.
diff --git a/tests/providers/qoder-scaffold-guard.test.ts b/tests/providers/qoder-scaffold-guard.test.ts
index 65f06d7bc15..02dd8f41212 100644
--- a/tests/providers/qoder-scaffold-guard.test.ts
+++ b/tests/providers/qoder-scaffold-guard.test.ts
@@ -35,6 +35,22 @@ describe("QoderScaffoldFilter", () => {
expect(first.text + filter.flush().text).toBe("Before.After.");
});
+ test("uses original-string offsets when Unicode lowercasing would expand", () => {
+ const expandingPrefix = "İ".repeat(64);
+ const filter = new QoderScaffoldFilter();
+ const result = filter.push(`${expandingPrefix}${REMINDER}After.`);
+ expect(result.fail).toBeNull();
+ expect(result.text + filter.flush().text).toBe(`${expandingPrefix}After.`);
+ expect(result.text).not.toContain("internal-notes");
+ });
+
+ test("uses original-string offsets to find a closer after expanding Unicode", () => {
+ const filter = new QoderScaffoldFilter();
+ const result = filter.push(`${"İ".repeat(64)}After.`);
+ expect(result.fail).toBeNull();
+ expect(result.text + filter.flush().text).toBe("After.");
+ });
+
test("catches a marker split across deltas", () => {
const filter = new QoderScaffoldFilter();
// The opening tag arrives in three pieces; a per-delta scan would miss it entirely.
@@ -159,6 +175,17 @@ describe("QoderScaffoldFilter", () => {
});
describe("guardQoderScaffolding", () => {
+ test("never emits a reminder after a Unicode case-folding expansion", () => {
+ const { events, emit } = collect();
+ const guarded = guardQoderScaffolding(emit);
+ const prefix = "İ".repeat(64);
+ guarded({ type: "text_delta", text: `${prefix}${REMINDER}` });
+ guarded({ type: "done", stopReason: "stop" });
+ expect(textOf(events)).toBe(prefix);
+ expect(textOf(events)).not.toContain("internal-notes");
+ expect(events[events.length - 1]!.type).toBe("done");
+ });
+
test("strips the reminder and still completes the turn", () => {
const { events, emit } = collect();
const guarded = guardQoderScaffolding(emit);
From c448a497942081990704b0da82f087e142923bd6 Mon Sep 17 00:00:00 2001
From: JUN
Date: Wed, 23 Sep 2026 05:12:00 +0900
Subject: [PATCH 10/17] fix(responses): keep a cyber-policy stop when a 5xx
body has malformed UTF-8
consumeComboFailure read 5xx bodies with fatalUtf8, so a single malformed
byte rejected the whole read and replaced an otherwise recognizable
cyber-policy refusal with "Provider error ". The combo then hopped
instead of stopping.
readBoundedResponseBody gains reportUtf8Validity: it decodes with
replacement characters and reports utf8Valid at EOF (true by construction
when fatalUtf8 is also set). consumeComboFailure keeps every existing trust
rule for malformed 5xx bodies -- no quota evidence, usage, or ordinary
classification -- and only lets the lenient decode through when it
identifies a cyber-policy refusal. The quota agreement with
shouldRetryCodexPoolAccountQuota is unchanged.
Reimplements #5307 with a narrower classification gate.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
---
src/lib/bounded-body.ts | 30 ++++++++++++++++++
src/server/responses/core-combo-failure.ts | 31 ++++++++++---------
structure/transports/inventory.md | 8 +++++
.../cyber-policy-error-fidelity.test.ts | 31 +++++++++++++++++++
tests/server/bounded-body.test.ts | 29 +++++++++++++++++
5 files changed, 114 insertions(+), 15 deletions(-)
diff --git a/src/lib/bounded-body.ts b/src/lib/bounded-body.ts
index 0b3769eaacf..effb247fb71 100644
--- a/src/lib/bounded-body.ts
+++ b/src/lib/bounded-body.ts
@@ -15,6 +15,8 @@ export interface BoundedBodyOptions {
* Reader cancellation and lock release still run. Defaults to false.
*/
fatalUtf8?: boolean;
+ /** Report UTF-8 validity without rejecting malformed bodies. */
+ reportUtf8Validity?: boolean;
/**
* Byte ceiling for retained body data. Defaults to BOUNDED_BODY_MAX_BYTES (64 KiB),
* which suits error bodies; callers materializing whole success payloads (e.g. a
@@ -44,6 +46,8 @@ export interface BoundedBodyResult {
oversized: boolean;
/** False means callers should use a status-only fallback, not `text`. */
displaySafe: boolean;
+ /** Present when reportUtf8Validity was requested and the retained body reached EOF. */
+ utf8Valid?: boolean;
}
export interface BoundedBytesOptions {
@@ -238,6 +242,14 @@ function decodeUtf8(chunks: readonly Uint8Array[], fatal: boolean, timedOut = fa
}
}
+function decodeUtf8WithValidity(bytes: Uint8Array): { text: string; utf8Valid: boolean } {
+ try {
+ return { text: decodeUtf8([bytes], true), utf8Valid: true };
+ } catch {
+ return { text: decodeUtf8([bytes], false), utf8Valid: false };
+ }
+}
+
/**
* Consume the original response body under strict memory and time bounds.
*
@@ -326,6 +338,24 @@ export async function readBoundedResponseBody(
const { value, done } = outcome as ReadableStreamReadResult;
if (done) {
+ if (options.reportUtf8Validity) {
+ const bytes = retained.subarray(0, retainedBytes);
+ // A fatal decode that returned already proved the bytes valid; still
+ // honour the reporting contract instead of dropping utf8Valid.
+ const decoded = options.fatalUtf8 === true
+ ? { text: decodeUtf8([bytes], true), utf8Valid: true }
+ : decodeUtf8WithValidity(bytes);
+ return {
+ text: decoded.text,
+ truncated: false,
+ timedOut: false,
+ totalTimedOut: false,
+ inactivityTimedOut: false,
+ oversized: false,
+ displaySafe: true,
+ utf8Valid: decoded.utf8Valid,
+ };
+ }
return {
text: decodeUtf8([retained.subarray(0, retainedBytes)], options.fatalUtf8 === true),
truncated: false,
diff --git a/src/server/responses/core-combo-failure.ts b/src/server/responses/core-combo-failure.ts
index c14c3bb680b..e222c2ccaea 100644
--- a/src/server/responses/core-combo-failure.ts
+++ b/src/server/responses/core-combo-failure.ts
@@ -45,27 +45,28 @@ export async function consumeComboFailure(
// a second body read, so this mirrors its normalization: raw 402/429, or a 5xx whose intact,
// display-safe body carries a recognized quota message.
let quotaConfirmedByBody = false;
+ const serverError = response.status >= 500 && response.status < 600;
try {
- const body = await readBoundedResponseBody(response, {
- signal,
- // Match shouldRetryCodexPoolAccountQuota before treating a 5xx body as quota evidence.
- fatalUtf8: response.status >= 500 && response.status < 600,
- });
- usage = usageFromComboFailureText(body.text);
- if (
- response.status >= 500 && response.status < 600
- && body.displaySafe && !body.truncated
- ) {
+ const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
+ // A 5xx body counts as quota or classification evidence only when it decoded as valid
+ // UTF-8, matching shouldRetryCodexPoolAccountQuota. A malformed byte keeps the status-only
+ // fallback, with one exception: a cyber-policy refusal must still stop the combo, so the
+ // replacement-decoded text may carry that verdict and nothing else.
+ const utf8Trusted = !serverError || body.utf8Valid === true;
+ if (utf8Trusted) usage = usageFromComboFailureText(body.text);
+ if (serverError && utf8Trusted && body.displaySafe && !body.truncated) {
const quotaMessage = codexQuotaFailureMessage(body.text);
quotaConfirmedByBody = quotaMessage !== undefined
&& isRateLimitOrQuotaFailureMessage(quotaMessage);
}
- if (body.displaySafe) {
+ if (body.displaySafe && !body.truncated) {
const normalized = normalizeUpstreamErrorText(body.text, fallback);
- classificationText = normalized.safeText;
- upstreamCode = normalized.code;
- upstreamMessage = normalized.message;
- upstreamType = normalized.type;
+ if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
+ classificationText = normalized.safeText;
+ upstreamCode = normalized.code;
+ upstreamMessage = normalized.message;
+ upstreamType = normalized.type;
+ }
}
} catch (error) {
if (signal?.aborted) throw error;
diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md
index bfd34825f7d..3cb8053a5ff 100644
--- a/structure/transports/inventory.md
+++ b/structure/transports/inventory.md
@@ -98,6 +98,14 @@ promises are observed, and a cancellation that never settles cannot extend the r
After an attached read, cleanup removes the abort listener, cancels any inactivity timer, and
attempts to release the reader lock. `tests/server/bounded-body.test.ts` covers these paths.
+`readBoundedResponseBody` accepts `reportUtf8Validity`: the body decodes with replacement
+characters instead of rejecting, and a result that reached EOF carries `utf8Valid`. Combined with
+`fatalUtf8`, a returned body is valid by construction and reports `true`. Timeout and oversized
+results omit the field. `consumeComboFailure` uses it for 5xx bodies: only a valid body supplies
+quota evidence, usage, or classification, and a malformed body keeps the status-only fallback
+unless its lenient decode identifies a cyber-policy refusal, which must still stop the combo
+(`tests/providers/cyber-policy-error-fidelity.test.ts`).
+
`src/oauth/orcarouter.ts` applies this reader to a successful `POST /api/v1/auth/keys` response
with a 65,536-byte (64 KiB) ceiling. One 30-second signal, combined with caller cancellation,
covers both fetching the response headers and consuming the body; no separate body or inactivity
diff --git a/tests/providers/cyber-policy-error-fidelity.test.ts b/tests/providers/cyber-policy-error-fidelity.test.ts
index 7adc39f3f22..02f5b49d17e 100644
--- a/tests/providers/cyber-policy-error-fidelity.test.ts
+++ b/tests/providers/cyber-policy-error-fidelity.test.ts
@@ -180,6 +180,37 @@ describe("cyber_policy error fidelity", () => {
});
});
+ test("malformed UTF-8 does not erase a cyber-policy stop", async () => {
+ const bytes = new Uint8Array([
+ ...new TextEncoder().encode(JSON.stringify(CYBER_ERROR_BODY)),
+ 0xff,
+ ]);
+ const failure = await consumeComboFailure(new Response(bytes, { status: 502 }));
+ expect(failure.response.status).toBe(400);
+ expect(failure.upstreamCode).toBe(CYBER_POLICY_ERROR_CODE);
+ expect(comboFailureDecision(502, failure.classificationText, {
+ code: failure.upstreamCode,
+ })).toBe("stop");
+ });
+
+ test("malformed UTF-8 keeps the status-only fallback for a non-cyber 5xx body", async () => {
+ // Only a cyber-policy verdict may be read from replacement-decoded text. Any other code
+ // or prose from a malformed body (here a quota code that would widen the cooldown scope)
+ // must not reach classification, exactly as before the body was decoded leniently.
+ const bytes = new Uint8Array([
+ ...new TextEncoder().encode(JSON.stringify({
+ error: { message: "You exceeded your current quota", type: "insufficient_quota", code: "insufficient_quota" },
+ })),
+ 0xff,
+ ]);
+ const failure = await consumeComboFailure(new Response(bytes, { status: 502 }));
+ expect(failure.response.status).toBe(502);
+ expect(failure.classificationText).toBe("Provider error 502");
+ expect(failure.upstreamCode).toBeUndefined();
+ expect(failure.resetAt).toBeUndefined();
+ expect(failure.usage).toBeUndefined();
+ });
+
test("drops Codex reset headers as well as Retry-After for a cyber-policy failure", async () => {
const upstream = new Response(JSON.stringify(CYBER_ERROR_BODY), {
status: 429,
diff --git a/tests/server/bounded-body.test.ts b/tests/server/bounded-body.test.ts
index 2a142719ae6..80d9cf0b76e 100644
--- a/tests/server/bounded-body.test.ts
+++ b/tests/server/bounded-body.test.ts
@@ -22,6 +22,35 @@ function responseFromChunks(...chunks: Uint8Array[]): Response {
}
describe("readBoundedResponseBody", () => {
+ test("reportUtf8Validity is honoured on the fatal decode path at EOF", async () => {
+ const valid = await readBoundedResponseBody(responseFromChunks(encoder.encode('{"ok":true}')), {
+ fatalUtf8: true,
+ reportUtf8Validity: true,
+ });
+ expect(valid.utf8Valid).toBe(true);
+ let caught: unknown;
+ try {
+ await readBoundedResponseBody(responseFromChunks(new Uint8Array([0xff])), {
+ fatalUtf8: true,
+ reportUtf8Validity: true,
+ });
+ } catch (error) { caught = error; }
+ expect(boundedBodyDecodeFailure(caught)).toBe("invalid_utf8");
+ });
+
+ test("reportUtf8Validity reports a malformed body at EOF without rejecting it", async () => {
+ const valid = await readBoundedResponseBody(responseFromChunks(encoder.encode("ok")), {
+ reportUtf8Validity: true,
+ });
+ expect(valid).toMatchObject({ text: "ok", utf8Valid: true, displaySafe: true, truncated: false });
+ const malformed = await readBoundedResponseBody(responseFromChunks(new Uint8Array([0x6f, 0xff])), {
+ reportUtf8Validity: true,
+ });
+ expect(malformed).toMatchObject({ text: "o\uFFFD", utf8Valid: false, displaySafe: true, truncated: false });
+ const unrequested = await readBoundedResponseBody(responseFromChunks(encoder.encode("ok")));
+ expect(unrequested.utf8Valid).toBeUndefined();
+ });
+
test("only actual decoder exceptions carry the decode discriminator", async () => {
for (const bytes of [new Uint8Array([0xff]), new Uint8Array([0xe2, 0x82])]) {
let caught: unknown;
From 5c9a16f1a0ec1c3568a5298e097414be7318f67f Mon Sep 17 00:00:00 2001
From: JUN
Date: Wed, 23 Sep 2026 05:15:26 +0900
Subject: [PATCH 11/17] docs: separate the campaign command from the corrected
rerun command
The branch and PR classification summaries showed the --no-renames form as though the campaign had used it. State the command that produced the recorded verdicts and the form any rerun must use, matching the correction in 010_method.md.
Follow-up to #5461.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
---
devlog/_fin/260904_repo_hygiene_campaign/000_plan.md | 4 +++-
.../260904_repo_hygiene_campaign/100_pr_verdicts.md | 11 ++++++-----
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md b/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
index 338920eed06..a27c412b9ef 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/000_plan.md
@@ -17,7 +17,9 @@ every contributor whose work is carried.
## Classification of local branches
-Every branch was scored on four independent axes rather than by name:
+Every branch was scored on four independent axes rather than by name. Axis 3 is
+shown in its corrected form; the campaign itself ran it without `--no-renames`
+(see the 2026-09-21 correction in 010_method.md):
1. `git merge-base --is-ancestor
origin/dev` — plain ancestry.
2. `git cherry origin/dev
` — patch-equivalence, which catches rebases.
diff --git a/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md b/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
index 8990abe5a3e..3ff033a1c98 100644
--- a/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
+++ b/devlog/_fin/260904_repo_hygiene_campaign/100_pr_verdicts.md
@@ -1,11 +1,12 @@
# 100 — Per-PR verdicts
Full classification of the 53 pull requests open when the campaign started.
-Method: fetch each PR head, take the files it touches
-(`git diff --no-renames --name-only origin/dev...`), then compare those
-exact paths two-dot against `origin/dev`. Remaining differences mean the work
-has not landed. (2026-09-21: `--no-renames` added so a rename cannot hide the
-deleted source side from the path set; see 010_method.md.)
+Method: fetch each PR head, take the files it touches, then compare those exact
+paths two-dot against `origin/dev`. Remaining differences mean the work has not
+landed. The verdicts below were produced with
+`git diff --name-only origin/dev...`; any rerun must use
+`git diff --no-renames --name-only origin/dev...` so a rename cannot hide
+the deleted source side from the path set (2026-09-21; see 010_method.md).
## Closed
From 9516f97805bb1fcd5823a35764fd03f65ec3d9aa Mon Sep 17 00:00:00 2001
From: JUN
Date: Wed, 23 Sep 2026 05:15:27 +0900
Subject: [PATCH 12/17] test(oauth): restore the pool-validator home even when
shutdown throws
A throwing server.stop skipped the OPENCODEX_HOME restore and temp-dir removal, leaking both into later cases. Run cleanup in an inner finally.
Follow-up to #5442.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
---
tests/server/account-pool-management-api.test.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/tests/server/account-pool-management-api.test.ts b/tests/server/account-pool-management-api.test.ts
index fa52f8e076f..30d32dcb5a7 100644
--- a/tests/server/account-pool-management-api.test.ts
+++ b/tests/server/account-pool-management-api.test.ts
@@ -668,10 +668,13 @@ describe("legacy pool contract goldens (#wp5)", () => {
expect(await codex({ stickyLimit })).toBe(200);
}
} finally {
- await server?.stop(true);
- if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
- else process.env.OPENCODEX_HOME = previousHome;
- removeTreeWithRetry(testDir);
+ try {
+ await server?.stop(true);
+ } finally {
+ if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
+ else process.env.OPENCODEX_HOME = previousHome;
+ removeTreeWithRetry(testDir);
+ }
}
});
From 873591c694bcccf508911f2f655472a8810052ac Mon Sep 17 00:00:00 2001
From: JUN
Date: Wed, 23 Sep 2026 05:15:37 +0900
Subject: [PATCH 13/17] test(integrations): model a killed defaults probe with
a type-safe result
The timeout case cast a result with exitCode null directly to typeof Bun.spawnSync, which strict TypeScript can reject, and its empty stdout could not tell an exit-code check from an empty read. Cast through unknown, cover null and non-zero exits, and return "1" on stdout so ignoring the exit code would visibly report Pro.
Follow-up to #5244.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
---
tests/clients/raycast-detect.test.ts | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/tests/clients/raycast-detect.test.ts b/tests/clients/raycast-detect.test.ts
index afdc3dbeb4f..3d5fc49e6a2 100644
--- a/tests/clients/raycast-detect.test.ts
+++ b/tests/clients/raycast-detect.test.ts
@@ -38,7 +38,7 @@ describe("detectRaycast", () => {
const spawnSync = ((command: string[], options: { timeout?: number }) => {
invocation = { command, timeout: options.timeout };
return { exitCode: 0, stdout: Buffer.from("1") };
- }) as typeof Bun.spawnSync;
+ }) as unknown as typeof Bun.spawnSync;
const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBe("1");
@@ -75,11 +75,14 @@ describe("detectRaycast", () => {
});
test("darwin: a timed-out or killed defaults probe is unknown, not a false positive", () => {
- // Bun reports exitCode === null when the 2s timeout kills the process.
- const spawnSync = (() => ({ exitCode: null, stdout: Buffer.from("") })) as typeof Bun.spawnSync;
- const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
- expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBeNull();
- expect(detectRaycast(deps).plan).toBe("unknown");
+ // A probe the 2s timeout kills reports a null or non-zero exit code; neither reads the
+ // preference. stdout carries "1" so a probe that ignored the exit code would report Pro.
+ for (const exitCode of [null, 143]) {
+ const spawnSync = (() => ({ exitCode, exitedDueToTimeout: true, stdout: Buffer.from("1") })) as unknown as typeof Bun.spawnSync;
+ const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
+ expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBeNull();
+ expect(detectRaycast(deps).plan).toBe("unknown");
+ }
});
test("win32: LOCALAPPDATA\\Programs\\Raycast is the install path and the plan is unknown", () => {
From 2c15f146748b7ff43dc7fdfa604a23aaf4d32ee6 Mon Sep 17 00:00:00 2001
From: JUN
Date: Wed, 23 Sep 2026 05:15:57 +0900
Subject: [PATCH 14/17] docs(structure): record the terminal unknown_flow GET
in the reauth contract
The dashboard contract said a non-2xx GET keeps cancellation ownership and polling, and that no replacement login POST can appear before DELETE settles. A GET 404 unknown_flow now ends the flow the same way the DELETE path does, so qualify both statements as applying to retryable GET errors and state the exception in the overview.
Follow-up to #5428.
Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
---
structure/gui-and-management-api.md | 2 +-
structure/overview.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md
index 5be71c0bfbc..0b95f0146af 100644
--- a/structure/gui-and-management-api.md
+++ b/structure/gui-and-management-api.md
@@ -420,7 +420,7 @@ single forms, and the shell pattern is the part worth keeping stable:
| Subagents | Featured-roster selection workspace (`gui/src/components/subagents-workspace/`). |
| Combos | Rail, detail panel, and an add flow (`gui/src/components/ComboWorkspace.tsx`). |
| Add provider | Catalog browser plus form and OAuth panes (`gui/src/components/provider-catalog/`, `gui/src/components/AddProviderModal.tsx`). The catalog browses four tabs — Accounts, Free, Local, Paid — where Local is a catalog-only bucket peeled out of `bucketPresets` after `presetTier` has classified; the workspace `providerTier` stays three-way, so the rail, the free-paid sort and the Free count still treat a local runtime as free. Search sits above the tabs and reaches every tab at once: while a query is live the list renders all four groups with headings and the strip becomes jump chips with counts rather than a tablist, because moving the selected tab would change the row kind under the user (a preset-select button becomes a login row). ArrowDown from the search input focuses the first enabled result action; if none is available, focus stays in the input. The tab strip wraps within narrow modals. Every nonempty note has a full-text button so narrow rows never hide content permanently; the native note dialog closes during teardown and restores focus to its trigger. Provider notes clamp to two lines and open in full in a stacked native `