Five routes changed the linked-cases registry: link, unlink, rename, and a
create or clone into a custom folder. Each read the file, checked it and wrote
it back on its own, so two requests that both read before either wrote lost
an entry. Two clones that claimed one name and finished together both passed
the name check, both answered success, and the later write replaced the
earlier entry. The same race made "a name linked while the clone ran is a
409" in test/routes/case-clone-custom-folder.test.ts fail about half the time.
- New src/web/linked-cases-writer.ts, mutateLinkedCases(), shaped like
mutateSettingsFile(): mutations run one at a time, each write is a temp
file renamed over the registry (through a symlink, keeping its mode), and
a file that is not a JSON object is refused (409 CONFLICT) instead of being
read as {} and overwritten.
- All five writers go through it, with their name checks inside the
callback, so exactly one of two requests for one name wins. Rename also
re-checks that the old name still points at the old folder, and drops its
manual registry undo, since a failed write no longer changes the file.
- Tests: test/linked-cases-writer.test.ts (writer behaviour, and concurrent
link/unlink through the routes with slowed writes, which fail on the old
code). Tests that forced a failed registry write with chmod 444 or a
directory now inject the failure at the rename, or expect the refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the user-facing fix end to end: POST /api/cases with a path two levels
down returns 200, lands in the linked-cases registry and is scaffolded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
safeRmHomeTree and isUnderTestHome answered "is this path under process.env.HOME?".
With test/setup.ts skipped (a bare vitest) HOME is the developer's real home, so every
real file passed, which is how the real linked-cases registry was deleted on 2026-10-10.
They now also require HOME to be the throwaway codeman-vitest-* directory setup.ts
creates, and the shared route harness throws when it is not, so every route test refuses
to start against a real ~/.codeman rather than just the one file that checked.
- Route test hygiene: each test works in its own mkdtemp folder, every
deletion goes through safeRmHomeTree, and the suite refuses to start
outside test/setup.ts's temp HOME, so a raw `npx vitest` can no longer
delete a real ~/projects or the live linked-cases registry.
- Path policy: the symlink-resolved target is also judged against the
resolved home, data dir and system roots (home reached through a link,
macOS /etc -> /private/etc); test expectations are realpath-safe.
- Refuse a target equal to or inside the caller's or the shared cases
directory, pointing at plain Create New (it would list twice, and
deleting the local copy removes files).
- The registry re-read comment no longer claims to prevent the
lost-update race; documented as narrowing it, like /api/cases/link.
- UI: the success toast names the folder the server created, the
"under ~/codeman-cases" blurb and name hint change while a custom
folder is ticked, a "/" parent previews and sends /<name> instead of
an empty path, and the new labels have zh-CN entries.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
POST /api/cases takes an optional path; Add Case > Create New gets a 'Create in a
custom folder' option with Browse. The folder is created (or an empty one filled),
scaffolded like a normal case and registered as a linked case. System, home,
credential and Codeman folders are refused; a folder with files is Link Existing's
job; a failure after the first write undoes what this call created. Admin only in
multi-user mode, like Link Existing.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>