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
|
||||
docs/migration/samples/_local/
|
||||
scripts/node_modules/
|
||||
scripts/package-lock.json
|
||||
|
||||
# Local env overrides
|
||||
.env
|
||||
|
|
|
|||
15
AGENTS.md
15
AGENTS.md
|
|
@ -246,3 +246,18 @@ To create a new workplan:
|
|||
1. Write the file following the format above
|
||||
2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or
|
||||
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
|
||||
|
||||
|
|
@ -23,3 +23,16 @@ lint: ## ruff
|
|||
|
||||
shell: ## Django 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 run manage.py migrate
|
||||
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
|
||||
|
|
@ -45,3 +47,10 @@ dev-claims form. Platform OIDC: set env vars in `docs/dev.md`.
|
|||
## 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
|
||||
(UC-008), full duplication and manual editing plus clipboard copy for external
|
||||
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-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-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-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 |
|
||||
|
|
@ -62,7 +63,12 @@
|
|||
| 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-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-0002 | closed | green | 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-0005 | open | green | docs/intakes/csoc-residuals.md |
|
||||
|
|
|
|||
|
|
@ -142,3 +142,38 @@ notes: |
|
|||
Pickup: docs/session-pickup.md
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
## 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_classes = ["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"]
|
||||
|
||||
[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",
|
||||
"description": "Local tooling for auth capture and design extract (no app runtime)",
|
||||
"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."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.usecase("CSOC-UC-008")
|
||||
@pytest.mark.django_db
|
||||
def test_full_protocol_duplicate_edit_and_clipboard_control(tmp_path, settings, client):
|
||||
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",
|
||||
)
|
||||
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)
|
||||
client.force_login(user)
|
||||
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 pageops.read_page(space.slug, "next-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={
|
||||
"slug": space.slug, "page_slug": "next-call",
|
||||
}), {"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"Duplicate page" 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"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