fix(cases): custom-folder create landing fixes (#535)

- 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>
This commit is contained in:
Codeman maintainer
2026-10-05 19:51:59 +02:00
parent 566365e127
commit 192a5994e0
9 changed files with 240 additions and 28 deletions
+36 -2
View File
@@ -1,7 +1,7 @@
/** @fileoverview Add Case → Create New → "Create in a custom folder", end to end: real server, real Chromium, real folders. */
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { basename, join } from 'node:path';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
@@ -127,6 +127,40 @@ describe('Create a case in a custom folder', () => {
expect(existsSync(join(busy, 'CLAUDE.md'))).toBe(false);
});
it('rewords the "under ~/codeman-cases" hints while a custom folder is picked', async () => {
await open();
expect(await page.textContent('#newCaseNameHint')).toMatch(/Created in ~\/codeman-cases/);
await page.click('label.checkbox-row:has(#newCaseCustomPathToggle)');
expect(await page.textContent('#newCaseNameHint')).toMatch(/parent folder below/);
expect(await page.textContent('#newCaseBlurb')).toMatch(/a folder you choose/);
await page.click('label.checkbox-row:has(#newCaseCustomPathToggle)');
expect(await page.textContent('#newCaseNameHint')).toMatch(/Created in ~\/codeman-cases/);
expect(await page.textContent('#newCaseBlurb')).toMatch(/under ~\/codeman-cases/);
});
it('keeps / as the root parent instead of sending an empty path', async () => {
await open();
await page.click('label.checkbox-row:has(#newCaseCustomPathToggle)');
await page.fill('#newCaseName', 'at-root');
await page.fill('#newCasePath', '/');
expect(await page.textContent('#newCasePathPreview')).toBe('Will create: /at-root');
expect(await page.evaluate(() => (window as any).app._newCaseTargetPath())).toBe('/at-root');
});
it('names the folder the server created in the success toast (~ expanded)', async () => {
await open();
await page.click('label.checkbox-row:has(#newCaseCustomPathToggle)');
await page.fill('#newCaseName', 'via-tilde');
await page.fill('#newCasePath', `~/${basename(parent)}`);
await page.evaluate(() => (window as any).app.submitCaseModal());
const target = join(realpathSync(parent), 'via-tilde');
await page.waitForFunction(
(t) => [...document.querySelectorAll('.toast')].some((el) => el.textContent?.includes(t)),
target
);
expect(await toastText()).not.toMatch(/created in ~\//);
});
it('starts unticked every time the modal opens', async () => {
await open();
await page.click('label.checkbox-row:has(#newCaseCustomPathToggle)');