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:
parent
c1ea5c7990
commit
07328a3412
13 changed files with 635 additions and 35 deletions
68
issues/003-model-max-length-silently-unenforced.md
Normal file
68
issues/003-model-max-length-silently-unenforced.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# 003 — `CharField(max_length=...)` limits are silently unenforced on ingest
|
||||
|
||||
**Status:** Fixed
|
||||
|
||||
## Description
|
||||
|
||||
`artifacts/models.py` declares length caps on several `Artifact` fields —
|
||||
`source` (80), `title` (200), `source_name` (120), `source_id` (120),
|
||||
`theme` (40) — but nothing on the ingest path enforces them. `views.py`'s
|
||||
`create_text_artifact`/`create_binary_artifact` call
|
||||
`Artifact.objects.create(...)` directly, which does **not** run Django's
|
||||
`full_clean()`/validators. SQLite (the DB in both dev and production per
|
||||
`docs/deployment.md` line 197) also doesn't enforce `VARCHAR(n)` length as
|
||||
a hard constraint on `INSERT`, so oversized values are written and read
|
||||
back verbatim with no error at any layer.
|
||||
|
||||
Confirmed against the local `test` account:
|
||||
|
||||
1. `POST /api/artifacts` with a 250-character `title` (limit 200) →
|
||||
`201`, title stored and returned in full, uncut.
|
||||
2. `POST /api/artifacts` with a 150-character `source` (limit 80) →
|
||||
`201`, `source` stored in full. Because `source_name` defaults to
|
||||
`source` when omitted (`source_fields()` in `views.py`), this also
|
||||
silently blew past `source_name`'s own 120-char limit in the same
|
||||
request.
|
||||
|
||||
For contrast, other validation on the same endpoint works correctly and
|
||||
returns a clean `400` — e.g. an invalid `tag_colors` hex value
|
||||
(`{"tag_colors": {"urgent": "notacolor"}}` → `400` with a helpful
|
||||
message). So the endpoint clearly has a validation convention; these
|
||||
five fields just aren't part of it.
|
||||
|
||||
Practically: a title long enough could break feed-row layout in the UI;
|
||||
`source` feeds directly into the on-disk storage path
|
||||
(`artifacts_store/{source}/{day}/...`) and the sidebar/label system, so
|
||||
an extreme value there has a larger blast radius than just cosmetics.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8001/api/artifacts \
|
||||
-H "Authorization: Bearer <test-account-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"source\":\"edge-case-test\",\"title\":\"$(python3 -c 'print("A"*250)')\",\"kind\":\"markdown\",\"content\":\"x\"}"
|
||||
# -> 201, title returned uncut at 250 chars
|
||||
```
|
||||
|
||||
## Suggested Fix
|
||||
|
||||
Add explicit length checks alongside the existing required-field/kind
|
||||
checks in `ArtifactListCreateView.post` and `ArtifactDetailView.patch`
|
||||
(and the two `create_*_artifact` helpers), returning `400` with a message
|
||||
naming the field and its limit — consistent with how `tag_colors` and
|
||||
`source_type` are already handled. A shared `validate_field_lengths(data)`
|
||||
helper (mirroring `validate_source_fields`) covering `source`, `title`,
|
||||
`source_name`, `source_id`, `theme` would keep it in one place.
|
||||
|
||||
## Resolution
|
||||
|
||||
Added `validate_field_lengths(data, fields)` in `artifacts/views.py`, called
|
||||
from `validate_source_fields` (covers `source`, `title`, `source_name`,
|
||||
`source_id`, `theme` on both text and binary `POST`, since both paths already
|
||||
routed through that function) and directly in `ArtifactDetailView.patch`
|
||||
(covers `title`, `source_name`, `source_id` — the only length-limited fields
|
||||
`PATCH` can touch). Returns `400` naming the field and its limit, matching
|
||||
the existing `tag_colors`/`source_type` convention. Tests added in
|
||||
`tests/api/test_artifacts.py` covering oversized `title`/`source` on create,
|
||||
an exact-limit boundary case, and oversized `title` on patch.
|
||||
Loading…
Add table
Add a link
Reference in a new issue