~bigbes/lethe

632218696e06e2ef0a7a588cb4ad249acbf50639 — Eugene Blikh a month ago 964d802
docs(lethe-web-ui-palette-savedsearch): record verify-driven fix-up + checks
1 files changed, 39 insertions(+), 0 deletions(-)

M docs/tasks/lethe-web-ui-palette-savedsearch.md
M docs/tasks/lethe-web-ui-palette-savedsearch.md => docs/tasks/lethe-web-ui-palette-savedsearch.md +39 -0
@@ 302,3 302,42 @@ No removed / renamed JSON fields anywhere. No schema modifications to existing t
- **PH2 — second `Session.id` call site also fixed (`web/src/routes/project.$.tsx:33`)**: Plan bullet 2.4 already flagged this. Design said "single existing call site"; a second call site shipped in #5 (lethe-web-ui-aggregates) between design approval and execute. User explicitly approved option 1 (fix both) before dispatch. Both call sites now pass `s.sessionId` to `params.id`; React `key` in `SessionsTable.tsx:61` keeps `s.id` (composite) intentionally — IV6's "both coexist" exists for exactly this asymmetry.
- **PH3 — `apiFetchVoid` helper added in `web/src/api/client.ts`** (outside PH3's declared Owns set): backend DELETE `/api/v1/saved-searches/{name}` returns 204 No Content; the existing `apiFetch<T>` calls `resp.json()` unconditionally and would throw `SyntaxError` on an empty body. The dispatcher's pre-flight authorized this exact helper so the foundation invariant ("all API calls flow through `apiFetch`/its sibling — no raw `fetch` outside `client.ts`") stays intact. Single new exported function; mirrors `apiFetch` except it skips JSON decode and returns `void`.
- **PH3 — `internal/server/web/dist/index.html` regen included in the commit** (outside PH3's declared Owns set): Vite stamps content-hash bundle filenames; `npm run build` rewrote `dist/index.html` to point at the new asset hashes. #5's verify recorded the same artifact as a follow-up commit (`f6611d7`); catching it in-phase here is the corrected pattern.
- **Verify — register `savedsearch.Repository`/`savedsearch.Handler` in the e2e test's local steward graph (`cmd/lethe/main_e2e_test.go`)**: PH1 added a `*savedsearch.Handler` `inject:""` field on `Server` and updated `cmd/lethe/main.go` accordingly, but missed the sibling `main_e2e_test.go` which constructs its own steward graph that mirrors `main.go`. `TestEndToEnd_MultiUserIsolation` panicked at `Inject` with `failed to find dependency: target=server.Server targetField=SavedSearches dependencyType=*savedsearch.Handler`. Verify-driven fix-up commit `964d802` adds the import and two `MustServiceAsset` lines, restoring the e2e test. Filed as a deviation because PH1's plan named only `cmd/lethe/main.go` for steward registration and the consistency sweep should have grepped for `MustServiceAsset` siblings before commit.

## Verify

**Result:** passed

Positive:
- CK1 — `go test ./internal/domain/savedsearch/... -race -count=1` → ok (22 tests)
- CK2 — `go test ./internal/... -count=1` → ok (all packages)
- CK2b — `go test ./cmd/lethe/... -count=1` → ok after fix-up commit `964d802` (initial run panicked: see Deviations)
- CK3 — `go build ./cmd/lethe/` → 32 MB binary
- CK4 — `cd web && npm test -- --run` → 50 passed across 4 files
- CK5 — `cd web && npm run build` → 376 modules, 491 KB JS / 150 KB gzip; `dist/index.html` matches tracked artifact

Negative:
- CK6 (slash in name) — `TestHandler_Create_SlashInNameReturns400` covers `POST {name:"a/b"}` → 400 VALIDATION
- CK7 (duplicate) — `TestHandler_Create_DuplicateReturns409` covers second POST same `(owner, name)` → 409 CONFLICT
- CK8 (missing) — `TestHandler_Delete_Sequence` covers second DELETE → 404 NOT_FOUND

Invariants / assumptions:
- CK9 (IV1) — `grep -iE 'TRIGGER|FOREIGN KEY|FTS|REFERENCES' 0002_saved_searches.up.sql` → empty
- CK10 (IV2) — owner derives from `auth.MustIdentity` via `ownerOf` (handler.go:46-48); `?owner=` on writes is rejected as INVALID, on reads is silently ignored — `TestHandler_List_OwnerParamIgnored` asserts the read path
- CK11 (IV3) — `PRIMARY KEY (owner, name)` in 0002 migration
- CK12 (IV4) — `Palette.tsx` consumes `useProjects`/`useSessions` unchanged; no new query params constructed in either hook; AS1 cap is client-side `.slice(0, 50)`
- CK13 (IV5) — `find web/src/routes -name 'settings*'` → only `routes/settings.tsx`; no sub-route files
- CK14 (IV6) — `Session.id` (composite, `${tool}/${host}/${session_id}`) and `Session.sessionId` (bare) both populated in `adaptSession`; `SessionsTable.tsx:61` keeps `s.id` as React `key` (unique cross-tool/host); `routes/index.tsx:48`, `routes/project.$.tsx:33`, `Palette.tsx:137` all use `s.sessionId` for `params.id`
- CK15 (IV7+UK1) — `validateName` (handler.go:54-72): `name == ""` → 400, `len(name) > 64` → 400, `strings.Contains(name, "/")` → 400. Called on Create + Update.
- CK16 (PC1) — saved-search palette activate at `Palette.tsx:141` → `navigate({ to: '/search', search: { q: item.query } })`; `/search` is the foundation stub, no execution wired
- CK17 (AS1) — `Palette.tsx:95` caps sessions list with `.slice(0, 50)` before mapping to `SessionItem`

Interfaces:
- CK18 (IF1) — `Session.sessionId: string` declared at `adapters.ts:23`, populated at `:40`, consumed by `Palette.tsx:32, 98, 101, 137`; bare id never replaced by composite anywhere on the URL path
- CK19 (IF2) — `useSavedSearches` exported from `web/src/features/settings/useSavedSearches.ts:9`, consumed at `Palette.tsx:5, 78` and `SavedSearchesSection.tsx`
- CK20 (IF3) — `GET /api/v1/saved-searches` returns `{ saved_searches: [SavedSearch] }` (top-level key); `Owner` field omitted via `json:"-"` (IV2). `TestHandler_List_OwnerFieldAbsentInJSON` asserts the JSON shape; frontend `useSavedSearches.ts:8, 16` consumes the `data.saved_searches` key.

Notes:

- **Browser smoke deferred** — `/settings` saved-search CRUD and the widened palette were not exercised in a real browser. tsc + vitest + the production bundle are clean and the backend is verified by handler tests, but a full Chrome walk requires the running server + an authenticated session. Task #10 (`lethe-oidc-stub`, ✓ Reviewed) ships the `cmd/oidc-stub` binary that unblocks this, but standing it up and driving the UI is out of reach in autonomous mode without user input. Recommended walk: (a) start `lethe` with the OIDC dev stub config, (b) sign in, (c) `⌘K` and confirm projects/sessions/saved-search items appear in fixed group order, (d) `/settings` and create / rename / delete a saved search, (e) press a saved-search row in the palette and confirm it lands on `/search?q=…` (stub).
- **Verify-driven fix-up commit** `964d802` — PH1 missed `cmd/lethe/main_e2e_test.go`'s steward graph; recorded under Deviations.