# Validation — V2.5.1 Reconnect / Presence / Heartbeat ## Regression: regelmäßiger HTTP 409 nach ~90 Sekunden 1. Browser-Client mit einem aktiven Task verbinden und mindestens 3 Minuten ohne F5 laufen lassen. 2. Prüfen, dass Auto-Guesses über die frühere 90-Sekunden-Grenze hinweg weiterlaufen. 3. DevTools → Network → WS beobachten: der Server sendet echte WebSocket-Ping-Control-Frames; Browser-Pongs verlängern die Read-Deadline. 4. Netzwerk für einige Sekunden trennen und wieder aktivieren. Der Browser muss automatisch reconnecten und ohne F5 weitertippen. 5. Während eines Task-Wechsels bzw. schnellen Reconnects darf ein verspätetes `ReleasePresence` der alten Verbindung die neue Presence nicht entfernen. Das wird zusätzlich durch `TestPresenceReconnectOldReleaseCannotDeleteNewLease` geprüft. 6. Shell-Client ebenfalls mindestens 3 Minuten laufen lassen, Verbindung kurz unterbrechen und prüfen, dass `[ws] wieder verbunden` erscheint und Guessing fortgesetzt wird. Maschinenlesbare 409-Codes: ```text presence_required sequence_mismatch task_config_changed selection_conflict task_inactive ``` ## V2.5 — Task-Auswahl, Vererbung und Shell-Client ### Browser-Landing-Page 1. Server mit mindestens zwei aktiven Tasks starten (`ACTIVE_TASK_COUNT=2`). 2. `/` in einem neuen Browserprofil öffnen. 3. Prüfen, dass vor der 3D-Ansicht die Task-Landing-Page erscheint. 4. Einen Task wählen und prüfen, dass WebSocket/Auto-Guess starten. 5. **TASKS** wählen, einen anderen Task anklicken und prüfen, dass keine `identity already connected`-Meldung entsteht. 6. Browser neu laden: die serverseitig zuletzt gewählte Task-Karte muss als ausgewählt markiert sein. ### Task-spezifisches NFT-Prompt + Admin-Draft 1. `/admin` → **TASK** → Task auswählen. 2. Anzeigename, Beschreibung, NFT-Prompt-Anweisungen und Negative Prompt eintippen, **noch nicht speichern**. 3. Mindestens einen Auto-Refresh-Zyklus (>3 s) abwarten und danach F5 drücken. 4. Prüfen, dass die noch nicht gespeicherten Eingaben weiter im Formular stehen. 5. **TASK-KONFIG SPEICHERN** verwenden und anschließend neu laden; nun müssen die Werte vom Server kommen. 6. Task gewinnen oder über Admin `close` beenden. 7. Folge-Task öffnen und prüfen, dass `range_bits`, task-spezifische Intervalle, Anzeigename, Beschreibung und beide Prompt-Felder identisch geerbt wurden. ### Shell-Client interaktiv ```bash go run ./cmd/client -url http://127.0.0.1:8080 ``` Prüfen: ```text tasks status map 25 leaderboard 20 nfts 10 use 2 status ``` Der Shell-Client muss weiter automatisch Tipps senden. Nach `use` darf der Server die neue WebSocket-Verbindung nicht wegen der alten Presence ablehnen. ### Shell-Client unbeaufsichtigt ```bash go run ./cmd/client \ -url http://127.0.0.1:8080 \ -non-interactive \ -task 1 \ -max-nodes 250 ``` Mindestens zwei Tippintervalle laufen lassen und im Admin prüfen, dass Guess-/Score-Aktivität vorhanden ist. Dann `Ctrl+C`; die Presence muss freigegeben werden. ### Browser-/Shell-Identity-Portabilität Im Browser eine Identity exportieren. Danach **Browser-Tab schließen**, damit die Single-Connection-Lease frei ist: ```bash export NEURALHUNT_IDENTITY_PASSPHRASE='test-passphrase' go run ./cmd/client \ -import ./neuralhunt-identity-....json \ -identity ./tmp-shell-identity.json ``` Die Client-ID muss mit der Browser-ID übereinstimmen. Umgekehrt: ```bash export NEURALHUNT_IDENTITY_PASSPHRASE='test-passphrase' go run ./cmd/client \ -identity ./tmp-shell-identity.json \ -export ./tmp-browser-import.json ``` Diese Datei anschließend über **Import** in einem Browserprofil laden; die Client-ID muss identisch bleiben. ### Automatische Tests Zusätzlich zu den bisherigen Tests: ```bash go test ./cmd/client ``` enthält PBKDF2-Testvektor sowie Raw-/verschlüsselten Identity-Roundtrip. `internal/data/successor_test.go` prüft mit einem echten SQLite-Treiber die Task-Vererbung, Idempotenz des Nachfolgers und das atomare Migrieren der Client-Auswahl. In dieser Erstellungsumgebung erfolgreich ausgeführt: ```bash node --check internal/webui/dist/app.js gofmt -w cmd/client/*.go internal/data/store.go internal/server/server.go internal/artifact/worker.go # kompletter Source-Compile mit lokalen API-Stubs für die drei nicht ladbaren Module go test -run '^$' ./... # echte Shell-Client-Krypto-/Identity-Tests; nur der WebSocket-Import ist dabei gestubbt go test ./cmd/client ``` Zusätzlich wurde eine **V2.4-artige SQLite-Datenbank** mit Python/SQLite angelegt und die Upgrade-Reihenfolge aus `OpenSQLite()` reproduziert. Dabei wurden die fünf neuen Task-Spalten, `client_task_selection` und der erst nach `parent_task_id` angelegte `tasks_parent_unique_idx` erfolgreich erzeugt. Das ist relevant, weil der Index bei Bestandsdatenbanken nicht vor dem `ALTER TABLE` angelegt werden darf. Ergebnis dieser ausführbaren Prüfungen: erfolgreich. ## Statisch geprüft / implementiert - UI ist weiterhin vollständig unter `internal/webui/dist/` eingebettet; kein npm/Vite-Build nötig. - `/leaderboard` ist eine SPA-Route und besitzt einen öffentlichen Live-WebSocket. - Mobile Mode ist für Client, Leaderboard und Admin implementiert; Admin bietet mobile MAP/TASKS/CONTROL-Navigation. - Das Leaderboard lädt Gewinner-Artefakte ausschließlich über `/api/public/artifacts/{task}/preview`. - Die öffentlichen Leaderboard-Daten enthalten NFT-Anzahl und letzte Wasserzeichen-Preview, aber keine Original-Artifact-URI. - Der frühere öffentliche `/artifacts/*`-File-Server ist entfernt; Original und Manifest benötigen ein Admin-JWT. - Raster-Previews werden serverseitig mit einem eingebrannten Bitmap-Wasserzeichen neu als PNG codiert; SVG-Previews erhalten eine wiederholte Wasserzeichenebene. - TARGET FIELD bildet Score monoton und direkt auf den Orbitalradius ab; Kameratiefe kann die wahrgenommene Nähe nicht mehr umkehren. - Score-Ringe: 0 / 25 / 50 / 75 / 90 / 95 / 99+; Vorder- und Rückhälfte werden zur Tiefenwahrnehmung unterschiedlich gerendert. - Pseudozufällige Client-zu-Client-Synapsen sind vollständig entfernt. - SIGNALWEGE verbinden ausschließlich Top-Kandidaten/eigenen Client mit dem Task-Core; Score-Verbesserungen erzeugen nur entlang dieses realen Pfads Signalimpulse. - Client lädt bei `task_changed` Range Bits, Seed, Revision, Intervalle und Punkte neu. - Client-Autosubmit verwendet keinen statischen `setInterval` mehr, sondern die jeweils aktuelle Task-Konfiguration. - Task-Aktionen werden in `task_actions` persistiert, jede Sekunde geprüft und mit Status/Audit gespeichert. - Alte SQLite-Datenbanken erhalten die neuen `tasks`-Spalten per `ALTER TABLE`-Migration. - `set_range_bits` unterstützt `preserve` und `reroll`. - Task-spezifische Tippintervalle überschreiben globale Runtime-Defaults. - OpenAI-, ComfyUI- und A1111-Provider speichern nur lokal erzeugte Ergebnisbytes + Manifest; kein S3 notwendig. - OpenAI-Provider validiert für `gpt-image-2` die dokumentierten Dimensionsgrenzen, bevor ein Request gesendet wird. - Provider-Secrets/URLs werden nicht über die Runtime-Settings-API exponiert. - ComfyUI unterstützt Workflow-Platzhalter für Prompt/Negative Prompt/Seed/Größe/Steps/Modell. - A1111 unterstützt optional Basic Auth und Checkpoint-Override pro Request. ## In dieser Umgebung nicht vollständig ausführbar `go test ./...` und `go mod tidy` können hier nicht abgeschlossen werden, weil ausgehender DNS/Netzwerkzugriff auf `proxy.golang.org` gesperrt ist. Der Versuch scheitert beim Download dieser bereits im `go.mod` referenzierten Module: ```text modernc.org/sqlite github.com/go-chi/chi/v5 github.com/gorilla/websocket ``` Dadurch konnten `internal/data`, `internal/server`, `internal/ws` und das komplette Binary hier nicht gegen frisch heruntergeladene Dependencies gebaut werden. Die direkt von ihnen unabhängigen geänderten Go-Packages wurden kompiliert/getestet. Docker ist in der Erstellungsumgebung ebenfalls nicht verfügbar. ## Empfohlene Tests auf deinem Rechner ```bash go mod tidy go test ./... go run ./cmd/server ``` Dann: 1. `/` öffnen und prüfen, dass TARGET FIELD standardmäßig aktiv ist. Einen Score-90-, Score-95- und Score-99-Punkt vergleichen: 99 muss unabhängig von Orbit/Neigung sichtbar auf dem kleinsten Ring liegen. 2. `ORBIT` laufen lassen und die Ansicht ziehen: Die Reihenfolge der Ringradien darf sich durch Kamerabewegung nicht umkehren. 3. `SIGNALWEGE` einschalten: Es dürfen keine Client→Client-Kanten erscheinen; nur ausgewählte Client→Task-Pfade sind erlaubt. 4. Zwei unabhängige Browserprofile auf `127.0.0.1` verbinden. 5. `/leaderboard` in einem dritten Tab öffnen; Punktänderungen sollen ohne manuelles Reload erscheinen. 6. Im Admin-Bereich einen aktiven Task auswählen und `pause` sofort ausführen. Clients müssen `PAUSE` anzeigen und keine Tipps senden. 7. `resume` ausführen; Autosubmit muss mit dem aktuellen Client-Intervall weiterlaufen. 8. `set_range_bits` im Modus `preserve` erhöhen; Client muss neue Bitzahl ohne Reload verwenden. 9. `set_range_bits` im Modus `reroll` ausführen; Punkte/Scores müssen auf 0 zurückfallen und neuer Seed/Revision aktiv werden. 10. Eine Range-Bit-Änderung fünf Minuten in die Zukunft planen und anschließend abbrechen; Status `cancelled` prüfen. 11. Zwei geplante Aktionen hintereinander setzen, z. B. 24 Bit und später 32 Bit, und Ausführung/Audit prüfen. 12. Einen kleinen Zahlenraum verwenden, einen Task lösen und lokales SVG + Manifest prüfen. 13. `/leaderboard` öffnen: das Gewinner-Artefakt muss in Galerie und Ranking nur als Wasserzeichen-Vorschau erscheinen. 14. Direkter Aufruf der früheren `/artifacts/...`-URI muss 404 liefern; Original/Manifest aus dem Admin-Dashboard müssen weiterhin nach Authentifizierung öffnen. 15. Browser auf <850 px setzen oder MOBILE aktivieren: Client-Details müssen einklappbar sein; im Admin müssen MAP/TASKS/CONTROL einzeln erreichbar sein. ### OpenAI Provider ```env ARTIFACT_PROVIDER=openai ARTIFACT_MODEL=gpt-image-2 OPENAI_API_KEY=... ARTIFACT_WIDTH=1024 ARTIFACT_HEIGHT=1024 ARTIFACT_QUALITY=medium ``` Task lösen bzw. bei einem abgeschlossenen Task `NFT-Bild neu erzeugen` ausführen. Erwartet: ```text /data/artifacts/artifact_/image.png /data/artifacts/artifact_/manifest.json ``` ### ComfyUI Provider ComfyUI starten, API-Workflow exportieren und im Workflow mindestens `{{PROMPT}}` sowie optional die anderen Platzhalter verwenden. Danach: ```env ARTIFACT_PROVIDER=comfyui COMFYUI_URL=http://127.0.0.1:8188 COMFYUI_WORKFLOW_PATH=/path/workflow-api.json ARTIFACT_MODEL=checkpoint.safetensors ``` ### AUTOMATIC1111 Provider A1111 mit `--api` starten: ```env ARTIFACT_PROVIDER=a1111 A1111_URL=http://127.0.0.1:7860 ARTIFACT_MODEL=checkpoint.safetensors ``` Bei `--api-auth user:pass` zusätzlich `A1111_USER` und `A1111_PASSWORD` setzen. ## Regressionen aus früheren Fixes - Auth challenge/login toleriert optionale JWK-Metadaten (`alg`, `use`, `kid` etc.). - Mehrere Challenges derselben Identität überschreiben einander nicht. - Eine Browser-ID darf weiterhin nur eine aktive Client-WebSocket-Verbindung besitzen. - Leere Listen werden als `[]`, nicht als `null`, an die UI geliefert. - Client-Punkt wird direkt beim WebSocket-Connect erzeugt. ## Additional v2 checks - SQLite schema parsed successfully with Python `sqlite3`, including `task_actions` and the live task override columns. - The legacy `migrations/001_init.sql` mirror was synchronized with `internal/data/schema.sql` so manual schema inspection does not show an obsolete layout. - Responsive CSS keeps the Admin Task Actions / Artifact control plane visible on common laptop widths (851–1280px). ## V2.1 package-integrity regression The V2 archive was missing `internal/data/`, which caused an old `store.go` to survive when users extracted over a previous checkout. That produced compile errors such as missing `Task.GuessMinIntervalSec`, `Task.Paused`, `LiveLeaderboard`, and `TaskAction`. V2.1 explicitly includes: - `internal/data/store.go` - `internal/data/schema.sql` - the matching V2 `internal/server/server.go` Always test from a clean extraction directory. ## V2.2 zusätzliche Validierung - `node --check internal/webui/dist/app.js` erfolgreich. - `go test ./...` wurde zusätzlich gegen lokale Compile-Stubs für `chi`, `gorilla/websocket` und `modernc.org/sqlite` ausgeführt, um alle Neural-Hunt-Packages inklusive `internal/server/watermark_test.go` ohne Netzwerkzugriff zu kompilieren. - `watermark_test.go` prüft Traversal-Abwehr, SVG-Wasserzeichen und Raster-Wasserzeichen. - Die erweiterten Leaderboard-/Artifact-SQL-Abfragen wurden gegen das reale `schema.sql` mit Python `sqlite3` ausgeführt. ## V2.4 Scale smoke test ```bash node --check internal/webui/dist/app.js go test ./internal/runtime ./internal/ws ./internal/server ./cmd/loadtest # Server in Terminal 1 go run ./cmd/server # Start small, then increase. go run ./cmd/loadtest -url http://127.0.0.1:8080 -clients 250 -ramp 10s -duration 1m -max-nodes 100 go run ./cmd/loadtest -url http://127.0.0.1:8080 -clients 1000 -ramp 30s -duration 2m -max-nodes 100 ``` Im Admin unter `/admin` sollten dabei `Guess/s`, `Improve/s`, `SQLite W/s`, `WS Frames/s`, `WS MB/s`, `Drops/s`, `Goroutines` und `Heap MB` live aktualisiert werden. Bei einem stabilen Test sollten `Drops/s` nahe 0 bleiben. Ein niedriger `SQLite W/s` relativ zu `Guess/s` bestätigt, dass verlierende Tipps den DB-Hotpath nicht mehr belasten. ## V2.5.2 Admin form stability regression The periodic 3-second admin telemetry refresh must never rebuild `#settingfields`. Only explicit task/tab changes or completed admin actions may refresh the control plane. Manual regression: 1. Open `/admin` and select `ARTIFACT`. 2. Open the Provider or Quality native select and leave it open for more than 6 seconds. 3. The dropdown must remain open while Overview/Performance counters continue updating. 4. Type into Prompt-Zusatz for more than 6 seconds; caret position and textarea scroll position must not jump. 5. Switch tabs and back; the draft value must still be restored. Static/syntax checks used for this release: ```bash node --check internal/webui/dist/app.js go test ./internal/webui ./internal/core ./internal/auth ./internal/artifact ./internal/settings ``` ## RIFT Medium + OpenAI usage/cost telemetry - Start with `ARTIFACT_PRESET=raccoon_full_art_v1` and no explicit `ARTIFACT_QUALITY`: the effective quality must be `medium`. - Generate one RIFT winner artifact and verify that the OpenAI request sends `quality=medium`. - Verify that `artifact_api_usage` receives one `artifact` row per successful winner-image API call and a `character_anchor` row when the canonical anchor is created. A successful call is logged even if the provider omits its optional `usage` block; in that case token counts remain zero and `estimated_cost_usd` is NULL. - When OpenAI returns usage data, verify that `input_tokens`, text/image input split, `output_tokens`, `total_tokens`, `estimated_cost_usd`, and `pricing_basis` are persisted and mirrored in the artifact manifest/provider metadata. - In Admin → Artifact, verify **KOSTEN HEUTE**, **Ø KOSTEN PRO KARTE**, **KOSTEN PRO 1.000 KARTEN**, and the recent-call table. Anchor cost belongs to today's total but must not affect the per-card average. - Verify `GET /api/admin/artifact/usage?day_start_ms=` returns the same aggregates. ## RIFT V2.8 — per-task style references + manual anchor - In Admin → ARTIFACT, with `OPENAI_API_KEY` configured and no existing `_collection/character_anchor.png`, click **RIFT-ANCHOR JETZT ERZEUGEN**. Verify exactly one OpenAI image-generation request is made, the file is stored at `data/artifacts/_collection/character_anchor.png`, the usage row has `kind=character_anchor`, and the UI changes to `LOCKED` without an overwrite button. - Verify a second direct `POST /api/admin/artifact/character-anchor` returns HTTP 409 while the anchor exists. - In Admin → TASK ACTIONS upload a JPEG/PNG style reference. Verify it is stored content-addressed under `data/artifacts/_styles/`, `tasks.nft_style_reference` receives only the basename, and the admin preview shows the uploaded image. - Verify the public Task landing card displays `/api/public/tasks//style-reference` and marks a custom task as `TASK STYLE`; tasks without an upload display the bundled default reference. - Complete a task and verify its successor inherits `nft_style_reference`. - Generate a RIFT winner card and inspect the multipart OpenAI edit request: `image[]` must contain two files in this order: `character_anchor.png`, then the task style reference. Provider metadata should contain `reference_mode=character-plus-task-style`, the character-anchor hash and the style-reference hash. - Use **AUF DEFAULT ZURÜCK** and verify the task DB reference is empty and generation falls back to `internal/artifact/assets/style_reference.jpg` without deleting shared content-addressed style files. ## V3.1 — Admin cleanup for stale non-winner profiles - In Admin → RUNTIME set e.g. `30 Tage` and click **PRÜFEN**. Verify the preview reports only clients whose `clients.last_seen` is older than the cutoff, that are not currently connected, and that have never appeared as `tasks.winner_client_id`. - Keep an old client connected via WebSocket: it must be reported as **aktuell verbunden geschützt** and never be deleted. - Create an old winner identity: it must be reported as **Gewinner geschützt** and never be deleted, regardless of age. - Confirm deletion and verify the client row is removed together with cascading `task_points`, `client_unlocks`, and `client_task_selection` rows. - Verify a recent non-winner remains untouched. - Verify WebSocket connect and disconnect update `clients.last_seen`, so a long-running session starts its inactivity window at disconnect rather than at its original login. - The API rejects cleanup windows shorter than one hour. Relevant automated tests: `internal/data/profile_cleanup_test.go` and `internal/server/profile_cleanup_test.go`. ## V3.5 Random Guess Lottery 1. Admin → RUNTIME: `Lotterie-Zeitfenster (s)=20`, `Max. gezogene Tipps je Task/Fenster=2` setzen und speichern. 2. Mindestens drei Clients mit demselben Task verbinden und innerhalb desselben Fensters je einen Tipp absenden lassen. 3. Bis zum Fensterende müssen die Requests auf die Losziehung warten. Danach dürfen höchstens zwei Clients `Tipp gezogen & geprüft` sehen; übrige Clients sehen `Tipp diesmal nicht gezogen`. 4. Admin-Telemetrie: `Reject/s` steigt für nicht gezogene Tipps; `Guess/s` zählt nur tatsächlich gezogene/ausgewertete Tipps. 5. Mit `Max. gezogene Tipps je Task/Fenster=0` speichern: Tipps müssen wieder ohne Lotterie-Verzögerung normal verarbeitet werden. 6. Bei mehreren aktiven Tasks prüfen, dass jeder Task sein eigenes Kontingent erhält. ## V3.8 security/hardening checks The production server exposes the public app on `HTTP_ADDR` (default `:8080`) and the private control plane on `ADMIN_HTTP_ADDR` (default `:8081`). Verify that the public listener returns 404 for `/admin` and `/api/admin/settings`, while the private listener serves `/admin` and requires the HttpOnly admin session cookie. For large synthetic load tests, temporarily set `sybil_pow_bits=0` and `sybil_warmup_sec=0` from the private Admin Runtime page (or use the matching `DEFAULT_...` values on a fresh database). Otherwise the load generator intentionally pays the same new-identity proof-of-work as a real Sybil client. Useful browser checks after HTTPS proxying: ```text /public: Content-Security-Policy and X-Frame-Options: DENY are present /admin: document.cookie does not expose neuralhunt_admin_session (HttpOnly) WS: normal browser websocket connects; a foreign browser Origin is rejected ``` The OpenAI circuit breaker uses rolling windows and successful usage rows. Set very small limits in Admin Runtime to verify that a winning artifact stays `pending` with an `OpenAI cost circuit breaker active` diagnostic instead of making another provider call. Restore the desired limits afterward. ## V3.9 portable identity + owned artifact recovery 1. Start the CLI with a persistent identity path and note the printed Client-ID and file path: ```bash NEURALHUNT_IDENTITY=/tmp/nh-client/identity.json go run ./cmd/client -url http://127.0.0.1:8080 ``` 2. Run `identity` and verify the same Client-ID/path is shown. Restart with the same file and verify the Client-ID is unchanged. 3. Set a passphrase of at least 12 characters and create an encrypted browser-compatible backup: ```bash export NEURALHUNT_IDENTITY_PASSPHRASE='correct horse battery staple' go run ./cmd/client -identity /tmp/nh-client/identity.json -export /tmp/neuralhunt-browser-import.json ``` 4. In the browser choose **IDENTITÄT → IMPORT**, select that JSON, enter the same passphrase and confirm the switch. The browser Client-ID must exactly equal the CLI Client-ID. The import validates that public/private P-256 keys can sign/verify and that the optional export Client-ID matches. 5. After a win is fully rendered, **MEINE NFTS** must list the artifact and **ORIGINAL** must download the unwatermarked artifact. A different identity requesting `/api/me/artifacts//download` must receive 404. 6. In the CLI, `my-nfts` lists only the authenticated identity's ready winner artifacts and `nft original ` downloads the original. `nfts` / `nft get` remain public watermarked previews. 7. Verify legacy encrypted exports without `format/clientId/iterations` metadata still import using 250,000 PBKDF2 iterations. Focused identity tests can run without the optional network/database dependencies: ```bash go test ./cmd/client/identity.go ./cmd/client/identity_test.go node --check internal/webui/dist/app.js ``` ## V4.0 Beacon Hunt + Hosted Service smoke test ### Beacon Hunt 1. Start the game and enable `Guess Lottery Max Accepted > 0` plus `Beacon Hunt` in Admin Runtime. 2. Open two browser/CLI identities and choose different paths (`PULSE`, `FLUX`, `ORBIT`). 3. Confirm requests wait until the window closes and that `/api/public/beacon//latest` returns the recorded drand round, signature, derived randomness and boosted path. 4. Disable Beacon Hunt again and confirm the legacy `crypto/rand` lottery still works. ### Hosted reward pairing For a direct local `go run` test (not Compose), set `CS_GAME_PUBLIC_URL=http://127.0.0.1:8080` and `CS_GAME_ADMIN_URL=http://127.0.0.1:8081`. On Customer Service startup, verify the log says `Hosted-Code/Reward-Control-Plane bereit`. A warning about DNS/connection, HTTP 404, or `CUSTOMER_SERVICE_SHARED_SECRET` must be fixed before generating a one-shot code. 1. Build with `make images-compose`, then start with `docker compose --profile hosted up -d` and route 8090 publicly over HTTPS; keep 8081/8091/8092 private. 2. Create/log in to a Customer Service account. 3. Log into the normal Neural Hunt browser with the desired reward identity and click **HOSTED CODE** (or run `hosted-code` in the CLI). 4. Paste the one-shot code into the Customer Service portal. Confirm the portal shows the proven Client-ID. Reusing the same code must fail. ### Protected manual credits 1. Set `CS_ALLOW_MANUAL_CREDITS=1` only in a test/private environment. 2. Log into the Customer Service admin on private port 8091 and grant a small amount of credits. 3. Verify the customer ledger contains a positive `manual_test_grant` entry. 4. Set `CS_ALLOW_MANUAL_CREDITS=0` again and verify grants are rejected. ### Managed workers / prepaid stop 1. Create a worker and assign an active task and Beacon path. 2. Start it. The worker should create/register its own P-256 identity and the game should install `worker -> reward owner` delegation. 3. Let the worker win a test task (use a deliberately tiny range only in a private test environment). Confirm `winner_client_id` is the main reward owner while `winner_worker_client_id` is the worker identity. 4. Reduce the prepaid balance to less than one billable minute and verify Customer Service stops the worker before another paid minute is allowed. 5. Stop/start the worker and confirm its Docker identity volume preserves the same Client-ID. 6. Download the worker identity, replace it with another valid raw identity, start again and confirm a new worker Client-ID registers while reward ownership remains delegated to the same main identity. ### PayPal Sandbox 1. Keep `PAYPAL_ENVIRONMENT=sandbox` and configure sandbox client ID/secret/webhook ID. 2. Buy the smallest test package from the Customer Service portal. 3. Confirm credits are added only after a server-confirmed `COMPLETED` capture whose amount and currency match the stored package. 4. Repeat the return/capture callback or webhook and verify the `paypal:` ledger reference prevents duplicate credits. 5. Leave live mode disabled until the provider/legal review for the actual product is complete. ## V4.1 split Docker images 1. Build the three roles locally: ```bash docker buildx bake --load ``` 2. Verify the image contents/entrypoints: ```bash docker image inspect neuralhunt-server:local --format '{{json .Config.Entrypoint}}' docker image inspect neuralhunt-customer-service:local --format '{{json .Config.Entrypoint}}' docker image inspect neuralhunt-worker:local --format '{{json .Config.Entrypoint}}' ``` Expected entrypoints are `/app/neuralhunt`, `/app/neuralhunt-customer-service` and `/app/neuralhunt-client` respectively. The server image should not contain the client or Customer Service binaries. 3. For local Hosted Service testing, build/tag the worker image before starting the hosted profile: ```bash make images-compose docker compose --profile hosted up -d ``` 4. Set `CS_WORKER_IMAGE` to a deliberately missing public image with `CS_WORKER_AUTO_PULL=true`; starting a worker should cause Docker Engine to pull the configured image. Repeat with `CS_WORKER_AUTO_PULL=false`; the request must fail before prepaid credits are charged. 5. For a private registry, configure a read-only token with `CS_WORKER_REGISTRY_SERVER/USERNAME/PASSWORD`, remove the local worker image and confirm an on-demand pull succeeds. Verify the credentials do not appear in the managed worker container environment (`docker inspect`). Focused stdlib-only tests for the Docker image orchestration can be run even without the application's external Go dependencies: ```bash go test ./internal/customer/docker.go ./internal/customer/docker_test.go ``` ## V4.2.5 worker identity-volume migration check After publishing the new worker image, start a hosted worker that already owns an identity volume from an older release. It must start without `permission denied`, keep the same client identity, and remain writable at `/identity/identity.json`. Do not delete the named volume for this test. ## V4.2.6 Customer engagement/admin smoke test 1. Start Game + Customer Service with the same `CUSTOMER_SERVICE_SHARED_SECRET`. In Docker ensure the Game container has: ```env CUSTOMER_SERVICE_INTERNAL_URL=http://customer-service:8092 ``` 2. In private Customer Admin set a small **Startguthaben**, e.g. `3`, register a fresh customer and confirm its first balance is exactly `3` Credits and the compact ledger contains `Startguthaben`. 3. Set a small **Reward pro positivem Worker-Tipp**, link a main reward identity, start a Hosted Worker, and let it produce evaluated guesses. A guess earns a reward only when it raises that Worker's personal best. Lottery-rejected/non-evaluated tickets and non-improving guesses must not add credits. 4. Stop Customer Service temporarily while a Hosted Worker gets a personal-best improvement, then restart it. Confirm the Game DB's pending outbox is delivered later and the Customer ledger receives the reward exactly once. 5. Set `PAYPAL_ENABLED=false`; after reload the public portal must not show **Credits kaufen** and must not request package data as part of normal portal loading. 6. Generate enough minute/reward entries to verify the Customer credit history groups repeated rows by UTC day and reason. Confirm the raw `credit_ledger` rows still exist separately in SQLite. 7. In private Customer Admin block a test user. Existing Customer sessions must become unauthorized, all its Worker leases must be revoked/stopped, and login must return 403 until the user is unblocked. 8. Disable **Benutzer-Anmeldung erlauben**. New login attempts must receive 503; an already-authenticated, non-blocked session intentionally remains valid. Re-enable afterwards. 9. Disable **Neue Registrierungen erlauben** and confirm both challenge/registration are rejected. Re-enable and set PoW to a small test value (e.g. 8) to verify the browser solves the challenge automatically. 10. Enable **Registrierung nur mit einmaligem Invite-Code**, create an invite in Customer Admin, register one account with it, then verify the same invite cannot create a second account. For production/open registration, do not rely on PoW as a one-human/one-account guarantee. Keep Traefik/Caddy rate/concurrency limits on registration/login routes and consider invite-only registration when free signup credits have meaningful value. ## V4.2.8 Admin control / ownership smoke tests 1. Customer Admin: enable `LIMIT AUFHEBEN` for one account, create more workers than `CS_MAX_WORKERS_PER_CUSTOMER`, and verify only global inventory/running caps still stop creation/start. 2. Disable the bypass and verify the normal per-customer limits apply again. 3. Server Admin: set an active task to `AUSLAUFEND`, complete/close it, and verify there is no child task with `parent_task_id=`; if the active pool needs replacement, a new root task may appear. 4. Transfer a ready collectible to another client identity. Verify the new identity sees it under `MEINE NFTS`, the old identity no longer does, while the task's historical `winner_client_id` is unchanged. 5. Server Admin → Artifact: queue an `ADMIN NFT DROP` for a client. Verify the new `drop_*` task is `completed/pending`, artifact generation proceeds through the normal worker/circuit breaker, and the resulting collectible appears for the target identity without incrementing historical wins. ## V4.2.9 Worker recovery / image rollout smoke tests 1. Einen Hosted Worker starten und dessen Client-ID notieren. 2. Die Verbindung zwischen Worker und Game Server länger als 90 Sekunden unterbrechen. Nach Wiederherstellung muss derselbe Container/WebSocket ohne neue Identity wieder aktiv werden; bei einem abgelaufenen JWT erfolgt automatisch ein kryptografisches Re-Login. 3. Einen Task abschließen lassen und den Game Server kurz während der Successor-Erstellung/Abfrage stören. Der Worker muss den Taskwechsel weiter versuchen und darf nicht nach einem einzelnen Fehler ohne WebSocket stehen bleiben. 4. Einen Worker auf einer alten, inzwischen abgeschlossenen Task-ID neu starten. Für Hosted Worker muss der CLI auf die serverseitig ausgewählte aktive Task zurückfallen und dieselbe Identity weiterverwenden. 5. Im Customer Admin bei einem Worker `IMAGE UPDATE` ausführen. Vorher laufend -> danach wieder laufend; vorher gestoppt -> danach weiterhin gestoppt. `/identity/identity.json` und Worker-Client-ID müssen erhalten bleiben. 6. Bei einem mutable Tag (z. B. `worker_latest`) ein neues Registry-Image publizieren und `IMAGE LADEN · ALLE WORKER AKTUALISIEREN` verwenden. Docker muss den Tag erneut pullen, obwohl er lokal bereits existiert. 7. Einen Worker bewusst über Portal/Admin stoppen. Die Docker-Restart-Policy `unless-stopped` darf ihn nicht wieder starten. Bei gesperrtem Benutzer oder widerrufenem Lease muss `/internal/workers/register` mit 409 ablehnen. ## V4.3 Service Controller smoke test 1. Start Customer Service with `CS_WORKER_ORCHESTRATION_MODE=controller`, a 32+ character `SERVICE_CONTROLLER_SHARED_SECRET`, and no mounted docker.sock. 2. Start one Service Controller on a worker host with matching secret, stable `SC_ID`, `SC_MASTER_URL` pointing to Customer Service `:8092`, and `SC_ADVERTISE_URL` pointing to the controller's private `:8102` address. 3. In Customer Admin, verify the controller becomes `ONLINE` and its heartbeat/capacity appears. 4. Create/start a customer worker. Verify its `controller_id` is the registered host and the actual container exists only on that host. 5. Download/upload the worker identity from the normal Customer Portal; both operations must be proxied through the assigned controller. 6. Use per-worker `IMAGE UPDATE`; verify the image is pulled/recreated on the remote host while the same named identity volume remains. 7. Put the controller into `DRAIN`; existing workers remain controllable, but a new worker must be placed on another online controller (or fail cleanly if none exists). 8. Stop the controller process and wait longer than `CS_CONTROLLER_OFFLINE_AFTER`; Master admin must show `OFFLINE` and must not assign new workers there. 9. Use the controller emergency UI on `:8101` over VPN to stop/restart a local managed worker and to execute `ALLE STOPPEN`. 10. Verify direct mode still works unchanged with `CS_WORKER_ORCHESTRATION_MODE=direct` and local docker.sock. ## V4.4 Neural Place Zusätzlich zu den bestehenden Tests sollte für Neural Place mindestens Folgendes geprüft werden: 1. Eine echte Best-Score-Steigerung erzeugt gemäß `place_points_per_score` Place-Punkte; ein schlechterer Tipp erzeugt keine Punkte. 2. Eine Score-Steigerung eines `identity_delegations`-Workers erhöht ausschließlich das Wallet des verknüpften Owners; `place_progress_events.source_client_id` bleibt die Worker-ID. 3. Zwei Placements ziehen zweimal `place_pixel_cost` ab, erhöhen die globale Revision monoton und schreiben zwei `place_pixel_events` – auch wenn derselbe Pixel übermalt wird. 4. Ein Placement ohne ausreichendes Guthaben liefert `insufficient_place_points` und verändert weder Wallet noch Canvas-Revision. 5. `/api/public/place` liefert nach Neustart denselben Canvas-Zustand; `/api/public/place/changes?after=` holt Live-Lücken nach. 6. `/place` synchronisiert Placements über `/api/place/ws` und fällt bei Verbindungsunterbrechungen auf den HTTP-Delta-Sync zurück. 7. Ein alter Place-Teilnehmer mit verdientem Guthaben oder Placements wird von der Admin-Profilbereinigung nicht gelöscht. Der Data-Layer enthält dafür `TestPlaceWorkerProgressCreditsOwnerAndFundsPlacements`; der Server-Cleanup enthält `TestProfileCleanupProtectsPlaceParticipant`. ## V4.5 Place Reward Economy Zusätzlich zu den V4.4-Place-Tests: 1. Ein tatsächlich gezogener Lotterie-Tipp erhält exakt `place_draw_points`; derselbe Task/Revision/Client/Seq-Reward ist idempotent. 2. Im Beacon-Modus erhält ein gezogener Tipp auf dem Boost-Pfad bei aktiviertem `place_draw_beacon_multiplier` exakt `place_draw_points × effektives Beacon-Gewicht`. 3. Ein gezogener Tipp ohne Score-Verbesserung behält seinen Draw-Bonus; eine zusätzliche Score-Verbesserung erzeugt separat den normalen Progress-Reward. 4. Ein verknüpfter Hosted Worker schreibt Draw- und Zeit-Rewards in das Owner-Wallet, bleibt aber als `source_client_id` im Audit sichtbar. 5. Der erste gültige signierte Request startet nur die Aktivzeitmessung. Nach mindestens `place_time_interval_sec` gezählter aktiver Zeit werden `place_time_points` pro vollem Intervall gutgeschrieben. 6. Eine Lücke größer als `place_time_max_gap_sec` wird nicht nachträglich als aktive Zeit vergütet. 7. Doppelte/alte Sequenznummern dürfen die Aktivzeit nicht vorwärts bewegen. Nach einem Task-Reroll wird aufgrund der neuen Task-Revision eine neue Zeit-State-Reihe verwendet. 8. `POST /api/admin/place/points` akzeptiert nur vorhandene Identitäten und positive Gutschriften; Worker-Ziele werden auf den Owner aufgelöst und der Grund wird in `place_bonus_events` protokolliert. 9. `/api/place/me` führt Progress-, Draw-, Zeit- und Admin-Earnings gemeinsam chronologisch auf. 10. Werte `0` für `place_draw_points` bzw. `place_time_points` deaktivieren nur die jeweilige Reward-Quelle, ohne die übrige Place-Ökonomie zu beeinflussen. Der Data-Layer enthält dafür `TestPlaceDrawTimeAndAdminRewardsCreditDelegatedOwner`. Das Long-Idle-Verhalten der prozesslokalen Aktivzeitmessung wird zusätzlich im Server-Smoke-Test über eine Pause oberhalb von `place_time_max_gap_sec` geprüft.