From e3c609f5f0009916699595d42ba7346311f742e7 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 25 Mar 2026 13:39:47 +0100 Subject: [PATCH] test: add coverage for lastUsedCase partial update and strict schema rejection Tests that partial PUT /api/settings with just lastUsedCase works correctly and that including modelConfig triggers strict Zod schema rejection (the bug fixed in #49). Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 ++-- test/routes/system-routes.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cb6b7a25..92b7ad38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | Task | Command | |------|---------| -| Dev server | `npx tsx src/index.ts web` | +| Dev server | `npm run dev` (or `npx tsx src/index.ts web`) | | Type check | `tsc --noEmit` | | Lint | `npm run lint` (fix: `npm run lint:fix`) | | Format | `npm run format` (check: `npm run format:check`) | @@ -78,7 +78,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | Production start | `npm run start` | | Production logs | `journalctl --user -u codeman-web -f` | -**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master (Node 22). Tests excluded (they spawn tmux). +**CI**: `.github/workflows/ci.yml` runs `typecheck`, `lint`, `format:check` on push to master/main and on PRs (Node 22). Tests excluded (they spawn tmux). **Code style**: Prettier (`singleQuote: true`, `printWidth: 120`, `trailingComma: "es5"`). ESLint flat config (`eslint.config.js`) allows `no-console`, warns on `@typescript-eslint/no-explicit-any`. Ignores: `app.js`, `scripts/**/*.mjs`, `src/web/public/vendor/**`, `tools/**`, `remotion/**`. diff --git a/test/routes/system-routes.test.ts b/test/routes/system-routes.test.ts index 453cef69..6a636d42 100644 --- a/test/routes/system-routes.test.ts +++ b/test/routes/system-routes.test.ts @@ -366,6 +366,36 @@ describe('system-routes', () => { expect(body.success).toBe(false); }); + it('saves lastUsedCase as partial update without overwriting other settings', async () => { + mockedReadFile.mockResolvedValue( + JSON.stringify({ showCost: true, showTokenCount: false, subagentTrackingEnabled: true }) as never + ); + + const res = await harness.app.inject({ + method: 'PUT', + url: '/api/settings', + payload: { lastUsedCase: 'my-test-case' }, + }); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body).success).toBe(true); + + const writtenContent = JSON.parse(mockedWriteFile.mock.calls[0][1] as string); + expect(writtenContent.lastUsedCase).toBe('my-test-case'); + expect(writtenContent.showCost).toBe(true); + expect(writtenContent.showTokenCount).toBe(false); + expect(writtenContent.subagentTrackingEnabled).toBe(true); + }); + + it('rejects settings with modelConfig (strict schema prevents full-object PUT)', async () => { + const res = await harness.app.inject({ + method: 'PUT', + url: '/api/settings', + payload: { lastUsedCase: 'test', modelConfig: { model: 'something' } }, + }); + // Fastify rejects unknown fields at schema validation level (400) before handler runs + expect(res.statusCode).toBe(400); + }); + it('rejects non-object body', async () => { const res = await harness.app.inject({ method: 'PUT',