Skip to content

test(usage): reproduce upstream settings-privacy and usage-network fixes on macOS + isolate the mutation sandbox - #58

Merged
axisrow merged 11 commits into
mainfrom
verify/settings-privacy
Oct 12, 2026
Merged

axisrow merged 11 commits into
mainfrom
verify/settings-privacy

Conversation

@axisrow

@axisrow axisrow commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Batch 4: reproduce upstream settings-privacy fixes on macOS + isolate the mutation sandbox

TDD verification per the /fix pipeline: red on the pre-fix tree → green on main under both runners (bun test + vitest/Node) → mutation check (StrykerJS) → lint. Follows the merged #55/#56/#57 wave; branched from current main.

Upstream CI only runs Linux; all three fixes protect user data (private settings permissions, an unreadable settings file, a shared-/tmp race), so the wave proves them on macOS.

Fix Red fact (pre-fix tree) Green bun Green vitest Mutation Commit
4c62e31 — private settings stay private on save/backup a 0600 ccstatusline settings comes back 0644 after a save; the .orig backup of a 0600 Claude settings is 0644 @ 75f07d6 4/4 8/8 backup-mode survivors killed (see below) afa0d45
475e401 — unreadable Claude settings are not overwritten (sirmalloc#627) the install promise resolves (overwrite happens) where the fixed code rejects @ baf37e1 3/3 8/8 abort-path survivors killed 2545f27
6aefa6b — fonts download into a private temp folder the observed download folder is the predictable ccstatusline-powerline-fonts-<timestamp> @ 9246095 1/1 8/8 powerline.ts:19x — 0 survivors 839d3d2

Incident found by this batch, fixed here

Running the mutation scope over config.ts/claude-settings.ts executed upstream's claude-settings.test.ts, whose install tests operate on the real home directory (one test resets the ccstatusline config path to ~/.config, another deletes CLAUDE_CONFIG_DIR without restoring it). A mutant breaking config validation materialised default settings over the live statusline config, and an install test pointed statusLine at a client inside .stryker-tmp — which Stryker then deletes, so the live Claude Code status line went empty. Reported upstream as sirmalloc#716.

Fix in 5cbc156: vitest.stryker.global-setup.ts redirects HOME/CLAUDE_CONFIG_DIR to a fresh temp directory for every sandboxed run. Verified by rerunning the full mutation scope with mtime guards on both real config files: stryker exit 0, BOTH-REAL-CONFIGS-UNTOUCHED.

Survivors triage (fix lines)

  • Killed by this batch's cases: the backup .mode write (via .bak existence + recreating a loose-permission .orig — writeFile alone keeps 0777), the install abort reason (/left unchanged/), both keepExisting branches (unreadable file, valid existing statusLine), the predictable mkdtemp folder.
  • Equivalent: writeFile(..., { encoding: '' }) writes the same utf-8 bytes; readFile with '' returns a Buffer that round-trips identically.
  • Triage (error-visibility only): removeIfExists never rethrowing a non-ENOENT unlink error, logErrors: false → true (console noise), a truncated message tail. Happy-path behavior identical in all three.
  • powerline.ts broader low score comes from font-directory selection and install-script variants this fix does not touch — outside the fix-line criterion.

Notes

  • Full suite on this branch: bun 3578 tests, 1 fail — the known macOS-only git-hardening-exec LFS case (pre-existing, see verify(tooling): Stryker mutation tooling + upstream 9246095 pilot (batch 1) #55); one transient ItemsEditor flake observed in a single full run, green in isolation.
  • Red tests target only pre-fix public API (saveSettings via initConfigPath, installStatusLine, installPowerlineFonts), so each red run fails on content, not on a missing module.

Commits

  1. 5cbc156 — fix(verify): isolate the mutation sandbox from the real home directory
  2. afa0d45 — test(settings): reproduce upstream 4c62e31 on macOS
  3. 2545f27 — test(install): reproduce upstream 475e401 on macOS
  4. 839d3d2 — test(powerline): reproduce upstream 6aefa6b on macOS

🤖 Generated with Claude Code


Batch 5 (added to this PR): usage-network fixes — e8ef3cb, 0bcc7eb, 610dcc9

Fix Red fact (pre-fix tree) Green bun Green vitest Mutation Commit
e8ef3cb — NO_PROXY keeps usage request off the proxy probe observes a proxy agent configured despite NO_PROXY @ 4c62e31 3/3 5/5 no-proxy.ts 94.23%, 3 equivalent survivors (empty-domain entries already rejected by domain !== '') 6d6f1d2
0bcc7eb — only clear the in-flight lock this render wrote a rate-limit lock written mid-request is gone after the observed success @ 475e401 1/1 5/5 subprocess-invisible (see below) e9b4f0c
610dcc9 — absolute deadline bounds a stalled proxy probe against a local accept-and-never-answer proxy never finishes (20s budget kills it) @ ae9721e 1/1 5/5 subprocess-invisible (see below) ab73b23

New shared harness: usage-probe.ts — the probe bundles with bun and runs under node with HOME/USERPROFILE in a temp directory (bun's os.homedir() ignores HOME, so in-process tests would touch the live ~/.cache/ccstatusline; node:https is not patchable through bun's ESM namespace, so the request spy is installed by a --require patch that precedes the bundle). Subprocesses spawn through Bun.spawnSync under bun (mock-leak guard), real spawnSync under vitest/Node.

Mutation notes: no-proxy.ts (the whole file is this fix) at 94.23% with equivalent survivors; claude-service-status.ts fix line killed; usage-fetch.ts and the probe-based tests are invisible to the mutation runner (child-process coverage is not observed), so those fix lines are proven by the red/green pairs above instead — the same triage class as powerline's uncovered paths in batch 4. proxy-agent.ts survivors are AbortController-fallback equivalents.

Commits: d935d03 (harness), 6d6f1d2 (e8ef3cb), e9b4f0c (0bcc7eb), ab73b23 (610dcc9).

axisrow and others added 8 commits October 12, 2026 15:26
Running Stryker over config.ts/claude-settings.ts/powerline.ts executed
upstream's claude-settings.test.ts, whose install tests reset the config
path to the real ~/.config/ccstatusline/settings.json and, after one test
deletes CLAUDE_CONFIG_DIR without restoring it, write the real
~/.claude/settings.json too. A mutant that broke config validation
materialised default settings over the live statusline config, and an
install test pointed statusLine at a client inside .stryker-tmp — which
Stryker deletes afterwards, so the Claude Code status line went empty
(upstream issue sirmalloc#716).

globalSetup now redirects HOME and CLAUDE_CONFIG_DIR to a fresh temp
directory for every sandboxed run. Verified by rerunning the same
mutation scope with mtime guards on both real config files: stryker exit
0, both untouched ("BOTH-REAL-CONFIGS-UNTOUCHED").

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream 4c62e31 keeps private settings private when saving and backing
up: the atomic-save temp file inherits the replaced file's permissions
(a 0600 settings came back 0644), and the .bak/.orig backups of Claude
Code's settings.json get the settings' permissions instead of the
umask default. Upstream CI only runs Linux; this proves the fix holds
on macOS.

Red: against the pre-fix tree (75f07d6) both cases fail — a 0600
ccstatusline settings comes back 0644 after a save, and the .orig backup
of a 0600 Claude settings is 0644.

Green: bun test 4/4 and vitest/Node 4/4 (three upstream-repro files).

Mutation (StrykerJS over config.ts/claude-settings.ts/powerline.ts):
the backup-mode lines' survivors are killed by covering the .bak
existence after a save, the recreation of an existing .orig with loose
permissions (writeFile alone keeps 0777), and a re-install over an
existing status line. Remaining survivors on fix lines are equivalent:
writeFile with encoding '' writes the same utf-8 bytes, and readFile
returning a Buffer rounds-trips identically.

Note: this file family runs installs against real home directories in
upstream's claude-settings.test.ts (issue sirmalloc#716);
the mutation sandbox is isolated from the real home by the preceding
commit's globalSetup.

Upstream commit: sirmalloc@4c62e31

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream 475e401 (sirmalloc#627) makes installStatusLine abort when the existing
Claude settings cannot be read or parsed, instead of overwriting them
with a settings.json holding only statusLine (permissions, hooks, env
lost). Upstream CI only runs Linux; this proves the fix holds on macOS.

Red: against the pre-fix tree (baf37e1) the install promise resolves
(the overwrite happens) where the fixed code rejects.

Green: bun test 3/3 and vitest/Node 8/8 (three upstream-repro files).

Mutation: the abort path's survivors are killed by asserting the exact
abort reason ("left unchanged") and covering both keepExisting branches
(keep-existing over an unreadable file, keep-existing over a valid
statusLine). Remaining survivors on fix lines are equivalent
(logErrors: false → true only changes console noise; a truncated
message tail).

Upstream commit: sirmalloc@475e401

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream 6aefa6b downloads Powerline fonts into an mkdtemp folder — a
random name created 0700 — instead of the predictable
os.tmpdir()/ccstatusline-powerline-fonts-<timestamp>, which another
account on a shared /tmp could create first and repopulate with its own
install.sh. Upstream CI only runs Linux; this proves the fix holds on
macOS.

Red: against the pre-fix tree (9246095) the observed download folder is
the predictable timestamp name (ccstatusline-powerline-fonts-<13
digits>), failing the random-name assertion. The mocked clone recreates
what git clone would, so the folder's name and permissions are observed
mid-install (the installer removes the folder in its finally).

Green: bun test 1/1 and vitest/Node 8/8 (three upstream-repro files).

Mutation: the mkdtemp line has zero surviving mutants in the batch scope
(powerline.ts:19x). The file's broader low score comes from code paths
this fix does not touch (font directory selection, install script
variants), outside the fix-line criterion.

Upstream commit: sirmalloc@6aefa6b

Co-Authored-By: Claude Code <noreply@anthropic.com>
The usage fetch code paths (proxy selection, lock handling, request
deadlines) can only be exercised in a child process: bun's os.homedir()
ignores HOME, so in-process tests would read and write the live
~/.cache/ccstatusline, and node:https is not patchable through bun's
ESM namespace. The harness bundles a probe script with bun (extensionless
imports resolve) and runs it with node, whose homedir() honors HOME; the
https.request spy is installed by a --require patch that runs before the
bundle (a CJS-require mutation alone would not reach the bundle's ESM
import bindings). Subprocesses go through Bun.spawnSync under bun
(sibling suites mock node:child_process and that mock leaks across files
in the shared runner); vitest/Node keeps file isolation and takes the
real spawnSync.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream e8ef3cb connects usage and status-page requests directly to
hosts NO_PROXY excludes: the usage request carries the account's OAuth
token, and a user keeping api.anthropic.com off a proxy (for example a
TLS-inspecting corporate one) had it sent through the proxy anyway.
Upstream CI only runs Linux; this proves the fix holds on macOS.

Red: against the pre-fix tree (4c62e31) the probe observes a proxy agent
configured for the request even though NO_PROXY excludes the host (both
spellings covered); the control case without an exclusion still sees the
agent. The probe runs in a subprocess (usage-probe.ts harness).

Green: bun test 3/3 and vitest/Node 5/5 (three batch files).

Mutation (StrykerJS over no-proxy.ts): 94.23%, the 3 survivors are
equivalent — splitting on one whitespace/comma character instead of runs
yields empty entries that the `domain !== ''` check already rejects, and
empty-domain mutants compare against a target that is never empty.

Upstream commit: sirmalloc@e8ef3cb

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream 0bcc7eb (sirmalloc#624) makes a successful fetch clear usage.lock only
when it still holds that render's own in-flight record: previously a
success unconditionally removed the lock, so with two concurrent renders
and one 429 the other's later success deleted the rate-limit lock and the
next render fetched inside the server's backoff window. Upstream CI only
runs Linux; this proves the fix holds on macOS.

Red: against the pre-fix tree (475e401) the probe's rate-limit lock
(written by a "concurrent render" mid-request) is gone after the
observed success — lockExistsAfter expected true, received false.

Green: bun test 1/1 and vitest/Node 5/5 (three batch files).

Mutation: usage-fetch.ts is exercised through the subprocess harness,
whose coverage the mutation runner cannot see (the probe is a child
process), so the fix lines report as uncovered rather than surviving;
the red/green pair above is the regression proof for this fix.

Upstream commit: sirmalloc@0bcc7eb

Co-Authored-By: Claude Code <noreply@anthropic.com>
Upstream 610dcc9 bounds the usage fetch with an absolute deadline: the
request's `timeout` only covers an idle socket, and behind HTTPS_PROXY
the request has no socket until the proxy answers CONNECT — a proxy that
accepts and never answers held the render forever, and its agent kept
the process alive afterwards. Upstream CI only runs Linux; this proves
the fix holds on macOS.

Red: against the pre-fix tree (ae9721e) the probe against a local TCP
proxy that accepts and never answers never finishes — the 20s probe
budget kills it (spawnSync ETIMEDOUT), where the fixed code finishes
with an api-error in about 6s (the 5s deadline plus overhead).

Green: bun test 1/1 and vitest/Node 5/5 (three batch files).

Mutation: usage-fetch.ts is exercised through the subprocess harness,
whose coverage the mutation runner cannot see (the probe is a child
process); proxy-agent.ts survivors are equivalent in any environment
that has AbortController (the typeof fallback targets ancient runtimes,
where controller?.abort() and controller.abort() are the same).

Upstream commit: sirmalloc@610dcc9

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow axisrow changed the title test(settings): reproduce upstream 4c62e31, 475e401, 6aefa6b on macOS + isolate the mutation sandbox test(usage): reproduce upstream settings-privacy and usage-network fixes on macOS + isolate the mutation sandbox Oct 12, 2026

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Вердикт: запрашиваю изменения — две находки по надёжности нового тестового хозяйства; сама содержательная часть (репродукции апстрим-фиксов на macOS и изоляция мутационной песочницы) корректна и проверена.

Сверено с исходником на этой ветке: месседж left unchanged и .orig-бэкап в claude-settings.ts (464/471), mkdtemp-префикс в powerline.ts (237), сохранение режима файла в config.ts (141-143), условная очистка usage.lock в usage-fetch.ts (846), NO_PROXY/no_proxy в no-proxy.ts, абсолютный дедлайн 5с на call-site usage-fetch.ts (1113). Ассерты соответствуют реальным поверхностям фиксов. Изоляция прока корректна: временный HOME/USERPROFILE, CLAUDE_CONFIG_DIR в temp, фейковый security в PATH — живые ключи и кэш не затрагиваются; vitest.stryker.global-setup.ts закрывает инцидент sirmalloc#716 правильным способом (редирект HOME до старта воркеров).

Inline-находки, обе требуют правки: (1) usage-probe.ts — ветка Bun в runProcess теряет options.timeout, контракт kill-after-timeoutMs из RunProbeOptions не работает под bun test, red-прогон stalled-proxy на pre-fix дереве висит бесконечно; (2) powerline-тест — наблюдение и очистка по префиксу в общем tmpdir: leftover от убитого прогона отравляет observed и ломает toHaveLength(1) на последующих прогонах, слепой rmSync удаляет чужие каталоги.

Не блокирующее: комментарий в vitest.stryker.global-setup.ts обещает изоляцию на каждый тестовый файл, а globalSetup выполняется один раз на прогон (по наследованию env этого достаточно, но комментарий неточен); песочница ccstatusline-mutation-home-* не удаляется после прогона; ветка git clone внутри mockedExecSync мертва — клонирование в powerline.ts идёт через execFileSync, жив только execFileSync-мок; без observe патч не активен, поэтому stalled-proxy честно гоняет реальный https через реальный агент — правильный дизайн, фиксирую как плюс.

Comment thread src/utils/__tests__/usage-probe.ts Resolved
axisrow and others added 3 commits October 12, 2026 22:02
The Bun branch of runProcess dropped options.timeout, so a hung probe
failed the stalled-proxy contract by hanging instead of failing; pass
it through like the node branch does. Bun.spawnSync kills the process
with SIGTERM on timeout (verified on Bun 1.3.13), which surfaces as a
non-zero exit here.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Prefix-scanning the shared tmpdir broke under leftovers from killed
runs (they landed in observed and failed toHaveLength(1) on every
later run) and the blind rmSync could delete other accounts'
directories. Observe and clean up only the target the execFileSync
mock already extracts from the installer's own clone call; drop the
dead git-clone branch in the execSync mock, since powerline.ts clones
via execFileSync.

Co-Authored-By: Claude Code <noreply@anthropic.com>
globalSetup runs once per run, not per test file — say so; return a
teardown that removes the sandbox home after the run.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Вердикт: утверждаю. Обе inline-находки прошлого раунда и все не блокирующие заметки закрыты в edcca2b, 1e23af7 и e69acdd: ветка Bun в runProcess пробрасывает timeout в bun.spawnSync (контракт kill-after-timeoutMs восстановлен, интерфейс BunRuntime обновлён); powerline-тест скоупит наблюдение и очистку на cloneTargets, извлечённые execFileSync-моком, — префикс-скан общего tmpdir и слепой rmSync удалены, заодно вычищена мёртвая ветка git clone в execSync-моке и неиспользуемые импорты; vitest.stryker.global-setup.ts возвращает teardown, удаляющий песочницу, комментарий исправлен на once per run. Дельта между ab73b23 и e69acdd других изменений не содержит; остальной дифф сверен с исходником в прошлом раунде (claude-settings.ts 464/471, powerline.ts 237, config.ts 141-143, usage-fetch.ts 846/1113, no-proxy.ts) и не менялся. Возражений нет.

@axisrow
axisrow merged commit 4f27b8b into main Oct 12, 2026
6 checks passed
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