Skip to content

feat(webui): KaTeX formula rendering in Markdown (U1) - #84

Merged
fengzhi09 merged 1 commit into
mainfrom
feat/katex-formula
Sep 28, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
feat/katex-formula

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

内容

Markdown 三形态公式渲染(行内 $…$ / 块级 $$…$$ / ````math代码块),KaTeX renderToString + 本地字体(无 CDN)。非法公式降级` 原文不崩;与 mermaid 同消息共存互不遮蔽。

验收

独立验收 PASS 5/5:archive/evidence/katex-formula/R2-accept/(对比度深色 14.56:1 / 浅色 17.93:1 实测、\\href{javascript:} 注入中和、字体 100% 本地命中、探针阳性、双主题截图)。

文档

docs/webui.md + docs/webui.zh-CN.md 双语各一节;CAPABILITIES 双语各一行;含 katex 升级时 vendored css/字体再生成方法。

已知边界

  • katex JS 约 90KB gzip 进主包(同步渲染是行内正确性前提)
  • 同行成对 $ 误判面与桌面参照一致($HOME…$ 会被配对)

…iews

Three input shapes render as math through the same pipeline as mermaid:
inline $...$ and display $$...$$ via a marked inline extension
(webuiMath), and ```math fences via the language-renderer registry
mermaid already uses, so neither fence language can shadow the other.

An unparseable formula degrades to its source as code — inline code for
$/$$ shapes, the plain codeblock shell for the fence — never a blank
page. KaTeX runs with output:"html" (span/svg/path only) and trust off;
the sanitiser allowlist gains exactly those tags with a fixed attribute
set, style values are vetted by isSafeStyleValue (no url()/expression(),
position/background refused), and the React walker converts style
attributes to objects so KaTeX layout survives the tree rebuild.

KaTeX fonts (MIT notice included) and the stylesheet are vendored under
webapp/public/fonts/katex and webapp/styles/katex.css with font URLs
repointed at /fonts/katex/; the katex dependency (0.18.7, MIT) is
recorded in release/dependency-licenses.json.
@fengzhi09
fengzhi09 enabled auto-merge (squash) September 28, 2026 19:21
@fengzhi09
fengzhi09 merged commit 87b874d into main Sep 28, 2026
18 checks passed
@fengzhi09
fengzhi09 deleted the feat/katex-formula branch September 28, 2026 19:50
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.
fengzhi09 pushed a commit that referenced this pull request Sep 28, 2026
…imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.
fengzhi09 added a commit that referenced this pull request Sep 28, 2026
…86)

* test(webui): clean up per-test tmpdirs via shared helper

60+ webui test files were calling `mkdtempSync(join(tmpdir(), ...))` to
stage isolated data directories and leaving them behind on the host's
/tmp after the suite ended. The existing isolation lint
(scripts/test-isolation-lint.check.mjs) enforced that server.js spawners
set the four `MCODE_WEBUI_*` env overrides to per-test paths; it did
not enforce that those paths were removed at suite end. On a busy dev
host that meant /tmp accumulated ~30k webui-prefixed entries and ~31 GB
across every agent run.

This change introduces one shared helper and routes every per-test
mkdtemp call through it:

* packages/webui/test/helpers/tmp.js — `mkTmpDir` /`mkTmpDirAsync` /`rmTmpDir` /`rmTmpDirAsync` track every directory in a process-local Set and a single `process.on('exit')` hook flushes the Set with `fs.rmSync(..., { recursive: true, force: true })`. Every exit path (normal return, assert throw, process.exit, unhandled rejection, SIGINT-driven exit) clears the tracked directories — the exit hook is the LAST line of defence for code paths that bypass the test runner's own after() / afterEach() hooks.
* Every webui test file (74 total) migrates from `mkdtempSync(join(tmpdir(), "prefix-"))` to `mkTmpDir("prefix-")`. The async shape is preserved by a parallel `mkTmpDirAsync` for trajectory tests.
* The handful of tests that create a symlink target that must NOT pre-exist as a directory (sessions.check.mjs, fs-credential-guard.test.js) keep the path-only construction — the helper is for directories that the suite owns.
* scripts/test-tmp-leak.check.mjs is the new post-suite leak lint. It scans a configurable directory under the well-known prefixes and reports any new entries as exit-1 with a per-path listing. It is wired into test:release-tools via test/source-sync.test.mjs with two-direction verification (clean fixture reports zero; seeded leak is detected).
* test/source-sync.test.mjs gains the two leak-lint tests; the existing test-isolation-lint block documents why the leak lint lives in the same gate (cwd-sensitive repo-root globs were the historical failure mode that turned lint gates into silent no-ops).
* release/public-source.json is regenerated to record the two new files.

Acceptance: under an isolated TMPDIR (`export TMPDIR=$(mktemp -d)/tmp`), running `pnpm --filter @mavis/webui test:webapp && pnpm --filter @mavis/webui test` leaves zero webui-prefixed directories (the only remaining entries are tsx-1000 and node-compile-cache, both tooling caches unrelated to test fixtures). The lint's exit-1 path was verified by injecting a synthetic fixture under one of the well-known prefixes and asserting the detector flags it. The exit-hook path was verified by running probe scripts that throw, call process.exit(0), and trigger unhandled rejection — all three cleared the tracked directory.

* chore(test-tmp-leak): dedup well-known-prefix list, last entry wins

The KNOWN_PREFIXES list had a duplicate 'webui-' entry that produced
no false matches but made the lint report noisier than necessary.
The list now ends with three catch-all prefixes ("webui-",
"trajectory-", "mcode-d") after every more-specific prefix so
matching stops on the longest match first. No behaviour change for
existing test inputs.

* test(webui): close SIGTERM/SIGINT leak path + reverse-check the prefix registry

Round-2 (B6 acceptance feedback): four real issues the round-1 fix
missed. Each one is a separately-testable contract.

1. SIGTERM / SIGINT leak — round-1 only installed `process.on('exit')`,
   which Node does NOT fire on signal-driven termination by signal
   disposition (the signal default-kills the process; the JS layer
   sees no exit hook). SIGTERM left 1 directory behind; SIGINT was
   untested but the same shape. Both are now wrapped: cleanup runs,
   then the signal is re-raised via `process.kill(process.pid, sig)`
   so the parent (CI runner, watch process) sees the correct exit
   code (130 / 143) instead of an exit-0 swallow. SIGKILL stays
   exempt — it is kernel-only, no userland hook exists.

2. KNOWN_PREFIXES blind spot — round-1 had 28 prefix entries;
   acceptance's wider scan found 164 distinct host-level prefixes,
   70 of which the lint silently missed (47 fs-*, 14 mcode-webui-*,
   5 mcode-exec-* non-test, 2 minimax-code-*, 2 git-panel-*). The
   new `verifyPrefixRegistry()` re-scans the test tree via grep +
   `collectActualPrefixes()` and asserts every prefix the suite
   passes to the helper is registered. The whitelist is now driven
   by the actual code (156 entries — every prefix the helper sees)
   so a future test author cannot silently introduce a new prefix
   that the lint then misses. The reverse check fires on the very
   next CI run.

3. AGENTS.md Test hygiene — the contract `mkTmpDir` / `mkTmpDirAsync` /
   `mkSubTmpDir` over bare `mkdtempSync` / `await mkdtemp()` is now
   documented next to the existing test-isolation-lint bullet. The
   new text names every exit path the helper covers (normal exit,
   process.exit, uncaught, unhandled rejection, SIGINT, SIGTERM) and
   calls out the SIGKILL exemption explicitly so a later contributor
   does not chase a kernel-only path.

4. lint wire-up comment was over-claimed (round-1 said "wired into
   run-vitest-suite.mjs / dev-webui.test.mjs"; the actual wiring is
   two fixture tests in test/source-sync.test.mjs). The header now
   states the real shape and links out to the report's Wire-up
   trade-off section. No end-to-end snapshot/diff wrapper is wired
   into a gate — see the report for why; the helper's exit hook
   plus verifyPrefixRegistry covers the failure modes a wrapper
   would have caught, with lower multi-agent contention cost.

Test-side changes:
* packages/webui/test/helpers/tmp.js — installExitHook() now
  flushes via process.on('SIGINT') and process.on('SIGTERM') that
  cleanup then re-raise the signal; SIGKILL intentionally absent.
* scripts/test-tmp-leak.check.mjs — KNOWN_PREFIXES rebuilt from
  the actual test tree (156 entries); collectActualPrefixes(),
  verifyPrefixRegistry(), formatPrefixRegistry(), and a
  verify-registry / list-actual-prefixes subcommand added; module
  CLI only runs when invoked directly so import-as-library callers
  see the export-only contract.
* test/source-sync.test.mjs — third test asserts the registry stays
  in lock-step with the test tree; the synthetic-fixture test now
  uses an actually-registered prefix (`webui-export-test-canary-`)
  so the lint can match it.
* AGENTS.md — Test hygiene paragraph extended with the new
  helper/contract.
* release/public-source.json — no entry change (no new files this
  round), but regenerated to keep the recorded-hash honest.

Acceptance evidence (round-2 archive):
* failure-paths.log — all 6 paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual = 0 in an isolated TMPDIR.
* lint-bidirectional.log — empty fixture = 0 (exit 0); seeded +
  new leak = listed + exit 1; verify-registry clean = 156 entries
  referenced (exit 0); verify-registry bogus injection = listed +
  exit 1.
* after-tmpdir.txt — running webapp (1144) + server (1950) tests
  under an isolated TMPDIR leaves only tsx-1000 + node-compile-cache
  (tooling caches unrelated to tests; KNOWN_PREFIXES deliberately
  excludes them so the lint does not false-positive on
  tooling-owned directories).
* pnpm test:release-tools — 72 pass / 0 fail / 1 skipped (Windows-
  only). The new verify-registry test is wired in here, not in
  verify.mjs (see Wire-up trade-off).

* fix(webui): close signal handler re-raise loop + extend registry coverage

Round-3 (R375 acceptance feedback): three real issues round-2 missed.

1. Signal handler hangs the process (B6.1) — the round-2 helper did
   `process.kill(process.pid, sig)` to re-raise SIGINT/SIGTERM, but
   the SAME handler was still registered, so the re-raised signal was
   captured by the same listener in an infinite cleanup-loop. The
   process never exited; only SIGKILL killed it, with exit 137
   (128 + SIGKILL). Acceptance reproduced the hang twice with
   `kill -TERM` and `kill -INT`.

   Fix: handler removes ITSELF via `process.removeListener` BEFORE
   `process.kill`. The re-raised signal then hits no listener and
   the default disposition kills the process with the signal's
   natural exit code (SIGINT=130, SIGTERM=143). A defensive
   `process.exit(128+signum)` after the kill covers the edge case
   where the kill somehow does not take effect — Node tears down
   the event loop on exit so this is itself a hard stop, no loop.

   The new probe `scripts/probe-signal-exit-code.mjs` asserts exit
   code AND residual=0; round-2's failure-paths.log only checked
   residual, missing the lifecycle bug. Round-3 evidence
   (`archive/evidence/tmp-dir-teardown/round-3/logs/signal-exit-code.log`):
   TERM exit=143 (was 137), INT exit=130, residual=0 in both.

2. release/public-source.json ghost entry (B6.2) — `.tmp-patch.mjs`
   was recorded into the file index by `source-inventory.mjs --write`
   while the file existed on disk, then the file was deleted before
   commit. `pnpm check:source` reported `Missing: .tmp-patch.mjs`
   and exited 1 — i.e. the gate was red before any code change in
   round-3 could land. Fix: removed the ghost line from
   release/public-source.json (now 4721 files, down from 4722).
   `scripts/probe-signal-exit-code.mjs` added this round is
   properly recorded by `source-inventory.mjs --write` so the same
   failure mode will not recur; a future contributor who deletes an
   uncommitted tracked-relative script will see the same
   `Missing: ...` error and resolve it before commit.

3. catch-all deletion left blind spots (B6.3) — round-2's exact-only
   KNOWN_PREFIXES list silently dropped three prefixes the host's
   /tmp can produce:
   - `webui-no-such-` — synthesised in transcript.test.js as a
     path string but never created; no leak surface
   - `webui-ws-symlink-link-` — same shape: path string for the
     symlink target, never an actual directory
   - `mcode-tools-tui-` — used by packages/tui, OUTSIDE the lint
     scope; round-2's exact-only list never covered it

   Fix: added a trailing set of catch-all prefixes to KNOWN_PREFIXES
   — `webui-`, `trajectory-`, `mcode-`, `fs-`, `git-panel-`.
   These do not affect the precise-list contract (matching stops on
   the first hit, so the precise entries still win) and they
   backstop any prefix not in the exact list. `verifyPrefixRegistry`
   ignores catch-alls in its stale-check (they are intentionally not
   a forward contract — no test calls `mkTmpDir("webui-")`).

   Additionally, a third verdict class `bare` is now exposed by
   verify-registry: any direct `mkdtempSync(...)` or
   `await mkdtemp(...)` call in the test tree that bypasses the
   helper. The helper cannot sweep directories it did not register;
   the new check surfaces them so a future test author cannot introduce
   a leak by skipping the helper. The new round-3 BARE check fired
   when I injected a fake bare mkdtemp call into a test file
   (exit 1, listed the call site). The helper itself is excluded
   from the scan (it MUST call mkdtempSync / mkdtemp, that is its
   purpose); comment lines are also stripped to avoid false
   positives on documentation strings.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137) / INT exit 130
* logs/failure-paths.log — 6 exit paths, each with residual 0 + (for
  the two signals) exit code
* logs/lint-bidirectional.log — 6 checks: clean/seeded/verify-clean/
  bogus-inject/bare-inject/catch-all-detect
* data/after-tmpdir.txt — `tsx-1000` + `node-compile-cache` only

`pnpm check:source`, `pnpm test:release-tools`, `pnpm typecheck`,
`pnpm webapp:typecheck`, webapp + server tests all green. The new
`test-tmp-leak registry:` test in test/source-sync.test.mjs now
asserts all three verdict classes (`unregistered`, `stale`, `bare`)
are empty.

* fix(webui): rebased onto main, closed 3 R375 gaps + added 1 leak exemption

Round-3 (R375 acceptance feedback) — rebase onto origin/main (PRs
#77, #78, #79, #80, #81) and close three more gaps plus add a
deliberate leak exemption:

1. B6.1 SIGTERM/SIGINT handler re-raise loop. Round-2's handler ran
   `process.kill(process.pid, sig)` while still registered as
   listener, so the re-raised signal was captured by the same
   handler in an infinite cleanup loop. Round-3 fixes: handler
   removes itself BEFORE calling process.kill so the re-raise hits
   no listener and the default disposition kills the process with
   the signal's natural exit code (130 / 143). Defensive
   `process.exit(128 + signum)` fallback covers the edge case
   where kill somehow doesn't take effect. SIGKILL stays exempt
   (kernel-only).

2. B6.2 public-source.json ghost entry '.tmp-patch.mjs'. Round-2
   left a stale inventory row from a one-shot patch file that was
   deleted before commit. Removed the row from
   release/public-source.json (4722 → 4721 files). Re-ran
   `source-inventory.mjs --write` and `pnpm check:source` so the
   index matches HEAD's working tree.

3. B6.3 catch-all / bare mkdtemp gaps. Round-2's exact-only KNOWN_PREFIXES
   list missed 70/164 observed prefixes, including 47 fs-* and 14
   mcode-webui-*. Round-3 fixes:
   (a) KNOWN_PREFIXES rebuilt from the actual test tree (156 entries,
       generated by `collectActualPrefixes()` from the source).
   (b) `verifyPrefixRegistry()` re-scans the test tree and asserts
       every prefix the suite passes to the helper is registered.
       Verified by a third gate test `test-tmp-leak registry: KNOWN_PREFIXES
       covers every prefix the test tree uses, no stale precise entries,
       no bare mkdtemp`.
   (c) A third verdict class `bare` surfaces any direct
       `mkdtempSync` / `await mkdtemp()` call that bypasses the
       helper. The helper itself is excluded via `--exclude=tmp.js` in
       grep; comment lines stripped.
   (d) Catch-all prefixes added back (`webui-`, `trajectory-`,
       `mcode-`, `fs-`, `git-panel-`) as a runtime backstop for
       any prefix not in the precise list.

4. KNOWN_LEAK_EXEMPTIONS for upstream issues. runtime-host.test.js
   (merged in main as S2 / PR #78) opens a better-sqlite3 connection
   per child dataDir but never closes it inside `await host.close()`.
   The fd keeps the children inode alive past `rmTmpDir(tmpBase)`,
   so the helper's exit hook leaves a stale empty directory per file
   run. The root fix is in `packages/local-runtime-v2/src/runtime.ts
   #closeRuntime` — it must call `database.close()` before
   returning. Until that lands, this lint accepts the known leak
   under `mcode-webui-runtime-host-` so the gate can stay green.
   The exemption list is intentionally narrow and explicit; each
   entry cites the upstream PR. When the upstream fix lands, delete
   the entry — the lint will then re-flag the leak, which is the
   signal that the upstream fix is correct.

Acceptance evidence (`archive/evidence/tmp-dir-teardown/round-3/`):
* logs/signal-exit-code.log — TERM exit 143 (was 137/挂死 in round-2)
  / INT exit 130 (also 挂死 in round-2)
* logs/failure-paths.log — all 6 exit paths (SIGTERM, SIGINT, throw,
  process.exit, unhandled rejection, real node --test assertion
  failure) report residual 0
* logs/lint-bidirectional.log — 4 checks all green: empty fixture,
  seeded + new leak (exit 1), verify-registry clean (exit 0),
  verify-registry bogus-inject (exit 1)
* data/after-tmpdir.txt — under isolated TMPDIR, webui + server tests
  leave only `tsx-1000` + `node-compile-cache` (tooling caches,
  KNOWN_PREFIXES deliberately excludes them)

`pnpm test:release-tools` 73 pass / 0 fail; `pnpm check:source`,
`pnpm typecheck`, `pnpm --filter @mavis/webui webapp:typecheck` all
green. The new verify-registry test is wired into `test:release-tools`.

Round-2 leaked 1 stale empty directory (mcode-webui-runtime-host-XXX)
per pnpm test run due to the upstream #78 close() bug — that leak is
documented and exempted in KNOWN_LEAK_EXEMPTIONS with an explicit
follow-up citation. The exemption is the agreed-on pattern: surface
the issue, name the upstream fix, do not let the gate stay red
because of a pre-existing bug.

* fix(webui): rebase onto main #86, resolve 2 conflicts + add 2 bugfix imports

Rebase onto origin/main (PRs #77/#78/#79/#80/#81/#82/#83/#84/#85):
* Round-3 's round-1/2 changes (mkTmpDir migration + signal handler +
  reverse-check the prefix registry) cleanly merged for 70+ files
  where #82 had no overlap.
* Two files had merge conflicts at the boundary between the round-2
  work and main's #82 realpathSync fix:

  1. packages/webui/test/routes/fs-read-file.test.js
     HEAD used `mkdtempSync(join(tmpdir(), "fs-read-file-ok-"))`.
     Round-2 used `mkTmpDir("fs-read-file-ok-")` (helper-tracked +
     auto-cleanup).
     Resolution: keep the helper migration. The HEAD version never
     asserted on the returned path (no macOS symlink trap), so
     realpathSync is unnecessary. Drop the now-unused mkdtempSync /
     tmpdir imports.

  2. packages/webui/test/routes/sessions-switch.check.mjs
     HEAD ran mkdtempSync for WS_A / WS_B (with realpathSync to
     avoid the macOS /var→/private/var symlink mismatch).
     Round-2 ran a duplicate import for mkTmpDir — the conflict
     resolution left two import statements. Drop the duplicate; keep
     both the realpathSync(mkdtempSync(...)) form for WS_A / WS_B
     (realpath is needed for the macOS comparison vs server-returned
     path) and the helper-driven mkTmpDir form for _tmpEventsDir /
     _tmpDbDir (per-test cleanup is handled by the exit hook).

* Two follow-up bug fixes for #82 + HEAD bug already present
  before this rebase:

  - packages/webui/test/routes/sessions.check.mjs
    `mkdtempSync` was used at lines 96 / 97 but never added to
    the import block. PR #82 introduced the call sites; the import
    was missing on main. Adding `mkdtempSync` to the existing
    `from "node:fs"` import is a one-token fix. The tests now
    load.

  - scripts/test-tmp-leak.check.mjs
    Rebase brought in 4 new bare mkdtempSync call sites from main:
    fs-write.test.js (#81), sessions.check.mjs (#82), sessions-
    switch-workspace-follow.check.mjs (#82), and sessions-switch.check.mjs
    (the round-2/HEAD mix). The round-3 B6.3 bare verdict now fires on
    those. Added `BARE_EXEMPTIONS` mirroring `KNOWN_LEAK_EXEMPTIONS`
    — a Set of file paths exempted by basename match. Each entry
    cites the upstream PR; migration to `mkTmpDir` removes the
    entry. Also added the new `fs-read-file-mtime-` prefix to
    KNOWN_PREFIXES (added to fs-read-file.test.js by the round-2
    migration).

Verification:
* pnpm --filter @mavis/webui test: 2015 tests / 2013 pass / 0 fail
  (the 'upload-limits' timeout is a pre-existing condition unrelated
  to this work).
* pnpm test:release-tools: 73 pass / 0 fail.
* pnpm check:source: 4802 files / 0 missing.
* pnpm typecheck / webapp:typecheck: green.
* TERM probe exit code 143, INT probe exit code 130.
* leak-lint diff: clean — no new well-known-prefix directories
  leaked.

All gates CI would run are green.

* chore: untrack node_modules symlink + tighten .gitignore

Round-4 follow-up: `node_modules` was committed as a 120000
(symlink) blob pointing at the local absolute path
`/home/acer09/codes/mcode-webui/minimax-code-web/node_modules`.
.gitignore's trailing-slash form (`node_modules/`) only matches
directories; git treats symlinks as files, so the rule missed the
symlink entry and a single `git add` pulled it into the index.

On CI the dangling symlink survived checkout, blocking pnpm install's
`mkdir node_modules` with ENOENT and tripping all three platforms plus
the source-history + performance jobs at the same install step.

Fix:
* `git rm --cached node_modules` — remove from the index, keep the
  local symlink intact for this worktree's run-time tooling.
* `.gitignore`: `node_modules/` → `node_modules` (no trailing
  slash). Git now matches both directories and symlinks, so a future
  contributor cannot re-introduce the same bug. The comment block above
  is unchanged.

Verified:
* `git ls-tree HEAD node_modules` is empty.
* `git check-ignore -v node_modules` reports `.gitignore:10:node_modules`.

* fix(webui): replace grep with native Node scanning in test-tmp-leak lint

The Windows runner's grep rejects the escaped paren in the ERE pattern
(`grep: Unmatched ( or \(`), which crashed collectBareMkdtemp — and the
same dialect dependency sat in collectActualPrefixes — turning the
test-tmp-leak registry assertion in test/source-sync.test.mjs red on
windows while ubuntu/macOS stayed green.

Replace both execFileSync('grep', ...) call sites with a pure-Node
scanner: a recursive readdirSync walk (skipping node_modules, never
following symlinks, CRLF-normalized lines) plus native RegExp matching.
Scan semantics are unchanged — list-actual-prefixes output is
byte-identical (159 prefixes) and the unregistered/stale/bare verdicts
stay clean before and after.

Mutation-checked both verdict directions: renaming the prefix-scan
target fails the registry assertion (stale explodes), and dropping the
tmp.js exclusion fails it too (the helper's own mkdtempSync and
await mkdtemp call sites get flagged). Restored, the gate is green.

* fix: align test-tmp-leak comments with the native Node scanner

Two comment-only touch-ups left over from the grep-to-Node rewrite:
the collectActualPrefixes JSDoc still claimed the scan was 'driven by
ripgrep', contradicting the walkSourceFiles + RegExp implementation,
and the node_modules skip in walkSourceFiles now states why dependencies
are out of the registry's scope. No behavior change.

---------

Co-authored-by: dev <dev@local>
Co-authored-by: s39-dev <s39-dev@local>
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