Establish use-case regression collection and real browser gate
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m2s
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m2s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a10e2d-a5ef-7e92-b03d-b1694e915680
This commit is contained in:
parent
7d21dbf705
commit
a1989f19db
24 changed files with 778 additions and 8 deletions
38
.forgejo/workflows/usecases.yaml
Normal file
38
.forgejo/workflows/usecases.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Uses the fleet's ubuntu-latest container label (node:20-bookworm).
|
||||||
|
# Archive checkout follows railiance-forge's non-root-host runner guidance.
|
||||||
|
name: Use-case regression gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
usecase-regressions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Check out the triggering revision and prepare test tools
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends python3 python3-venv make ca-certificates curl
|
||||||
|
python3 -m venv /tmp/usecase-bootstrap
|
||||||
|
/tmp/usecase-bootstrap/bin/pip install uv==0.5.9
|
||||||
|
SHORT=$(printf '%s' "$GITHUB_SHA" | cut -c1-12)
|
||||||
|
mkdir -p source
|
||||||
|
curl -fsSL "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" -o /tmp/usecase-source.tar.gz
|
||||||
|
tar -xzf /tmp/usecase-source.tar.gz -C source --strip-components=1
|
||||||
|
cd source
|
||||||
|
/tmp/usecase-bootstrap/bin/uv sync --locked --python 3.12
|
||||||
|
npm ci --prefix scripts
|
||||||
|
cd scripts
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
- name: Run full application and established use-case gate
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export PATH="/tmp/usecase-bootstrap/bin:$PATH"
|
||||||
|
cd source
|
||||||
|
make check
|
||||||
|
echo "JUnit report: source/var/test-results/check.xml"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,7 +10,6 @@
|
||||||
**/*cookie*.txt
|
**/*cookie*.txt
|
||||||
docs/migration/samples/_local/
|
docs/migration/samples/_local/
|
||||||
scripts/node_modules/
|
scripts/node_modules/
|
||||||
scripts/package-lock.json
|
|
||||||
|
|
||||||
# Local env overrides
|
# Local env overrides
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
15
AGENTS.md
15
AGENTS.md
|
|
@ -246,3 +246,18 @@ To create a new workplan:
|
||||||
1. Write the file following the format above
|
1. Write the file following the format above
|
||||||
2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or
|
2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or
|
||||||
State Hub API is unavailable.
|
State Hub API is unavailable.
|
||||||
|
|
||||||
|
## Established use-case regression gate
|
||||||
|
|
||||||
|
For changes to established product behavior or its tests, run `make check` before
|
||||||
|
commit: it includes the required real-browser journeys and registry coverage
|
||||||
|
validation. `make test` is the fast iteration command and explicitly deselects
|
||||||
|
browser tests; it is not the full acceptance gate. Install locked Node/browser
|
||||||
|
prerequisites with `make setup-usecases` (see docs/usecases/README.md).
|
||||||
|
|
||||||
|
Established journeys live in docs/usecases/registry.json, with user-confirmed
|
||||||
|
behavior contracts, synthetic fixtures and mandatory fast/browser test node IDs.
|
||||||
|
Do not skip/xfail or silently remove a protected journey to pass the gate. Update
|
||||||
|
contract, mapping and tests together for intentional user-approved changes.
|
||||||
|
Never use private protocols, live accounts, production URLs or email sends in
|
||||||
|
regression tests. Automated success does not mark transfer accepted by the user.
|
||||||
|
|
|
||||||
15
Makefile
15
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: help sync migrate run test lint shell
|
.PHONY: help sync migrate run test lint shell check test-usecases test-browser setup-usecases
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
|
@ -23,3 +23,16 @@ lint: ## ruff
|
||||||
|
|
||||||
shell: ## Django shell
|
shell: ## Django shell
|
||||||
uv run manage.py shell
|
uv run manage.py shell
|
||||||
|
|
||||||
|
setup-usecases: ## Install locked Node deps and Chromium for use-case checks
|
||||||
|
npm ci --prefix scripts
|
||||||
|
cd scripts && npx playwright install chromium
|
||||||
|
|
||||||
|
test-usecases: ## Required established journeys (fast + real browser), JUnit report
|
||||||
|
uv run pytest --verify-usecases -m usecase --junitxml=var/test-results/usecases.xml
|
||||||
|
|
||||||
|
test-browser: ## Browser journeys against isolated test app
|
||||||
|
uv run pytest --verify-usecases -m browser --junitxml=var/test-results/browser.xml
|
||||||
|
|
||||||
|
check: ## Full regression gate, including every established browser journey
|
||||||
|
uv run pytest --verify-usecases -m '' --junitxml=var/test-results/check.xml
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -17,7 +17,9 @@ spaces/pages with Title/Abstract/Visual + page copy/transfer —
|
||||||
uv sync
|
uv sync
|
||||||
uv run manage.py migrate
|
uv run manage.py migrate
|
||||||
make run # http://127.0.0.1:8008
|
make run # http://127.0.0.1:8008
|
||||||
make test
|
make test # fast tests
|
||||||
|
make setup-usecases
|
||||||
|
make check # required full regression gate, including browser use cases
|
||||||
```
|
```
|
||||||
|
|
||||||
With `DEBUG` and `OIDC_ENABLED=false` (defaults), **Sign in** uses a local
|
With `DEBUG` and `OIDC_ENABLED=false` (defaults), **Sign in** uses a local
|
||||||
|
|
@ -45,3 +47,10 @@ dev-claims form. Platform OIDC: set env vars in `docs/dev.md`.
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Target Revenue Source License (TRSL V1C1). See [`LICENSE`](LICENSE).
|
Target Revenue Source License (TRSL V1C1). See [`LICENSE`](LICENSE).
|
||||||
|
|
||||||
|
## Established use cases
|
||||||
|
|
||||||
|
[Use-case collection and test infrastructure](docs/usecases/README.md) protects
|
||||||
|
user-confirmed workflows from regression, starting with ReichelAg weekly calls.
|
||||||
|
Run `make check` before accepting changes; automated success and productive
|
||||||
|
transfer acceptance remain separate.
|
||||||
|
|
|
||||||
6
SCOPE.md
6
SCOPE.md
|
|
@ -89,3 +89,9 @@ Delivery: `docs/deploy.md`; residual records: `docs/intakes/csoc-residuals.md`.
|
||||||
First user-confirmed review case: ReichelAg weekly founder-call protocols
|
First user-confirmed review case: ReichelAg weekly founder-call protocols
|
||||||
(UC-008), full duplication and manual editing plus clipboard copy for external
|
(UC-008), full duplication and manual editing plus clipboard copy for external
|
||||||
email. See docs/usecases/reichelag-weekly-call.md; live transfer is pending.
|
email. See docs/usecases/reichelag-weekly-call.md; live transfer is pending.
|
||||||
|
|
||||||
|
Established use-case regression protection is in docs/usecases/registry.json
|
||||||
|
and tests/usecases/, starting with ReichelAg. `make check` runs fast/application
|
||||||
|
and actual-browser journeys with mandatory test coverage validation and JUnit
|
||||||
|
reports. Forgejo workflow is configured; remote runner/required-merge-check
|
||||||
|
acceptance remains CSOC-IN-0005. User transfer acceptance stays separate.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
| workplan | CSOC-WP-0006 | finished | — | workplans/CSOC-WP-0006-stage-1-space-page-capabilities.md |
|
| workplan | CSOC-WP-0006 | finished | — | workplans/CSOC-WP-0006-stage-1-space-page-capabilities.md |
|
||||||
| workplan | CSOC-WP-0007 | finished | — | workplans/CSOC-WP-0007-safe-transfer-readiness.md |
|
| workplan | CSOC-WP-0007 | finished | — | workplans/CSOC-WP-0007-safe-transfer-readiness.md |
|
||||||
| workplan | CSOC-WP-0008 | active | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
| workplan | CSOC-WP-0008 | active | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
||||||
|
| workplan | CSOC-WP-0009 | finished | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md |
|
||||||
| task | CSOC-WP-0001-T01 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
| task | CSOC-WP-0001-T01 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
||||||
| task | CSOC-WP-0001-T02 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
| task | CSOC-WP-0001-T02 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
||||||
| task | CSOC-WP-0001-T03 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
| task | CSOC-WP-0001-T03 | done | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md |
|
||||||
|
|
@ -62,7 +63,12 @@
|
||||||
| task | CSOC-WP-0008-T08 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
| task | CSOC-WP-0008-T08 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
||||||
| task | CSOC-WP-0008-T09 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
| task | CSOC-WP-0008-T09 | progress | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
||||||
| task | CSOC-WP-0008-T10 | done | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
| task | CSOC-WP-0008-T10 | done | — | workplans/CSOC-WP-0008-production-transfer-acceptance.md |
|
||||||
|
| task | CSOC-WP-0009-T01 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md |
|
||||||
|
| task | CSOC-WP-0009-T02 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md |
|
||||||
|
| task | CSOC-WP-0009-T03 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md |
|
||||||
|
| task | CSOC-WP-0009-T04 | done | — | workplans/CSOC-WP-0009-usecase-regression-infrastructure.md |
|
||||||
| intake | CSOC-IN-0001 | closed | blue | docs/intakes/csoc-residuals.md |
|
| intake | CSOC-IN-0001 | closed | blue | docs/intakes/csoc-residuals.md |
|
||||||
| intake | CSOC-IN-0002 | closed | green | docs/intakes/csoc-residuals.md |
|
| intake | CSOC-IN-0002 | closed | green | docs/intakes/csoc-residuals.md |
|
||||||
| intake | CSOC-IN-0003 | open | blue | docs/intakes/csoc-residuals.md |
|
| intake | CSOC-IN-0003 | open | blue | docs/intakes/csoc-residuals.md |
|
||||||
| intake | CSOC-IN-0004 | open | green | docs/intakes/csoc-residuals.md |
|
| intake | CSOC-IN-0004 | open | green | docs/intakes/csoc-residuals.md |
|
||||||
|
| intake | CSOC-IN-0005 | open | green | docs/intakes/csoc-residuals.md |
|
||||||
|
|
|
||||||
|
|
@ -142,3 +142,38 @@ notes: |
|
||||||
Pickup: docs/session-pickup.md
|
Pickup: docs/session-pickup.md
|
||||||
state_hub_intake_id: "019ffac3-fc7d-7850-93b6-b0d289881ec7"
|
state_hub_intake_id: "019ffac3-fc7d-7850-93b6-b0d289881ec7"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSOC-IN-0005 — Activate required Forgejo use-case regression check
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: CSOC-IN-0005
|
||||||
|
kind: intake
|
||||||
|
title: "Verify Forgejo use-case runner, retain reports and require the regression check"
|
||||||
|
lane: green
|
||||||
|
status: open
|
||||||
|
priority: high
|
||||||
|
owner: coulomb-social
|
||||||
|
repo: coulomb-social
|
||||||
|
origin: residual
|
||||||
|
origin_ref: CSOC-WP-0009
|
||||||
|
description: |
|
||||||
|
Local make check and the ReichelAg fast/real-browser contract pass.
|
||||||
|
.forgejo/workflows/usecases.yaml configures main push, PR and manual runs
|
||||||
|
on the existing ubuntu-latest container label. The source SHA archive is
|
||||||
|
publicly readable (HTTP 200 checked 2026-10-06); no credential embedded.
|
||||||
|
|
||||||
|
Through the Forgejo/railiance-forge owner lane, verify repo Actions and
|
||||||
|
runner admission, Python/Node/browser dependency bootstrap and the exact
|
||||||
|
usecase-regressions job on a triggering commit. Record a successful remote
|
||||||
|
run with the commit SHA, make check result and JUnit report receipt.
|
||||||
|
Configure report retention/artifacts and make the named job a required
|
||||||
|
merge check. Confirm a failing journey prevents merge and a passing repair
|
||||||
|
unblocks it. Do not claim branch enforcement from the local gate alone.
|
||||||
|
notes: |
|
||||||
|
Infrastructure home: docs/usecases/README.md.
|
||||||
|
Runner/profile changes and protected-branch administration require the
|
||||||
|
appropriate owner path; no shared runner or production change made here.
|
||||||
|
state_hub_intake_id: "01a1110f-e2ec-736f-8209-e14425b62373"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,14 @@
|
||||||
|
# Use-case regression gate — 2026-10-06
|
||||||
|
|
||||||
|
WP-0009 establishes the use-case collection/test infrastructure for ReichelAg
|
||||||
|
UC-008. See docs/usecases/README.md and registry.json. Before accepting changes,
|
||||||
|
run `make check` (includes required real-browser journey); `make test` is fast
|
||||||
|
only. Synthetic fixtures and test DB/content are isolated from private notes.
|
||||||
|
CI runner/admission/required-check receipts remain CSOC-IN-0005; real-user
|
||||||
|
transfer/deployment remain WP-0008.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Current implementation focus — 2026-10-06
|
# Current implementation focus — 2026-10-06
|
||||||
|
|
||||||
CSOC-WP-0008 now targets the first **user-reviewed use-case transfer off Bubble**.
|
CSOC-WP-0008 now targets the first **user-reviewed use-case transfer off Bubble**.
|
||||||
|
|
|
||||||
118
docs/usecases/README.md
Normal file
118
docs/usecases/README.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Use-case collection and regression infrastructure
|
||||||
|
|
||||||
|
This collection records the jobs people actually do in coulomb.social and keeps
|
||||||
|
established behavior working while it is rebuilt and moved off Bubble.
|
||||||
|
Start with **CSOC-UC-008, ReichelAg weekly founder calls**: duplicate the full
|
||||||
|
previous protocol, edit the new agenda/notes manually, copy for external email.
|
||||||
|
|
||||||
|
## Collection
|
||||||
|
|
||||||
|
- `catalog.md`: discovery backlog, including inferred candidates.
|
||||||
|
- `registry.json`: executable index of established journeys and required tests.
|
||||||
|
- `reichelag-weekly-call.md`: user-confirmed behavior and remaining manual review.
|
||||||
|
- `first-transfer-review.md`: real-user migration acceptance walkthrough.
|
||||||
|
- `tests/usecases/fixtures/`: synthetic scenarios; never private protocol content.
|
||||||
|
|
||||||
|
Confirmation, regression protection and transfer acceptance are independent.
|
||||||
|
`established: true` means a user-confirmed behavior contract must keep passing;
|
||||||
|
it does **not** mean the use case has been accepted for productive transfer.
|
||||||
|
Only the user's review can grant that, with an explicit acceptance reference.
|
||||||
|
|
||||||
|
## Run the gate
|
||||||
|
|
||||||
|
Requires Python 3.12+, uv, Node 20+ and npm. Locked Python/Node dependencies are
|
||||||
|
tracked. First setup (Chromium OS libraries must be available):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --locked
|
||||||
|
make setup-usecases
|
||||||
|
# On a fresh Linux CI runner, install browser OS libraries too:
|
||||||
|
cd scripts && npx playwright install --with-deps chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # Fast application checks; browser tests explicitly deselected
|
||||||
|
make test-usecases # All registered journey tests, including actual browser UI
|
||||||
|
make test-browser # Only browser journeys, still validates registry coverage
|
||||||
|
make check # Required full application + established use-case/browser gate
|
||||||
|
```
|
||||||
|
|
||||||
|
`make check` is the release/change validation command. Browser tests are required,
|
||||||
|
not conditionally skipped when Node, Playwright or Chromium is absent. A missing
|
||||||
|
dependency or test mapping fails the gate. JUnit results are written under
|
||||||
|
`var/test-results/` (gitignored). The ordinary `make test` is intentionally the
|
||||||
|
fast iteration command, not proof that all established journeys still work.
|
||||||
|
|
||||||
|
The registry validates **collected node IDs before deselection**: deleting or
|
||||||
|
renaming a protected test, removing its use-case marker, or giving it the wrong
|
||||||
|
browser/fast layer fails with `Use-case regression contract failed`. Disabling a
|
||||||
|
protected check with skip/xfail is also rejected. Update mappings deliberately
|
||||||
|
when refactoring; do not delete a required behavior to turn a failure green.
|
||||||
|
|
||||||
|
## What ReichelAg protection covers
|
||||||
|
|
||||||
|
Fast test: real application POSTs, full protocol duplication, edit/source
|
||||||
|
independence, rendered clipboard controls and reader access.
|
||||||
|
|
||||||
|
Browser test: isolated Django DB and temporary content root; synthetic writer,
|
||||||
|
reader and outsider; actual sign-in/list/page/duplicate/edit/save/reopen screens;
|
||||||
|
real static clipboard script; heading/list export as HTML/plain text; blocked
|
||||||
|
clipboard manual selection; sample rich email-editor consumption; reader mutation
|
||||||
|
and outsider read denial. It checks that browser writes persist and the original
|
||||||
|
protocol remains unchanged. Unexpected external browser requests are blocked.
|
||||||
|
|
||||||
|
Clipboard writes are intercepted using native browser ClipboardItem objects;
|
||||||
|
there is no OS clipboard export of private notes, external issuer or email send.
|
||||||
|
The test uses developer sign-in in its isolated test instance. It does **not**
|
||||||
|
prove production OIDC, restart/backup recovery, actual ReichelAg corpus fidelity,
|
||||||
|
or compatibility with the user's usual mail client. Those remain WP-0008 review
|
||||||
|
and deployment tasks.
|
||||||
|
|
||||||
|
The smaller `node scripts/test-copy-content.mjs` check remains available for
|
||||||
|
clipboard component debugging; the required browser gate exercises the actual
|
||||||
|
Django screens instead of substituting a hand-built page.
|
||||||
|
|
||||||
|
## Add the next established use case
|
||||||
|
|
||||||
|
1. Give it a stable `CSOC-UC-NNN` ID. Describe actor, goal, original steps,
|
||||||
|
expected outcome, evidence confidence, privacy and migration differences.
|
||||||
|
Keep an inferred case in the catalogue until the user confirms it.
|
||||||
|
2. Write a synthetic fixture with `synthetic: true` and matching `usecase_id`.
|
||||||
|
No real business content, member identifiers, tokens or recordings in fixtures.
|
||||||
|
3. Add fast and real-browser tests with `@pytest.mark.usecase("CSOC-UC-NNN")`;
|
||||||
|
browser tests also have `@pytest.mark.browser`. Use the isolated live-server
|
||||||
|
pattern; drive visible user steps rather than recreating app markup.
|
||||||
|
4. Register exact test node IDs, narrative/fixture paths and the preserved behavior
|
||||||
|
contract in `registry.json`; run `make check`. Established tests cannot skip.
|
||||||
|
5. Separately record real-user review/transfer acceptance with its receipt.
|
||||||
|
If behavior changes intentionally, update the user-approved contract and tests
|
||||||
|
together; keep unresolved gaps in live work records.
|
||||||
|
|
||||||
|
## CI and operational boundary
|
||||||
|
|
||||||
|
`.forgejo/workflows/usecases.yaml` requests the existing fleet ubuntu-latest
|
||||||
|
container runner on main pushes, pull requests and manual dispatch. It uses the
|
||||||
|
triggering SHA archive, locked installs, browser OS dependencies and `make check`.
|
||||||
|
It neither deploys the app nor imports production data. No credentials are
|
||||||
|
embedded; archive checkout needs existing public/read admission.
|
||||||
|
|
||||||
|
Local gate success is not a remote CI receipt. Confirm the runner label can run
|
||||||
|
the job and the repository has Actions enabled; preserve reports/artifacts and
|
||||||
|
make `usecase-regressions` a required merge check through the Forgejo owner lane.
|
||||||
|
That external admission/enforcement work is tracked as **CSOC-IN-0005**. Until
|
||||||
|
confirmed, the workflow is configured and the local gate is mandatory by this
|
||||||
|
repository's documented process; protected-branch enforcement is not claimed.
|
||||||
|
|
||||||
|
## Verification receipt — 2026-10-06
|
||||||
|
|
||||||
|
- `make check`: 88 passed, including the actual ReichelAg browser journey.
|
||||||
|
- `make test-usecases` equivalent command: 2 passed (fast + browser), 86 deselected.
|
||||||
|
- JUnit XML: `var/test-results/check.xml` (88 tests, 0 failures/errors) and
|
||||||
|
`usecases.xml` (2 tests, 0 failures/errors).
|
||||||
|
- Deliberately collecting only `tests/test_reichelag_workflow.py` with
|
||||||
|
`--verify-usecases` returns exit 4 and identifies the missing browser check.
|
||||||
|
- Ruff passes changed Python infrastructure/test files; whitespace check passes.
|
||||||
|
- Remote Forgejo run and merge enforcement are not asserted; CSOC-IN-0005 owns
|
||||||
|
those receipts. Existing repository-wide lint debt remains WP-0008-T07.
|
||||||
|
|
@ -7,6 +7,10 @@ Bubble inventory/capability map; no fresh authenticated Bubble observation has
|
||||||
been performed in this session. All transfer decisions are **pending user review**. ReichelAg weekly founder
|
been performed in this session. All transfer decisions are **pending user review**. ReichelAg weekly founder
|
||||||
calls are now the first user-confirmed workflow: see reichelag-weekly-call.md.
|
calls are now the first user-confirmed workflow: see reichelag-weekly-call.md.
|
||||||
|
|
||||||
|
Executable regression protection for established cases: registry.json and
|
||||||
|
[the test infrastructure](README.md). `make check` includes required browser
|
||||||
|
journeys; candidate entries below are not all established or protected yet.
|
||||||
|
|
||||||
## Transfer workflow
|
## Transfer workflow
|
||||||
|
|
||||||
For every journey: capture a real example and original steps → describe actor,
|
For every journey: capture a real example and original steps → describe actor,
|
||||||
|
|
|
||||||
31
docs/usecases/registry.json
Normal file
31
docs/usecases/registry.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"id": "CSOC-UC-008",
|
||||||
|
"title": "ReichelAg weekly founder-call protocols",
|
||||||
|
"established": true,
|
||||||
|
"confirmation": "user-confirmed",
|
||||||
|
"transfer_status": "pending-user-review",
|
||||||
|
"narrative": "docs/usecases/reichelag-weekly-call.md",
|
||||||
|
"fixture": "tests/usecases/fixtures/reichelag-weekly-call.json",
|
||||||
|
"contract": [
|
||||||
|
"Duplicate the complete previous protocol without changing the original",
|
||||||
|
"Edit and save the next agenda/protocol by hand and reopen it",
|
||||||
|
"Copy only rendered content with headings and bullet structure for external email",
|
||||||
|
"Fall back to selected manual text when clipboard writes are denied",
|
||||||
|
"Readers may read and copy but not modify; outsiders cannot read",
|
||||||
|
"The app does not send the email or need Bubble for these steps"
|
||||||
|
],
|
||||||
|
"tests": {
|
||||||
|
"fast": ["tests/test_reichelag_workflow.py::test_full_protocol_duplicate_edit_and_clipboard_control"],
|
||||||
|
"browser": ["tests/usecases/test_reichelag_browser.py::test_weekly_call_through_real_ui"]
|
||||||
|
},
|
||||||
|
"manual_review": [
|
||||||
|
"Reconcile the latest actual ReichelAg protocol and its links/assets",
|
||||||
|
"Paste into the user's usual mail client and review presentation",
|
||||||
|
"Confirm live persistence and measured paired restore before transfer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -79,3 +79,12 @@ and reader copy visibility. New workflow test passes Ruff.
|
||||||
HTML/plain payload construction, plain-only fallback and denied/manual selection.
|
HTML/plain payload construction, plain-only fallback and denied/manual selection.
|
||||||
Clipboard writes are intercepted with synthetic content; no real clipboard or
|
Clipboard writes are intercepted with synthetic content; no real clipboard or
|
||||||
email recipient is used. This does not certify pasting into the user's mail client.
|
email recipient is used. This does not certify pasting into the user's mail client.
|
||||||
|
|
||||||
|
|
||||||
|
## Continuous regression protection
|
||||||
|
|
||||||
|
Registered in registry.json with a synthetic scenario and mandatory fast/browser
|
||||||
|
node IDs. `make check` drives the actual isolated app through duplication, manual
|
||||||
|
editing, reopen, HTML/plain/manual clipboard export and reader/outsider checks.
|
||||||
|
See README.md for setup, reports and extending protection to further cases.
|
||||||
|
This protects established behavior without marking productive transfer accepted.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,11 @@ DJANGO_SETTINGS_MODULE = "coulomb_social.settings.test"
|
||||||
python_files = ["test_*.py", "tests.py"]
|
python_files = ["test_*.py", "tests.py"]
|
||||||
python_classes = ["Test*"]
|
python_classes = ["Test*"]
|
||||||
python_functions = ["test_*"]
|
python_functions = ["test_*"]
|
||||||
addopts = "--tb=short -q"
|
addopts = "--tb=short -q -m 'not browser'"
|
||||||
|
markers = [
|
||||||
|
"usecase(id): regression protection for a registered user journey",
|
||||||
|
"browser: mandatory in make check; needs Node, Playwright and Chromium",
|
||||||
|
]
|
||||||
testpaths = ["tests", "coulomb_social"]
|
testpaths = ["tests", "coulomb_social"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|
|
||||||
57
scripts/package-lock.json
generated
Normal file
57
scripts/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "coulomb-social-scripts",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "coulomb-social-scripts",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.62.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,6 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Local tooling for auth capture and design extract (no app runtime)",
|
"description": "Local tooling for auth capture and design extract (no app runtime)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "^1.49.0"
|
"playwright": "1.62.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
141
scripts/test-usecase-browser.mjs
Normal file
141
scripts/test-usecase-browser.mjs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Real UI traversal with synthetic protocols; no production hosts or email.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const base = new URL(process.argv[2]);
|
||||||
|
if (base.protocol !== 'http:' || !['localhost', '127.0.0.1'].includes(base.hostname)) {
|
||||||
|
throw new Error('Use-case browser runner requires an isolated loopback test server');
|
||||||
|
}
|
||||||
|
const scenario = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
|
||||||
|
assert.equal(scenario.synthetic, true);
|
||||||
|
const browser = await chromium.launch({headless: true});
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
// Fail closed on unexpected integrations. Only the isolated app can be contacted.
|
||||||
|
await context.route('**/*', route => {
|
||||||
|
if (new URL(route.request().url()).origin === base.origin) return route.continue();
|
||||||
|
return route.abort();
|
||||||
|
});
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
window.clipboardMode = 'rich';
|
||||||
|
window.clipboardReceipts = [];
|
||||||
|
Object.defineProperty(navigator, 'clipboard', {configurable: true, value: {
|
||||||
|
write: async items => {
|
||||||
|
if (window.clipboardMode !== 'rich') throw new Error('Rich copy blocked');
|
||||||
|
const item = items[0];
|
||||||
|
window.clipboardReceipts.push({
|
||||||
|
html: await (await item.getType('text/html')).text(),
|
||||||
|
text: await (await item.getType('text/plain')).text(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
writeText: async text => {
|
||||||
|
if (window.clipboardMode === 'denied') throw new Error('Copy blocked');
|
||||||
|
window.clipboardReceipts.push({text});
|
||||||
|
},
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
page.setDefaultTimeout(10000);
|
||||||
|
const jsErrors = [];
|
||||||
|
page.on('pageerror', error => jsErrors.push(error.message));
|
||||||
|
const detail = `${base.origin}/app/spaces/${scenario.space_slug}/`;
|
||||||
|
const original = `${detail}?page=${scenario.previous_slug}`;
|
||||||
|
async function login(role) {
|
||||||
|
await page.goto(`${base.origin}/auth/dev-login/`);
|
||||||
|
await page.locator('input[name=subject]').fill(scenario[`${role}_subject`]);
|
||||||
|
await page.locator('input[name=tenant]').fill(scenario.tenant);
|
||||||
|
await page.getByRole('button', {name: 'Establish session'}).click();
|
||||||
|
await page.waitForURL('**/app/');
|
||||||
|
}
|
||||||
|
await login('writer');
|
||||||
|
assert.match(await page.locator('main').innerText(), new RegExp(scenario.space_title));
|
||||||
|
await page.locator(`a[href="/app/spaces/${scenario.space_slug}/"]`).click();
|
||||||
|
await page.getByRole('link', {name: scenario.previous_title, exact: true}).click();
|
||||||
|
await page.getByRole('link', {name: 'Duplicate page', exact: true}).click();
|
||||||
|
await page.getByLabel('New page slug').fill(scenario.next_slug);
|
||||||
|
await page.locator('input[name=title]').fill(scenario.next_title);
|
||||||
|
await page.getByRole('button', {name: 'Create duplicate'}).click();
|
||||||
|
await page.waitForURL(`**?page=${scenario.next_slug}`);
|
||||||
|
assert.match(await page.locator('#page-content').innerText(), /Completed point/);
|
||||||
|
await page.getByRole('link', {name: 'Edit page', exact: true}).click();
|
||||||
|
assert.equal(await page.getByLabel('Body (markdown)').inputValue(), scenario.previous_body);
|
||||||
|
await page.getByLabel('Body (markdown)').fill(scenario.next_body);
|
||||||
|
await page.getByRole('button', {name: 'Save page'}).click();
|
||||||
|
await page.waitForURL(`**?page=${scenario.next_slug}`);
|
||||||
|
await page.reload();
|
||||||
|
assert.match(await page.locator('#page-content').innerText(), /Follow-up point/);
|
||||||
|
assert.doesNotMatch(await page.locator('#page-content').innerText(), /Completed point/);
|
||||||
|
// Execute the actual static script loaded by the real detail page, not a fixture DOM.
|
||||||
|
for (const mode of ['rich', 'plain', 'denied']) {
|
||||||
|
await page.evaluate(mode => {
|
||||||
|
window.clipboardMode = mode;
|
||||||
|
window.clipboardReceipts = [];
|
||||||
|
}, mode);
|
||||||
|
await page.getByRole('button', {name: 'Copy content to clipboard'}).click();
|
||||||
|
await page.waitForFunction(() => !document.getElementById('copy-page-content').disabled);
|
||||||
|
const result = await page.evaluate(() => ({
|
||||||
|
receipts: window.clipboardReceipts,
|
||||||
|
status: document.getElementById('copy-content-status').textContent,
|
||||||
|
manual: document.getElementById('copy-content-manual').value,
|
||||||
|
hidden: document.getElementById('copy-content-manual').hidden,
|
||||||
|
selected: document.getElementById('copy-content-manual').selectionEnd,
|
||||||
|
}));
|
||||||
|
const text = mode === 'denied' ? result.manual : result.receipts[0].text;
|
||||||
|
assert.match(text, /Next weekly call/);
|
||||||
|
assert.match(text, /- Follow-up point/);
|
||||||
|
assert.match(text, / - Nested detail/);
|
||||||
|
assert.match(text, /- Another agenda item/);
|
||||||
|
assert.match(text, /New notes with emphasis/);
|
||||||
|
assert.doesNotMatch(text, /Edit page|Duplicate page|Source:|content-plane|Sign out/);
|
||||||
|
if (mode === 'denied') {
|
||||||
|
assert.equal(result.receipts.length, 0);
|
||||||
|
assert.equal(result.hidden, false);
|
||||||
|
assert.equal(result.selected, text.length);
|
||||||
|
assert.match(result.status, /Ctrl\+C or Cmd\+C/);
|
||||||
|
assert.doesNotMatch(result.status, /Content copied/);
|
||||||
|
} else {
|
||||||
|
assert.match(result.status, /Content copied/);
|
||||||
|
if (mode === 'rich') {
|
||||||
|
const html = result.receipts[0].html;
|
||||||
|
assert.match(html, /<ul>/);
|
||||||
|
assert.match(html, /<strong>emphasis<\/strong>/);
|
||||||
|
// A synthetic rich email editor can consume the same exported HTML.
|
||||||
|
const email = await context.newPage();
|
||||||
|
await email.setContent('<div id="email-draft" contenteditable="true"></div>');
|
||||||
|
await email.locator('#email-draft').evaluate((node, html) => {node.innerHTML = html;}, html);
|
||||||
|
assert.equal(await email.locator('#email-draft li').count(), 3);
|
||||||
|
assert.match(await email.locator('#email-draft').innerText(), /New notes with emphasis/);
|
||||||
|
await email.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.goto(original);
|
||||||
|
assert.match(await page.locator('#page-content').innerText(), /Completed point/);
|
||||||
|
assert.doesNotMatch(await page.locator('#page-content').innerText(), /New notes/);
|
||||||
|
await login('reader');
|
||||||
|
await page.goto(`${detail}?page=${scenario.next_slug}`);
|
||||||
|
assert.match(await page.locator('#page-content').innerText(), /New notes/);
|
||||||
|
assert.equal(await page.getByRole('link', {name: 'Edit page', exact: true}).count(), 0);
|
||||||
|
assert.equal(await page.getByRole('button', {name: 'Copy content to clipboard'}).count(), 1);
|
||||||
|
await page.getByRole('button', {name: 'Copy content to clipboard'}).click();
|
||||||
|
await page.waitForFunction(() => !document.getElementById('copy-page-content').disabled);
|
||||||
|
assert.match(await page.locator('#copy-content-status').innerText(), /Content copied/);
|
||||||
|
const csrf = await page.locator('input[name=csrfmiddlewaretoken]').first().inputValue();
|
||||||
|
const deniedPost = await page.request.post(`${detail}pages/${scenario.next_slug}/edit/`, {
|
||||||
|
form: {csrfmiddlewaretoken: csrf, title: 'Unauthorized change',
|
||||||
|
slug: scenario.next_slug, body: 'Unauthorized reader edit'},
|
||||||
|
});
|
||||||
|
assert.equal(deniedPost.status(), 403);
|
||||||
|
assert.match(await deniedPost.text(), /read-only/);
|
||||||
|
const denied = await page.goto(`${detail}pages/${scenario.next_slug}/edit/`);
|
||||||
|
assert.equal(denied.status(), 403);
|
||||||
|
await login('outsider');
|
||||||
|
const forbidden = await page.goto(`${detail}?page=${scenario.next_slug}`);
|
||||||
|
assert.equal(forbidden.status(), 404);
|
||||||
|
assert.equal(await page.locator('#page-content').count(), 0);
|
||||||
|
assert.deepEqual(jsErrors, []);
|
||||||
|
console.log(`${scenario.usecase_id}: real UI duplication/edit/reopen/clipboard/access passed`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
27
tests/conftest.py
Normal file
27
tests/conftest.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Keep registered use-case coverage present before any marker deselection."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.usecases.registry import load_cases, validate_coverage
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--verify-usecases", action="store_true",
|
||||||
|
help="Fail if established use cases lose registered test coverage")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
if not config.getoption("--verify-usecases"):
|
||||||
|
return
|
||||||
|
collected = {}
|
||||||
|
for item in items:
|
||||||
|
collected[item.nodeid] = {
|
||||||
|
"usecases": {m.args[0] for m in item.iter_markers("usecase") if m.args},
|
||||||
|
"browser": item.get_closest_marker("browser") is not None,
|
||||||
|
"disabled": any(item.get_closest_marker(m) for m in ("skip", "skipif", "xfail")),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
validate_coverage(load_cases(), collected)
|
||||||
|
except (KeyError, ValueError, TypeError, OSError) as exc:
|
||||||
|
raise pytest.UsageError(f"Use-case regression contract failed: {exc}") from exc
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""UC-008 synthetic acceptance; contains no private ReichelAg notes."""
|
"""UC-008 synthetic acceptance; contains no private ReichelAg notes."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
@ -9,6 +12,7 @@ from coulomb_social.apps.spaces.models import SpaceMembership
|
||||||
from coulomb_social.apps.spaces.services import create_space, put_space_page
|
from coulomb_social.apps.spaces.services import create_space, put_space_page
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usecase("CSOC-UC-008")
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings, client):
|
def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings, client):
|
||||||
settings.CONTENT_ROOT = str(tmp_path / "content")
|
settings.CONTENT_ROOT = str(tmp_path / "content")
|
||||||
|
|
@ -18,7 +22,9 @@ def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings,
|
||||||
issuer="https://issuer.test", subject="writer",
|
issuer="https://issuer.test", subject="writer",
|
||||||
)
|
)
|
||||||
space = create_space(member, title="Synthetic weekly calls", slug="weekly-calls")
|
space = create_space(member, title="Synthetic weekly calls", slug="weekly-calls")
|
||||||
body = "# Weekly call\n\n- Open question\n- Completed point\n\n## Decisions\n\nNotes.\n"
|
scenario = json.loads((Path(__file__).parent / "usecases/fixtures/reichelag-weekly-call.json")
|
||||||
|
.read_text(encoding="utf-8"))
|
||||||
|
body = scenario["previous_body"]
|
||||||
put_space_page(member, space, "previous-call", title="Previous call", body=body)
|
put_space_page(member, space, "previous-call", title="Previous call", body=body)
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
response = client.post(reverse("spaces:page_copy", kwargs={
|
response = client.post(reverse("spaces:page_copy", kwargs={
|
||||||
|
|
@ -27,7 +33,7 @@ def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings,
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert pageops.read_page(space.slug, "next-call").body == body
|
assert pageops.read_page(space.slug, "next-call").body == body
|
||||||
assert pageops.read_page(space.slug, "previous-call").body == body
|
assert pageops.read_page(space.slug, "previous-call").body == body
|
||||||
edited = "# Next call\n\n- Follow-up point\n\n## Protocol\n\nNew notes.\n"
|
edited = scenario["next_body"]
|
||||||
response = client.post(reverse("spaces:page_edit", kwargs={
|
response = client.post(reverse("spaces:page_edit", kwargs={
|
||||||
"slug": space.slug, "page_slug": "next-call",
|
"slug": space.slug, "page_slug": "next-call",
|
||||||
}), {"title": "Next call", "slug": "next-call", "body": edited})
|
}), {"title": "Next call", "slug": "next-call", "body": edited})
|
||||||
|
|
@ -38,7 +44,7 @@ def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings,
|
||||||
assert b"Copy content to clipboard" in response.content
|
assert b"Copy content to clipboard" in response.content
|
||||||
assert b"Duplicate page" in response.content
|
assert b"Duplicate page" in response.content
|
||||||
assert b'id="page-content"' in response.content
|
assert b'id="page-content"' in response.content
|
||||||
assert b"<li>Follow-up point</li>" in response.content
|
assert b"Follow-up point" in response.content
|
||||||
assert b"spaces/copy-content.js" in response.content
|
assert b"spaces/copy-content.js" in response.content
|
||||||
assert b"Completed point" not in response.content
|
assert b"Completed point" not in response.content
|
||||||
|
|
||||||
|
|
|
||||||
1
tests/usecases/__init__.py
Normal file
1
tests/usecases/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Established user journeys and their synthetic regression scenarios."""
|
||||||
16
tests/usecases/fixtures/reichelag-weekly-call.json
Normal file
16
tests/usecases/fixtures/reichelag-weekly-call.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"usecase_id": "CSOC-UC-008",
|
||||||
|
"synthetic": true,
|
||||||
|
"space_slug": "weekly-calls",
|
||||||
|
"space_title": "Synthetic weekly calls",
|
||||||
|
"previous_slug": "previous-call",
|
||||||
|
"previous_title": "Previous weekly call",
|
||||||
|
"previous_body": "# Weekly call\n\n- Open question\n - Follow-up detail\n- Completed point\n\n## Decisions\n\nPrevious notes.\n",
|
||||||
|
"next_slug": "next-call",
|
||||||
|
"next_title": "Next weekly call",
|
||||||
|
"next_body": "# Next weekly call\n\n- Follow-up point\n - Nested detail\n- Another agenda item\n\n## Protocol\n\nNew notes with **emphasis**.\n",
|
||||||
|
"writer_subject": "usecase-writer",
|
||||||
|
"reader_subject": "usecase-reader",
|
||||||
|
"outsider_subject": "usecase-outsider",
|
||||||
|
"tenant": "tenant:synthetic-usecases"
|
||||||
|
}
|
||||||
68
tests/usecases/registry.py
Normal file
68
tests/usecases/registry.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""Versioned journey registry and mandatory collected-test coverage validation."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
REGISTRY = ROOT / "docs/usecases/registry.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_cases():
|
||||||
|
data = json.loads(REGISTRY.read_text(encoding="utf-8"))
|
||||||
|
if data.get("schema_version") != 1 or not isinstance(data.get("cases"), list):
|
||||||
|
raise ValueError("Unsupported use-case registry schema")
|
||||||
|
cases = data["cases"]
|
||||||
|
if not cases:
|
||||||
|
raise ValueError("The use-case registry cannot be empty")
|
||||||
|
ids = set()
|
||||||
|
for case in cases:
|
||||||
|
uid = case["id"]
|
||||||
|
if uid in ids:
|
||||||
|
raise ValueError(f"Duplicate use-case id: {uid}")
|
||||||
|
ids.add(uid)
|
||||||
|
if case["confirmation"] not in {"inferred", "observed", "user-confirmed"}:
|
||||||
|
raise ValueError(f"Unknown confirmation for {uid}")
|
||||||
|
if case["transfer_status"] not in {"pending-user-review", "accepted", "deferred"}:
|
||||||
|
raise ValueError(f"Unknown transfer status for {uid}")
|
||||||
|
if case["transfer_status"] == "accepted" and not case.get("acceptance_ref"):
|
||||||
|
raise ValueError(f"Accepted case {uid} needs a user acceptance reference")
|
||||||
|
for field in ("narrative", "fixture"):
|
||||||
|
path = (ROOT / case[field]).resolve()
|
||||||
|
if not path.is_relative_to(ROOT) or not path.is_file():
|
||||||
|
raise ValueError(f"Missing or outside-repo {field} for {uid}")
|
||||||
|
fixture = json.loads((ROOT / case["fixture"]).read_text(encoding="utf-8"))
|
||||||
|
if fixture.get("synthetic") is not True or fixture.get("usecase_id") != uid:
|
||||||
|
raise ValueError(f"Fixture must be synthetic and linked to {uid}")
|
||||||
|
if not isinstance(case["established"], bool):
|
||||||
|
raise ValueError(f"established must be a boolean for {uid}")
|
||||||
|
if set(case["tests"]) != {"fast", "browser"}:
|
||||||
|
raise ValueError(f"Expected fast and browser test layers for {uid}")
|
||||||
|
if case["established"]:
|
||||||
|
if case["confirmation"] != "user-confirmed" or not case["contract"]:
|
||||||
|
raise ValueError(f"Established case {uid} needs user confirmation and contract")
|
||||||
|
for layer in ("fast", "browser"):
|
||||||
|
if not case["tests"].get(layer):
|
||||||
|
raise ValueError(f"Established case {uid} missing {layer} coverage")
|
||||||
|
return cases
|
||||||
|
|
||||||
|
|
||||||
|
def validate_coverage(cases, collected):
|
||||||
|
"""Require exact node IDs, UC marker, and correct fast/browser layer."""
|
||||||
|
ids = {case["id"] for case in cases}
|
||||||
|
for nodeid, markers in collected.items():
|
||||||
|
if markers["usecases"] - ids:
|
||||||
|
raise ValueError(f"Use-case test has an unregistered ID: {nodeid}")
|
||||||
|
for case in cases:
|
||||||
|
if not case["established"]:
|
||||||
|
continue
|
||||||
|
for layer, nodeids in case["tests"].items():
|
||||||
|
for nodeid in nodeids:
|
||||||
|
markers = collected.get(nodeid)
|
||||||
|
if markers is None:
|
||||||
|
raise ValueError(f"{case['id']} missing collected {layer} test: {nodeid}")
|
||||||
|
if case["id"] not in markers["usecases"]:
|
||||||
|
raise ValueError(f"{nodeid} is not marked for {case['id']}")
|
||||||
|
if markers.get("disabled"):
|
||||||
|
raise ValueError(f"Protected use-case test cannot skip/xfail: {nodeid}")
|
||||||
|
if markers["browser"] != (layer == "browser"):
|
||||||
|
raise ValueError(f"Wrong regression layer for {nodeid}")
|
||||||
51
tests/usecases/test_reichelag_browser.py
Normal file
51
tests/usecases/test_reichelag_browser.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""Actual browser user journey against an isolated Django live server."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from coulomb_social.apps.members.models import Member, User
|
||||||
|
from coulomb_social.apps.spaces import pageops
|
||||||
|
from coulomb_social.apps.spaces.models import SpaceMembership
|
||||||
|
from coulomb_social.apps.spaces.services import create_space, put_space_page
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SCENARIO = ROOT / "tests/usecases/fixtures/reichelag-weekly-call.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usecase("CSOC-UC-008")
|
||||||
|
@pytest.mark.browser
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
def test_weekly_call_through_real_ui(tmp_path, settings, request):
|
||||||
|
settings.DEBUG = True
|
||||||
|
settings.OIDC_ENABLED = False
|
||||||
|
settings.USER_ENGINE_BASE_URL = ""
|
||||||
|
settings.FLEX_AUTH_BASE_URL = ""
|
||||||
|
settings.FORGEJO_BASE_URL = ""
|
||||||
|
settings.CONTENT_ROOT = str(tmp_path / "content")
|
||||||
|
settings.STATIC_ROOT = str(tmp_path / "static")
|
||||||
|
scenario = json.loads(SCENARIO.read_text(encoding="utf-8"))
|
||||||
|
actors = {}
|
||||||
|
for role in ("writer", "reader", "outsider"):
|
||||||
|
subject = scenario[f"{role}_subject"]
|
||||||
|
actors[role] = Member.objects.create(
|
||||||
|
user=User.objects.create_user(username=subject), tenant_id=scenario["tenant"],
|
||||||
|
user_engine_user_id=subject, issuer="https://local.dev/issuer", subject=subject,
|
||||||
|
)
|
||||||
|
space = create_space(actors["writer"], title=scenario["space_title"],
|
||||||
|
slug=scenario["space_slug"])
|
||||||
|
put_space_page(actors["writer"], space, scenario["previous_slug"],
|
||||||
|
title=scenario["previous_title"], body=scenario["previous_body"])
|
||||||
|
SpaceMembership.objects.create(space=space, member=actors["reader"], role="viewer")
|
||||||
|
server = request.getfixturevalue("live_server")
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", str(ROOT / "scripts/test-usecase-browser.mjs"), str(server), str(SCENARIO)],
|
||||||
|
cwd=ROOT, capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
# Browser POSTs must persist the new protocol without changing the old one.
|
||||||
|
assert (pageops.read_page(space.slug, scenario["previous_slug"]).body
|
||||||
|
== scenario["previous_body"])
|
||||||
|
assert pageops.read_page(space.slug, scenario["next_slug"]).body == scenario["next_body"]
|
||||||
105
workplans/CSOC-WP-0009-usecase-regression-infrastructure.md
Normal file
105
workplans/CSOC-WP-0009-usecase-regression-infrastructure.md
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
id: CSOC-WP-0009
|
||||||
|
type: workplan
|
||||||
|
title: "Established use-case collection and executable regression gate"
|
||||||
|
domain: communication
|
||||||
|
repo: coulomb-social
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: coulomb-social
|
||||||
|
created: "2026-10-06"
|
||||||
|
updated: "2026-10-06"
|
||||||
|
related:
|
||||||
|
- CSOC-WP-0008
|
||||||
|
state_hub_workstream_id: "d9ad740d-82a9-5130-b7da-29901542409a"
|
||||||
|
---
|
||||||
|
|
||||||
|
# CSOC-WP-0009 — Keep established use cases working
|
||||||
|
|
||||||
|
Milestone: every established journey has a versioned behavior contract and a
|
||||||
|
repeatable regression check. Starting with ReichelAg weekly protocols, a change
|
||||||
|
that breaks duplication, manual editing, clipboard export or access fails the
|
||||||
|
required local/CI check. Automated success never grants productive-transfer
|
||||||
|
acceptance in place of the user.
|
||||||
|
|
||||||
|
## T01 — Establish the collection and coverage contract
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CSOC-WP-0009-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "8aaf65d3-9931-52a1-8b2c-63888316abf8"
|
||||||
|
```
|
||||||
|
|
||||||
|
Machine registry links stable UC IDs to narrative, synthetic scenario fixtures,
|
||||||
|
mandatory fast and browser test node IDs, user-confirmation and transfer status.
|
||||||
|
Validate registry and collected tests; an established case cannot lose coverage
|
||||||
|
silently. Document how candidate → confirmed → regression-protected → user
|
||||||
|
accepted records are maintained without conflating those dimensions.
|
||||||
|
|
||||||
|
## T02 — Exercise ReichelAg through the actual browser UI
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CSOC-WP-0009-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "40ef120b-eccf-5350-a905-af45a6e38ab8"
|
||||||
|
```
|
||||||
|
|
||||||
|
Isolated Django test DB and temporary CONTENT_ROOT, synthetic actors and protocols.
|
||||||
|
Playwright drives real login/list/detail/duplicate/edit/save/reopen/copy screens;
|
||||||
|
verify full-copy/source independence, payload formatting, manual fallback,
|
||||||
|
reader edit denial and outsider isolation. Serve the real static script. No
|
||||||
|
production hosts, real notes, issuer, outgoing mail or private credentials.
|
||||||
|
|
||||||
|
## T03 — Make the regression gate reproducible
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CSOC-WP-0009-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "d950b4d6-faf4-5e95-8679-35e887dfa01f"
|
||||||
|
```
|
||||||
|
|
||||||
|
One make check command runs full application and use-case/browser checks and
|
||||||
|
writes JUnit reports. Provide fast/browser targets and locked Node dependencies,
|
||||||
|
bootstrap instructions and CI integration. Missing browser/dependencies must
|
||||||
|
fail the required gate; default fast tests may explicitly deselect browser tests.
|
||||||
|
CI activation depends on actual Forgejo runner admission; do not claim a remote
|
||||||
|
run from local tests alone.
|
||||||
|
|
||||||
|
## T04 — Verify and sync
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CSOC-WP-0009-T04
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "7aa0b568-c385-551b-b3e4-b43b3ce6e7f3"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run required gate, changed-file lint and coverage validation. Prove a missing
|
||||||
|
mandatory test is rejected. Document receipts, file any CI admission residual as
|
||||||
|
a live record, finish this infrastructure workplan, log, commit and sync.
|
||||||
|
|
||||||
|
## Implementation evidence
|
||||||
|
|
||||||
|
- Registry, fixture and stable fast/browser test mappings established for UC-008.
|
||||||
|
- Actual Django screens/static script exercised by Chromium against isolated
|
||||||
|
synthetic data. Full gate: **88 passed**; changed Python files pass Ruff.
|
||||||
|
- Omitted browser test collection deliberately rejected with pytest exit 4
|
||||||
|
and a missing registered browser-test error; not silently skipped.
|
||||||
|
- Locked Playwright dependencies, setup targets, JUnit reports and Forgejo
|
||||||
|
main/PR/manual workflow configured. Source archive read checked HTTP 200.
|
||||||
|
- Remote runner receipt, report retention and required merge-check enforcement
|
||||||
|
handed off before completion as **CSOC-IN-0005**, origin residual WP-0009.
|
||||||
|
- Productive transfer/live deployment/private protocol review remain WP-0008;
|
||||||
|
this infrastructure does not grant user acceptance.
|
||||||
|
|
||||||
|
## Completion
|
||||||
|
|
||||||
|
Required gate rerun after final browser permission checks: 88 passed; JUnit
|
||||||
|
reports show zero failures/errors. Reader clipboard copying succeeds while a
|
||||||
|
valid-CSRF reader POST is rejected as read-only. User-case coverage-loss probe
|
||||||
|
returned exit 4 as expected. AGENTS.md now requires make check for established
|
||||||
|
behavior changes. CSOC-IN-0005 is already registered as the remote CI enforcement
|
||||||
|
residual. All local infrastructure tasks complete; no production change made.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue