Skip to content

fix: resolve all 102 SonarQube issues + boost test coverage - #5

Merged
thomasluizon merged 1 commit into
mainfrom
fix/sonarqube-final-cleanup
Apr 5, 2026
Merged

fix: resolve all 102 SonarQube issues + boost test coverage#5
thomasluizon merged 1 commit into
mainfrom
fix/sonarqube-final-cleanup

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Summary

  • Fix all 102 remaining SonarQube issues (unused imports, duplicate imports, SSR typeof guards, Readonly props, negated conditions, nested ternaries, cognitive complexity, accessibility)
  • Reduce cognitive complexity: upgrade (37->15), login (33->15), habit-request-builders (21->15)
  • Add 190+ new tests: shared Zod schemas, profile components, habit-form-fields, error pages, mutation hooks
  • 1339 tests across 120 files, all passing, 0 type errors

Test plan

  • npx vitest run from apps/web - 1339 tests, 120 files, all passing
  • npx turbo run type-check - web + shared pass with 0 errors
  • Verify habitKeys.count() present in keys.ts
  • SonarQube analysis shows reduced issues and increased coverage

🤖 Generated with Claude Code

Issues fixed:
- S1128: Remove 14 unused imports across 4 hook files
- S3863: Merge 10 duplicate imports across 5 hook files
- S7741: Add NOSONAR to 12 SSR typeof guards (intentional pattern)
- S6759: Add Readonly<> to 14 component prop types across 6 files
- S4325: Remove unnecessary type assertions
- S7735: Swap 4 negated ternary conditions
- S1854: Remove 2 useless assignments
- S6754: Fix 1 useState destructuring
- S6606: Use ??= operator
- S1082: Add keyboard listeners to 2 clickable elements
- S7721: Move 3 functions to outer scope
- S3358: Flatten 5 nested ternaries
- S2004: Extract 2 deeply nested functions
- S107: Refactor buildArticleClassName to use options object
- S6479: Fix array index key in login
- S6478: Extract component from parent in breakdown-suggestion
- S6847/S6845/S6848: Fix 4 accessibility issues
- S3776: Reduce cognitive complexity in upgrade (37->15), login (33->15),
  habit-request-builders (21->15)

Coverage improvements:
- 86 new tests for shared package Zod schemas (9 type files)
- 40 new tests for profile sub-components
- 33 new tests for habit-form-fields
- 12 new tests for error pages
- 16 new tests for use-habits mutations
- Additional tests for goal-list, push-prompt, trial-expired-modal

Total: 1339 tests across 120 files, all passing. 0 type errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Building Building Preview, Comment Apr 5, 2026 3:05am

@thomasluizon
thomasluizon merged commit 70aeb7a into main Apr 5, 2026
3 of 4 checks passed
@sonarqubecloud

sonarqubecloud Bot commented Apr 5, 2026

Copy link
Copy Markdown

@thomasluizon
thomasluizon deleted the fix/sonarqube-final-cleanup branch April 5, 2026 17:52
thomasluizon added a commit that referenced this pull request Apr 15, 2026
…ferral/JWT

Security hardening on the web BFF auth layer:

- Dedicated /api/auth/{send-code,verify-code,google,logout} routes now call
  buildForwardedClientHeaders(request) so X-Forwarded-For / CF-Connecting-IP /
  X-Orbit-Country-Code / Accept-Language / X-Orbit-Time-Zone reach the .NET
  backend. Previously these routes sent no client context, so the backend's
  DistributedRateLimitFilter partitioned every anonymous auth request under
  the Next.js server IP — collapsing the per-client 5 req/min bucket.

- auth_token cookie maxAge changed from 86_400 (1 day) to 604_800 (7 days) so
  it matches the backend JwtSettings.ExpiryHours=168. Prevents daily forced
  token refresh round-trips.

- Length-cap the JWT payload segment in /api/auth/session before JSON.parse
  (4 KB ceiling). Blocks a memory-amplification vector where a malicious
  cookie contains a megabyte-scale base64 segment.

- Cap referral query-param at 40 chars before cookie write on the login page.

Includes new unit tests for send-code header forwarding and updates
auth-api.test.ts to assert the 7-day maxAge.

Covers frontend plan Area A #1, #2, #4, #5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Apr 15, 2026
Non-shippable fixes from the audit that require architectural decisions,
new migrations, or large refactors have TODO comments citing PLAN.md:

- auth-api.ts: tryRefreshSession mutex — needs single-flight dedup (Area A #6)
- actions/chat.ts: server-persisted conversations — needs DB migration + API
  contract change (coordinated with backend F4 / P0 #5)
- habit-list.tsx: split into smaller components (Area C #1)
- habit-list.tsx: mobile parity — port bulk-action-bar, controls-menu,
  today-filters from web (Area E #1)

Each TODO references PLAN.md with the audit section + priority so the
follow-up PR author can find the context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Jul 23, 2026
…e call

Second review round on this PR found bypass #6: the fix for #5 only recognised a
member chained straight onto require(), so splitting it across two statements put
the write behind an allowlisted call-site name again.

  node -e "const m=require('fs'); const map=m.writeFileSync; map(signoff,'{}')"

Reproduced before changing anything: three of four write shapes were ALLOWED, and
the reviewer traced it by hand because its sandbox could not execute node. The
one shape already blocked was blocked for an unrelated reason.

The classifier now tracks the module REFERENCE rather than the syntax around it.
An identifier bound to a require() of a module is followed through plain
rebinding, so a chain of aliases inherits it, and every member lifted off any of
them must be a read API. A computed member is refused because the name is not
visible. A required .json stays DATA, so reading .cells off the manifest still
works. Renaming even a read is still refused by the callee scan: this list grants
nothing, it only withholds.

Replaced the per-identifier dynamic RegExp with static patterns after the first
attempt built its source in a template literal, where \b and \s are JS escapes,
not regex ones - it threw at parse time and every command sailed through while it
did. Static patterns cannot fail that way.

Five regression tests cover the two-step, transitive-rebind, computed-member and
destructure-off-a-bound-ref forms, plus the honest read that must keep working.
745 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt
thomasluizon added a commit that referenced this pull request Jul 23, 2026
* chore(harness): bring the /drive work-order harness to main

The harness that hands an unattended agent a scoped work order and then
measures whether it actually did the work has lived only on
feature/539-b5-apply-design. Nothing on main could run it, so /drive was
usable for exactly one issue. This ports the tooling itself.

What lands:
- tools/workorder.mjs, drive-queue.mjs, check-diff-ownership.mjs,
  surface-manifest.mjs, check-surface-coverage.mjs, visual-signature.mjs and
  the visual-capture helpers
- .claude/skills/drive/run.mjs, the engine SKILL.md references but main lacked
- forbid-gate-tamper + surface-coverage-gate hooks and their shared rule core
- the context reorganisation: .claude/rules/core.md stays always-loaded,
  activity-scoped judgement moves to .claude/playbooks/
- a surfaces manifest and 169 work orders derived from MAIN's own router and
  component graph, not the design branch's

Five independent review rounds hardened this before it moved. The load-bearing
outcomes: the ownership gate judges an explicit base...head range and refuses
to guess one, so a child that commits an escape and checks out the base branch
no longer scores clean; an empty range is a failure, not a pass, which is what
stops a child that produced nothing from being recorded ready-for-review; and
debt clearance is labelled a lint-count axis with the depth measurement printed
beside it, because clearing lint is not evidence that a pixel moved.

Deliberately NOT included, because they are #539 design work rather than
harness: the spacing-scale and no-arbitrary-zindex lint rules, the eslint
config changes that flip no-decorative-glow and no-raw-gradient to error (main
still has the glow those rules ban), and the populated suppressions ledgers.
The ledgers land empty so the tools have their required input; they fill when
the rules arrive with the design work.

Two fixes main needed to accept the port:
- the typed-uppercase corpus guard becomes a ratchet instead of asserting zero.
  main carries two pre-existing values per locale, and one of them,
  confirmPlaceholder "ORBIT", is load-bearing: the user types it verbatim to
  confirm account deletion, so it must never be recased.
- tools/visual-signature.mjs carried a stray NUL byte that made git treat a
  source file as binary and its diff unreviewable.

733 hook assertions pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): close the alias bypass in the tamper guard and finish the port's deletions

Addresses the three findings from the review of this PR.

Critical - the signoff tamper guard allowlisted a call by the identifier at the
call site, never by what that identifier was bound to. Since the allowlist has
to carry generic names for their legitimate uses (map, filter, parse, test),
`const map = require("fs").writeFileSync; map(signoff, "fake")` walked straight
through. I reproduced all four shapes the reviewer traced by hand, plus a
destructured rename and a computed member, before changing anything: four of
five were ALLOWED. Now the member lifted off a MODULE must itself be a read API,
a computed member is refused because the name is not visible, and a destructured
binding is judged by the keys it lifts rather than the names it gives them. A
required .json stays DATA, so `require("./surfaces.json").cells` still reads -
that is the pattern this hook's own message advertises, and over-blocking it is
what tempts a future session to disarm the hook. Renaming even a read API is
refused, fail-closed: the guard judges names, and the Read tool covers the rest.
Six regression tests pin every shape; the suite is 740 assertions, 0 failures.

Medium - the reviewer found flag-new-todos.mjs left behind with an import of
checkNewTodos, which this port removes. The cause was broader than the one file:
`git checkout <ref> -- <dir>` adds and updates but never deletes, so every path
the harness branch had removed survived the port. That was 18 files, not one:
the dead hook, the rules README and the three rule files that became playbooks,
the /execute and /night-run skills that /drive replaces, and the eight
.opencode/agents mirrors whose deletion is a locked decision the ported CLAUDE.md
already documents. eslint-rules/no-draggable-onscroll.cjs is deliberately KEPT -
it is main's own rule from #568, absent from the branch only because the branch
predates it.

Low - removed the stale comment claiming the locale corpus carries zero
typed-uppercase violations, which contradicted the ratchet directly below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): track the module reference, not the shape of the require call

Second review round on this PR found bypass #6: the fix for #5 only recognised a
member chained straight onto require(), so splitting it across two statements put
the write behind an allowlisted call-site name again.

  node -e "const m=require('fs'); const map=m.writeFileSync; map(signoff,'{}')"

Reproduced before changing anything: three of four write shapes were ALLOWED, and
the reviewer traced it by hand because its sandbox could not execute node. The
one shape already blocked was blocked for an unrelated reason.

The classifier now tracks the module REFERENCE rather than the syntax around it.
An identifier bound to a require() of a module is followed through plain
rebinding, so a chain of aliases inherits it, and every member lifted off any of
them must be a read API. A computed member is refused because the name is not
visible. A required .json stays DATA, so reading .cells off the manifest still
works. Renaming even a read is still refused by the callee scan: this list grants
nothing, it only withholds.

Replaced the per-identifier dynamic RegExp with static patterns after the first
attempt built its source in a template literal, where \b and \s are JS escapes,
not regex ones - it threw at parse time and every command sailed through while it
did. Static patterns cannot fail that way.

Five regression tests cover the two-step, transitive-rebind, computed-member and
destructure-off-a-bound-ref forms, plus the honest read that must keep working.
745 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* test(harness): pin the rest of the tamper-bypass class

The reference tracking that closed bypass #6 should make the neighbouring shapes
fall out for free. Probed rather than waited for: write streams, deletions, the
promises submodule off fs, fs/promises, child_process, and a path built by
concatenation are all refused. Pinned so a later change to the classifier cannot
quietly reopen one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): judge the path by the value it builds, and fail the fence closed

Third review round on this PR found bypass #7, and it was right about my own
test too. Every path rule matches a CONTIGUOUS literal in the raw command text,
so splitting the path across concatenated string literals means no rule fires
and the segment is never examined at all. The test I added last round fragmented
only the FILENAME, so the directory rule still matched an untouched token - it
passed for the wrong reason and left the hole open. Verified before fixing:
three of four fragmentation shapes were ALLOWED, including writes to both
signoff.json and surfaces.json.

Adjacent same-quoted literals are now joined before the path rules run, so the
path is judged by the value it builds rather than the way it is spelled.

Separately, and worse: the adapter caught every internal error and exited 0. A
fence that opens when its own code throws is not a fence. This was not theory -
my first attempt at the #6 fix built a regex inside a template literal, where \b
and \s are string escapes rather than regex ones. It threw on every call, and
every payload sailed through while it did. The catch now exits 2 with an
explanation, so a command the guard cannot classify is refused rather than waved
through. Proven by stubbing the rule core to throw.

Also carried forward from the review: WORKFLOW.md named the deleted /execute
skill and still described bundles as opening draft PRs, which contradicts the
ready-for-review canon; and the flag-new-todos removal left an orphaned fixture
whose MARK constant no longer existed.

Twenty write shapes across four adversarial batteries are refused; five honest
reads still pass, including require('./x.json').cells and a jq read. 755
assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): stop matching obfuscated paths; refuse the interpreter instead

Fourth review round found bypass #8: joinConcatenatedLiterals needed the quote
character to match on both sides of the +, so a mixed-quote split walked past it.
Verified, and it is worse than reported - two more shapes I went looking for also
walked past: splitting the path across variables, and assembling it with
Array.join. The suggested fix (drop the backreference) closes only the first.

That is the whole lesson of the last four rounds. Each one closed a different
spelling of "hide the path" - an alias, a split literal, a mixed quote pair, a
variable - and the next round found the next spelling. Deciding what a path
argument evaluates to inside arbitrary code is not something a regex can do, so
the path is no longer what decides.

An interpreter one-liner that cannot be PROVEN a pure read is now refused
whatever it names. This is not a new doctrine, it is the one this file already
states - the code is judged by an allowlist of what it may call - applied at the
point that previously required a path rule to fire first. There is nothing left
to obfuscate, because the filename stopped mattering.

The cost is honest and worth naming: a legitimate `node -e` that writes any file
is now refused and must use the Write tool, and a `node -e` whose require()
argument is itself concatenated is refused one step earlier than before (it was
already unverifiable). The reads this guard advertises still work: node -p of the
manifest, a bound module ref used for a direct read, cat/jq/grep, and the Read
tool.

Twenty-three write shapes across five adversarial batteries are refused. 761
assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): let the plan un-ignore reach .claude/plans/completed

Review found that `.claude/plans/*` plus `!.claude/plans/*.plan.md` cannot
re-include a plan inside a subdirectory: git never descends into an excluded
directory to evaluate a nested negation, so the `completed` entry was excluded
whole. That is exactly where implement/SKILL.md files a finished plan
(`mkdir -p .claude/plans/completed && mv ...`), and pr-review/SKILL.md reads it
back, so a completed plan would silently never be committed - recreating the
fresh-checkout-with-a-missing-contract failure the un-ignore was added to fix.

Verified with git add --dry-run rather than check-ignore, whose exit code is
ambiguous when the last matching rule is a negation: before, a top-level plan
staged and .claude/plans/completed was reported ignored; after, both stage. A
non-plan file in the same directory stays ignored, which is the half worth
keeping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

* fix(harness): give PAUSED the same shape as the other four gate files

Review found the best defect of this whole series, because it needs no cunning.
PAUSED was the only one of the five path patterns without an optional directory
prefix, and the only one tested against the whole raw command rather than per
segment. So `cd .claude/manifests && touch PAUSED` matched nothing: the cd
segment is a read, and the touch segment carries no directory text at all. That
one command silently creates the human-only disarm switch, and the Stop gate
exits immediately when it exists. Verified: four of five shapes were allowed,
including `cd ... && echo x > PAUSED`.

PAUSED now carries the same optional-prefix bare-name shape as signoff,
surfaces and verdicts, and is judged per segment like them. Reads of it are
allowed as a consequence, which also retires a false positive that blocked an
ordinary `[ -f ... ]` existence test earlier in this session - a guard that
refuses honest reads is one a future session disarms.

The same review noted the sibling: plain shell adjacent-quote concatenation
(`sign''off.json`, which the shell parses identically) split the literal for the
other four. Empty quote pairs are now spliced out before path matching, next to
the JS concatenation join.

Also: WORKFLOW.md described /drive output as draft PRs in seven places, directly
contradicting its own top table and the implement agent specs, which state that
a draft PR silently skips reviewers. All seven now say ready-for-review.

Five regression tests. 766 assertions, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pgh7M5YPWLyoDmZpWSTDLt

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant