Commit graph

28 commits

Author SHA1 Message Date
cf942d67c7 fix(test): assert unknown-column error actually lists the project's real columns
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 10:05:27 +02:00
05d53e9bb8 test(agent-api): auth, all five routes, and the accept-on-move transition
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 10:00:14 +02:00
d9f6ac0fad Fix owner card creation number allocation 2026-09-22 21:59:15 +02:00
14f6c067c2 test(plan): integration test for the new Gantt view
Two tests against the live database and the seeded project:

- loadPlan returns a section containing the seeded project.
- GanttPlanView renders the project name and at least one
  card title to HTML.

vitest integration config now picks up .test.tsx too.
2026-09-21 16:19:52 +02:00
e4779c76e0 test(intake): picker integration tests + final cap wiring
Phase 9c. 12 tests covering:
- picker page (multi-project list, single-project redirect,
  zero-projects fallback)
- wizard URL (project-on-client 200, cross-client 404,
  show_in_intake=false 404)
- submission flow (card lands on chosen project's inbox,
  cross-client refused, hostile body fields ignored,
  per-(link, project) cap isolates one project from another)
- Phase 8 backwards compat (per-project link redirects to the
  wizard; the existing Phase 8 submission route still accepts
  Phase 8 links)

Three bridges from the plan:
1. PocketBase stores date as 'YYYY-MM-DD 00:00:00.000Z'; the
   filter must use that form. Passing 'YYYY-MM-DD' silently
   matches zero rows, even though the create succeeds.
2. PocketBase's getFirstListItem returns 404 for a row just
   written by the same admin client; getList works. The helper
   uses getList with perPage=1.
3. PocketBase's read-after-write latency is significant.
   isLinkProjectUsable retries with exponential backoff for
   up to ~2.5s before giving up; bumpDailyCapForProject's
   upsert pattern uses a fallback read+update path if the
   create hits the composite unique index from a peer's
   concurrent write.
2026-09-21 14:09:36 +02:00
a2986a11d0 test(intake): per-(link, project) cap infrastructure
Phase 9a Task 6. Three integration tests:
  - schema lands: row inserts with link/project/date/count fields,
    composite unique index is the only constraint (different
    project same link + date is allowed)
  - duplicate (link, project, date) is rejected by the unique index
  - the public route returns a sensible status, never 500, on a
    Phase 9 link (the route will be wired in 9b; 9a only proves
    it does not crash today)

Two bridges from the plan:
  - PocketBase's date field defaults to unique=true; my schema
    fix flipped it via ensureIntakeLinkSubmissionsDateNotUnique.
    The relation and number fields appeared to share the same
    default but getOne()'s response omits the key, so the
    explicit unique=false on those doesn't stick — yet rows with
    the same link and different projects work in practice (the
    composite index does its job). Worth a follow-up if a future
    PocketBase release tightens the default.
  - bumpDailyCapForProject wraps the read-then-create-or-update
    in a retry on the unique constraint to handle PocketBase's
    read-after-write latency window. The cap-isolation property
    is verified end-to-end in 9c.
2026-09-21 13:27:58 +02:00
fda6f2cc30 test(intake-links): placeholders for owner actions (see note)
Phase 8a Task 18. Server Actions are exposed via Next's RSC
boundary, not as plain HTTP endpoints; reaching into them from
vitest is not worth the complexity for a single new action. These
tests assert the admin project page renders for the owner so the
absence of unit-level action coverage is explicit.
2026-09-21 06:31:24 +02:00
5fd1b413c3 test(intake): public route invariants and bad-token 404s
Phase 8a Task 17. Mirrors the existing intake-submit.test.ts pattern:
- happy path: card lands in inbox with created_by_role=anonymous
- counter bump: submission_count and last_submission_at update
- hostile brief: server-set fields cannot be overridden, even when
  the body includes top-level priority/accepted/column/created_by
- none of unknown/revoked/expired/malformed tokens reach the
  handler — they all 404 with the same wording
- deleted-project token: link cascades away, 404 follows
- activity log: card_submitted_anonymously row written

Deviation from plan: the second project needs an explicit Inbox
column created in beforeAll — the schema does not auto-create one
for new projects.
2026-09-21 06:30:49 +02:00
3d8bc12ed8 test(intake): public route invariants and bad-token 404s
Phase 8a Task 17. Mirrors the existing intake-submit.test.ts pattern:
- happy path: card lands in inbox with created_by_role=anonymous
- counter bump: submission_count and last_submission_at update
- hostile brief: server-set fields cannot be overridden, even when
  the body includes top-level priority/accepted/column/created_by
- none of unknown/revoked/expired/malformed tokens reach the
  handler — they all 404 with the same wording
- deleted-project token: link cascades away, 404 follows
- activity log: card_submitted_anonymously row written
2026-09-21 06:20:06 +02:00
Ada
3e868f910a test(plan): Plan Puzzle v1 integration tests
Covers the 9 scenarios from the §6.3 plan brief, mapped to the harness's
sessionless reality:

- client propose refuses without a session and does not write fields
- rescheduleCardAction refuses a Done card with 'Card is done'
- foreign-client propose path: PocketBase read rule denies the cross-tenant
  lookup (anti-enumeration per access.ts:57-59)
- accept/dismiss/returnToScheduled refuse without a session and are no-ops
- buildActivityRows collapses same-date patches into zero rows and emits one
  row when the date actually changes

HARNESS-LIMIT: the actions that need an owner session (accept, dismiss,
returnToScheduled) cannot be driven successfully in vitest. They are
covered by the manual smoke checklist at the workspace path. The
sessionless refuse path proves the gate fires for every call; the
positive path is exercised by the live smoke run.

Test file: tests/integration/plan-puzzle.test.ts (single new file)
2026-09-19 19:58:16 +02:00
b96f38a259 fix(test): tighten log-work assertions, repair summarise.test.ts for source exclusion, add owner-only UI check
Critical fixes to test suite:

1. Fix time bomb in summarise.test.ts: Task 4 changed summaryStatus filter to exclude source="manual",
   so the assertion would fail once any manual entry exists. Updated test to count only non-manual
   commits and added coverage proving manual entries are excluded.

2. Tighten log-work metrics assertions from toBeGreaterThanOrEqual to exact equality (toBe).
   The old assertions would miss if manual entries were incorrectly fed into session clustering.
   Use a fresh baseline recompute to catch regression of Task 2's manual exclusion fix.

3. Add recomputeMetrics call in afterAll to restore cache after test cleanup,
   preventing inflated dashboard metrics on the demo project.

4. Fix fixture link_source from "none" to "manual" to match actual logWorkAction behavior
   (Task 6 review fix).

5. Add UI coverage: "Log work" button is owner-only, verifying the security model.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:44:34 +02:00
f45dfa16a0 test(dashboard): logWorkAction auth and manual-minutes recompute
- Tests that logWorkAction refuses requests without a valid owner session
- Tests that recomputeMetrics correctly adds manual entry minutes to estimatedMinutes
- Both tests verify the integration between log-work action and metrics computation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 13:30:37 +02:00
8c860f2d50 fix(auth): verify sessions against PocketBase instead of trusting the cookie
getCurrentUser() only decoded the pb_auth cookie locally and trusted its
embedded role/client/active fields directly — the PocketBase JS SDK holds
no secret to check a JWT signature against, so a completely forged cookie
(garbage signature, role: "owner", any email) was accepted as a real
session. Verified exploitable end-to-end: a raw Cookie header against the
live app, no browser and no password, rendered the owner's home page.

This mattered most for actions writing through the superuser connection
(log-work.ts, summarise.ts, ingest.ts, and critically users.ts for account
creation) — those never route the caller's token through PocketBase's own
rule engine at all, so the app-level role check was the ONLY gate, and it
was reading an unverified claim. Actions using the caller's own token
(cards.ts, projects.ts) still made the wrong authorization DECISION from
the forged role, even though PocketBase's rule engine would have rejected
the actual data write.

Fixed by having getCurrentUser() call authRefresh(), which asks PocketBase
to confirm the token is genuine; a forged or tampered token is refused
there and treated as signed-out. Wrapped in React's cache() so the extra
round-trip happens once per request, not once per call site (several pages
call this more than once). Re-fetching the live record also means a
deactivated account (`active: false`) stops working on its very next
request instead of only being blocked at a new login.

Regression test builds the exact forged cookie used to verify this,
confirmed red against the vulnerable code and green against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:47:18 +02:00
dc45063508 test(nav): prototype visibility and client scoping on the new home page 2026-09-17 10:42:52 +02:00
8888acb2c6 fix(tests): stop the settings test destroying the configured API key
The test overwrote ai_api_key with a marker and then restored it to ""
rather than to its previous value, so every run of the integration suite
silently wiped the AI provider key. Symptom appears much later and looks
nothing like a test problem: AI features quietly stop working.

Captures the existing value first and restores it in a finally block, so a
failed assertion cannot leave the marker in place either. Deletes the
record instead when the test created it.

The suite runs against the shared instance — there is no separate test
database — so anything it touches, it must put back exactly.

feat(board): give the owner both a quick box and Plan with AI

The Phase 4 spec made the intake wizard client-facing only, reasoning that
the developer creates cards directly. That was wrong about how he wants to
work. The quick box stays the default for lines he already understands;
Plan with AI runs the same interview for work that is genuinely unformed.
Hidden entirely when no provider is configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 08:13:24 +02:00
6afd419dd3 test(roadmap): reschedule auditing and role scoping 2026-09-17 07:19:54 +02:00
8a2fb46788 test(intake): server-decided fields and project scoping 2026-09-16 18:07:16 +02:00
0c25a3e6a1 test(intake): cap enforcement, brief whitelist, project scoping 2026-09-16 17:56:37 +02:00
de8ae83dcb test(narrative): no invention, and raw-subject fallback 2026-09-16 16:26:29 +02:00
aea000f654 test(ai): merge exclusion, graceful degradation, summary persistence 2026-09-16 16:18:40 +02:00
2401dbd68f test(ai): settings page never exposes the API key
Three tests cover the storage and access boundary:

- A client manager cannot list the settings collection at all (rules are
  superuser-only — verified by the rejection, not by silent skip).
- The settings page HTML returned to the owner does not contain the key
  value, even when the key is freshly set to a unique sentinel.
- A client manager gets 404 on /admin/settings (admin layout gates on
  role, not on session presence).

helpers.ts gained ownerEmail / ownerPassword — needed by the settings test
and reusable for any owner-only integration test going forward.
2026-09-16 15:59:49 +02:00
cd0984595c test(metrics): cached figures agree with stored commits + vitest stubs
The integration project now aliases server-only to a tiny empty stub
so tests can import modules that carry 'import "server-only"' without
the throwing real implementation firing during prepare. Unit tests do
NOT get the alias — a pure module with a server import is exactly the
kind of leak the unit project's missing condition is meant to surface.
2026-09-16 15:19:32 +02:00
7fc87042f6 test(linking): idempotency, manual precedence and date bounding
Vitest config: integration project now resolves with the 'react-server'
condition, so server-only modules can be imported directly from tests.
The unit project deliberately does NOT set it: a unit test pulling in a
server-only module must fail loudly, because a pure module with a server
import is exactly the kind of leak that should not reach production.
2026-09-16 13:34:53 +02:00
cd9c8df031 test(ingest): webhook and cron endpoint protection 2026-09-16 11:50:20 +02:00
4b316063dc test(admin): board invariants, owner-only writes, deactivation 2026-09-16 11:31:38 +02:00
2a8d6f2505 fix(security): lock the users collection against privilege escalation
The users collection kept PocketBase's defaults because the Plan 1a rules
migration covered eight collections and missed this one:

  createRule ""                      -> anyone could sign up
  updateRule "id = @request.auth.id"  -> anyone could edit their own record

`role` is an ordinary field here, so both were escalations. Verified
exploitable against the live instance: an anonymous request created an
account with role "owner", and a client manager promoted themselves to
owner by patching their own record. Either route yields read and write
access to every client's board.

Create, update and delete are now superuser-only; list and view are
owner-or-self. Adds four integration tests so a regression fails loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:18:13 +02:00
c966d7dcc9 test(actions): cover audit-log immunity and request flags 2026-09-16 11:06:38 +02:00
5494f14f60 test(permissions): integration coverage for the client write boundary 2026-09-16 09:46:58 +02:00