Files
meshcore-analyzer/cmd/server
Kpa-clawbot 2a07716885 fix(geo-prune): relocate DELETE to ingestor via file-marker queue (#738 / #1289)
GREEN for the invariant test added in the prior commit. Resolves the dead-on-
arrival blocker introduced by PR #738: the server opens SQLite mode=ro after
#1283/#1289, so the `confirm=true` path of `/api/admin/prune-geo-filter` would
fail in production with "attempt to write a readonly database". The author's
tests only passed because setupTestDB uses `:memory:` without mode=ro.

## Architecture: file-marker queue (chosen over IPC/HTTP proxy)

Server → marker file → ingestor → result file → server. New package
`internal/prunequeue` defines the on-disk protocol:

  <dir(dbPath)>/prune-requests/request-<id>.json   (written by server)
  <dir(dbPath)>/prune-requests/result-<id>.json    (written by ingestor)

Atomic via os.Rename. The ingestor removes the request file as part of writing
the result, so the directory naturally drains.

Why file markers over an internal localhost HTTP socket:
  * No new listener / port / auth surface to manage.
  * Inherits the same backup/permissions story as the SQLite file — they share
    a directory and a deployment.
  * Survives ingestor restarts: pending requests are processed on next boot.
  * Trivial to inspect / clear with `ls` / `trash` during ops.
  * Matches the existing maintenance-ticker pattern in cmd/ingestor/main.go.

## HTTP surface (unchanged for clients in spirit)

POST /api/admin/prune-geo-filter
  * dry-run (default): same response shape as before — preview list.
  * confirm=true: now returns 202 Accepted + {requestId, statusUrl, count, nodes}.
    Body must still carry the pubkeys snapshot from the preview (TOCTOU guard
    preserved — the ingestor honors the list verbatim, does NOT re-evaluate
    geo_filter membership).

GET /api/admin/prune-geo-filter/status?id=<id>  (NEW, requires API key)
  * pending: 200 {requestId, status:"pending"}
  * done:    200 {requestId, status:"done", deleted, requestedAt, completedAt}
  * error:   200 {requestId, status:"error", error, ...}
  * 404 when neither marker nor result exist.
  * 400 for invalid ids (path-traversal guard: hex/[0-9a-f] only, ≤64 chars).

## TOCTOU + cascade-cleanup from prior CR

  * TOCTOU: server snapshots the preview list at confirm time and writes the
    PRUNED list (intersection of preview + still-outside) into the marker.
    The ingestor does NOT re-check geo_filter — it executes exactly what the
    operator confirmed.
  * Cascade cleanup: ingestor's DeleteNodesByPubkeys chunks under SQLite's
    variable limit (500 per stmt). It deliberately limits scope to the `nodes`
    table; the schema has no FKs, so observation/transmission rows are left
    for the regular packet-retention ticker. Documented in-line.

## Read-only invariant test

Extends cmd/server/readonly_invariant_test.go (TestServerDBHasNoWriteMethods)
to forbid `DeleteNodesByPubkeys` on the server `*DB`. With this commit the
method is gone, so the test passes:

  $ go test -run TestServerDBHasNoWriteMethods ./...
  ok      github.com/corescope/server     0.190s

## Tests

* cmd/server/routes_test.go: rewrote the confirm-test to assert 202 +
  marker file existence; added pending→done status-roundtrip test;
  added 404 + path-traversal-id rejection tests. Old TestDeleteNodesByPubkeys
  removed (the method now lives in cmd/ingestor).
* cmd/ingestor/prune_geofilter_test.go: end-to-end queue test (request file
  on disk → RunPendingPruneRequests → result file + DB rows actually deleted),
  plus empty-queue no-op.
* internal/prunequeue/prunequeue_test.go: round-trip, path-traversal rejection,
  missing-result returns (nil, nil).

## Files

* internal/prunequeue/{prunequeue.go,prunequeue_test.go,go.mod} — new package
* cmd/ingestor/prune_geofilter.go — Store.DeleteNodesByPubkeys + RunPendingPruneRequests
* cmd/ingestor/prune_geofilter_test.go — queue end-to-end test
* cmd/ingestor/main.go — startup pass + 15s ticker for the queue, Stop on shutdown
* cmd/server/db.go — DeleteNodesByPubkeys removed; only GetNodesForGeoPrune (read) remains
* cmd/server/routes.go — handlePruneGeoFilter rewritten to enqueue;
  handlePruneGeoFilterStatus added; route registered
* cmd/server/routes_test.go — new tests; old direct-DELETE coverage removed
2026-05-21 03:09:50 +00:00
..