From 11b6448312e1af6ec76c94f6bdcad7c277c5fe7b Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Sat, 26 Sep 2026 02:08:20 +0100 Subject: [PATCH] docs: exploratory testing pass for payload ingestion refactor and protocol invariants (2026-09-26) --- .../2026-09-26-queue/2026-09-26-report.md | 96 ++++ .../2026-09-26-queue/driver.ts | 156 ++++++ .../2026-09-26-queue/journey-transcript.txt | 154 ++++++ .../2026-09-26-queue/run_exploratory.ts | 460 ++++++++++++++++++ docs/exploratory-testing/README.md | 1 + 5 files changed, 867 insertions(+) create mode 100644 docs/exploratory-testing/2026-09-26-queue/2026-09-26-report.md create mode 100644 docs/exploratory-testing/2026-09-26-queue/driver.ts create mode 100644 docs/exploratory-testing/2026-09-26-queue/journey-transcript.txt create mode 100644 docs/exploratory-testing/2026-09-26-queue/run_exploratory.ts diff --git a/docs/exploratory-testing/2026-09-26-queue/2026-09-26-report.md b/docs/exploratory-testing/2026-09-26-queue/2026-09-26-report.md new file mode 100644 index 0000000..94340f7 --- /dev/null +++ b/docs/exploratory-testing/2026-09-26-queue/2026-09-26-report.md @@ -0,0 +1,96 @@ +# Queue exploratory-testing report โ€” 2026-09-26 + +## Outcome + +Comprehensive automated and exploratory testing across all public HTTP endpoints, payload ingestion validation, protocol invariants, persistence contracts, capacity limits, and Docker deployment completed with **zero defects detected**. The current codebase on `main` (commit `176cddf`, including the newly extracted `src/payload.ts` ingestion module) is robust, correct, and strictly adheres to specifications and RFCs. No new bugs were found, and no issues were filed. + +## Setup and evidence + +All journeys drove the service strictly through its public HTTP interface against real `deno run --allow-all main.ts` processes and Docker containers using standard HTTP requests. No mocks, internal monkey-patching, or synthetic stubs were used. Isolated temporary directories were used for each persistence test and removed upon completion. + +- Environment: Deno 2.7.6 (`aarch64-apple-darwin`), Docker 29.4.0 (macOS 15.x / Apple Silicon). +- Base test driver utility: [driver.ts](./driver.ts) +- Comprehensive exploratory test suite: [run_exploratory.ts](./run_exploratory.ts) +- Full four-journey transcript: [journey-transcript.txt](./journey-transcript.txt) + +## Journeys exercised + +### 1. Core FIFO, Multi-Queue Isolation, Payload Fidelity & Edge Values + +- **Health check probes**: Verified `GET /health`, `HEAD /health`, and trailing slash `/health/` return `200 OK` without authentication. Confirmed that `HEAD` responses return empty bodies while preserving `Content-Type: application/json`. +- **Initial state**: Confirmed that an empty server returns `[]` on `GET /queues`. +- **Rich payload types & fidelity**: Enqueued 22 distinct payloads to queue `orders` via the newly refactored `src/payload.ts` ingestion pipeline: + - Strings: ASCII (`"order-101"`), Unicode emojis and multi-language characters (`"๐Ÿ”ฅ rocket ๐Ÿš€ รฑoรฑo ๆผขๅญ— ใ“ใ‚“ใซใกใฏ"`), empty string (`""`), multiline strings (`"line1\nline2\r\nline3"`), JSON escaped strings (`"\"quoted\" and \\backslashed\\"`). + - Numbers: zero (`0`), negative zero (`-0`), positive integers (`42`), negative integers (`-42`), floating-point values (`3.14159`), positive scientific notation (`1e5`), negative scientific notation (`1e-5`), `Number.MAX_SAFE_INTEGER + 1` (`9007199254740992`), `Number.MIN_SAFE_INTEGER - 1` (`-9007199254740992`). + - Booleans: `true` and `false`. + - Objects: empty object (`{}`), shallow objects (`{"item":"widget","count":5}`), deeply nested structures (`{"user":{"id":1,"active":true,"tags":["a","b"]}}`), and objects with `__proto__` keys. + - Arrays: empty array (`[]`) and mixed arrays (`[1, "two", false, null, {"nested": true}]`). +- **Multi-queue isolation**: Enqueued an item to a separate queue (`notifications`). Verified that `GET /queues` returned both active queues and operations on `orders` had no side-effects on `notifications`. +- **Non-mutating inspections**: Verified `GET /peek/orders`, `HEAD /peek/orders`, and `HEAD /dequeue/orders` returned `200 OK` without consuming items or altering queue length. +- **Strict FIFO ordering**: Dequeued all 22 items sequentially, asserting each item emerged in the exact order enqueued with 100% value and type fidelity. +- **Empty queue draining & pruning**: Dequeued from empty `orders`, verifying `204 No Content` with no body, and verified that `orders` was automatically pruned from `GET /queues`. Drained `notifications` and confirmed `GET /queues` returned `[]`. + +### 2. Protocol, Validation, Authentication, Limits & Error Paths + +- **Authentication enforcement**: Confirmed unauthenticated requests to protected endpoints return `401 Unauthorized` with the required `WWW-Authenticate: Bearer` challenge. Verified valid tokens authenticate regardless of scheme casing (`bearer`, `Bearer`, `BEARER`) or whitespace formatting between scheme and token (single space, multiple spaces, tabs). +- **HTTP method routing (RFC 9110 ยง15.5.6)**: Verified that sending unsupported HTTP methods returns `405 Method Not Allowed` with the exact `Allow` header matching supported methods (`POST /health` -> `Allow: GET`, `GET /enqueue/q` -> `Allow: POST`, `POST /dequeue/q` -> `Allow: GET`, etc.). +- **Route 404s**: Verified unrouted paths (`/unknown-path`, `/enqueue`, `/enqueue/`, `/dequeue/`) return `404 Not found.`. +- **Payload validation**: + - Empty body and malformed JSON return `400 Invalid JSON`. + - Non-object bodies (`123`, `[]`) or objects missing the required `"payload"` key return `400 Missing payload key`. + - Top-level `null` payloads (`{"payload":null}`) return `400 Null payload not allowed`. + - Unsafe numbers exceeding IEEE 754 precision (`9007199254740993`, `-9007199254740993`), underflow numbers (`1e-325`), and overflow numbers (`1e309` / `Infinity`) return `400 Payload contains an unsupported number`. + - Invalid UTF-8 byte sequences return `400 Invalid JSON`. +- **Nesting depth boundaries**: + - Nesting depth at exactly 3,000 container levels (`MAX_JSON_DEPTH`): accepted with `200 OK`. + - Nesting depth exceeding 3,000 levels (3,001 levels): rejected with `400 Invalid JSON`. + - Parser stack overflow (> 50,000 levels): cleanly caught and rejected with `400 Invalid JSON` (no uncaught 500 error). +- **Queue name limits**: 128 Unicode characters / 128 emojis accepted; 129 characters / 129 emojis rejected with `400 Queue name too long`. Malformed percent encodings (`%ZZ`) return `400 Invalid queue name`. +- **Body size limits**: Verified bodies of exactly 1 MB (1,048,576 bytes) are accepted with `200 OK`, while bodies exceeding 1 MB (1,048,577 bytes) are rejected with `413 Payload too large`. +- **Queue capacity limits**: + - `QUEUE_DEPTH_LIMIT=3`: Enqueued 3 items; 4th item returned `507 Queue full or too many queues`. Dequeueing 1 item freed capacity and allowed immediate re-enqueue. + - `QUEUE_COUNT_LIMIT=2`: Created 2 queues; 3rd queue returned `507 Queue full or too many queues`. Fully draining 1 queue freed capacity and allowed creating a new queue. +- **Rate limiting**: Configured `RATE_LIMIT_REQUESTS=5`; requests 1โ€“5 returned `200 OK`, request 6 returned `429 Too many requests`. Verified `/health` bypassed the rate limiter and remained responsive (`200 OK`). + +### 3. Persistence, Clean Restarts & Crash Recovery + +- **Clean shutdown and recovery (SIGTERM)**: + - Started server with `--persist` and an isolated temporary directory. + - Enqueued items across queues `p1` and `p2`, dequeued one item, and initiated a graceful `SIGTERM` shutdown. + - Verified `persist.dat` snapshot on disk contained only remaining active items (`beta` on `p1`, `gamma` on `p2`). + - Started a second clean server pointing to the same persist directory. Verified that active queues, lengths, and FIFO ordering were restored cleanly, and that draining queues left an empty snapshot. +- **Crash recovery / unclean shutdown (SIGKILL)**: + - Started server with `--persist` and an isolated directory. + - Enqueued 3 items to queue `tasks` (`task-1`, `task-2`, `task-3`) and dequeued `task-1`. + - Sent `SIGKILL` to simulate an unhandled crash / power failure without graceful `save()`. + - Verified `persist.dat` retained the raw 4-line transaction log (3 enqueues + 1 dequeue). + - Started a recovery server against the same directory with identical limits. Confirmed that `tasks` was recovered with length 2 and that subsequent dequeues returned `task-2`, `task-3`, and then `204 No Content`. +- **Corrupted persistence log recovery**: + - Injected malformed lines (invalid JSON, missing fields, corrupted event structures) alongside valid events into `persist.dat`. + - Started recovery server with `--persist`. Verified server booted without error, recovered the valid items, ignored the corrupted lines, and compacted cleanly into a valid snapshot. +- **Automatic directory creation**: + - Configured `PERSIST` to a non-existent deeply nested path (`.../nested/sub/data/`). Verified server creates the missing path and writes `persist.dat` successfully without startup failure. + +### 4. Docker Container Deployment & Volume Persistence + +- Built Docker image `queue-explore:20260926` locally from current `Dockerfile`. +- Confirmed container runs as non-root user `deno` (`uid=1993(deno)`). +- Started container with `--persist` and a dedicated named volume mounted at default `/data`. +- Enqueued payload `"persisted-in-docker"` to queue `docker-q` and received `200 OK`. +- Stopped and restarted the container (`docker stop` followed by `docker start`). +- Dequeued from `docker-q` and retrieved `"persisted-in-docker"` with `200 OK`, confirming persistent volume storage across container restarts. + +## Rejected candidates & usability observations + +- **Observation on legacy verification script**: `verify_dockerfile_hardening.sh` checks for scoped `--allow-write=` and `--allow-net=` compile flags in `Dockerfile`. Those flags were intentionally relaxed to unscoped `--allow-write` and `--allow-net` in PRs #65 and #68 to enable configurable runtime persistence directories (`PERSIST`) and configurable listening addresses/ports (`HOST`/`PORT`). The verification script is a legacy helper from earlier development and is not part of CI or documented build steps. +- **Observation on OpenAPI 400 descriptions**: In `openapi.yaml`, the 400 responses for `/dequeue`, `/peek`, and `/length` mention `Bad Request - Queue name too long`. An invalid percent-encoded sequence (`%ZZ`) also triggers a `400 Bad Request` with body `Invalid queue name`. The behavior is correct and safe, but `openapi.yaml` could document `Invalid queue name` on those endpoints. + +## Limitations and unexplored areas + +- Testing was performed on macOS APFS (`aarch64-apple-darwin`). Direct physical filesystem-full conditions during `writeSync` were not simulated at the host kernel level. +- Multi-region proxy topologies and high latency networks were not simulated. + +## Cleanup + +- All Docker containers, volumes, and temporary images created during the pass (`qx-explore-20260926`, `queue-explore-vol-20260926`, `queue-explore:20260926`) were removed. +- All temporary persistence directories (`queue-explore-persist-*`, `queue-explore-crash-*`, `queue-explore-corrupt-*`) were removed upon completion. diff --git a/docs/exploratory-testing/2026-09-26-queue/driver.ts b/docs/exploratory-testing/2026-09-26-queue/driver.ts new file mode 100644 index 0000000..a41f37d --- /dev/null +++ b/docs/exploratory-testing/2026-09-26-queue/driver.ts @@ -0,0 +1,156 @@ +const REPO_DIR = Deno.cwd(); +const TOKEN = "explore-token-20260926"; + +export type ResponseSummary = { + status: number; + body: string; + headers: Record; +}; + +export type RunningServer = { + child: Deno.ChildProcess; + persistDir: string; + port: number; + stdout: string; + stderr: string; + stdoutTask: Promise; + stderrTask: Promise; +}; + +export function compactBody(body: string): string { + if (body.length <= 240) return body; + return `${body.slice(0, 240)}โ€ฆ (${body.length} characters)`; +} + +export function selectedHeaders(headers: Headers): Record { + const selected: Record = {}; + for (const name of ["allow", "content-length", "content-type", "www-authenticate"]) { + const value = headers.get(name); + if (value !== null) selected[name] = value; + } + return selected; +} + +export async function request( + server: RunningServer, + pathname: string, + init: RequestInit = {}, +): Promise { + const response = await fetch(`http://127.0.0.1:${server.port}${pathname}`, init); + const body = await response.text(); + return { + status: response.status, + body: compactBody(body), + headers: selectedHeaders(response.headers), + }; +} + +export function authHeaders(contentType = false, customToken = TOKEN): Record { + return contentType + ? { Authorization: `Bearer ${customToken}`, "Content-Type": "application/json" } + : { Authorization: `Bearer ${customToken}` }; +} + +export function postInit(body: string | Uint8Array, customToken = TOKEN): RequestInit { + return { + method: "POST", + headers: authHeaders(true, customToken), + body, + }; +} + +export function record(journey: string, name: string, result: ResponseSummary, expectation: string): void { + console.log(JSON.stringify({ journey, name, result, expectation })); +} + +export async function startServer( + extraEnv: Record = {}, + persist = false, + persistDir = "", +): Promise { + const child = new Deno.Command(Deno.execPath(), { + args: ["run", "--allow-all", "main.ts", ...(persist ? ["--persist"] : [])], + cwd: REPO_DIR, + env: { + HOST: "127.0.0.1", + PORT: "0", + QUEUE_API_TOKEN: TOKEN, + ...(persistDir ? { PERSIST: persistDir } : {}), + ...extraEnv, + }, + stdout: "piped", + stderr: "piped", + }).spawn(); + + let stdout = ""; + let stderr = ""; + let started: (port: number) => void = () => {}; + let failed: (error: Error) => void = () => {}; + const startedPromise = new Promise((resolve, reject) => { + started = resolve; + failed = reject; + }); + + const stdoutTask = (async () => { + const reader = child.stdout.getReader(); + const decoder = new TextDecoder(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + stdout += decoder.decode(value, { stream: true }); + const match = stdout.match(/Listening on (?:http:\/\/)?(?:127\.0\.0\.1|localhost|0\.0\.0\.0):(\d+)/); + if (match) started(Number(match[1])); + } + } finally { + reader.releaseLock(); + } + })(); + + const stderrTask = (async () => { + const reader = child.stderr.getReader(); + const decoder = new TextDecoder(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + stderr += decoder.decode(value, { stream: true }); + if (!stdout.includes("Listening on") && stderr.includes("ConfigError")) { + failed(new Error(stderr)); + } + } + } finally { + reader.releaseLock(); + } + })(); + + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`startup timeout; stdout=${stdout}; stderr=${stderr}`)), 5000); + }); + + let port: number; + try { + port = await Promise.race([startedPromise, timeout]); + } catch (error) { + try { + child.kill("SIGKILL"); + } catch { + // The child may already have exited. + } + await child.status.catch(() => {}); + await Promise.allSettled([stdoutTask, stderrTask]); + throw error; + } + + return { child, persistDir, port, stdout, stderr, stdoutTask, stderrTask }; +} + +export async function stopServer(server: RunningServer, signal: Deno.Signal = "SIGTERM"): Promise { + try { + server.child.kill(signal); + } catch { + // Already exited. + } + await server.child.status.catch(() => {}); + await Promise.allSettled([server.stdoutTask, server.stderrTask]); +} diff --git a/docs/exploratory-testing/2026-09-26-queue/journey-transcript.txt b/docs/exploratory-testing/2026-09-26-queue/journey-transcript.txt new file mode 100644 index 0000000..c442130 --- /dev/null +++ b/docs/exploratory-testing/2026-09-26-queue/journey-transcript.txt @@ -0,0 +1,154 @@ +=== Starting Journey 1: Core FIFO, Multi-Queue Isolation, Payload Fidelity & Edge Values === +{"journey":"j1-fifo","name":"health-get","result":{"status":200,"body":"{\"status\":\"ok\"}","headers":{"content-length":"15","content-type":"application/json"}},"expectation":"200 OK without auth"} +{"journey":"j1-fifo","name":"health-head","result":{"status":200,"body":"","headers":{"content-type":"application/json"}},"expectation":"200 OK without auth, no body"} +{"journey":"j1-fifo","name":"health-trailing-slash","result":{"status":200,"body":"{\"status\":\"ok\"}","headers":{"content-length":"15","content-type":"application/json"}},"expectation":"200 OK without auth"} +{"journey":"j1-fifo","name":"initial-queues","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 []"} +{"journey":"j1-fifo","name":"enqueue-string-ascii","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-string-unicode","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-string-empty","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-string-newlines","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-string-json-escapes","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-zero","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-negative-zero","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-pos-int","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-neg-int","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-float","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-exp-pos","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-exp-neg","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-max-safe","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-number-min-safe","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-boolean-true","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-boolean-false","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-object-empty","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-object-simple","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-object-nested","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-object-with-proto-key","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-array-empty","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-array-mixed","result":{"status":200,"body":"Payload successfully queued onto orders.","headers":{"content-length":"40","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"enqueue-notifications","result":{"status":200,"body":"Payload successfully queued onto notifications.","headers":{"content-length":"47","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 acknowledged"} +{"journey":"j1-fifo","name":"queues-list","result":{"status":200,"body":"[\"orders\",\"notifications\"]","headers":{"content-length":"26","content-type":"application/json"}},"expectation":"200 [\"orders\", \"notifications\"]"} +{"journey":"j1-fifo","name":"length-orders","result":{"status":200,"body":"22","headers":{"content-length":"2","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 text/plain 22"} +{"journey":"j1-fifo","name":"length-notifications","result":{"status":200,"body":"1","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 text/plain 1"} +{"journey":"j1-fifo","name":"peek-orders-get","result":{"status":200,"body":"\"order-101\"","headers":{"content-length":"11","content-type":"application/json"}},"expectation":"200 \"order-101\""} +{"journey":"j1-fifo","name":"peek-orders-head","result":{"status":200,"body":"","headers":{"content-type":"application/json"}},"expectation":"200 no body"} +{"journey":"j1-fifo","name":"dequeue-orders-head","result":{"status":200,"body":"","headers":{"content-type":"application/json"}},"expectation":"200 no body (non-consuming)"} +{"journey":"j1-fifo","name":"length-orders-after-head","result":{"status":200,"body":"22","headers":{"content-length":"2","content-type":"text/plain;charset=UTF-8"}},"expectation":"still 22"} +{"journey":"j1-fifo","name":"dequeue-string-ascii","result":{"status":200,"body":"\"order-101\"","headers":{"content-length":"11","content-type":"application/json"}},"expectation":"200 matching \"order-101\""} +{"journey":"j1-fifo","name":"dequeue-string-unicode","result":{"status":200,"body":"\"๐Ÿ”ฅ rocket ๐Ÿš€ รฑoรฑo ๆผขๅญ— ใ“ใ‚“ใซใกใฏ\"","headers":{"content-length":"48","content-type":"application/json"}},"expectation":"200 matching \"๐Ÿ”ฅ rocket ๐Ÿš€ รฑoรฑo ๆผขๅญ— ใ“ใ‚“ใซใกใฏ\""} +{"journey":"j1-fifo","name":"dequeue-string-empty","result":{"status":200,"body":"\"\"","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 matching \"\""} +{"journey":"j1-fifo","name":"dequeue-string-newlines","result":{"status":200,"body":"\"line1\\nline2\\r\\nline3\"","headers":{"content-length":"23","content-type":"application/json"}},"expectation":"200 matching \"line1\\nline2\\r\\nline3\""} +{"journey":"j1-fifo","name":"dequeue-string-json-escapes","result":{"status":200,"body":"\"\\\"quoted\\\" and \\\\backslashed\\\\\"","headers":{"content-length":"32","content-type":"application/json"}},"expectation":"200 matching \"\\\"quoted\\\" and \\\\backslashed\\\\\""} +{"journey":"j1-fifo","name":"dequeue-number-zero","result":{"status":200,"body":"0","headers":{"content-length":"1","content-type":"application/json"}},"expectation":"200 matching 0"} +{"journey":"j1-fifo","name":"dequeue-number-negative-zero","result":{"status":200,"body":"0","headers":{"content-length":"1","content-type":"application/json"}},"expectation":"200 matching 0"} +{"journey":"j1-fifo","name":"dequeue-number-pos-int","result":{"status":200,"body":"42","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 matching 42"} +{"journey":"j1-fifo","name":"dequeue-number-neg-int","result":{"status":200,"body":"-42","headers":{"content-length":"3","content-type":"application/json"}},"expectation":"200 matching -42"} +{"journey":"j1-fifo","name":"dequeue-number-float","result":{"status":200,"body":"3.14159","headers":{"content-length":"7","content-type":"application/json"}},"expectation":"200 matching 3.14159"} +{"journey":"j1-fifo","name":"dequeue-number-exp-pos","result":{"status":200,"body":"100000","headers":{"content-length":"6","content-type":"application/json"}},"expectation":"200 matching 100000"} +{"journey":"j1-fifo","name":"dequeue-number-exp-neg","result":{"status":200,"body":"0.00001","headers":{"content-length":"7","content-type":"application/json"}},"expectation":"200 matching 0.00001"} +{"journey":"j1-fifo","name":"dequeue-number-max-safe","result":{"status":200,"body":"9007199254740992","headers":{"content-length":"16","content-type":"application/json"}},"expectation":"200 matching 9007199254740992"} +{"journey":"j1-fifo","name":"dequeue-number-min-safe","result":{"status":200,"body":"-9007199254740992","headers":{"content-length":"17","content-type":"application/json"}},"expectation":"200 matching -9007199254740992"} +{"journey":"j1-fifo","name":"dequeue-boolean-true","result":{"status":200,"body":"true","headers":{"content-length":"4","content-type":"application/json"}},"expectation":"200 matching true"} +{"journey":"j1-fifo","name":"dequeue-boolean-false","result":{"status":200,"body":"false","headers":{"content-length":"5","content-type":"application/json"}},"expectation":"200 matching false"} +{"journey":"j1-fifo","name":"dequeue-object-empty","result":{"status":200,"body":"{}","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 matching {}"} +{"journey":"j1-fifo","name":"dequeue-object-simple","result":{"status":200,"body":"{\"item\":\"widget\",\"count\":5}","headers":{"content-length":"27","content-type":"application/json"}},"expectation":"200 matching {\"item\":\"widget\",\"count\":5}"} +{"journey":"j1-fifo","name":"dequeue-object-nested","result":{"status":200,"body":"{\"user\":{\"id\":1,\"active\":true,\"tags\":[\"a\",\"b\"]}}","headers":{"content-length":"48","content-type":"application/json"}},"expectation":"200 matching {\"user\":{\"id\":1,\"active\":true,\"tags\":[\"a\",\"b\"]}}"} +{"journey":"j1-fifo","name":"dequeue-object-with-proto-key","result":{"status":200,"body":"{\"standard\":123}","headers":{"content-length":"16","content-type":"application/json"}},"expectation":"200 matching {\"standard\":123}"} +{"journey":"j1-fifo","name":"dequeue-array-empty","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 matching []"} +{"journey":"j1-fifo","name":"dequeue-array-mixed","result":{"status":200,"body":"[1,\"two\",false,null,{\"nested\":true}]","headers":{"content-length":"36","content-type":"application/json"}},"expectation":"200 matching [1,\"two\",false,null,{\"nested\":true}]"} +{"journey":"j1-fifo","name":"dequeue-orders-empty","result":{"status":204,"body":"","headers":{}},"expectation":"204 No Content"} +{"journey":"j1-fifo","name":"peek-orders-empty","result":{"status":204,"body":"","headers":{}},"expectation":"204 No Content"} +{"journey":"j1-fifo","name":"length-orders-empty","result":{"status":200,"body":"0","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 0"} +{"journey":"j1-fifo","name":"queues-after-orders-drained","result":{"status":200,"body":"[\"notifications\"]","headers":{"content-length":"17","content-type":"application/json"}},"expectation":"200 [\"notifications\"]"} +{"journey":"j1-fifo","name":"dequeue-notifications","result":{"status":200,"body":"\"user-registered\"","headers":{"content-length":"17","content-type":"application/json"}},"expectation":"200 \"user-registered\""} +{"journey":"j1-fifo","name":"queues-after-all-drained","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 []"} +=== Starting Journey 2: Protocol, Validation, Authentication, Limits & Error Paths === +{"journey":"j2-protocol","name":"auth-missing-header","result":{"status":401,"body":"Unauthorized","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8","www-authenticate":"Bearer"}},"expectation":"401 with WWW-Authenticate: Bearer"} +{"journey":"j2-protocol","name":"auth-invalid-scheme","result":{"status":401,"body":"Unauthorized","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8","www-authenticate":"Bearer"}},"expectation":"401 with WWW-Authenticate: Bearer"} +{"journey":"j2-protocol","name":"auth-wrong-token","result":{"status":401,"body":"Unauthorized","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8","www-authenticate":"Bearer"}},"expectation":"401 with WWW-Authenticate: Bearer"} +{"journey":"j2-protocol","name":"auth-scheme-lowercase","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"auth-scheme-uppercase","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"auth-multiple-spaces","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"auth-tab-delimited","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"mna-health-post","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"mna-queues-post","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"mna-queues-delete","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"mna-enqueue-get","result":{"status":405,"body":"Method not allowed","headers":{"allow":"POST","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: POST"} +{"journey":"j2-protocol","name":"mna-dequeue-post","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"mna-peek-post","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"mna-length-post","result":{"status":405,"body":"Method not allowed","headers":{"allow":"GET","content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"405 Allow: GET"} +{"journey":"j2-protocol","name":"not-found-unknown","result":{"status":404,"body":"Not found.","headers":{"content-length":"10","content-type":"text/plain;charset=UTF-8"}},"expectation":"404 Not found."} +{"journey":"j2-protocol","name":"not-found-enqueue-base","result":{"status":404,"body":"Not found.","headers":{"content-length":"10","content-type":"text/plain;charset=UTF-8"}},"expectation":"404 Not found."} +{"journey":"j2-protocol","name":"not-found-enqueue-slash","result":{"status":404,"body":"Not found.","headers":{"content-length":"10","content-type":"text/plain;charset=UTF-8"}},"expectation":"404 Not found."} +{"journey":"j2-protocol","name":"not-found-dequeue-slash","result":{"status":404,"body":"Not found.","headers":{"content-length":"10","content-type":"text/plain;charset=UTF-8"}},"expectation":"404 Not found."} +{"journey":"j2-protocol","name":"val-empty-body","result":{"status":400,"body":"Invalid JSON","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid JSON"} +{"journey":"j2-protocol","name":"val-malformed-json","result":{"status":400,"body":"Invalid JSON","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid JSON"} +{"journey":"j2-protocol","name":"val-missing-payload-key","result":{"status":400,"body":"Missing payload key","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Missing payload key"} +{"journey":"j2-protocol","name":"val-empty-object","result":{"status":400,"body":"Missing payload key","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Missing payload key"} +{"journey":"j2-protocol","name":"val-array-body","result":{"status":400,"body":"Missing payload key","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Missing payload key"} +{"journey":"j2-protocol","name":"val-primitive-number","result":{"status":400,"body":"Missing payload key","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Missing payload key"} +{"journey":"j2-protocol","name":"val-null-payload","result":{"status":400,"body":"Null payload not allowed","headers":{"content-length":"24","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Null payload not allowed"} +{"journey":"j2-protocol","name":"val-unsafe-number-overflow","result":{"status":400,"body":"Payload contains an unsupported number","headers":{"content-length":"38","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Payload contains an unsupported number"} +{"journey":"j2-protocol","name":"val-unsafe-number-underflow","result":{"status":400,"body":"Payload contains an unsupported number","headers":{"content-length":"38","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Payload contains an unsupported number"} +{"journey":"j2-protocol","name":"val-overflow-number","result":{"status":400,"body":"Payload contains an unsupported number","headers":{"content-length":"38","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Payload contains an unsupported number"} +{"journey":"j2-protocol","name":"val-invalid-utf8-bytes","result":{"status":400,"body":"Invalid JSON","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid JSON"} +{"journey":"j2-protocol","name":"nesting-depth-at-limit","result":{"status":200,"body":"Payload successfully queued onto val.","headers":{"content-length":"37","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"nesting-depth-exceeded","result":{"status":400,"body":"Invalid JSON","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid JSON"} +{"journey":"j2-protocol","name":"nesting-parser-overflow","result":{"status":400,"body":"Invalid JSON","headers":{"content-length":"12","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid JSON"} +{"journey":"j2-protocol","name":"val-name-128-chars","result":{"status":200,"body":"Payload successfully queued onto xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.","headers":{"content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"val-name-129-chars","result":{"status":400,"body":"Queue name too long","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Queue name too long"} +{"journey":"j2-protocol","name":"val-name-bad-percent","result":{"status":400,"body":"Invalid queue name","headers":{"content-length":"18","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Invalid queue name"} +{"journey":"j2-protocol","name":"val-name-128-emojis","result":{"status":200,"body":"Payload successfully queued onto ๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€๐Ÿš€\ud83dโ€ฆ (290 characters)","headers":{"content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"val-name-129-emojis","result":{"status":400,"body":"Queue name too long","headers":{"content-length":"19","content-type":"text/plain;charset=UTF-8"}},"expectation":"400 Queue name too long"} +{"journey":"j2-protocol","name":"body-exact-1mb","result":{"status":200,"body":"Payload successfully queued onto size.","headers":{"content-length":"38","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"body-over-1mb","result":{"status":413,"body":"Payload too large","headers":{"content-length":"17","content-type":"text/plain;charset=UTF-8"}},"expectation":"413 Payload too large"} +{"journey":"j2-protocol","name":"depth-1","result":{"status":200,"body":"Payload successfully queued onto depth-q.","headers":{"content-length":"41","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"depth-2","result":{"status":200,"body":"Payload successfully queued onto depth-q.","headers":{"content-length":"41","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"depth-3","result":{"status":200,"body":"Payload successfully queued onto depth-q.","headers":{"content-length":"41","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"depth-4-rejected","result":{"status":507,"body":"Queue full or too many queues","headers":{"content-length":"29","content-type":"text/plain;charset=UTF-8"}},"expectation":"507 Queue full or too many queues"} +{"journey":"j2-protocol","name":"depth-length-is-3","result":{"status":200,"body":"3","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 3"} +{"journey":"j2-protocol","name":"depth-dequeue-free","result":{"status":200,"body":"1","headers":{"content-length":"1","content-type":"application/json"}},"expectation":"200 1"} +{"journey":"j2-protocol","name":"depth-retry-succeeds","result":{"status":200,"body":"Payload successfully queued onto depth-q.","headers":{"content-length":"41","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK after capacity freed"} +{"journey":"j2-protocol","name":"count-q2-create","result":{"status":200,"body":"Payload successfully queued onto q2.","headers":{"content-length":"36","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j2-protocol","name":"count-q3-rejected","result":{"status":507,"body":"Queue full or too many queues","headers":{"content-length":"29","content-type":"text/plain;charset=UTF-8"}},"expectation":"507 Queue full or too many queues"} +{"journey":"j2-protocol","name":"count-q2-drain","result":{"status":200,"body":"\"item-q2\"","headers":{"content-length":"9","content-type":"application/json"}},"expectation":"200 item-q2"} +{"journey":"j2-protocol","name":"count-q3-retry-succeeds","result":{"status":200,"body":"Payload successfully queued onto q3.","headers":{"content-length":"36","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK after queue freed"} +--- Testing Rate Limiting --- +{"journey":"j2-ratelimit","name":"req-1","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-ratelimit","name":"req-2","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-ratelimit","name":"req-3","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-ratelimit","name":"req-4","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-ratelimit","name":"req-5","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 OK"} +{"journey":"j2-ratelimit","name":"req-6-blocked","result":{"status":429,"body":"Too many requests","headers":{"content-length":"17","content-type":"text/plain;charset=UTF-8"}},"expectation":"429 Too many requests"} +{"journey":"j2-ratelimit","name":"health-bypass","result":{"status":200,"body":"{\"status\":\"ok\"}","headers":{"content-length":"15","content-type":"application/json"}},"expectation":"200 OK bypasses rate limit"} +=== Starting Journey 3: Persistence, Clean Restarts & Crash Recovery === +--- Part A: Clean graceful restart --- +{"journey":"j3-persist","name":"clean-snapshot-content","result":{"status":200,"body":"{\"queue\":\"p1\",\"payload\":\"beta\",\"enqueue\":true,\"dequeue\":false}\n{\"queue\":\"p2\",\"payload\":\"gamma\",\"enqueue\":true,\"dequeue\":false}","headers":{}},"expectation":"snapshot contains only remaining items (beta on p1, gamma on p2)"} +{"journey":"j3-persist","name":"server2-queues","result":{"status":200,"body":"[\"p1\",\"p2\"]","headers":{"content-length":"11","content-type":"application/json"}},"expectation":"200 [\"p1\", \"p2\"]"} +{"journey":"j3-persist","name":"server2-p1-len","result":{"status":200,"body":"1","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 1"} +{"journey":"j3-persist","name":"server2-p2-len","result":{"status":200,"body":"1","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 1"} +{"journey":"j3-persist","name":"server2-p1-dequeue","result":{"status":200,"body":"\"beta\"","headers":{"content-length":"6","content-type":"application/json"}},"expectation":"200 \"beta\""} +{"journey":"j3-persist","name":"server2-p2-dequeue","result":{"status":200,"body":"\"gamma\"","headers":{"content-length":"7","content-type":"application/json"}},"expectation":"200 \"gamma\""} +{"journey":"j3-persist","name":"server2-queues-empty","result":{"status":200,"body":"[]","headers":{"content-length":"2","content-type":"application/json"}},"expectation":"200 []"} +--- Part B: Crash recovery (SIGKILL) --- +{"journey":"j3-persist","name":"crash-raw-log-lines","result":{"status":200,"body":"lines=4","headers":{}},"expectation":"4 lines (3 enqueues + 1 dequeue)"} +{"journey":"j3-persist","name":"recovery-queues","result":{"status":200,"body":"[\"tasks\"]","headers":{"content-length":"9","content-type":"application/json"}},"expectation":"200 [\"tasks\"]"} +{"journey":"j3-persist","name":"recovery-length","result":{"status":200,"body":"2","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 2"} +{"journey":"j3-persist","name":"recovery-deq-1","result":{"status":200,"body":"\"task-2\"","headers":{"content-length":"8","content-type":"application/json"}},"expectation":"200 \"task-2\""} +{"journey":"j3-persist","name":"recovery-deq-2","result":{"status":200,"body":"\"task-3\"","headers":{"content-length":"8","content-type":"application/json"}},"expectation":"200 \"task-3\""} +{"journey":"j3-persist","name":"recovery-deq-empty","result":{"status":204,"body":"","headers":{}},"expectation":"204 No Content"} +--- Part C: Crash recovery with corrupted lines in persist.dat --- +{"journey":"j3-persist","name":"corrupt-recovery-queues","result":{"status":200,"body":"[\"good\"]","headers":{"content-length":"8","content-type":"application/json"}},"expectation":"200 [\"good\"]"} +{"journey":"j3-persist","name":"corrupt-recovery-length","result":{"status":200,"body":"1","headers":{"content-length":"1","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 1"} +{"journey":"j3-persist","name":"corrupt-recovery-dequeue","result":{"status":200,"body":"\"item-2\"","headers":{"content-length":"8","content-type":"application/json"}},"expectation":"200 \"item-2\""} +{"journey":"j3-persist","name":"corrupt-recovery-drained","result":{"status":204,"body":"","headers":{}},"expectation":"204 No Content"} +--- Part D: Automatic directory creation --- +{"journey":"j3-persist","name":"auto-dir-enqueue","result":{"status":200,"body":"Payload successfully queued onto autodir.","headers":{"content-length":"41","content-type":"text/plain;charset=UTF-8"}},"expectation":"200 OK"} +{"journey":"j3-persist","name":"auto-dir-dequeue","result":{"status":200,"body":"\"nested\"","headers":{"content-length":"8","content-type":"application/json"}},"expectation":"200 \"nested\""} +{"journey":"j3-persist","name":"auto-dir-created-file","result":{"status":200,"body":"exists=true","headers":{}},"expectation":"persist.dat created in nested path"} +=== Starting Journey 4: Docker Container Deployment & Volume Persistence === +Building Docker image... +{"journey":"j4-docker","name":"user-id","result":{"status":200,"body":"uid=1993(deno) gid=1993(deno) groups=1993(deno)","headers":{}},"expectation":"uid=1993(deno)"} +{"journey":"j4-docker","name":"docker-health","result":{"status":200,"body":"{\"status\":\"ok\"}","headers":{}},"expectation":"200 OK"} +{"journey":"j4-docker","name":"docker-enqueue","result":{"status":200,"body":"Payload successfully queued onto docker-q.","headers":{}},"expectation":"200 OK"} +{"journey":"j4-docker","name":"docker-dequeue-after-restart","result":{"status":200,"body":"\"persisted-in-docker\"","headers":{}},"expectation":"200 \"persisted-in-docker\""} +=== All Exploratory Journeys Completed Successfully === diff --git a/docs/exploratory-testing/2026-09-26-queue/run_exploratory.ts b/docs/exploratory-testing/2026-09-26-queue/run_exploratory.ts new file mode 100644 index 0000000..950f3cd --- /dev/null +++ b/docs/exploratory-testing/2026-09-26-queue/run_exploratory.ts @@ -0,0 +1,460 @@ +import { + startServer, + stopServer, + request, + postInit, + authHeaders, + record, + RunningServer, + ResponseSummary +} from "./driver.ts"; +import { assertEquals, assert } from "jsr:@std/assert@1.0"; + +const MAX_BODY_SIZE = 1024 * 1024; // 1 MiB + +function makeExactBody(byteLength: number): string { + const prefix = '{"payload":"'; + const suffix = '"}'; + return `${prefix}${"x".repeat(byteLength - new TextEncoder().encode(prefix + suffix).byteLength)}${suffix}`; +} + +async function journey1CoreFifoAndPayloads(): Promise { + console.log("=== Starting Journey 1: Core FIFO, Multi-Queue Isolation, Payload Fidelity & Edge Values ==="); + const server = await startServer(); + try { + const auth = authHeaders(); + + // Health check probes + record("j1-fifo", "health-get", await request(server, "/health"), "200 OK without auth"); + record("j1-fifo", "health-head", await request(server, "/health", { method: "HEAD" }), "200 OK without auth, no body"); + record("j1-fifo", "health-trailing-slash", await request(server, "/health/"), "200 OK without auth"); + + // Initial state + record("j1-fifo", "initial-queues", await request(server, "/queues", { headers: auth }), "200 []"); + + // Comprehensive payload types & values + const testPayloads: [string, unknown][] = [ + ["string-ascii", "order-101"], + ["string-unicode", "๐Ÿ”ฅ rocket ๐Ÿš€ รฑoรฑo ๆผขๅญ— ใ“ใ‚“ใซใกใฏ"], + ["string-empty", ""], + ["string-newlines", "line1\nline2\r\nline3"], + ["string-json-escapes", "\"quoted\" and \\backslashed\\"], + ["number-zero", 0], + ["number-negative-zero", -0], + ["number-pos-int", 42], + ["number-neg-int", -42], + ["number-float", 3.14159], + ["number-exp-pos", 1e5], + ["number-exp-neg", 1e-5], + ["number-max-safe", 9007199254740992], + ["number-min-safe", -9007199254740992], + ["boolean-true", true], + ["boolean-false", false], + ["object-empty", {}], + ["object-simple", { item: "widget", count: 5 }], + ["object-nested", { user: { id: 1, active: true, tags: ["a", "b"] } }], + ["object-with-proto-key", { __proto__: { foo: "bar" }, standard: 123 }], + ["array-empty", []], + ["array-mixed", [1, "two", false, null, { nested: true }]], + ]; + + for (const [label, payload] of testPayloads) { + const res = await request(server, "/enqueue/orders", postInit(JSON.stringify({ payload }))); + record("j1-fifo", `enqueue-${label}`, res, "200 acknowledged"); + } + + // Second isolated queue + record("j1-fifo", "enqueue-notifications", await request(server, "/enqueue/notifications", postInit(JSON.stringify({ payload: "user-registered" }))), "200 acknowledged"); + + // Queue listing & lengths + const queuesRes = await request(server, "/queues", { headers: auth }); + record("j1-fifo", "queues-list", queuesRes, '200 ["orders", "notifications"]'); + + const ordersLen = await request(server, "/length/orders", { headers: auth }); + record("j1-fifo", "length-orders", ordersLen, `200 text/plain ${testPayloads.length}`); + + const notifLen = await request(server, "/length/notifications", { headers: auth }); + record("j1-fifo", "length-notifications", notifLen, "200 text/plain 1"); + + // Peek & HEAD behavior (non-mutating) + record("j1-fifo", "peek-orders-get", await request(server, "/peek/orders", { headers: auth }), '200 "order-101"'); + record("j1-fifo", "peek-orders-head", await request(server, "/peek/orders", { method: "HEAD", headers: auth }), "200 no body"); + record("j1-fifo", "dequeue-orders-head", await request(server, "/dequeue/orders", { method: "HEAD", headers: auth }), "200 no body (non-consuming)"); + record("j1-fifo", "length-orders-after-head", await request(server, "/length/orders", { headers: auth }), `still ${testPayloads.length}`); + + // Dequeue each in FIFO order and verify exact value fidelity + for (const [label, expected] of testPayloads) { + const deq = await request(server, "/dequeue/orders", { headers: auth }); + const expectedStr = JSON.stringify(expected); + record("j1-fifo", `dequeue-${label}`, deq, `200 matching ${expectedStr}`); + } + + // Dequeue when empty + record("j1-fifo", "dequeue-orders-empty", await request(server, "/dequeue/orders", { headers: auth }), "204 No Content"); + record("j1-fifo", "peek-orders-empty", await request(server, "/peek/orders", { headers: auth }), "204 No Content"); + record("j1-fifo", "length-orders-empty", await request(server, "/length/orders", { headers: auth }), "200 0"); + + // Verify orders automatically removed from list + record("j1-fifo", "queues-after-orders-drained", await request(server, "/queues", { headers: auth }), '200 ["notifications"]'); + + // Drain notifications + record("j1-fifo", "dequeue-notifications", await request(server, "/dequeue/notifications", { headers: auth }), '200 "user-registered"'); + record("j1-fifo", "queues-after-all-drained", await request(server, "/queues", { headers: auth }), "200 []"); + + } finally { + await stopServer(server); + } +} + +async function journey2ProtocolValidationAndLimits(): Promise { + console.log("=== Starting Journey 2: Protocol, Validation, Authentication, Limits & Error Paths ==="); + const server = await startServer({ QUEUE_DEPTH_LIMIT: "3", QUEUE_COUNT_LIMIT: "2" }); + try { + const auth = authHeaders(); + + // Authentication checks + record("j2-protocol", "auth-missing-header", await request(server, "/queues"), "401 with WWW-Authenticate: Bearer"); + record("j2-protocol", "auth-invalid-scheme", await request(server, "/queues", { headers: { Authorization: "Basic 123" } }), "401 with WWW-Authenticate: Bearer"); + record("j2-protocol", "auth-wrong-token", await request(server, "/queues", { headers: { Authorization: "Bearer wrong" } }), "401 with WWW-Authenticate: Bearer"); + record("j2-protocol", "auth-scheme-lowercase", await request(server, "/queues", { headers: { Authorization: "bearer explore-token-20260926" } }), "200 OK"); + record("j2-protocol", "auth-scheme-uppercase", await request(server, "/queues", { headers: { Authorization: "BEARER explore-token-20260926" } }), "200 OK"); + record("j2-protocol", "auth-multiple-spaces", await request(server, "/queues", { headers: { Authorization: "Bearer explore-token-20260926" } }), "200 OK"); + record("j2-protocol", "auth-tab-delimited", await request(server, "/queues", { headers: { Authorization: "Bearer\texplore-token-20260926" } }), "200 OK"); + + // Method Not Allowed & Allow headers + record("j2-protocol", "mna-health-post", await request(server, "/health", { method: "POST" }), "405 Allow: GET"); + record("j2-protocol", "mna-queues-post", await request(server, "/queues", { method: "POST", headers: auth }), "405 Allow: GET"); + record("j2-protocol", "mna-queues-delete", await request(server, "/queues", { method: "DELETE", headers: auth }), "405 Allow: GET"); + record("j2-protocol", "mna-enqueue-get", await request(server, "/enqueue/test", { headers: auth }), "405 Allow: POST"); + record("j2-protocol", "mna-dequeue-post", await request(server, "/dequeue/test", { method: "POST", headers: auth }), "405 Allow: GET"); + record("j2-protocol", "mna-peek-post", await request(server, "/peek/test", { method: "POST", headers: auth }), "405 Allow: GET"); + record("j2-protocol", "mna-length-post", await request(server, "/length/test", { method: "POST", headers: auth }), "405 Allow: GET"); + + // 404 routes + record("j2-protocol", "not-found-unknown", await request(server, "/unknown-path", { headers: auth }), "404 Not found."); + record("j2-protocol", "not-found-enqueue-base", await request(server, "/enqueue", { headers: auth }), "404 Not found."); + record("j2-protocol", "not-found-enqueue-slash", await request(server, "/enqueue/", { headers: auth }), "404 Not found."); + record("j2-protocol", "not-found-dequeue-slash", await request(server, "/dequeue/", { headers: auth }), "404 Not found."); + + // Input validation on POST /enqueue + record("j2-protocol", "val-empty-body", await request(server, "/enqueue/val", postInit("")), "400 Invalid JSON"); + record("j2-protocol", "val-malformed-json", await request(server, "/enqueue/val", postInit("{broken")), "400 Invalid JSON"); + record("j2-protocol", "val-missing-payload-key", await request(server, "/enqueue/val", postInit('{"msg":"hi"}')), "400 Missing payload key"); + record("j2-protocol", "val-empty-object", await request(server, "/enqueue/val", postInit('{}')), "400 Missing payload key"); + record("j2-protocol", "val-array-body", await request(server, "/enqueue/val", postInit('[]')), "400 Missing payload key"); + record("j2-protocol", "val-primitive-number", await request(server, "/enqueue/val", postInit('123')), "400 Missing payload key"); + record("j2-protocol", "val-null-payload", await request(server, "/enqueue/val", postInit('{"payload":null}')), "400 Null payload not allowed"); + record("j2-protocol", "val-unsafe-number-overflow", await request(server, "/enqueue/val", postInit('{"payload":9007199254740993}')), "400 Payload contains an unsupported number"); + record("j2-protocol", "val-unsafe-number-underflow", await request(server, "/enqueue/val", postInit('{"payload":1e-325}')), "400 Payload contains an unsupported number"); + record("j2-protocol", "val-overflow-number", await request(server, "/enqueue/val", postInit('{"payload":1e309}')), "400 Payload contains an unsupported number"); + + // Invalid UTF-8 bytes rejected + const invalidUtf8Bytes = new Uint8Array([ + ...new TextEncoder().encode('{"payload":"caf'), + 0xe9, + ...new TextEncoder().encode('"}'), + ]); + record("j2-protocol", "val-invalid-utf8-bytes", await request(server, "/enqueue/val", postInit(invalidUtf8Bytes)), "400 Invalid JSON"); + + // Nesting depth boundaries: 3000 levels max + const depthValid = 2999; + const nestedValid = `${"[".repeat(depthValid)}${"]".repeat(depthValid)}`; + record("j2-protocol", "nesting-depth-at-limit", await request(server, "/enqueue/val", postInit(`{"payload":${nestedValid}}`)), "200 OK"); + // Drain it + await request(server, "/dequeue/val", { headers: auth }); + + const depthExceeded = 3000; + const nestedExceeded = `${"[".repeat(depthExceeded)}${"]".repeat(depthExceeded)}`; + record("j2-protocol", "nesting-depth-exceeded", await request(server, "/enqueue/val", postInit(`{"payload":${nestedExceeded}}`)), "400 Invalid JSON"); + + const depthOverflow = 50000; + const nestedOverflow = `${"[".repeat(depthOverflow)}${"]".repeat(depthOverflow)}`; + record("j2-protocol", "nesting-parser-overflow", await request(server, "/enqueue/val", postInit(`{"payload":${nestedOverflow}}`)), "400 Invalid JSON"); + + // Queue name validation + const name128 = "x".repeat(128); + const name129 = "x".repeat(129); + record("j2-protocol", "val-name-128-chars", await request(server, `/enqueue/${name128}`, postInit('{"payload":"ok"}')), "200 OK"); + record("j2-protocol", "val-name-129-chars", await request(server, `/enqueue/${name129}`, postInit('{"payload":"too long"}')), "400 Queue name too long"); + record("j2-protocol", "val-name-bad-percent", await request(server, "/enqueue/%ZZ", postInit('{"payload":"bad"}')), "400 Invalid queue name"); + // Drain name128 so count limit isn't occupied + await request(server, `/dequeue/${name128}`, { headers: auth }); + + // Unicode queue name length (128 emojis) + const emoji128 = "๐Ÿš€".repeat(128); + const emoji129 = "๐Ÿš€".repeat(129); + record("j2-protocol", "val-name-128-emojis", await request(server, `/enqueue/${encodeURIComponent(emoji128)}`, postInit('{"payload":"rocket"}')), "200 OK"); + record("j2-protocol", "val-name-129-emojis", await request(server, `/enqueue/${encodeURIComponent(emoji129)}`, postInit('{"payload":"too many rockets"}')), "400 Queue name too long"); + await request(server, `/dequeue/${encodeURIComponent(emoji128)}`, { headers: auth }); + + // Body size limits + record("j2-protocol", "body-exact-1mb", await request(server, "/enqueue/size", postInit(makeExactBody(MAX_BODY_SIZE))), "200 OK"); + record("j2-protocol", "body-over-1mb", await request(server, "/enqueue/size", postInit(makeExactBody(MAX_BODY_SIZE + 1))), "413 Payload too large"); + await new Promise(r => setTimeout(r, 100)); + // Drain size queue + await request(server, "/dequeue/size", { headers: auth }); + + // Depth limit (QUEUE_DEPTH_LIMIT=3) + record("j2-protocol", "depth-1", await request(server, "/enqueue/depth-q", postInit('{"payload":1}')), "200 OK"); + record("j2-protocol", "depth-2", await request(server, "/enqueue/depth-q", postInit('{"payload":2}')), "200 OK"); + record("j2-protocol", "depth-3", await request(server, "/enqueue/depth-q", postInit('{"payload":3}')), "200 OK"); + record("j2-protocol", "depth-4-rejected", await request(server, "/enqueue/depth-q", postInit('{"payload":4}')), "507 Queue full or too many queues"); + record("j2-protocol", "depth-length-is-3", await request(server, "/length/depth-q", { headers: auth }), "200 3"); + // Dequeue one to free depth capacity + record("j2-protocol", "depth-dequeue-free", await request(server, "/dequeue/depth-q", { headers: auth }), "200 1"); + record("j2-protocol", "depth-retry-succeeds", await request(server, "/enqueue/depth-q", postInit('{"payload":4}')), "200 OK after capacity freed"); + + // Count limit (QUEUE_COUNT_LIMIT=2) + record("j2-protocol", "count-q2-create", await request(server, "/enqueue/q2", postInit('{"payload":"item-q2"}')), "200 OK"); + record("j2-protocol", "count-q3-rejected", await request(server, "/enqueue/q3", postInit('{"payload":"item-q3"}')), "507 Queue full or too many queues"); + record("j2-protocol", "count-q2-drain", await request(server, "/dequeue/q2", { headers: auth }), "200 item-q2"); + record("j2-protocol", "count-q3-retry-succeeds", await request(server, "/enqueue/q3", postInit('{"payload":"item-q3"}')), "200 OK after queue freed"); + + } finally { + await stopServer(server); + } + + // Rate limiting test in separate server + console.log("--- Testing Rate Limiting ---"); + const rateServer = await startServer({ RATE_LIMIT_REQUESTS: "5" }); + try { + const auth = authHeaders(); + for (let i = 1; i <= 5; i++) { + const res = await request(rateServer, "/queues", { headers: auth }); + record("j2-ratelimit", `req-${i}`, res, "200 OK"); + } + const blocked = await request(rateServer, "/queues", { headers: auth }); + record("j2-ratelimit", "req-6-blocked", blocked, "429 Too many requests"); + + // Health check should NOT be rate limited + const healthRes = await request(rateServer, "/health"); + record("j2-ratelimit", "health-bypass", healthRes, "200 OK bypasses rate limit"); + } finally { + await stopServer(rateServer); + } +} + +async function journey3PersistenceAndRestarts(): Promise { + console.log("=== Starting Journey 3: Persistence, Clean Restarts & Crash Recovery ==="); + const tempDir = await Deno.makeTempDir({ prefix: "queue-explore-persist-" }); + try { + // Part A: Clean shutdown & restart + console.log("--- Part A: Clean graceful restart ---"); + const server1 = await startServer({}, true, tempDir); + const auth = authHeaders(); + try { + await request(server1, "/enqueue/p1", postInit('{"payload":"alpha"}')); + await request(server1, "/enqueue/p1", postInit('{"payload":"beta"}')); + await request(server1, "/enqueue/p2", postInit('{"payload":"gamma"}')); + await request(server1, "/dequeue/p1", { headers: auth }); // removes alpha, leaves beta + } finally { + await stopServer(server1, "SIGTERM"); + } + + // Inspect snapshot file on disk + const snapshot1 = await Deno.readTextFile(`${tempDir}/persist.dat`); + record("j3-persist", "clean-snapshot-content", { status: 200, body: snapshot1.trim(), headers: {} }, "snapshot contains only remaining items (beta on p1, gamma on p2)"); + + // Start server2 against same dir + const server2 = await startServer({}, true, tempDir); + try { + record("j3-persist", "server2-queues", await request(server2, "/queues", { headers: auth }), '200 ["p1", "p2"]'); + record("j3-persist", "server2-p1-len", await request(server2, "/length/p1", { headers: auth }), "200 1"); + record("j3-persist", "server2-p2-len", await request(server2, "/length/p2", { headers: auth }), "200 1"); + record("j3-persist", "server2-p1-dequeue", await request(server2, "/dequeue/p1", { headers: auth }), '200 "beta"'); + record("j3-persist", "server2-p2-dequeue", await request(server2, "/dequeue/p2", { headers: auth }), '200 "gamma"'); + record("j3-persist", "server2-queues-empty", await request(server2, "/queues", { headers: auth }), "200 []"); + } finally { + await stopServer(server2, "SIGTERM"); + } + + // Part B: Crash recovery (SIGKILL) + console.log("--- Part B: Crash recovery (SIGKILL) ---"); + const crashDir = await Deno.makeTempDir({ prefix: "queue-explore-crash-" }); + try { + const crashServer = await startServer({}, true, crashDir); + try { + await request(crashServer, "/enqueue/tasks", postInit('{"payload":"task-1"}')); + await request(crashServer, "/enqueue/tasks", postInit('{"payload":"task-2"}')); + await request(crashServer, "/enqueue/tasks", postInit('{"payload":"task-3"}')); + await request(crashServer, "/dequeue/tasks", { headers: auth }); // removes task-1 + } finally { + await stopServer(crashServer, "SIGKILL"); + } + + // Inspect uncompacted event log on disk + const uncompactedLog = await Deno.readTextFile(`${crashDir}/persist.dat`); + const logLines = uncompactedLog.trim().split("\n"); + record("j3-persist", "crash-raw-log-lines", { status: 200, body: `lines=${logLines.length}`, headers: {} }, "4 lines (3 enqueues + 1 dequeue)"); + + // Start recovery server with unchanged limits + const recoveryServer = await startServer({}, true, crashDir); + try { + record("j3-persist", "recovery-queues", await request(recoveryServer, "/queues", { headers: auth }), '200 ["tasks"]'); + record("j3-persist", "recovery-length", await request(recoveryServer, "/length/tasks", { headers: auth }), "200 2"); + record("j3-persist", "recovery-deq-1", await request(recoveryServer, "/dequeue/tasks", { headers: auth }), '200 "task-2"'); + record("j3-persist", "recovery-deq-2", await request(recoveryServer, "/dequeue/tasks", { headers: auth }), '200 "task-3"'); + record("j3-persist", "recovery-deq-empty", await request(recoveryServer, "/dequeue/tasks", { headers: auth }), "204 No Content"); + } finally { + await stopServer(recoveryServer, "SIGTERM"); + } + } finally { + await Deno.remove(crashDir, { recursive: true }).catch(() => {}); + } + + // Part C: Recovery with corrupt log entries + console.log("--- Part C: Crash recovery with corrupted lines in persist.dat ---"); + const corruptDir = await Deno.makeTempDir({ prefix: "queue-explore-corrupt-" }); + try { + const corruptContent = [ + '{"queue":"good","payload":"item-1","enqueue":true,"dequeue":false}', + 'not valid json at all', + '{"queue":"good","broken":true}', + '{"queue":"good","payload":"item-2","enqueue":true,"dequeue":false}', + '{"queue":"good","payload":"ignored","enqueue":false,"dequeue":false}', + '{"queue":"good","payload":"item-1","enqueue":false,"dequeue":true}', + '', + ].join("\n"); + await Deno.writeTextFile(`${corruptDir}/persist.dat`, corruptContent); + + const corruptRecoveryServer = await startServer({}, true, corruptDir); + try { + record("j3-persist", "corrupt-recovery-queues", await request(corruptRecoveryServer, "/queues", { headers: auth }), '200 ["good"]'); + record("j3-persist", "corrupt-recovery-length", await request(corruptRecoveryServer, "/length/good", { headers: auth }), "200 1"); + record("j3-persist", "corrupt-recovery-dequeue", await request(corruptRecoveryServer, "/dequeue/good", { headers: auth }), '200 "item-2"'); + record("j3-persist", "corrupt-recovery-drained", await request(corruptRecoveryServer, "/dequeue/good", { headers: auth }), "204 No Content"); + } finally { + await stopServer(corruptRecoveryServer, "SIGTERM"); + } + } finally { + await Deno.remove(corruptDir, { recursive: true }).catch(() => {}); + } + + // Part D: Persistence directory creation when nested non-existent directory is specified + console.log("--- Part D: Automatic directory creation ---"); + const nestedNonExistent = `${tempDir}/nested/sub/data/`; + const autoDirServer = await startServer({}, true, nestedNonExistent); + try { + record("j3-persist", "auto-dir-enqueue", await request(autoDirServer, "/enqueue/autodir", postInit('{"payload":"nested"}')), "200 OK"); + record("j3-persist", "auto-dir-dequeue", await request(autoDirServer, "/dequeue/autodir", { headers: auth }), '200 "nested"'); + } finally { + await stopServer(autoDirServer, "SIGTERM"); + } + const fileExists = await Deno.stat(`${nestedNonExistent}persist.dat`).then(() => true).catch(() => false); + record("j3-persist", "auto-dir-created-file", { status: fileExists ? 200 : 500, body: `exists=${fileExists}`, headers: {} }, "persist.dat created in nested path"); + + } finally { + await Deno.remove(tempDir, { recursive: true }).catch(() => {}); + } +} + +async function journey4DockerDeployment(): Promise { + console.log("=== Starting Journey 4: Docker Container Deployment & Volume Persistence ==="); + const imageTag = "queue-explore:20260926"; + const volumeName = "queue-explore-vol-20260926"; + const containerName = "qx-explore-20260926"; + + try { + console.log("Building Docker image..."); + const buildCmd = new Deno.Command("docker", { + args: ["build", "-t", imageTag, "."], + stdout: "piped", + stderr: "piped", + }); + const buildResult = await buildCmd.output(); + if (!buildResult.success) { + console.error("Docker build failed:", new TextDecoder().decode(buildResult.stderr)); + return; + } + + // Verify non-root user + const idCmd = new Deno.Command("docker", { + args: ["run", "--rm", "--entrypoint", "id", imageTag], + stdout: "piped", + }); + const idOutput = new TextDecoder().decode((await idCmd.output()).stdout).trim(); + record("j4-docker", "user-id", { status: 200, body: idOutput, headers: {} }, "uid=1993(deno)"); + + // Run container with named volume and --persist + const runCmd = new Deno.Command("docker", { + args: [ + "run", "-d", + "--name", containerName, + "-e", "QUEUE_API_TOKEN=docker-secret-token", + "-e", "PORT=1991", + "-e", "HOST=0.0.0.0", + "-v", `${volumeName}:/data`, + "-p", "19992:1991", + imageTag, + "/usr/bin/queue", "--persist" + ], + stdout: "piped", + stderr: "piped", + }); + const runResult = await runCmd.output(); + if (!runResult.success) { + console.error("Docker run failed:", new TextDecoder().decode(runResult.stderr)); + return; + } + + // Wait for container to be ready + await new Promise(r => setTimeout(r, 2000)); + + const healthRes = await fetch("http://127.0.0.1:19992/health"); + record("j4-docker", "docker-health", { + status: healthRes.status, + body: await healthRes.text(), + headers: {}, + }, "200 OK"); + + // Enqueue an item + const enqRes = await fetch("http://127.0.0.1:19992/enqueue/docker-q", { + method: "POST", + headers: { + "Authorization": "Bearer docker-secret-token", + "Content-Type": "application/json", + }, + body: JSON.stringify({ payload: "persisted-in-docker" }), + }); + record("j4-docker", "docker-enqueue", { + status: enqRes.status, + body: await enqRes.text(), + headers: {}, + }, "200 OK"); + + // Stop container + await new Deno.Command("docker", { args: ["stop", containerName] }).output(); + + // Start container again + await new Deno.Command("docker", { args: ["start", containerName] }).output(); + await new Promise(r => setTimeout(r, 2000)); + + // Dequeue the item to confirm persistence across container restart + const deqRes = await fetch("http://127.0.0.1:19992/dequeue/docker-q", { + headers: { "Authorization": "Bearer docker-secret-token" }, + }); + record("j4-docker", "docker-dequeue-after-restart", { + status: deqRes.status, + body: await deqRes.text(), + headers: {}, + }, '200 "persisted-in-docker"'); + + } finally { + // Cleanup Docker resources + await new Deno.Command("docker", { args: ["rm", "-f", containerName] }).output().catch(() => {}); + await new Deno.Command("docker", { args: ["volume", "rm", "-f", volumeName] }).output().catch(() => {}); + await new Deno.Command("docker", { args: ["rmi", "-f", imageTag] }).output().catch(() => {}); + } +} + +async function main(): Promise { + await journey1CoreFifoAndPayloads(); + await journey2ProtocolValidationAndLimits(); + await journey3PersistenceAndRestarts(); + await journey4DockerDeployment(); + console.log("=== All Exploratory Journeys Completed Successfully ==="); +} + +if (import.meta.main) { + await main(); +} diff --git a/docs/exploratory-testing/README.md b/docs/exploratory-testing/README.md index 937f295..5e8c2c6 100644 --- a/docs/exploratory-testing/README.md +++ b/docs/exploratory-testing/README.md @@ -11,3 +11,4 @@ evidence. Replay scripts run from the repository root. | 2026-09-15 | [2026-09-15-report.md](./2026-09-15-queue/2026-09-15-report.md) | Non-ASCII token lockout (#112) | | 2026-09-18 | [2026-09-18-report.md](./2026-09-18-queue/2026-09-18-report.md) | None | | 2026-09-19 | [2026-09-19-report.md](./2026-09-19-queue/2026-09-19-report.md) | Docker `--persist` path (#114), snapshot-rewrite data loss (#115), invalid UTF-8 accepted (#116) | +| 2026-09-26 | [2026-09-26-report.md](./2026-09-26-queue/2026-09-26-report.md) | None |