Skip to content

Retry deactivated workspaces during rotation - #99

Merged
ndycode merged 3 commits into
ndycode:mainfrom
sdip15fa:feature/deactivated-workspace-rotation
Mar 24, 2026
Merged

ndycode merged 3 commits into
ndycode:mainfrom
sdip15fa:feature/deactivated-workspace-rotation

Conversation

@sdip15fa

@sdip15fa sdip15fa commented Mar 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • treat deactivated_workspace as a hard removal for every account entry sharing the same refresh token, then restart traversal onto a healthy remaining account
  • keep the existing debounced storage path and document the Windows rename-retry / token-redaction invariant relied on by this write path
  • restore the zero-removal fallback by cooling down the deactivated account when grouped removal races to 0
  • cover both detail.code and legacy error.code envelopes in the touched deactivated-workspace tests

Verification

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm test
  • npm test -- --run test/index.test.ts test/login-runner.test.ts test/storage.test.ts

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of deactivated workspace events: linked accounts are removed as a group when applicable, retries/restarts now depend on whether grouped removals succeeded, warnings show affected count or account label, and when no grouped removals occur the account is cooled down and persistence is triggered.
  • Tests

    • Updated tests and mocked error responses to cover grouped-removal scenarios, the no-removal path, and the adjusted error payload shape.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr upgrades the deactivated-workspace handler from single-account removal to grouped removal — all accounts sharing the same refresh token are evicted together via removeAccountsWithSameRefreshToken. a new restartAccountTraversalAfterWorkspaceDeactivation flag drives the outer-loop restart so the plugin correctly retries against the remaining healthy accounts rather than returning 503 immediately. a defensive fallback (cool-down + saveToDiskDebounced) handles the unexpected case where grouped removal returns 0.

  • grouped removal: removeAccountsWithSameRefreshToken replaces the old removeAccount call, evicting all stale entries sharing the deactivated refresh token in one shot.
  • outer-loop restart: new flag mirrors the existing restartAccountTraversalWithFallback pattern — sets on successful grouped removal, triggers continue on the outer while (true), resets accountCount/attempted cleanly.
  • defensive fallback: when removedCount === 0 (should not happen in practice), the account is cooled down and disk is saved, but the windows/redaction comment present on the happy path is missing here — minor doc gap per the project's merge policy.
  • test updates: happy-path test now uses a separate-token healthy fallback (correct) and a real removeAccountsWithSameRefreshToken mock; new test covers the removedCount === 0 cool-down path; both touched fetch mocks now include the dual error/detail envelope to cover both code paths in isDeactivatedWorkspaceError.

Confidence Score: 4/5

  • logic is correct and tests are comprehensive; one doc-only gap on the fallback write path per the project's windows/token-safety policy remains.
  • the grouped removal logic and outer-loop restart are sound, test coverage for both the happy path and the removedCount === 0 fallback is good, and the dual envelope mock fix is correct. the only remaining item is adding the windows/redaction comment to the fallback saveToDiskDebounced call — a one-line doc fix that doesn't affect runtime correctness.
  • index.ts lines 2137–2146 (fallback saveToDiskDebounced missing windows/redaction comment)

Important Files Changed

Filename Overview
index.ts replaces single-account removeAccount with grouped removeAccountsWithSameRefreshToken, introduces restartAccountTraversalAfterWorkspaceDeactivation flag to drive the outer-loop restart, and adds a defensive fallback (cool-down + debounced save) when grouped removal returns 0. logic is sound; the fallback saveToDiskDebounced call lacks the windows/redaction comment that the happy path carries.
test/index.test.ts updated the grouped-removal happy-path test (adds a distinct-token healthy fallback, correct removeAccountsWithSameRefreshToken mock, and dual error/detail envelope), and adds a new test for the removedCount === 0 cool-down path. coverage looks complete for the changed behaviour.

Sequence Diagram

sequenceDiagram
    participant OL as outer while(true)
    participant IL as inner while(attempted)
    participant AM as accountManager

    OL->>OL: reset accountCount, attempted, flags
    IL->>AM: getCurrentOrNextForFamilyHybrid()
    AM-->>IL: deadWorkspace (shared-refresh)
    IL->>API: POST /chat [access-org-dead]
    API-->>IL: 402 deactivated_workspace
    IL->>AM: refundToken + recordFailure
    IL->>AM: withFlaggedAccountStorageTransaction (flag record)
    IL->>AM: removeAccountsWithSameRefreshToken(deadWorkspace)
    AM-->>IL: removedCount = 2
    IL->>AM: saveToDiskDebounced()
    IL->>IL: restartAccountTraversalAfterWorkspaceDeactivation = true
    IL->>IL: break
    OL->>OL: if restartAccountTraversalAfterWorkspaceDeactivation → continue
    OL->>OL: reset accountCount=1, attempted=∅, flags=false
    IL->>AM: getCurrentOrNextForFamilyHybrid()
    AM-->>IL: healthyFallback (healthy-refresh)
    IL->>API: POST /chat [access-org-live]
    API-->>IL: 200 OK
    IL->>AM: recordSuccess
    IL-->>OL: return 200
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: index.ts
Line: 2137-2146

Comment:
**missing windows/redaction comment on fallback `saveToDiskDebounced` call**

the happy path (lines 2117–2121) has the required comment explaining that `saveToDiskDebounced` reuses the EPERM/EBUSY retry path and that failures stay redacted. the fallback branch here calls the same function without any such note. per the project's merge policy, every write path in a code change needs to document its Windows filesystem and token-leakage defences.

```suggestion
					logWarn(
						`[${PLUGIN_NAME}] Expected grouped account removal after workspace deactivation, but removed ${removedCount}.`,
					);
					// Same saveToDiskDebounced EPERM/EBUSY retry path as above;
					// persistence failures are logged without token material (see
					// test/login-runner.test.ts redaction checks).
					accountManager.markAccountCoolingDown(
						account,
						ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS,
						"auth-failure",
					);
					accountManager.saveToDiskDebounced();
					break;
```

**Rule Used:** What: Every code change must explain how it defend... ([source](https://app.greptile.com/review/custom-context?memory=637a42e6-7a78-40d6-9ef8-6a45e02e73b6))

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "fix(pr99):restore-zero-removal-fallback" | Re-trigger Greptile

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

Copilot AI review requested due to automatic review settings March 24, 2026 07:47
@sdip15fa
sdip15fa requested a review from ndycode as a code owner March 24, 2026 07:47
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Mar 24, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c93b442-0caa-411d-a634-7f51429e8b96

📥 Commits

Reviewing files that changed from the base of the PR and between 81a4d79 and f18ca84.

📒 Files selected for processing (2)
  • index.ts
  • test/index.test.ts

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting


📝 Walkthrough

Walkthrough

Added a new traversal-control flag restartAccountTraversalAfterWorkspaceDeactivation and refactored deactivated-workspace handling to remove accounts by shared refresh token (removeAccountsWithSameRefreshToken). Traversal restart/loop decisions now depend on grouped-removal results and the new flag; tests and mocks updated to reflect grouped removals and expanded error payloads.

Changes

Cohort / File(s) Summary
Traversal & Error Handling
index.ts
Introduced restartAccountTraversalAfterWorkspaceDeactivation; replaced single-account removal with removeAccountsWithSameRefreshToken(account) and conditional save/restart only when removedCount > 0; adjusted control flow so the new flag parallels restartAccountTraversalWithFallback; changed warning/logging and cooldown/save behavior when no grouped removals occur.
Tests & Mocks
test/index.test.ts
Rewrote deactivated-workspace test to include duplicate account and fallback account, expect removeAccountsWithSameRefreshToken (not removeAccount), update in-test removal mock to filter/splice by refreshToken and reindex; added test for grouped-removal returning 0 asserting cooldown, saveToDiskDebounced, and 503 response; expanded mocked upstream 402 bodies to include top-level detail mirroring error.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Traversal as TraversalLoop
participant Fetcher as FetchHandler
participant Store as AccountStore
participant Disk as DiskPersist
participant UI as Toast/Logger

Traversal->>Fetcher: fetch account
Fetcher->>Fetcher: receives 402 deactivated-workspace error
Fetcher->>Store: removeAccountsWithSameRefreshToken(account)
alt removedCount > 0
    Store-->>Fetcher: removedCount (>0)
    Fetcher->>Disk: saveToDiskDebounced()
    Fetcher->>UI: show warning with removedCount
    Fetcher->>Traversal: set restartAccountTraversalAfterWorkspaceDeactivation = true
    Traversal-->>Fetcher: continue/restart based on flag
else removedCount == 0
    Store-->>Fetcher: removedCount (0)
    Fetcher->>Store: markAccountCoolingDown(account, AUTH_FAILURE_COOLDOWN_MS)
    Fetcher->>Disk: saveToDiskDebounced()
    Fetcher->>UI: log warning about no grouped removals
    Fetcher-->>Traversal: break/stop or continue based on config
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled tokens, found a pair,

I nudged them out with careful care,
If none were gone, I stamped the ground,
I saved my hops and made a sound,
A tidy rabbit restarts the round ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description covers what changed and why, but omits required compliance confirmations and testing checkboxes from the template. Add the 'Testing' section with checkboxes for lint, build, and test steps; add the 'Compliance Confirmation' section with required checkboxes; and ensure all template sections are addressed.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Retry deactivated workspaces during rotation' clearly and concisely describes the main change: enabling retry/restart behavior when deactivated workspaces are encountered during account traversal rotation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.ts (1)

2086-2136: ⚠️ Potential issue | 🟠 Major

Use removeAccountsWithSameRefreshToken() instead of removeAccount() for workspace deactivation.

When a workspace is deactivated at line 2117, only the current account entry is removed via removeAccount(). This diverges from the auth-failure handling at line 1878, which correctly uses removeAccountsWithSameRefreshToken() to remove all workspace variants. If duplicate entries exist for the same workspace (same refreshToken, different organization/account IDs), the unremoved variants can re-enter rotation. Additionally, the fallback to markAccountCoolingDown(..., "auth-failure") is semantically incorrect—a deactivated workspace should be permanently removed, not temporarily cooled down as an auth failure.

Replace the removeAccount() call with removeAccountsWithSameRefreshToken() and remove or replace the cooldown fallback, since a deactivated workspace should not be eligible for retry after cooldown expiry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 2086 - 2136, The workspace deactivation branch
currently calls accountManager.removeAccount(account) and then falls back to
accountManager.markAccountCoolingDown(..., "auth-failure"); replace the
single-account removal with
accountManager.removeAccountsWithSameRefreshToken(account) to purge all variants
sharing the same refreshToken (use the same account variable and keep
accountLabel), retain accountManager.saveToDiskDebounced(), attempted.clear(),
accountCount = accountManager.getAccountCount(),
restartAccountTraversalAfterWorkspaceDeactivation = true and the showToast call,
and remove the markAccountCoolingDown(...) fallback entirely so deactivated
workspaces are permanently removed rather than cooled down; ensure any existing
logic that depended on removeAccount's boolean return is adjusted to the new
method's semantics (e.g., check whether any accounts were removed if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@index.ts`:
- Around line 2086-2136: The workspace deactivation branch currently calls
accountManager.removeAccount(account) and then falls back to
accountManager.markAccountCoolingDown(..., "auth-failure"); replace the
single-account removal with
accountManager.removeAccountsWithSameRefreshToken(account) to purge all variants
sharing the same refreshToken (use the same account variable and keep
accountLabel), retain accountManager.saveToDiskDebounced(), attempted.clear(),
accountCount = accountManager.getAccountCount(),
restartAccountTraversalAfterWorkspaceDeactivation = true and the showToast call,
and remove the markAccountCoolingDown(...) fallback entirely so deactivated
workspaces are permanently removed rather than cooled down; ensure any existing
logic that depended on removeAccount's boolean return is adjusted to the new
method's semantics (e.g., check whether any accounts were removed if needed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d1626d4e-499a-4e19-89bd-01159b5e2df2

📥 Commits

Reviewing files that changed from the base of the PR and between c3364f7 and c7bed33.

📒 Files selected for processing (2)
  • index.ts
  • test/index.test.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the account rotation logic to treat deactivated_workspace responses as retriable, enabling failover to sibling workspaces while still removing (or cooling down) the dead workspace from rotation.

Changes:

  • Adjust test fixtures to return detail.code = "deactivated_workspace" instead of the previous error.code shape.
  • Add a new restart path in the rotation traversal to restart selection after a workspace deactivation (including removal/cooldown + persistence).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
test/index.test.ts Updates mocked API error payload shape to validate detail.code handling for deactivated workspaces.
index.ts Adds a dedicated restart flag/path to retry rotation after detecting a deactivated workspace and updating the account pool.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/index.test.ts Outdated
Comment on lines 3190 to 3192
return new Response(JSON.stringify({ detail: { code: "deactivated_workspace", message: "workspace dead" } }), {
status: 402,
});

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change replaces the previously-covered { error: { code } } payload shape with { detail: { code } }, which reduces regression coverage for the earlier shape. If the production handler is intended to tolerate both formats (common with API gateways / different error envelopes), consider adding an additional test case (or parameterizing this one) that exercises the legacy error.code shape as well.

Suggested change
return new Response(JSON.stringify({ detail: { code: "deactivated_workspace", message: "workspace dead" } }), {
status: 402,
});
return new Response(
JSON.stringify({
error: { code: "deactivated_workspace", message: "workspace dead" },
detail: { code: "deactivated_workspace", message: "workspace dead" },
}),
{
status: 402,
},
);

Copilot uses AI. Check for mistakes.
Comment thread index.ts
Comment on lines +1785 to 1790
let accountCount = accountManager.getAccountCount();
const attempted = new Set<number>();
let restartAccountTraversalWithFallback = false;
let restartAccountTraversalAfterWorkspaceDeactivation = false;

while (attempted.size < Math.max(1, accountCount)) {

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation/formatting in this hunk looks inconsistent (notably the while (attempted.size ...) line appearing flush-left relative to surrounding control flow). Even if it compiles, this makes it difficult to visually validate block structure and increases the risk of brace/control-flow mistakes. Running the project formatter (or adjusting indentation to match surrounding style) would make the traversal/restart logic much safer to review and maintain.

Copilot uses AI. Check for mistakes.
Comment thread index.ts
Comment on lines +2438 to +2451
if (restartAccountTraversalWithFallback) {
break;
}
if (restartAccountTraversalAfterWorkspaceDeactivation) {
break;
}
}

if (restartAccountTraversalWithFallback) {
continue;
}
if (restartAccountTraversalWithFallback) {
continue;
}
if (restartAccountTraversalAfterWorkspaceDeactivation) {
continue;
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The restart handling is duplicated in two places (a break gate and a continue gate) for both flags. This duplication is easy to accidentally diverge over time (e.g., adding a new restart reason but only updating one site). Consider consolidating into a single shouldRestartTraversal (or a single enum-like restartReason) and handling it once, so there’s only one authoritative place controlling the restart decision.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.ts (1)

2086-2140: ⚠️ Potential issue | 🟠 Major

Restart traversal even when grouped removal returns 0.

Because accountManager is shared across fetches, another concurrent request can remove the same refresh-token group first. In that case this branch keeps the old attempted/accountCount snapshot and can skip an eligible sibling after indices shift instead of rotating to it.

♻️ Proposed fix
 					const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
 					if (removedCount > 0) {
 						accountManager.saveToDiskDebounced();
-						restartAccountTraversalAfterWorkspaceDeactivation = true;
 						const removalMessage = removedCount > 1
 							? `Workspace deactivated. Removed ${removedCount} related entries from rotation and switching accounts.`
 							: `Workspace deactivated. Removed ${accountLabel} from rotation and switching accounts.`;
 						await showToast(
 							removalMessage,
 							"warning",
 							{ duration: toastDurationMs },
 						);
-						break;
-					}
-
-					logWarn(
-						`[${PLUGIN_NAME}] Expected grouped account removal after workspace deactivation, but removed ${removedCount}.`,
-					);
+					} else {
+						logWarn(
+							`[${PLUGIN_NAME}] Expected grouped account removal after workspace deactivation, but removed ${removedCount}. Restarting traversal anyway because the pool may already have changed.`,
+						);
+					}
+					restartAccountTraversalAfterWorkspaceDeactivation = true;
 					break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 2086 - 2140, When workspaceDeactivated is true we must
always restart the account traversal even if
accountManager.removeAccountsWithSameRefreshToken(account) returns 0 to avoid
using a stale attempted/accountCount snapshot and skipping siblings after
concurrent removals; update the branch that handles removedCount to set
restartAccountTraversalAfterWorkspaceDeactivation = true before the removalCount
check (or set it in both the removedCount > 0 and removedCount === 0 paths) and
ensure showToast/early break logic still runs for the removedCount > 0 case
while preserving the existing logWarn + break for the removedCount === 0 case;
key symbols: workspaceDeactivated,
accountManager.removeAccountsWithSameRefreshToken,
restartAccountTraversalAfterWorkspaceDeactivation, showToast.
🧹 Nitpick comments (1)
index.ts (1)

1785-1788: Collapse the two traversal-restart flags into one.

Both flags only drive the same break/continue path. A single restartAccountTraversal boolean would remove duplicated control flow and make future retry reasons easier to wire correctly.

Also applies to: 2441-2454

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 1785 - 1788, Replace the two boolean flags
restartAccountTraversalWithFallback and
restartAccountTraversalAfterWorkspaceDeactivation with a single flag
restartAccountTraversal; initialize restartAccountTraversal next to accountCount
and attempted (where accountCount = accountManager.getAccountCount() is set),
then update all control flow checks and assignments that reference either of the
two old flags (including the other occurrence around the 2441-2454 block) to use
restartAccountTraversal instead so the break/continue logic is driven by one
unified boolean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/index.test.ts`:
- Around line 3207-3210: Update the 402 test fixture so that one mocked Response
only contains detail.code (and omits error.code) to ensure parsing relies on
detail; locate the Response JSON object created in the test (the object with
keys "error" and "detail" returned via new Response(JSON.stringify({...})) in
test/index.test.ts) and remove or null out the top-level error.code for at least
one of the 402 cases (keep detail.code present) so the test verifies handling
when only detail.code is provided.

---

Outside diff comments:
In `@index.ts`:
- Around line 2086-2140: When workspaceDeactivated is true we must always
restart the account traversal even if
accountManager.removeAccountsWithSameRefreshToken(account) returns 0 to avoid
using a stale attempted/accountCount snapshot and skipping siblings after
concurrent removals; update the branch that handles removedCount to set
restartAccountTraversalAfterWorkspaceDeactivation = true before the removalCount
check (or set it in both the removedCount > 0 and removedCount === 0 paths) and
ensure showToast/early break logic still runs for the removedCount > 0 case
while preserving the existing logWarn + break for the removedCount === 0 case;
key symbols: workspaceDeactivated,
accountManager.removeAccountsWithSameRefreshToken,
restartAccountTraversalAfterWorkspaceDeactivation, showToast.

---

Nitpick comments:
In `@index.ts`:
- Around line 1785-1788: Replace the two boolean flags
restartAccountTraversalWithFallback and
restartAccountTraversalAfterWorkspaceDeactivation with a single flag
restartAccountTraversal; initialize restartAccountTraversal next to accountCount
and attempted (where accountCount = accountManager.getAccountCount() is set),
then update all control flow checks and assignments that reference either of the
two old flags (including the other occurrence around the 2441-2454 block) to use
restartAccountTraversal instead so the break/continue logic is driven by one
unified boolean.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9570622b-769c-46e5-9942-bbfda0a04e9f

📥 Commits

Reviewing files that changed from the base of the PR and between c7bed33 and 81a4d79.

📒 Files selected for processing (2)
  • index.ts
  • test/index.test.ts

Comment thread test/index.test.ts
Comment on lines +3207 to +3210
return new Response(JSON.stringify({
error: { code: "deactivated_workspace", message: "workspace dead" },
detail: { code: "deactivated_workspace", message: "workspace dead" },
}), {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make at least one 402 fixture detail.code-only to avoid false positives.

At Line 3207 and Line 4290, the mocked payload includes both error.code and detail.code. That can still pass if detail.code parsing breaks, because the legacy field remains available. For this PR objective, at least one path should only provide detail.code.

Suggested fixture adjustment
-return new Response(JSON.stringify({
-  error: { code: "deactivated_workspace", message: "workspace dead" },
-  detail: { code: "deactivated_workspace", message: "workspace dead" },
-}), {
+return new Response(JSON.stringify({
+  detail: { code: "deactivated_workspace", message: "workspace dead" },
+}), {
   status: 402,
 });

Also applies to: 4290-4293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 3207 - 3210, Update the 402 test fixture so
that one mocked Response only contains detail.code (and omits error.code) to
ensure parsing relies on detail; locate the Response JSON object created in the
test (the object with keys "error" and "detail" returned via new
Response(JSON.stringify({...})) in test/index.test.ts) and remove or null out
the top-level error.code for at least one of the 402 cases (keep detail.code
present) so the test verifies handling when only detail.code is provided.

@ndycode
ndycode merged commit 039b0f3 into ndycode:main Mar 24, 2026
1 of 2 checks passed
ndycode added a commit that referenced this pull request Apr 6, 2026
…tation

Retry deactivated workspaces during rotation
ndycode added a commit that referenced this pull request Apr 6, 2026
…tation

Retry deactivated workspaces during rotation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants