docs(18.1-09): complete gap-closure tests, phase gate and release plan

This commit is contained in:
Jakub Zych
2026-10-16 13:53:57 +02:00
parent 03b8cc2770
commit f64ae79e1d

View File

@@ -0,0 +1,316 @@
---
phase: 18.1-payment-gateway-subscriptions
plan: 09
subsystem: payments
tags: [stripe, subscriptions, testing, postgres, testcontainers, phase-gate, bfcache, release, gap-closure]
status: complete
requires:
- phase: 18.1-payment-gateway-subscriptions
provides: "plan 07 (CR-01 fresh-state webhook job, WithSessionLock, SubscriptionRetriever, plan resolution, WR-06 sync), plan 08 (CR-02 strict price, WR-02/WR-03 idempotency, guard and lock, WR-01 Detached bulk action, WR-07 page restore), plan 06 (scripts/check-phase18.1.sh, release order)"
provides:
- "Named tests for every gap-closure fix of plans 07 and 08 in four repositories (framework cabana, pgsub, pgstripe, layer)"
- "scripts/check-phase18.1.sh requires the new names per stage and package; layers minimum raised to 43"
- "18.1-VALIDATION.md rows for every task of plans 07 to 09 (validated, nyquist_compliant)"
- "Layer package.json 0.2.1"
- "Release per the reply push-all-with-plugin-tags: framework v0.2.1, pgsub v0.1.0, pgstripe v0.1.0, layer v0.2.1"
affects: [18.1-verify-work, pgsub-consumers, pgstripe-consumers, layer-consumers, cabana-consumers]
actuals:
tokens: 25537 # chars/4 over the added lines of the realized diff (4 repos, tasks 1-4)
tasks: 5
commits: 12 # MEASURED per repo before this SUMMARY commit: summercms.go 7 + pgsub 1 + pgstripe 2 + layer 2
commits: 12
plan_head_before: "summercms.go f82ac0f2f5c322db3913f022d83fc45088b29a77; pgsub b6738d3f0246e586268caeb219e53ff6991790b2; pgstripe c9bd95308531320ea879a41e8e00da6a7b670d7b; layer 34cf926c4558446cd96c74fd94306b7727eb50ec"
plan_head_after: "summercms.go 03b8cc2; pgsub 2e21084; pgstripe cc80ea7; layer 22368c5 (before this SUMMARY commit and the release commits)"
tech-stack:
added: []
patterns:
- "Stripe fake failure switches (setFailRetrieve) and recorded request headers for idempotency assertions"
- "Open-transaction probe in a fake gateway hook (pg_stat_activity idle in transaction) to prove a provider call runs outside a transaction"
- "Row-lock probe from a second connection (SELECT ... FOR UPDATE NOWAIT) to prove a detached bulk action holds no lock"
key-files:
created:
- modules/cabana/bulk_detached_test.go
- ../sm-pgstripe-plugin/subscriptions/ordering_test.go
- ../sm-pgstripe-plugin/subscriptions/gateway_hardening_test.go
- ../sm-paymentgatewaysubscriptions-plugin/models/checkout_price_test.go
- ../sm-paymentgatewaysubscriptions-plugin/plan_resolution_test.go
- ../sm-paymentgatewaysubscriptions-plugin/sync_subscription_test.go
- ../sm-paymentgatewaysubscriptions-plugin/checkout_guard_test.go
- ../sm-paymentgatewaysubscriptions-plugin/admin_sync_detached_test.go
modified:
- scripts/check-phase18.1.sh
- scripts/check-phase10.sh
- modules/cabana/README.md
- ../sm-pgstripe-plugin/subscriptions/stripefake_test.go
- ../sm-pgstripe-plugin/subscriptions/decode_test.go
- ../sm-pgstripe-plugin/subscriptions/README.md
- ../sm-paymentgatewaysubscriptions-plugin/fixture_plugin_test.go
- ../sm-paymentgatewaysubscriptions-plugin/README.md
- ../vue-paymentgateway-layer/tests/subscribe.test.ts
- ../vue-paymentgateway-layer/tests/subscriptionComponents.test.ts
- ../vue-paymentgateway-layer/package.json
- ../vue-paymentgateway-layer/README.md
- .planning/phases/18.1-payment-gateway-subscriptions/18.1-VALIDATION.md
- .planning/phases/18.1-payment-gateway-subscriptions/deferred-items.md
key-decisions:
- "The framework release tag is v0.2.1, not the v0.1.27 the plan proposed: v0.1.27 and v0.2.0 already exist on origin (other sessions' releases), and tags are immovable"
- "First plugin tags v0.1.0 on pgsub and pgstripe; pgstripe requires pgsub v0.1.0; replace lines stay (sm-paymentgateway-plugin is untagged, IN-08)"
- "TestDetachedBulkAction is named outside ^TestBulkAction so plan 08's count of 13 stays meaningful"
patterns-established:
- "A gap closure's last plan names one test per review finding and the phase gate requires each by stage and package"
requirements-completed: [SC-5, D-22, CR-01, CR-02, WR-01, WR-02, WR-03, WR-04, WR-06, WR-07]
coverage:
- id: D1
description: "CR-01: the three review sequences (checkout race in either order and concurrent, deleted before created, stale cancel flag) plus retrieval failure retried and no Stripe call for duplicates or other apps"
requirement: CR-01
verification:
- kind: integration
ref: "sm-pgstripe-plugin/subscriptions/ordering_test.go#TestEventOrdering"
status: pass
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/plan_resolution_test.go#TestApplyDeletedNotStored"
status: pass
human_judgment: false
- id: D2
description: "CR-02: strict per-currency checkout price (models, gateway, API 422 before any gateway call)"
requirement: CR-02
verification:
- kind: unit
ref: "sm-paymentgatewaysubscriptions-plugin/models/checkout_price_test.go#TestCheckoutPriceID"
status: pass
- kind: integration
ref: "sm-pgstripe-plugin/subscriptions/gateway_hardening_test.go#TestGatewayStrictPrice"
status: pass
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/checkout_guard_test.go#TestCustomerAPIGuards"
status: pass
human_judgment: false
- id: D3
description: "WR-02/WR-03: first-write-wins customer id, guarded checkout (open statuses refused, concurrent checkouts of one subscriber never overlap at the gateway), idempotency keys"
requirement: WR-03
verification:
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/checkout_guard_test.go#TestCheckoutGuarded"
status: pass
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/checkout_guard_test.go#TestCustomerIDFirstWriteWins"
status: pass
- kind: integration
ref: "sm-pgstripe-plugin/subscriptions/gateway_hardening_test.go#TestGatewayIdempotencyKeys"
status: pass
- kind: unit
ref: "sm-paymentgatewaysubscriptions-plugin/models/checkout_price_test.go#TestTerminalStatuses"
status: pass
human_judgment: false
- id: D4
description: "WR-01: detached admin bulk actions (framework) and the plan sync actions run without a transaction or row lock across gateway calls"
requirement: WR-01
verification:
- kind: integration
ref: "summercms.go/modules/cabana/bulk_detached_test.go#TestDetachedBulkAction"
status: pass
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/admin_sync_detached_test.go#TestAdminSyncDetached"
status: pass
human_judgment: false
- id: D5
description: "WR-04/WR-06: plan resolution by price, stripe_prices, product and plan_code; fresh sync writes gateway-owned columns only under the lock"
requirement: WR-06
verification:
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/plan_resolution_test.go#TestPlanResolution"
status: pass
- kind: integration
ref: "sm-paymentgatewaysubscriptions-plugin/sync_subscription_test.go#TestSyncSubscriptionFresh"
status: pass
- kind: integration
ref: "sm-pgstripe-plugin/subscriptions/gateway_hardening_test.go#TestRetrieveSubscription"
status: pass
- kind: unit
ref: "sm-pgstripe-plugin/subscriptions/decode_test.go#TestDecodeSubscription"
status: pass
human_judgment: false
- id: D6
description: "WR-07: back/forward-cache restore of the subscribe and manage flows, guarded navigation, component wiring, the already-subscribed copy"
requirement: WR-07
verification:
- kind: unit
ref: "vue-paymentgateway-layer/tests/subscribe.test.ts#subscriptions: a persisted pageshow restores the subscribe flow after a redirect"
status: pass
- kind: unit
ref: "vue-paymentgateway-layer/tests/subscriptionComponents.test.ts#subscriptions: the three components wire watchPageRestore on mount and remove it on unmount"
status: pass
human_judgment: false
- id: D7
description: "Live Stripe test-mode checkout (created and updated in either order), portal cancel_at, and the browser pass including Back from Checkout"
verification: []
human_judgment: true
rationale: "Manual-only rows kept in 18.1-VALIDATION.md; they need a real Stripe test account and a browser"
duration: "plan start 2026-10-16T10:55:35Z (tasks 1-4 by the first executor, Task 5 by a continuation executor)"
completed: 2026-10-16
---
# Phase 18.1 Plan 09: Gap-closure tests, phase gate and release Summary
**Named Postgres, Docker-free and TAP tests pin every fix of plans 07 and 08 (CR-01 ordering, CR-02 strict price, WR-01 detached bulk actions, WR-02/03 guard and idempotency, WR-04/06 plan resolution and fresh sync, WR-07 page restore). The phase gate requires each test by name, and the gap closure was released as framework v0.2.1, pgsub v0.1.0, pgstripe v0.1.0 and layer v0.2.1.**
## Commits per repository (tasks 1 to 4)
| Task | summercms.go | pgsub | pgstripe | layer |
|------|--------------|-------|----------|-------|
| 1 CR-01 ordering suite (tracer) | `0fc909d` gate | | `768447b` | |
| 2 pgsub coverage | `5a446d6` gate | `2e21084` | | |
| 3 pgstripe and framework coverage | `fa91e2a` cabana test, `b6e6736` gate | | `cc80ea7` | |
| 4 layer coverage, 0.2.1, gate, VALIDATION | `3cb2252` gate, `48e2642` check-phase10 fix, `03b8cc2` VALIDATION and deferred items | | | `c535975` specs, `22368c5` 0.2.1 |
The tracer gate after Task 1 re-ran the postgres stage end to end and it passed. A mutation check (applying the event snapshot again) turned 5 of the 7 `TestEventOrdering` subtests red.
## Tests added
**Framework (cabana)**
- `TestDetachedBulkAction` (Postgres):
- a detached Run sees no transaction (`TxFromContext` false);
- another connection's `SELECT ... FOR UPDATE NOWAIT` on a selected row succeeds during Run;
- the empty, absent and partial-selection rules (affected 0 without Run, 409);
- a `ValidationError` answers 422, and a row written before it stays written;
- the same action without `Detached` still holds the transaction and the lock.
**pgsub**
- Docker-free: `TestCheckoutPriceID` (CR-02), `TestTerminalStatuses` (WR-03).
- Postgres:
- `TestPlanResolution` (WR-04): price, stripe_prices, product, plan_code (inactive plan included), the unresolved error, an existing row keeps its plan;
- `TestApplyDeletedNotStored` (CR-01): the sentinel, nil row, no event;
- `TestSyncSubscriptionFresh` (WR-06): retriever and legacy paths, gateway-owned columns only, no events, waits for a held `SubscriptionLockKey`;
- `TestCheckoutGuarded` (WR-03): the six open statuses refused without a gateway call, canceled and incomplete_expired allowed, concurrent checkouts of one subscriber never overlap, two subscribers in parallel, one customer;
- `TestCustomerIDFirstWriteWins` (WR-02);
- `TestCustomerAPIGuards` (WR-03, CR-02): 409 for past_due, unpaid, incomplete and paused with the check order unchanged, 422 for a currency without a price;
- `TestAdminSyncDetached` (WR-01): no gateway call while the session is idle in transaction, a failed save is a failure line, ids saved after the request context was cancelled.
**pgstripe**
- `TestEventOrdering` (Postgres, CR-01). Subtests:
- `checkout_race_updated_first`
- `checkout_race_concurrent`
- `deleted_before_created`
- `stale_cancel_flag_no_reactivated`
- `retrieve_error_retries`
- `duplicate_makes_no_stripe_call`
- `other_app_makes_no_stripe_call`
- Gateway tests (Postgres, for the settings):
- `TestGatewayStrictPrice` (CR-02);
- `TestGatewayIdempotencyKeys` (WR-02, WR-03);
- `TestRetrieveSubscription` (CR-01, WR-06).
- `TestDecodeSubscription` gained the price's product (string and expanded) and `plan_code`. Job arguments still hold no personal data.
- Fake: `setFailRetrieve(bool)`, and `fakeRequest` now keeps the `Idempotency-Key` header.
**Layer**
- Seven new `subscriptions:` specs, which raises the count from 36 to 43:
- a persisted pageshow restores the subscribe flow;
- a non-persisted pageshow changes nothing;
- `watchPageRestore`'s remover and its server no-op;
- a throwing navigation re-enables the button;
- the manage flow restores;
- the components' mount and unmount wiring;
- the already-subscribed copy in en and pl.
- `package.json` is 0.2.1. `pnpm-lock.yaml` is unchanged.
## Gate result
The Task 5 sheet run of `PHASE181_REQUIRE_POSTGRES=1 bash scripts/check-phase18.1.sh --all` (clean trees, then `--all`) printed `final-push-sheet-ok`:
| Stage | Result |
|-------|--------|
| self-test | PASS |
| go | 5 required framework tests |
| plugins | pgsub models 5, contracts 1, root 5; pgstripe root 4, subscriptions 3; vet and `-short` green |
| admin | vue-tsc; vitest 1429 tests in 93 files |
| layers | 112 TAP tests, 0 failures; 43 named `subscriptions:` (minimum 43) |
| docs | TestDocsTree, docs:build, OpenAPI check, dist check, phase 10 hygiene |
| postgres | framework 4; pgsub 21; pgstripe root 4, subscriptions 12 |
No suite was NOT RUN.
## VALIDATION
`18.1-VALIDATION.md` stays `status: validated`, `nyquist_compliant: true` and `wave_0_complete: true`, revalidated 2026-10-16. It has one row per task of plans 07, 08 and 09. The manual-only rows remain:
- a live test-mode checkout, now also watching created and updated in either order;
- the portal `cancel_at` check;
- the browser pass, now including Back from Checkout.
## Task 5: release decision
**Reply (verbatim):** "push-all-with-plugin-tags, v0.2.1"
The plan proposed framework tag `v0.1.27`. Since the plan was written, `v0.1.27` and `v0.2.0` were created on origin by other sessions, so the framework tag is `v0.2.1`. Tags are immovable, so the existing tags are left untouched. The plugin requires are raised to `v0.2.1` accordingly.
**Push sheet.** Every repository was 0 commits behind its origin after `git fetch origin --tags`. Unpushed commits:
- summercms.go master: 27 ahead before this SUMMARY.
- This phase's commits: plans 07 to 09.
- Other sessions' committed work, approved for this push: quick-261016-0fm (admin theme and locale preferences, the dist rebuild for v0.2.1), quick-261015-w7f (docs since v0.1.14, deployed on v0.2.0), the fast task "summercms.io Alpha 0.2 copy", and `9921768` docs(19.3-05).
- pgsub main: 7 ahead (plans 07 to 09).
- pgstripe main: 8 ahead (plans 07 to 09).
- layer main: 3 ahead (plans 08 and 09).
**Order** (plan 06's order, with plugin tags):
1. framework master, then annotated `v0.2.1` on this SUMMARY's commit;
2. pgsub: require bump to summercms `v0.2.1` (its own commit), main, annotated `v0.1.0`;
3. pgstripe: require bump to summercms `v0.2.1` and pgsub `v0.1.0` (its own commit), main, annotated `v0.1.0`;
4. layer: main, annotated `v0.2.1` on `22368c5`.
Replace lines stay in both plugins. Nothing is force-pushed. The pushed hashes and tag objects are recorded in the release record below, which is added after the pushes.
**Open item:** the layer's `scripts/check-git-install.sh` still targets `vue-journal-layer`, so an install check of layer `v0.2.1` needs that script fixed first. See `deferred-items.md`.
## Decisions Made
- Release per the reply `push-all-with-plugin-tags, v0.2.1`. The framework tag is `v0.2.1` instead of `v0.1.27`, because `v0.1.27` and `v0.2.0` already exist on origin.
- New test files per finding group, so gate names map one to one to review findings.
- The layer component specs render on the server, so the mount and unmount wiring is asserted from the component sources.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] The phase 10 hygiene allow-list missed the admin theme and locale cache**
- **Found during:** Task 4 (`check-phase18.1.sh docs` failed).
- **Issue:** `usePreferences.ts` from quick task 261016-0fm caches the theme and locale codes in localStorage. That is an accepted threat of that task (T-261016-0fm-08), but the `check-phase10.sh` hygiene allow-list was not updated, so the docs stage failed. Tokens stay forbidden.
- **Fix:** the allow-list now accepts the two preference keys.
- **Committed in:** `48e2642`.
### Adaptations
1. **The fixture `fakeGateway` gained an `onSync` hook** (pgsub `fixture_plugin_test.go`). `TestAdminSyncDetached` uses it as the open-transaction probe at the moment of the gateway call.
2. **`TestDetachedBulkAction` uses test-only actions on the existing roster fixture**, through its wrap hook. No production fixture changed, and the test names no application or provider.
3. **pnpm store-dir mismatch after the 0.2.1 bump.** The layer's `pnpm test` aborted without a TTY (`ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`). `pnpm install --frozen-lockfile --offline --store-dir /media/nvme/dev/golem15/summercms.io/summercms/.pnpm-store` fixed it with the lockfile sha256 unchanged and no package added. It is recorded in `deferred-items.md`.
4. **Pre-existing gofmt drift in `modules/cabana` token files.** These are committed files from another session and are not touched here. They are logged in `deferred-items.md`; vet and the tests pass.
5. **Framework tag `v0.2.1` instead of `v0.1.27`**, per the human's reply (see Task 5).
6. **Most coverage tasks had no failing test first.** They test behaviour plans 07 and 08 already shipped, and no production bug surfaced, so no `fix(18.1-09)` commit exists. The plan is `type: execute`, so the plan-level TDD gate does not apply.
**Total deviations:** 1 auto-fixed (Rule 3) and 6 adaptations.
**Impact on plan:** test-side plumbing and one gate script allow-list. No scope or dependency was added.
## Deferred items
| Item | Status |
|------|--------|
| Layer `scripts/check-git-install.sh` targets `vue-journal-layer` | open (blocks an install check of layer v0.2.1) |
| Review findings outside the gap closure (WR-05, IN-01 to IN-08) | open |
| gofmt drift in `modules/cabana` token files | open (another session's files) |
| pnpm store-dir mismatch in the layer | resolved for the gate; seen again and fixed locally |
## Known Stubs
None. This plan adds tests, gate entries and a version string only.
## Self-Check: PASSED
- All created test files exist in their repositories (`bulk_detached_test.go`, `ordering_test.go`, `gateway_hardening_test.go`, `checkout_price_test.go`, `plan_resolution_test.go`, `sync_subscription_test.go`, `checkout_guard_test.go`, `admin_sync_detached_test.go`).
- The commits `0fc909d`, `5a446d6`, `fa91e2a`, `b6e6736`, `3cb2252`, `48e2642`, `03b8cc2`, `2e21084`, `768447b`, `cc80ea7`, `c535975` and `22368c5` exist in their repositories.