Skip to content

feat(journeys): journey file format and prescale journeys verify - #7

Merged
pyjeebz merged 2 commits into
mainfrom
feat/journey-format
Nov 2, 2026
Merged

pyjeebz merged 2 commits into
mainfrom
feat/journey-format

Conversation

@pyjeebz

@pyjeebz pyjeebz commented Nov 2, 2026

Copy link
Copy Markdown
Owner

A journey is a user's path through the app (log in, add to cart, place an order) saved as the requests it makes, in the app's own repo as prescale/journeys/<name>.yaml. This adds the file format (version 1) and two commands: prescale journeys lists them, and prescale journeys verify <url> replays each once, as one user, naming the step that no longer answers as expected. A journey that fails with no load is stale (the app changed, or it's broken), never "broke under load". Replaying journeys at load builds on this in the next PRs.

  • Placeholders: {{name}} from an earlier step's extract (body.<field>, header.<name>), {{unique.email}} / {{unique.id}} fresh per run, {{env.NAME}} for secrets.
  • Secrets never go in the file: a literal value under a password, secret, token, API key, Authorization or Cookie key is refused, in JSON, string bodies, query strings and headers.
  • Every request stays on the app: paths are relative to the URL given (http://host/app + /api/cart), and any request or redirect to another scheme, host or port fails the step.
  • Journeys that change data run only against local hosts or a host named with --allow-writes; side_effect and destructive journeys are never loaded.
  • The example shop gains a JSON API (login → cart → order, drawing from the same pool as /checkout) and two journeys in examples/journeys/.

How it was verified

Against the real example shop:

$ SHOP_PASSWORD=demo prescale journeys verify http://localhost:8411 --dir examples/journeys
✓ checkout (checkout, 3 steps)
   1. log in  POST /api/login  200  25ms
   2. add to cart  POST /api/cart  201  4ms
   3. place order  POST /api/carts/{{cart_id}}/order  201  127ms
✓ All 2 journeys replay.                                   (exit 0)
  • Route changed (/api/cart → /api/basket in a copy): stale checkout … add to cart POST /api/basket returned 404, expected 201: {"error": "not found"}, step 3 (not run), exit 1.
  • App stopped: couldn't reach the app (ConnectError), reported as "couldn't run", not stale; exit 1.
  • SHOP_PASSWORD unset: refused before any request, exit 2. Writes against a non-local host without --allow-writes: refused, exit 2.
  • The original smoke still holds on the extended shop: "Survives ~100", connection_pool (high confidence) · culprit /checkout.
  • The wheel built from cli/ contains prescale_cli/journeys/ and commands/journeys.py.
  • ruff check cli/src: clean · pytest cli/tests -q: 285 passed (50 new).

Risk

Tier: risky · Public surface: new command prescale journeys / journeys verify (exit 0 all replay, 1 stale or unreachable, 2 invalid input or refused); journey file format v1.

Rollback: reverting removes the command and loader; this slice writes nothing to disk and uploads nothing. What a revert can't undo is files users have already committed, which is why the format is versioned and strict (unknown fields rejected), so later versions can add fields without guessing.

Fresh-context review: blocked on one real escape. A path like //other-host/x, or a rendered /{{next}} where the server returns /other-host/x, made urljoin send the request (including writes) to another host, past the --allow-writes gate. Reproduced and fixed: the loader rejects //, URLs are built by appending to the base (never urljoin), and a request hook checks the origin of every request, redirect hops included. Also fixed from the review: cross-origin redirects, rich-markup crashes on step names, numeric header values, false positives and gaps in the secrets rule, non-finite weights, and the example shop dropping connections on bad input. Each has a test.

Notes for review

Path semantics are now part of format v1: a step's path is relative to the URL you run against, including any base path.



Generated by Claude Code

Copilot AI balanced review requested due to automatic review settings November 2, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@pyjeebz
pyjeebz merged commit fc73cbb into main Nov 2, 2026
3 checks passed
@pyjeebz pyjeebz mentioned this pull request Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants