Skip to content

refactor(web): break stores/auth and api/client cycle via leaf handler registry#2082

Merged
Aureliolo merged 2 commits into
mainfrom
refactor/stores-auth-api-client-circular-dep
May 23, 2026
Merged

refactor(web): break stores/auth and api/client cycle via leaf handler registry#2082
Aureliolo merged 2 commits into
mainfrom
refactor/stores-auth-api-client-circular-dep

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Breaks the real static cycle stores/auth.ts -> api/endpoints/auth.ts -> api/client.ts -> (dyn) stores/auth.ts and drops the dpdm --skip-imports exemption that PR #2078 carried as a temporary patch (tracked in ADR-0006 "Exemption ledger").

What changed

  • New leaf module web/src/api/unauthorized-handler.ts with setUnauthorizedHandler / notifyUnauthorized / _resetUnauthorizedHandlerForTests. Depends on nothing under @/.
  • web/src/api/client.ts 401 branch switched from dynamic import('@/stores/auth').then(...).catch(...) to a static notifyUnauthorized() call. The .catch fallback is removed; with static imports the handler is wired before any HTTP call can land a 401 (via main.tsx -> App.tsx -> router -> guards -> stores/auth).
  • web/src/stores/auth.ts registers the closure at module init: setUnauthorizedHandler(() => useAuthStore.getState().handleUnauthorized()).
  • web/package.json lint:circular simplified to dpdm --no-warning --no-tree --exit-code circular:1 src/main.tsx (no --skip-imports).
  • .github/workflows/ci.yml dashboard-lint job now runs npm run lint:circular.

Test plan

  • npm --prefix web run type-check: clean.
  • npm --prefix web run lint: 0 warnings.
  • npm --prefix web run lint:circular: exits 0 with no cycles (no --skip-imports).
  • New regression web/src/__tests__/api/no-circular-deps.test.ts: asserts lint:circular has no --skip-imports and that dpdm reports zero cycles. Invokes dpdm via node node_modules/dpdm/lib/bin/dpdm.js (cross-platform, no shell: true deprecation).
  • Full web unit suite: 266 files / 3232 tests pass, zero leaked handles.
  • Pre-push hook ran the dpdm gate during push and passed.
  • Existing 401 integration test (web/src/__tests__/api/client.test.ts:329-372) covers the rewired path end-to-end.

Review coverage

Reduced pre-PR review covering only the agents whose scope overlaps this change: frontend-reviewer + code-reviewer. The two HIGH/CRITICAL flags raised (module-init race condition, closure memory leak) were verified false positives against the actual static-ESM import graph and Zustand's singleton store lifetime; no action items remained.

Closes #2072

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2026

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 5 package(s) with unknown licenses.
See the Details below.

License Issues

.github/workflows/cla.yml

PackageVersionLicenseIssue Type
Aureliolo/synthorg/.github/actions/checkout2592118NullUnknown License

.github/workflows/dev-release.yml

PackageVersionLicenseIssue Type
Aureliolo/synthorg/.github/actions/checkout2592118NullUnknown License

.github/workflows/lighthouse.yml

PackageVersionLicenseIssue Type
Aureliolo/synthorg/.github/actions/checkout2592118NullUnknown License

.github/workflows/refresh-test-durations.yml

PackageVersionLicenseIssue Type
Aureliolo/synthorg/.github/actions/checkout2592118NullUnknown License

.github/workflows/zizmor.yml

PackageVersionLicenseIssue Type
Aureliolo/synthorg/.github/actions/checkout2592118NullUnknown License
Allowed Licenses: MIT, MIT-0, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0, PSF-2.0, Unlicense, 0BSD, CC0-1.0, CC-BY-3.0, CC-BY-4.0, Python-2.0, Python-2.0.1, LicenseRef-scancode-free-unknown, LicenseRef-scancode-protobuf, LicenseRef-scancode-google-patent-license-golang, ZPL-2.1, LGPL-2.0-only, LGPL-2.0-or-later, LGPL-2.1-only, LGPL-2.1-or-later, LGPL-3.0-only, LGPL-3.0-or-later, BlueOak-1.0.0, OFL-1.1
Excluded from license check: pkg:pypi/codespell@2.4.2, pkg:pypi/yamllint@1.38.0, pkg:pypi/mem0ai@2.0.1, pkg:pypi/numpy@2.4.4, pkg:pypi/qdrant-client@1.17.1, pkg:pypi/posthog@7.9.12, pkg:pypi/aiohttp@3.13.5, pkg:pypi/cyclonedx-python-lib@11.7.0, pkg:pypi/fsspec@2026.3.0, pkg:pypi/griffelib@2.0.2, pkg:pypi/grpcio@1.80.0, pkg:pypi/charset-normalizer@3.4.6, pkg:pypi/wrapt@2.1.2, pkg:pypi/pytest-codspeed@4.5.0, pkg:pypi/hypothesis@6.152.4, pkg:pypi/litellm@1.83.14, pkg:pypi/openai@2.33.0, pkg:pypi/pyngrok@8.1.2, pkg:pypi/tokenizers@0.23.1, pkg:pypi/typer@0.25.0, pkg:npm/@img/sharp-wasm32@0.33.5, pkg:npm/@img/sharp-win32-ia32@0.33.5, pkg:npm/@img/sharp-win32-x64@0.33.5, pkg:npm/json-schema-typed@8.0.2, pkg:npm/victory-vendor@37.3.6, pkg:pypi/scikit-learn@1.8.0, pkg:pypi/torch@2.11.0, pkg:pypi/cuda-bindings@13.2.0, pkg:pypi/cuda-pathfinder@1.5.0, pkg:pypi/cuda-toolkit@13.0.2, pkg:pypi/nvidia-cublas@13.1.0.3, pkg:pypi/nvidia-cuda-cupti@13.0.85, pkg:pypi/nvidia-cuda-nvrtc@13.0.88, pkg:pypi/nvidia-cuda-runtime@13.0.96, pkg:pypi/nvidia-cudnn-cu13@9.19.0.56, pkg:pypi/nvidia-cufft@12.0.0.61, pkg:pypi/nvidia-cufile@1.15.1.6, pkg:pypi/nvidia-curand@10.4.0.35, pkg:pypi/nvidia-cusolver@12.0.4.66, pkg:pypi/nvidia-cusparse@12.6.3.3, pkg:pypi/nvidia-cusparselt-cu13@0.8.0, pkg:pypi/nvidia-nccl-cu13@2.28.9, pkg:pypi/nvidia-nvjitlink@13.0.88, pkg:pypi/nvidia-nvshmem-cu13@3.4.5, pkg:pypi/nvidia-nvtx@13.0.85, pkg:pypi/pillow@12.2.0

OpenSSF Scorecard

PackageVersionScoreDetails
actions/Aureliolo/synthorg/.github/actions/checkout 2592118 🟢 7.1
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/25 approved changesets -- score normalized to 2
Dependency-Update-Tool🟢 10update tool detected
Maintained⚠️ 0project was created within the last 90 days. Please review its contents carefully
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 9license file detected
Signed-Releases🟢 105 out of the last 5 releases have a total of 10 signed artifacts.
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
SAST🟢 10SAST tool detected
Packaging🟢 10packaging workflow detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 022 existing vulnerabilities detected
Contributors⚠️ 0project has 0 contributing companies or organizations -- score normalized to 0
CI-Tests🟢 1024 out of 24 merged PRs checked by a CI test -- score normalized to 10
actions/Aureliolo/synthorg/.github/actions/checkout 2592118 🟢 7.1
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/25 approved changesets -- score normalized to 2
Dependency-Update-Tool🟢 10update tool detected
Maintained⚠️ 0project was created within the last 90 days. Please review its contents carefully
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 9license file detected
Signed-Releases🟢 105 out of the last 5 releases have a total of 10 signed artifacts.
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
SAST🟢 10SAST tool detected
Packaging🟢 10packaging workflow detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 022 existing vulnerabilities detected
Contributors⚠️ 0project has 0 contributing companies or organizations -- score normalized to 0
CI-Tests🟢 1024 out of 24 merged PRs checked by a CI test -- score normalized to 10
actions/Aureliolo/synthorg/.github/actions/checkout 2592118 🟢 7.1
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/25 approved changesets -- score normalized to 2
Dependency-Update-Tool🟢 10update tool detected
Maintained⚠️ 0project was created within the last 90 days. Please review its contents carefully
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 9license file detected
Signed-Releases🟢 105 out of the last 5 releases have a total of 10 signed artifacts.
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
SAST🟢 10SAST tool detected
Packaging🟢 10packaging workflow detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 022 existing vulnerabilities detected
Contributors⚠️ 0project has 0 contributing companies or organizations -- score normalized to 0
CI-Tests🟢 1024 out of 24 merged PRs checked by a CI test -- score normalized to 10
actions/Aureliolo/synthorg/.github/actions/checkout 2592118 🟢 7.1
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/25 approved changesets -- score normalized to 2
Dependency-Update-Tool🟢 10update tool detected
Maintained⚠️ 0project was created within the last 90 days. Please review its contents carefully
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 9license file detected
Signed-Releases🟢 105 out of the last 5 releases have a total of 10 signed artifacts.
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
SAST🟢 10SAST tool detected
Packaging🟢 10packaging workflow detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 022 existing vulnerabilities detected
Contributors⚠️ 0project has 0 contributing companies or organizations -- score normalized to 0
CI-Tests🟢 1024 out of 24 merged PRs checked by a CI test -- score normalized to 10
actions/Aureliolo/synthorg/.github/actions/checkout 2592118 🟢 7.1
Details
CheckScoreReason
Code-Review⚠️ 2Found 7/25 approved changesets -- score normalized to 2
Dependency-Update-Tool🟢 10update tool detected
Maintained⚠️ 0project was created within the last 90 days. Please review its contents carefully
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 9license file detected
Signed-Releases🟢 105 out of the last 5 releases have a total of 10 signed artifacts.
Pinned-Dependencies🟢 9dependency not pinned by hash detected -- score normalized to 9
SAST🟢 10SAST tool detected
Packaging🟢 10packaging workflow detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 022 existing vulnerabilities detected
Contributors⚠️ 0project has 0 contributing companies or organizations -- score normalized to 0
CI-Tests🟢 1024 out of 24 merged PRs checked by a CI test -- score normalized to 10

Scanned Files

  • .github/workflows/cla.yml
  • .github/workflows/dev-release.yml
  • .github/workflows/lighthouse.yml
  • .github/workflows/refresh-test-durations.yml
  • .github/workflows/zizmor.yml

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 049e5085-1d5a-48c0-9e20-c07a98a69412

📥 Commits

Reviewing files that changed from the base of the PR and between 215da43 and f7955e4.

📒 Files selected for processing (30)
  • .github/workflows/apko-lock.yml
  • .github/workflows/auto-rollover.yml
  • .github/workflows/ci-preflight.yml
  • .github/workflows/ci.yml
  • .github/workflows/cla.yml
  • .github/workflows/cli.yml
  • .github/workflows/codspeed.yml
  • .github/workflows/dast.yml
  • .github/workflows/dependency-review.yml
  • .github/workflows/dev-release.yml
  • .github/workflows/docker.yml
  • .github/workflows/evals.yml
  • .github/workflows/finalize-release.yml
  • .github/workflows/graduate.yml
  • .github/workflows/lighthouse.yml
  • .github/workflows/pages-preview.yml
  • .github/workflows/pages.yml
  • .github/workflows/pyright.yml
  • .github/workflows/python-audit.yml
  • .github/workflows/refresh-test-durations.yml
  • .github/workflows/release.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/secret-scan.yml
  • .github/workflows/sync-labels.yml
  • .github/workflows/zizmor.yml
  • web/package.json
  • web/src/__tests__/api/no-circular-deps.test.ts
  • web/src/api/client.ts
  • web/src/api/unauthorized-handler.ts
  • web/src/stores/auth.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (6)
!(src/synthorg/persistence/**)

📄 CodeRabbit inference engine (CLAUDE.md)

Only src/synthorg/persistence/ may import sqlite/psycopg or emit raw SQL

Files:

  • .github/workflows/finalize-release.yml
  • .github/workflows/cla.yml
  • .github/workflows/evals.yml
  • .github/workflows/dev-release.yml
  • .github/workflows/pages.yml
  • .github/workflows/dependency-review.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/apko-lock.yml
  • .github/workflows/secret-scan.yml
  • .github/workflows/codspeed.yml
  • .github/workflows/zizmor.yml
  • .github/workflows/ci-preflight.yml
  • .github/workflows/graduate.yml
  • .github/workflows/dast.yml
  • .github/workflows/sync-labels.yml
  • .github/workflows/lighthouse.yml
  • .github/workflows/refresh-test-durations.yml
  • web/src/api/client.ts
  • .github/workflows/auto-rollover.yml
  • .github/workflows/python-audit.yml
  • .github/workflows/release.yml
  • .github/workflows/pyright.yml
  • web/package.json
  • web/src/api/unauthorized-handler.ts
  • web/src/stores/auth.ts
  • web/src/__tests__/api/no-circular-deps.test.ts
  • .github/workflows/pages-preview.yml
  • .github/workflows/cli.yml
  • .github/workflows/ci.yml
  • .github/workflows/docker.yml
web/src/**/*.{js,jsx,ts,tsx,mts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{js,jsx,ts,tsx,mts}: Always use createLogger from @/lib/logger; never bare console.warn/console.error/console.debug in application code. Variable name must always be log. Only logger.ts itself may use bare console methods. Use log.debug() (DEV-only, stripped in production), log.warn(), log.error().
Pass dynamic/untrusted values as separate args to logger calls (not interpolated into the message string) so they go through sanitizeArg
Attacker-controlled fields inside structured objects must be wrapped in sanitizeForLog() before embedding in log calls
Error-code constants (MANDATORY): import ErrorCode and ErrorCategory from @/api/types/errors (re-exported from the generated web/src/api/types/error-codes.gen.ts). Discriminate on ErrorCode.<NAME>, never on raw integer literals.
Use @eslint-react/web-api-no-leaked-fetch to detect fetch() in effects without AbortController cleanup

Files:

  • web/src/api/client.ts
  • web/src/api/unauthorized-handler.ts
  • web/src/stores/auth.ts
  • web/src/__tests__/api/no-circular-deps.test.ts
web/src/**/*.{ts,tsx,mts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,mts}: Use @typescript-eslint/no-floating-promises to forbid unawaited promises so async work cannot survive the test that scheduled it and trip the active-handle gate
Use @typescript-eslint/no-misused-promises (with checksVoidReturn: { attributes: false }) to forbid passing async functions where the callsite ignores the returned promise. React 19 async event handlers stay allowed via the attributes: false exemption.

Files:

  • web/src/api/client.ts
  • web/src/api/unauthorized-handler.ts
  • web/src/stores/auth.ts
  • web/src/__tests__/api/no-circular-deps.test.ts
web/src/stores/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/stores/**/*.ts: List reads (fetch*) must set error: string | null on the store instead of toasting
Test teardown (MANDATORY): any new store that schedules timers or attaches event listeners must expose an equivalent cleanup hook and register it in the global afterEach. The global afterEach in web/src/test-setup.tsx already calls useToastStore.getState().dismissAll(), cancelPendingPersist(), and useThemeStore.getState().teardown().

Files:

  • web/src/stores/auth.ts
web/src/{api/endpoints,stores}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

Cursor pagination (MANDATORY): list endpoints must use opaque cursor-based paging via PaginationMeta. Stores must keep nextCursor + hasMore in state (not offset arithmetic) and early-return when !hasMore || !nextCursor. Display counts must come from data.length.

Files:

  • web/src/stores/auth.ts
web/src/{stores,**/*.test.{ts,tsx}}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Active-handle gate (MANDATORY): every unit test runs under web/test-infra/active-handle-tracker.ts, which fails any test that leaks an event-loop-holding resource. A new store that schedules timers / attaches listeners MUST expose a teardown hook and register it in the global afterEach; otherwise the gate fails the first test that triggers the schedule.

Files:

  • web/src/__tests__/api/no-circular-deps.test.ts
🔇 Additional comments (30)
.github/workflows/apko-lock.yml (1)

56-56: LGTM!

Also applies to: 271-271

.github/workflows/auto-rollover.yml (1)

47-47: LGTM!

Also applies to: 241-241

.github/workflows/ci-preflight.yml (1)

40-40: LGTM!

.github/workflows/ci.yml (1)

47-47: LGTM!

Also applies to: 115-115, 186-186, 212-212, 231-231, 279-279, 298-298, 352-352, 528-528, 645-645, 732-732, 814-814, 968-968, 1043-1043, 1074-1074, 1100-1100, 1113-1114, 1126-1126, 1155-1155, 1202-1202, 1246-1246, 1271-1271, 1345-1345

.github/workflows/cla.yml (1)

240-240: LGTM!

.github/workflows/cli.yml (1)

35-35: LGTM!

Also applies to: 57-57, 91-91, 126-126, 197-197, 227-227, 262-262, 379-379

.github/workflows/codspeed.yml (1)

61-61: LGTM!

Also applies to: 95-95, 121-121

.github/workflows/dast.yml (1)

40-40: LGTM!

Also applies to: 244-244

.github/workflows/dependency-review.yml (1)

17-17: LGTM!

.github/workflows/dev-release.yml (1)

52-52: LGTM!

Also applies to: 288-288

.github/workflows/docker.yml (1)

68-68: LGTM!

Also applies to: 156-156, 219-219, 271-271, 307-307, 337-337, 368-368, 398-398, 429-429, 459-459, 490-490, 529-529, 615-615, 643-643, 778-778, 903-903, 952-952, 989-989, 1017-1017, 1054-1054, 1101-1101, 1149-1149, 1230-1230, 1257-1257, 1284-1284, 1311-1311, 1351-1351, 1437-1437

.github/workflows/evals.yml (1)

30-30: LGTM!

Also applies to: 73-73

.github/workflows/finalize-release.yml (1)

826-826: LGTM!

.github/workflows/graduate.yml (1)

52-52: LGTM!

.github/workflows/lighthouse.yml (1)

43-43: LGTM!

Also applies to: 110-110, 158-158

.github/workflows/pages-preview.yml (1)

106-106: LGTM!

Also applies to: 293-293

.github/workflows/pages.yml (1)

36-36: LGTM!

.github/workflows/pyright.yml (1)

30-30: LGTM!

.github/workflows/python-audit.yml (1)

17-17: LGTM!

Also applies to: 99-99

.github/workflows/refresh-test-durations.yml (1)

64-64: LGTM!

.github/workflows/release.yml (1)

36-36: LGTM!

Also applies to: 66-66, 348-348

.github/workflows/scorecard.yml (1)

22-22: LGTM!

Also applies to: 57-57

.github/workflows/secret-scan.yml (1)

23-23: LGTM!

Also applies to: 64-64

.github/workflows/sync-labels.yml (1)

24-24: LGTM!

.github/workflows/zizmor.yml (1)

29-29: ⚡ Quick win

Pin checkout action SHA is valid and consistent

The pinned checkout action SHA 25921183f274c930bf473dc0339376bda0961eaf exists in Aureliolo/synthorg, and all references to Aureliolo/synthorg/.github/actions/checkout@... across .github/workflows/ use the same SHA.

web/src/api/unauthorized-handler.ts (1)

11-59: LGTM!

web/src/api/client.ts (1)

19-19: LGTM!

Also applies to: 111-114

web/src/stores/auth.ts (1)

11-11: LGTM!

Also applies to: 204-213

web/package.json (1)

21-21: LGTM!

web/src/__tests__/api/no-circular-deps.test.ts (1)

1-55: LGTM!


Walkthrough

This PR eliminates a real circular dependency between api/client.ts and stores/auth.ts by introducing callback injection. A new leaf module unauthorized-handler.ts provides a callback registration interface for 401 responses. The API client notifies the handler instead of importing the auth store; the auth store registers itself as the handler at initialization. The dpdm lint exemption is removed, and a regression test ensures the cycle stays broken.

Suggested labels: type:tech-debt, scope:ci, prio:medium

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains extensive GitHub Actions workflow updates to pin the shared checkout action across ~20 workflow files, which are out of scope to the stated circular-dependency refactoring objective for issue #2072. Isolate the circular-dependency fix (web/ code changes and ci.yml dashboard-lint) into a separate PR from the widespread checkout action pinning across all workflows.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main refactoring: breaking a circular dependency in the web module using a leaf handler registry pattern.
Description check ✅ Passed The description is detailed, on-topic, explains the circular dependency problem, describes the solution (leaf module, handler registry), lists all changes, and includes comprehensive test results.
Linked Issues check ✅ Passed The PR directly addresses issue #2072 by breaking the circular dependency via a leaf handler module, removing the dpdm --skip-imports exemption, and ensuring lint:circular exits 0 with no cycles.
Docstring Coverage ✅ Passed Docstring coverage is 66.67% which is sufficient. The required threshold is 40.00%.

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


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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 23, 2026

Merging this PR will not alter performance

✅ 54 untouched benchmarks


Comparing refactor/stores-auth-api-client-circular-dep (f7955e4) with main (2592118)

Open in CodSpeed

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a long-standing circular dependency in the web architecture by implementing a registry pattern for unauthorized access handling. By introducing a lightweight leaf module to mediate communication between the API client and the authentication store, the change eliminates the need for dynamic imports and temporary linting exemptions, resulting in a cleaner and more robust module graph.

Highlights

  • Circular Dependency Resolution: Introduced a new leaf module, unauthorized-handler.ts, to decouple api/client and stores/auth, effectively breaking the static circular dependency.
  • Refactored 401 Handling: Replaced dynamic imports in the API client's 401 interceptor with a static call to the new handler, ensuring the auth store is wired before any HTTP requests occur.
  • CI/CD and Linting: Removed the --skip-imports exemption from the lint:circular script and added a regression test to ensure no circular dependencies are reintroduced.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 23, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the 401 unauthorized handling to resolve a circular dependency between the API client and the auth store. It introduces a new unauthorized-handler leaf module to decouple the components and adds a regression test to ensure the circular dependency is not reintroduced. Review feedback suggests restoring a defensive fallback for 401 responses to prevent them from being silently ignored if the handler is not yet registered, and recommends enhancing the handler registry to support multiple subscribers for better extensibility.

Comment thread web/src/api/client.ts
// We only need to sync the Zustand auth state. Routed through the
// leaf `unauthorized-handler` module so the client has no static
// dependency on the auth store.
notifyUnauthorized()
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.

medium

The removal of the fallback redirect logic (previously in the .catch block) introduces a risk where 401 responses might be silently ignored if the apiClient is used in a context where the authStore has not been initialized (e.g., standalone tools, specific test environments, or early bootstrap phases). While the main application wires this at module init, the previous implementation provided a defensive safety net. Consider adding a default behavior or a warning in the notifyUnauthorized call if no handler is present to ensure session expiry is always handled visibly.

Comment thread web/src/api/unauthorized-handler.ts Outdated
Comment on lines +11 to +13
export function setUnauthorizedHandler(handler: UnauthorizedHandler | null): void {
current = handler
}
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.

medium

The current implementation uses a single-slot registry (let current). In a larger application, this pattern is susceptible to 'singleton collision' where multiple modules might attempt to register different handlers, with the last one silently overwriting previous ones. While currently only authStore uses this, using a Set of handlers or an event-emitter pattern would be more robust and extensible for future requirements (e.g., logging 401s in a separate observability module).

@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.11%. Comparing base (904f2fb) to head (f7955e4).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2082      +/-   ##
==========================================
+ Coverage   85.04%   87.11%   +2.06%     
==========================================
  Files        2251     2251              
  Lines      130269   130269              
  Branches    10748        0   -10748     
==========================================
+ Hits       110792   113482    +2690     
- Misses      16759    16772      +13     
+ Partials     2718       15    -2703     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Aureliolo added 2 commits May 24, 2026 01:12
- zizmor: replace orphaned checkout SHA 31a45a7 with 2592118 (origin/main HEAD) across 25 workflow files; the prior SHA was not reachable from any branch, failing impostor-commit on PR runs
- gemini(unauthorized-handler): convert single-slot registry to Set-based multi-subscriber; setUnauthorizedHandler now returns an unsubscribe closure so HMR and observability subscribers can co-exist without silent overwrites
- gemini(unauthorized-handler): log warning when notifyUnauthorized fires with no subscribers (early bootstrap, standalone tools, test envs) so 401s never silently disappear
@Aureliolo Aureliolo force-pushed the refactor/stores-auth-api-client-circular-dep branch from 215da43 to f7955e4 Compare May 23, 2026 23:15
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 23, 2026 23:16 — with GitHub Actions Inactive
@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added prio:medium Should do, but not blocking scope:ci type:tech-debt labels May 23, 2026
@Aureliolo Aureliolo merged commit 72c6648 into main May 23, 2026
105 checks passed
@Aureliolo Aureliolo deleted the refactor/stores-auth-api-client-circular-dep branch May 23, 2026 23:51
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 23, 2026 23:51 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request May 24, 2026
Rebased on origin/main to pick up PR #2082's workflow SHA refresh, then folded fixes for the six failing checks (zizmor, lychee, 3x test flake symptoms, CI Pass) plus the three gemini-code-assist inline comments.

lychee.yml: bump local action SHA 31a45a72592118 to match the rest of the workflows on main (the old SHA is no longer reachable in the repo, which is why zizmor's impostor-commit check fired across every workflow); rename version: to lycheeVersion: (the input name in v2 of lycheeverse/lychee-action — the old name was silently ignored, so the action ran default v0.23.0 instead of the pinned v0.24.2).

renovate.json (gemini #1): \s+ → \s* so the binary-tool matcher catches unindented shell variables (LYCHEE_VERSION sits at column 0, not after YAML indent). Also add lycheeVersion: alternation to the action-input matcher so renovate keeps tracking the lychee bump path after the rename above.

scripts/install_cli_tools.sh (gemini #2): handle GOPATH multi-entry strings — take the first colon/semicolon-separated entry, since go install writes to the first entry's bin/ and the raw joined string breaks mkdir -p.

scripts/install_cli_tools.sh (gemini #3): switch tmpdir cleanup trap from RETURN to EXIT so set -e failures (failed curl / sha256sum / tar) still trigger cleanup; expand ${tmpdir} at trap-install time since the local goes out of scope before EXIT fires.

lychee.toml: exclude dash.cloudflare.com (always 403 to unauthenticated probes — referenced from docs/guides/fork-setup.md as a navigation pointer) and docs.sigstore.dev (intermittent timeouts under the strict 10s ceiling — referenced from docs/guides/deployment.md).

Test failures (Test Unit shard 4, Test E2E, Test Conformance SQLite) are pre-existing pytest_sessionstart cross-worker flakes — the os.abort() in tests/conftest.py is the documented forensic mechanism for capturing stacks when the FileLock races. The same flake hit main's prior CI run at 22:29 and self-healed on the next run at 23:51; re-pushing should re-trigger and likely clear them. CI Pass is the aggregator; it will go green once the above clear.
Aureliolo added a commit that referenced this pull request May 24, 2026
Rebased on origin/main to pick up PR #2082's workflow SHA refresh, then folded fixes for the six failing checks (zizmor, lychee, 3x test flake symptoms, CI Pass) plus the three gemini-code-assist inline comments.

lychee.yml: bump local action SHA 31a45a72592118 to match the rest of the workflows on main (the old SHA is no longer reachable in the repo, which is why zizmor's impostor-commit check fired across every workflow); rename version: to lycheeVersion: (the input name in v2 of lycheeverse/lychee-action — the old name was silently ignored, so the action ran default v0.23.0 instead of the pinned v0.24.2).

renovate.json (gemini #1): \s+ → \s* so the binary-tool matcher catches unindented shell variables (LYCHEE_VERSION sits at column 0, not after YAML indent). Also add lycheeVersion: alternation to the action-input matcher so renovate keeps tracking the lychee bump path after the rename above.

scripts/install_cli_tools.sh (gemini #2): handle GOPATH multi-entry strings — take the first colon/semicolon-separated entry, since go install writes to the first entry's bin/ and the raw joined string breaks mkdir -p.

scripts/install_cli_tools.sh (gemini #3): switch tmpdir cleanup trap from RETURN to EXIT so set -e failures (failed curl / sha256sum / tar) still trigger cleanup; expand ${tmpdir} at trap-install time since the local goes out of scope before EXIT fires.

lychee.toml: exclude dash.cloudflare.com (always 403 to unauthenticated probes — referenced from docs/guides/fork-setup.md as a navigation pointer) and docs.sigstore.dev (intermittent timeouts under the strict 10s ceiling — referenced from docs/guides/deployment.md).

Test failures (Test Unit shard 4, Test E2E, Test Conformance SQLite) are pre-existing pytest_sessionstart cross-worker flakes — the os.abort() in tests/conftest.py is the documented forensic mechanism for capturing stacks when the FileLock races. The same flake hit main's prior CI run at 22:29 and self-healed on the next run at 23:51; re-pushing should re-trigger and likely clear them. CI Pass is the aggregator; it will go green once the above clear.
Aureliolo added a commit that referenced this pull request May 24, 2026
Rebased on origin/main to pick up PR #2082's workflow SHA refresh, then folded fixes for the six failing checks (zizmor, lychee, 3x test flake symptoms, CI Pass) plus the three gemini-code-assist inline comments.

lychee.yml: bump local action SHA 31a45a72592118 to match the rest of the workflows on main (the old SHA is no longer reachable in the repo, which is why zizmor's impostor-commit check fired across every workflow); rename version: to lycheeVersion: (the input name in v2 of lycheeverse/lychee-action — the old name was silently ignored, so the action ran default v0.23.0 instead of the pinned v0.24.2).

renovate.json (gemini #1): \s+ → \s* so the binary-tool matcher catches unindented shell variables (LYCHEE_VERSION sits at column 0, not after YAML indent). Also add lycheeVersion: alternation to the action-input matcher so renovate keeps tracking the lychee bump path after the rename above.

scripts/install_cli_tools.sh (gemini #2): handle GOPATH multi-entry strings — take the first colon/semicolon-separated entry, since go install writes to the first entry's bin/ and the raw joined string breaks mkdir -p.

scripts/install_cli_tools.sh (gemini #3): switch tmpdir cleanup trap from RETURN to EXIT so set -e failures (failed curl / sha256sum / tar) still trigger cleanup; expand ${tmpdir} at trap-install time since the local goes out of scope before EXIT fires.

lychee.toml: exclude dash.cloudflare.com (always 403 to unauthenticated probes — referenced from docs/guides/fork-setup.md as a navigation pointer) and docs.sigstore.dev (intermittent timeouts under the strict 10s ceiling — referenced from docs/guides/deployment.md).

Test failures (Test Unit shard 4, Test E2E, Test Conformance SQLite) are pre-existing pytest_sessionstart cross-worker flakes — the os.abort() in tests/conftest.py is the documented forensic mechanism for capturing stacks when the FileLock races. The same flake hit main's prior CI run at 22:29 and self-healed on the next run at 23:51; re-pushing should re-trigger and likely clear them. CI Pass is the aggregator; it will go green once the above clear.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prio:medium Should do, but not blocking scope:ci type:tech-debt

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix stores/auth.ts to api/client.ts circular dependency (drop dpdm --skip-imports)

1 participant