Fix issues #002-#006 found by the LLM test harness

- #006 (high, data loss): make_room_for() no longer deletes unkept
  artifacts on a doomed request - checks whether eviction can possibly
  succeed before deleting anything. Also fixed a related latent bug found
  while testing: the function returned evicted Artifact objects after
  calling .delete() on them, so their .id was always None (Django clears
  pk on delete); now returns ids captured beforehand.
- #003: added server-side field-length validation (source/title/
  source_name/source_id/theme) on POST and PATCH, matching the model's
  own max_length limits, which were previously unenforced on the ingest
  path entirely.
- #004: ported daybook_post.py's content-shape validation
  (html_fragment/html_full/deck/markdown) to the server, closing the gap
  for any client that doesn't go through the helper script.
- #005: PATCH now rejects wrong-typed `tags` (previously silently wiped
  existing tags) and rejects immutable fields (content/source/kind/day)
  with 400 instead of silently no-op'ing them.
- #002: deleted the misleading skills/daybook-posting/.env.example (a
  zero-dependency script that never reads .env files) and clarified
  SKILL.md/docs/llm_posting_helper.md that these must be real
  process/shell env vars.

Adds tests/unit/ for storage-layer tests. 46 tests passing (was 25).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-02 13:38:59 +01:00
parent c1ea5c7990
commit 07328a3412
13 changed files with 635 additions and 35 deletions

View file

@ -0,0 +1,72 @@
# 005 — `PATCH /api/artifacts/{id}` silently mishandles invalid/immutable input instead of rejecting it
**Status:** Fixed
## Description
`ArtifactDetailView.patch` (`artifacts/views.py`) applies whatever
recognized keys are present in the request body without validating their
type or rejecting fields it can't act on. Two confirmed effects:
**1. A wrong-typed `tags` value silently wipes existing tags (data loss).**
`normalize_tags()` returns `[]` for anything that isn't a `str` or
`list`. `ArtifactDetailView.patch` calls it unconditionally whenever
`"tags"` is a key in the request body, with no type check first.
Reproduced:
```bash
# Artifact has tags ["important", "keep-me"]
curl -X PATCH .../api/artifacts/a74de286 -d '{"tags": 12345}' ...
# -> 200, tags silently become []
```
An agent that sends a malformed `tags` value (e.g. a stray int from a bad
template substitution) gets a `200` and loses the artifact's existing
tags with no indication anything went wrong.
**2. Immutable fields (`content`, `source`, `kind`, `day`) are silently
no-ops instead of rejected.**
`docs/api.md`'s "Not editable yet" table lists these fields and why, but
doesn't say what happens if a client sends them anyway. In practice:
`PATCH` with `{"content": "...", "source": "...", "kind": "...", "day":
"..."}` returns `200` with the artifact **unchanged** — the fields are
silently dropped, not rejected. An agent attempting to fix a typo in
`content` via `PATCH` (a very natural thing to try, since `PATCH` is the
only mutate-after-create endpoint) gets a success response that looks
like it worked but did nothing.
Both are instances of the same root cause: `patch()` doesn't validate
before applying, unlike `POST`'s `validate_source_fields()`.
## Suggested Fix
- Type-check `tags` (must be `str` or `list`) before calling
`normalize_tags`; return `400` on other types instead of silently
emptying it. Same principle likely applies to `metadata` (already
checked) and could apply to `source_prompt`/`source_name`/`source_id`
(currently accept anything via implicit str coercion via Django, but
worth a pass).
- Either make sending an immutable field (`content`/`source`/`kind`/`day`)
in a `PATCH` body a `400` ("field is not editable"), or explicitly
document that unknown/immutable keys are ignored — right now the
behavior is undocumented and easy to mistake for success.
## Resolution
Went with explicit rejection for both, over silent-ignore-and-document:
- `tags` is now type-checked (`str` or `list`) before `normalize_tags` runs;
any other type returns `400` and leaves existing tags untouched.
- Sending `content`, `source`, `kind`, or `day` in a `PATCH` body now returns
`400` naming the offending field(s), via a new `IMMUTABLE_PATCH_FIELDS`
check at the top of `ArtifactDetailView.patch`, before any other
processing — chosen over documenting the no-op because a `200` that does
nothing is easy to mistake for success.
`docs/api.md`'s "Not editable" table now says so explicitly instead of just
listing reasons. Tests added: wrong-typed `tags` is rejected and leaves
existing tags intact, and each of the four immutable fields is rejected
individually.