Renaming replaced the project page's h1 with an input, so a page an owner
opened had no heading at all. The sidebar smoke test caught it on CI — and
it was right to: every other view here has a heading, and it is what a
screen reader announces on arrival.
The heading stays and becomes a field when clicked. Same rename, one more
click, and the page keeps its outline.
The more interesting half is why that passed locally. The test clicked a
link and immediately asserted a heading was visible — and `toBeVisible`
was being satisfied by the heading of the page it had just left, before
the SPA swapped it out. On this laptop the old h1 lingered long enough to
answer; on a slower runner it did not. So the test did not check that the
new page rendered, it checked that some page had at some point.
waitForURL closes that. Verified by putting the broken heading back with
the strengthened test in place: it now fails locally with the same message
CI gave.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A project could be created and shared and never renamed or deleted. Both
have worked through the API since the beginning; there was simply nothing
to click.
The heading is now the field — a heading that happens to be editable
rather than a form that looks like one — and saves on blur, with Escape
putting the old name back. Owner only, and the Inbox keeps its plain
heading because it cannot be either renamed away or deleted.
Deleting needed more than a button. It is soft, with thirty days to
change your mind, and restoring has been possible the whole time — from
an id, which is exactly what you no longer have once the project has gone
from the screen. A recovery window nobody can reach is not one. So
GET /trash/projects lists what you have deleted, with how many tasks went
with it and how long is left, and Settings → Data has the button.
Owned, not shared: a member of a project somebody else deleted has no
business bringing it back, and there is a test that says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verdande could not be added to Claude at all. The documented setup —
POST /api/v1/mcp with an Authorization header — cannot be configured from
Claude's custom-connector dialog, which asks for a name, a URL and OAuth
client credentials and has no field for a bearer token. The instructions
described something the interface does not offer.
Worse, the obvious guess failed silently in the most misleading way
available: /mcp was not a route, so the SPA fallback answered 200 with a
page of HTML. The connector reported a successful connection and then
failed to parse the response, which reads as a broken client rather than
a missing endpoint.
So /mcp exists now and takes its credential in the query string, exactly
as the calendar feed does and for the same reason: a client that cannot
be told to send a header means the address is the credential. The
request logger records paths without their query strings, so the key does
not reach the log.
It is deliberately outside /api/v1 and accepts the key and nothing else.
Reading a session cookie here would make it a state-changing POST outside
the CSRF check — a cross-site request that acts as whoever is signed in —
and there is a test that fails if that ever becomes true.
The API path is unchanged for anything that can send a header. The tokens
page now shows the connector address next to the token it belongs to,
since that is the one moment the token exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite passed on a laptop and failed on the runner with zero links
found. locator.all() is a snapshot and does not auto-wait: calling it
straight after goto() measures how fast the machine is, not what the page
contains.
An assertion on the first link waits for hydration, and the hrefs are
collected once and re-resolved on each pass rather than held as element
handles — navigating re-renders the sidebar, so a handle from before the
click points at an element that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The brief asked for these under Definition of done and they were never
written. 610 Go tests, none of which opens a browser.
Six flows: create the first account and sign in, quick add and tick a
task off, click every link in the sidebar, open a task and edit it, walk
every settings tab, and check that the manifest's icons and the service
worker can actually be fetched. Every assertion is on rendered content
rather than on a URL — the binary serves the SPA shell for anything it
does not recognise, so toHaveURL passes on precisely the bug this exists
to catch.
The suite builds the frontend, copies it into cmd/verdande/webbuild and
starts the real binary, every run. Merely starting the server would test
whatever frontend was copied there last, which during development is
nearly always the wrong one, and a green report on code that is not
running is worse than no report.
Chromium only. A smoke test catches a build that does not work at all;
running it in three engines finds the same breakage three times.
They found four bugs the first time they ran, fixed in the commits before
this one: concurrent writes returning SQLITE_BUSY, a completed task
keeping its row, the same task landing in the list twice, and a database
failure presenting as everybody being logged out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>