Skip to content

test(stand): scenario-design skills and agents, and a SCENARIOS.md coverage triage - #2323

Merged
ktursunov merged 3 commits into
mainfrom
test-plan-and-scenarios
Aug 10, 2026
Merged

test(stand): scenario-design skills and agents, and a SCENARIOS.md coverage triage#2323
ktursunov merged 3 commits into
mainfrom
test-plan-and-scenarios

Conversation

@ktursunov

@ktursunov ktursunov commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Tooling to turn docs/product/SCENARIOS.md into coverage for the deployed-stand suite (tests/stand/), plus a first triage pass. Relates to #2188 ([research] quality vectors) and feeds the gate program in #1404.

No product code changes. Four skills, two agents, one derived artifact, and corrections to suite comments the tooling was written from.

Why this shape

The product doc turns out to be unusually testable. §1.1 Reach is a decision table — 4 personas × 7 dimensions with explicit Never cells — and §5 is twelve numbered invariants. Those are assertions someone already wrote down. Its four personas also map cleanly onto seeded fixtures, and the seed enforces the distinction the doc cares about: the ceo holds the insight-admin realm role but no admin row in person_roles, so seniority is refused by require_admin, while admin_operator sits outside the org chart entirely.

Deliberately not a local test-case management system — no case bodies, no execution status, no per-run results.

What's here

Skill Owns
insight-stand Operating the stand: up/seed/test/down, aiming a run, session fixtures, markers, mutation policy, failure triage
stand-scenarios SCENARIOS.md → testable claims. Five questions in order, with persona-reach.md and invariants.md as grounding references
stand-api-test tests/stand/api/ — catalogue discipline, caller choice, status codes, models, scratch policy, the coverage gate
stand-ui-test tests/stand/ui/ — the UI-vs-API justification rule, locators, page-object split, fidelity

Agents: stand-scenario-designer (read-only, returns claims) and stand-test-auditor (adversarial — would this fail for the right reason?).

tests/stand/SCENARIO-COVERAGE.md is the first pass: surface verdicts for S-1…S-10, ten clauses the shipped tests already prove without naming them, 15 designed claims, and the gaps. Five of ten scenarios produce claims; S-2, S-3 and S-6 have no surface and that verdict is a deliverable, not a failure. No claim asserts a metric value — golden_metrics is empty by design.

Findings worth a decision

Two group-size thresholds are enforced, and they disagree. §5 rule 10 says a group figure is not shown below four people. MIN_PEER_N = 5 is compiled into the peer SQL server-side (analytics/src/domain/metric_results/compiler.rs:25); MIN_COHORT = 4 is client-side (within-team-peer.ts:17). A cohort of exactly four measured members sees a median on screen that the API declines to compute. Appendix C already lists the threshold as needing confirmation — this is the concrete version of that question.

§5 rule 12's obvious test cannot be written. "The same cohort at team scope versus organization scope" can't be requested: a peer request carries only cohort_key, all 59 definitions declare org_unit, and the cohort CTE selects on the target's cohort_id independently of entity.ids. A narrower claim survives.

Appendix C's first open point is answerable now — ADMIN gaining no implicit data visibility is enforced at the API and already tested.

Suite corrections

The skills were drafted from suite comments, several of which were wrong — so those are fixed here too rather than left to mislead the next reader:

  • pytest_sessionfinish claimed browser journeys record into the coverage ledger. coverage.record has one call site (ApiClient.request) and no ui/ module builds a client, so a ui-only run produces neither ledger nor catalogue and the gate cannot run on that pair.
  • Operation counts had drifted three ways — 45, 45, 48 — against an actual 27. Rephrased so they need no number.
  • The identity contract lists only 200 for 19 operations, not 18; analytics has no /v1/admin/* route; requires_catalogue advertised a table_columns part that isn't implemented.
  • README's "does not cover" listed three things that have since landed.

api-test and playwright-cli descriptions gained the reciprocal half of a boundary that only ran one way. Both suites exercise /v1/metric-results and both have a coverage gate, so the route name never disambiguated them; and this repo has no .spec.ts at all, so "add a Playwright test" always means the pytest journeys.

Review notes

  • Every non-gitignored path referenced across the skills resolves; no broken relative links; ruff clean.
  • The 15 designed claims are not implemented — this PR is the tooling and the triage. Implementation is follow-up work.
  • Five parallel audits verified the skills against source before this landed; 14 factual errors were found and corrected, including one where the headline finding was backwards.

Summary by CodeRabbit

  • New Features

    • Added guidance for designing, auditing, and implementing deployed-environment API and browser tests.
    • Added scenario coverage documentation, including persona reachability, invariant checks, priorities, and known gaps.
    • Added tooling to calculate and validate current quality-test coverage counts.
  • Documentation

    • Expanded testing guidance for authentication, fixtures, accessibility, runtime expectations, coverage, and troubleshooting.
    • Updated quality examples with refreshed metric and connector totals and snapshot caveats.

@ktursunov
ktursunov requested a review from a team as a code owner August 7, 2026 06:52
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added documentation for deployed-stand API and UI testing, scenario claim design, seeded persona reachability, invariant coverage, QA agents, and repository-derived quality-vector denominator counts.

Changes

Deployed Stand QA

Layer / File(s) Summary
Stand testing skills
.claude/skills/api-test/SKILL.md, .claude/skills/insight-stand/*, .claude/skills/stand-api-test/*, .claude/skills/stand-ui-test/*, .claude/skills/playwright-cli/SKILL.md
Documents stand lifecycle, fixtures, authentication, API and UI test boundaries, browser workflows, cleanup, coverage ledgers, and troubleshooting.
Scenario claims and reachability
.claude/skills/stand-scenarios/*
Defines claim-design rules and records persona reach, invariant status, scenario coverage, unavailable surfaces, blockers, and seed gaps.
QA agent workflows
.claude/agents/stand-scenario-designer.md, .claude/agents/stand-test-auditor.md
Adds read-only agents for scenario claim design and adversarial stand-test audits with structured output requirements.
Coverage contracts and denominator tooling
.claude/skills/quality-vector-tests/*
Adds repository-derived denominator counts, source-move detection, --check failure handling, and refreshed illustrative examples.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: aleksdotbar

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: stand scenario-design skills and agents plus SCENARIOS.md coverage triage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test-plan-and-scenarios

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (1)
.claude/skills/playwright-cli/SKILL.md (1)

3-3: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Use npx --no-install for the pinned Playwright CLI path.

src/frontend declares playwright@1.61.1, but npx playwright test still permits installing a different transient playwright package if it is not already present locally. Change this to require the lockfile version or use npx --no-install playwright@1.61.1 test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/playwright-cli/SKILL.md at line 3, Update the Playwright CLI
command guidance in the skill description to prevent transient package
installation, using the repository’s pinned playwright@1.61.1 version via the
lockfile or npx --no-install. Replace the unqualified “npx playwright test”
reference while preserving the existing test-project guidance.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/agents/stand-scenario-designer.md:
- Around line 3-5: Remove Bash from the tools list in the
stand-scenario-designer agent definition, leaving only Read, Glob, and Grep.
Preserve the agent’s read-only claim-design scope and all other frontmatter
unchanged.

In @.claude/agents/stand-test-auditor.md:
- Line 3: Update the description for the stand-test auditor to allow
metric-value comparisons in api/analytics/test_drilldown.py when they are
validated against independent evidence, while continuing to prohibit hardcoded
numeric metric expectations elsewhere.
- Line 85: Update the `## DO NOT` guidance in `stand-test-auditor.md` to add “Do
not” before “assume,” so the instruction explicitly requires verifying symbols
or routes with grep rather than assuming they exist.

In @.claude/skills/api-test/SKILL.md:
- Line 3: Update the api-test skill description for src/ingestion/tests/e2e/api/
to state that the in-process rig has authentication enabled, uses gateway JWTs,
and sets auth_disabled: false, matching api-auth-tenancy.md and api_coverage.py.
Keep the deployed stand description separate and unchanged as the
Keycloak-session and service-image environment.

In @.claude/skills/insight-stand/SKILL.md:
- Around line 255-259: Update the ledger descriptions in
.claude/skills/insight-stand/SKILL.md lines 255-259 and
.claude/skills/stand-ui-test/SKILL.md lines 207-210 to distinguish
PersonaSession constructing an ApiClient from UI journeys recording requests:
replace wording that says UI journeys never build a client with wording that
they do not call ApiClient.request, while preserving the API-only ledger
behavior.
- Around line 56-63: Update the playwright install command in
.claude/skills/insight-stand/SKILL.md lines 56-63 to include --frozen, and apply
the same command change in .claude/skills/stand-ui-test/SKILL.md lines 190-192.
Keep both commands under the tests project so browser installation uses the
locked dependency set.
- Around line 123-124: Replace the mutable
ghcr.io/constructorfabric/insight-ui-tests:latest image reference with the same
immutable published ref in the documented test commands across
tests/stand/README.md, .claude/skills/insight-stand/SKILL.md (lines 123-124),
and .claude/skills/stand-ui-test/SKILL.md (line 201). Keep the command behavior
unchanged apart from pinning the image consistently in all three files.

In @.claude/skills/stand-ui-test/SKILL.md:
- Around line 79-80: Update the URL guidance in the stand-UI test instructions
so localhost is required only for --image containerized runs or in-namespace
plain-HTTP runs. Allow secure HTTPS hostnames for remote HTTPS workflows,
preserving the existing __Host- cookie behavior guidance.

In `@tests/stand/api/test_gateway.py`:
- Around line 75-76: Align the route-wide docstring claim with the test
assertions in the parameterized operation sweep: either move the `/auth/login`
detail and context["reason"] == "no_session" checks into the per-operation
assertions, or narrow the docstring to describe only ALL_OPERATIONS[0]. Ensure
the documentation does not claim identical actionable refusal bodies for every
route unless those assertions run for every operation.

In `@tests/stand/README.md`:
- Around line 149-150: Update the gap-count statement in the README section to
match its four top-level bullets: change “Three gaps” to “Four gaps,” while
leaving the JWT verification bullet as a separate top-level item.
- Around line 163-166: Update the caveat in the README near the cluster-stand
explanation to accurately describe the implemented requires_seed behavior:
missing seed fixtures abort collection rather than skip tests. Do not claim a
skip path unless an explicit cross-tenant skip behavior is implemented and
tested.

In `@tests/stand/SCENARIO-COVERAGE.md`:
- Around line 103-113: Update the coverage-status labels for claims lacking
direct test evidence, including S1-I-01 and S1-I-02, from [VERIFIED] to
[SUPPORTED] or [INFERRED] as appropriate. Apply the same evidence-based labeling
to the cited visibility scope, R1-01/R1-02, R11-01/R11-02, R12, S1-I-03, and R6
entries, preserving [VERIFIED] only where the referenced evidence directly
proves the claim.

---

Nitpick comments:
In @.claude/skills/playwright-cli/SKILL.md:
- Line 3: Update the Playwright CLI command guidance in the skill description to
prevent transient package installation, using the repository’s pinned
playwright@1.61.1 version via the lockfile or npx --no-install. Replace the
unqualified “npx playwright test” reference while preserving the existing
test-project guidance.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 724a5413-5220-49f8-a179-25a6a19226c7

📥 Commits

Reviewing files that changed from the base of the PR and between 2193228 and b67d729e973b6ad1d3549c99c08373a193f26b5b.

📒 Files selected for processing (19)
  • .claude/agents/stand-scenario-designer.md
  • .claude/agents/stand-test-auditor.md
  • .claude/skills/api-test/SKILL.md
  • .claude/skills/insight-stand/SKILL.md
  • .claude/skills/playwright-cli/SKILL.md
  • .claude/skills/stand-api-test/SKILL.md
  • .claude/skills/stand-scenarios/SKILL.md
  • .claude/skills/stand-scenarios/invariants.md
  • .claude/skills/stand-scenarios/persona-reach.md
  • .claude/skills/stand-ui-test/SKILL.md
  • tests/lib/insight_stand/coverage.py
  • tests/pyproject.toml
  • tests/stand/README.md
  • tests/stand/SCENARIO-COVERAGE.md
  • tests/stand/api/conftest.py
  • tests/stand/api/schemas/__init__.py
  • tests/stand/api/test_gateway.py
  • tests/stand/conftest.py
  • tests/stand/meta/test_coverage_gate.py

Comment thread .claude/agents/stand-scenario-designer.md
Comment thread .claude/agents/stand-test-auditor.md Outdated
Comment thread .claude/agents/stand-test-auditor.md Outdated
Comment thread .claude/skills/api-test/SKILL.md Outdated
Comment thread .claude/skills/insight-stand/SKILL.md
Comment thread .claude/skills/stand-ui-test/SKILL.md Outdated
Comment thread tests/stand/api/test_gateway.py Outdated
Comment thread tests/stand/README.md Outdated
Comment thread tests/stand/README.md Outdated
Comment thread .claude/skills/stand-scenarios/SCENARIO-COVERAGE.md
@ktursunov
ktursunov marked this pull request as draft August 7, 2026 07:35
@ktursunov
ktursunov force-pushed the test-plan-and-scenarios branch 2 times, most recently from 78a9d74 to b3a8f0d Compare August 7, 2026 11:29
@ktursunov
ktursunov marked this pull request as ready for review August 7, 2026 11:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
tests/stand/SCENARIO-COVERAGE.md (2)

166-170: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep authorization stable in R9-01.

If role revocation changes visibility, the second read can return an error or an empty result. Equal fallback responses would not prove that historical figures stayed unchanged. Use a caller whose access remains stable, assert both reads return success, and compare the metric payload for the same past window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/stand/SCENARIO-COVERAGE.md` around lines 166 - 170, Update the R9-01
scenario to use a caller whose authorization remains unchanged across the role
revocation, assert both /v1/metric-results reads succeed, and compare their
metric payloads for the same historical window; do not treat matching error or
empty fallback responses as agreement.

136-140: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Make the S9-A-03 oracle prove universal scope.

The claim says every other viewer remains unchanged, but “two subchart reads” does not define a baseline or cover every non-grantee. Capture each relevant viewer’s reach before the grant, apply the grant, and compare the same viewers afterward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/stand/SCENARIO-COVERAGE.md` around lines 136 - 140, Update the S9-A-03
test coverage oracle to capture each relevant viewer’s reach before granting
visibility, then apply the grant through /v1/visibility and read the same
subcharts afterward. Compare every non-grantee’s post-grant reach with its
baseline while separately verifying the grantee gains visibility, so the test
proves the grant is scoped rather than universal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/quality-vector-tests/scripts/counts.sh:
- Around line 88-91: Update the "stand tests" count command in counts.sh to
replace the paste-and-bc summation pipeline with an awk-based sum, ensuring the
count still returns zero when no test definitions are found and does not depend
on paste.

In @.claude/skills/quality-vector-tests/SKILL.md:
- Around line 263-270: Update the statement near the catalog-count guidance to
clarify that no repo-wide denominator in this skill is authoritative, rather
than claiming the skill contains no numbers. Preserve the existing instruction
to re-run the counting script and its MOVED/--check behavior.
- Around line 69-71: Update every documented counts.sh invocation to use the
repository-root path .claude/skills/quality-vector-tests/scripts/counts.sh: in
.claude/skills/quality-vector-tests/SKILL.md at lines 69-71, 177-182, and
238-240, and in .claude/skills/quality-vector-tests/references/example-lean.md
at lines 8-11 and .claude/skills/quality-vector-tests/references/example-port.md
at lines 3-4. Replace the existing scripts/counts.sh and ../scripts/counts.sh
references without changing the surrounding workflow.

In @.claude/skills/stand-scenarios/invariants.md:
- Around line 157-187: Update R10 and R11 in the invariants documentation to
explicitly mark the threshold disagreement and missing S-2 conclusion-type
exclusion surface as unresolved. Align the corresponding entries in
tests/stand/SCENARIO-COVERAGE.md, and do not mark R10 complete until the chosen
product threshold is applied consistently to both frontend MIN_COHORT and
backend MIN_PEER_N enforcement.

---

Nitpick comments:
In `@tests/stand/SCENARIO-COVERAGE.md`:
- Around line 166-170: Update the R9-01 scenario to use a caller whose
authorization remains unchanged across the role revocation, assert both
/v1/metric-results reads succeed, and compare their metric payloads for the same
historical window; do not treat matching error or empty fallback responses as
agreement.
- Around line 136-140: Update the S9-A-03 test coverage oracle to capture each
relevant viewer’s reach before granting visibility, then apply the grant through
/v1/visibility and read the same subcharts afterward. Compare every
non-grantee’s post-grant reach with its baseline while separately verifying the
grantee gains visibility, so the test proves the grant is scoped rather than
universal.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e2b4a7d-8882-4cf9-8074-04502ed82aa6

📥 Commits

Reviewing files that changed from the base of the PR and between b67d729e973b6ad1d3549c99c08373a193f26b5b and b3a8f0db50658f4dc39d9fcfda9ccd802cfb4f96.

📒 Files selected for processing (7)
  • .claude/skills/quality-vector-tests/SKILL.md
  • .claude/skills/quality-vector-tests/references/example-lean.md
  • .claude/skills/quality-vector-tests/references/example-port.md
  • .claude/skills/quality-vector-tests/scripts/counts.sh
  • .claude/skills/stand-scenarios/SKILL.md
  • .claude/skills/stand-scenarios/invariants.md
  • tests/stand/SCENARIO-COVERAGE.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude/skills/stand-scenarios/SKILL.md

Comment thread .claude/skills/quality-vector-tests/scripts/counts.sh
Comment thread .claude/skills/quality-vector-tests/SKILL.md
Comment thread .claude/skills/quality-vector-tests/SKILL.md Outdated
Comment thread .claude/skills/stand-scenarios/invariants.md
@hello1101n

Copy link
Copy Markdown
Contributor

@ktursunov please answer / solve code rabbit comments

@ktursunov

Copy link
Copy Markdown
Contributor Author

Worked through all 16 review comments — 12 accepted, 3 declined (reasons on the individual threads), 1 recorded below rather than actioned. Applied in 4e81a298. Resolving the threads.

Two were real errors in this PR

  • api-test described the in-process rig as running "with auth disabled". It runs auth-enabledapi-auth-tenancy.md records auth_disabled: false and a signed ES256 gateway JWT per request. That false clause was also doing the work of distinguishing the two suites; the honest discriminator is how each authenticates.
  • "browser journeys never build a client" is false — personas.py:257 builds an ApiClient for every persona. The conclusion holds for a better reason: coverage.record fires in ApiClient.request, so recording follows issuing a request, not constructing a client.

Contradictions this PR introduced — the localhost rule stated absolutely while another section documents aiming at https://<stand> (the real constraint is a trustworthy origin); "Three gaps" against four bullets; "no number appears in this skill" above a table of numbers.

Usability bugs in its own additions — five of six counts.sh references were unrunnable from the repo root, and the stand-test sum depended on bc, whose absence would have been reported as MOVED by the very guard added to prevent false answers.

Declined: the test_gateway docstring already draws the distinction it was asked to draw; :latest for the ui-tests image is what e2e-stand.yml:77 itself runs, so pinning the docs would desynchronise them from the workflow.


Not actioned, recorded here so it is not lost — the requires_seed / cross-tenant point (thread on tests/stand/README.md) is a real contradiction and it predates this PR:

requires_seed with a missing fixture raises pytest.UsageError, which aborts the whole session at collection. Three separate places promise a skip instead:

  • src/ingestion/tools/seed/insight_seed/identity.py:457"and the tests that need one skip"
  • src/ingestion/tools/seed/insight_seed/manifest.py:262"would turn every test that declares requires_seed("other_tenant_lead") from a skip into a failure"
  • tests/stand/README.md"skip rather than fail"

SEED_CROSS_TENANT_FIXTURE defaults on for compose and is off on a cluster stand, and two shipped tests declare that fixture (test_visible_persons.py:42, test_profiles.py:117). So on a cluster stand the entire suite aborts before any test runs, and the abort message advises Re-seed the stand, which cannot help — the omission is deliberate.

This is a coverage gap that misreports itself as an environment fault; it is not evidence that tenant isolation is broken there. Left untouched deliberately: three authors describing a skip looks like unfinished wiring rather than a chosen behaviour, and editing the comments to match the code would cement it and erase the evidence of intent. Belongs to whoever owns that switch.

@ktursunov
ktursunov force-pushed the test-plan-and-scenarios branch from 4e81a29 to 6c30469 Compare August 10, 2026 04:57
@ktursunov
ktursunov requested a review from aleksdotbar August 10, 2026 05:03
@ktursunov
ktursunov force-pushed the test-plan-and-scenarios branch from 6c30469 to 5b89e0e Compare August 10, 2026 10:03
Konstantin Tursunov added 2 commits August 10, 2026 18:08
…d suite

Turn docs/product/SCENARIOS.md into coverage for tests/stand/ without standing
up a parallel test-case database.

The product doc is already shaped for this: §1.1 Reach is a decision table
(4 personas x 7 dimensions with explicit "Never" cells) and §5 is twelve
numbered invariants. Its four personas map onto seeded fixtures, and the seed
enforces the distinction the doc cares about — the ceo holds the insight-admin
realm role but no admin row in person_roles, and admin_operator sits outside
the org chart entirely.

  insight-stand    operating the stand — up/seed/test/down, aiming a run
                   (base URL and manifest are two independent facts), session
                   fixtures, markers, mutation policy, triage
  stand-scenarios  SCENARIOS.md -> testable claims; five questions in order,
                   with persona-reach.md and invariants.md as the grounding
                   references, and SCENARIO-COVERAGE.md as the output
  stand-api-test   tests/stand/api/ — catalogue discipline, caller choice,
                   status codes, models, scratch policy, xfail policy, the gate
  stand-ui-test    tests/stand/ui/ — the UI-vs-API justification rule,
                   accessibility-first locators, page-object split, fidelity

  stand-scenario-designer  read-only; returns claims, writes no test code
  stand-test-auditor       adversarial; asks whether an assertion proves the
                           claim its name makes, and whether the test would
                           fail on an empty stand

SCENARIO-COVERAGE.md is the first pass: surface verdicts for S-1..S-10, the
clauses shipped tests already prove, 15 designed claims, and the gaps. Five of
ten scenarios produce claims; S-2, S-3 and S-6 have no surface. No claim
asserts a metric value — golden_metrics is empty by design.

Two findings worth a product decision are recorded there rather than acted on:
the group-size threshold is enforced server-side (MIN_PEER_N = 5) but the
frontend and the product doc both say four; and §5 rule 12's obvious test
cannot be requested, because a peer request carries only cohort_key and the
cohort is derived from the target rather than the request.

Documentation only — no product or test code changes.

Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
…rrying stale numbers

Two pre-existing skills gained a same-named sibling in the previous commit and
needed the other half of a boundary that only ran one way.

`api-test` now says it is the IN-PROCESS rig and redirects deployed-stand work
to `stand-api-test`, stating why the route name cannot be the discriminator:
both suites exercise /v1/metric-results, /v1/queries and /v1/metric-drilldown,
and both have a per-operation coverage gate. It also no longer claims the rig
runs "with auth disabled" — `api-auth-tenancy.md` records `auth_disabled: false`
and a signed ES256 gateway JWT on every request.

`playwright-cli`'s description was thirteen unqualified words whose "work with
Playwright tests" clause matched "add a Playwright test" more literally than
`stand-ui-test` did. It now describes what the skill contains (the binary for
interactive driving, plus Node/TypeScript `npx playwright test`) and draws the
line by runner: this repository has no .spec.ts specs at all, and every
committed browser test is pytest under tests/stand/ui/.

`quality-vector-tests` carried denominators that had gone stale, and one of its
counting commands no longer worked at all: the catalog left `builtin.rs` for
`include_str!("registry.yaml")`, so the documented grep kept returning a
number — 0 — which reads exactly like an answer. A denominator of 0 in a target
is worse than no target.

So the counts are taken rather than quoted now. `scripts/counts.sh` prints each
denominator beside the path it came from, and every count proves its source
first: a moved source reports MOVED, never zero. `--check` exits non-zero for
CI. Verified against the real regression by pointing the catalog count back at
builtin.rs — it reports MOVED and --check exits 1.

The skill body carries no repo-wide number. Where a number is illustrative it
stays and is labelled a snapshot, because the worked examples teach the shape
of a finished target and that shape does not move.

Documentation only.

Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
@ktursunov
ktursunov force-pushed the test-plan-and-scenarios branch from 5b89e0e to 8ac8ee6 Compare August 10, 2026 10:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/insight-stand/SKILL.md:
- Line 243: Update the “Login loops, then 503” triage row to limit the diagnosis
to in-namespace plain-HTTP base URLs whose host is not localhost; do not imply
that remote HTTPS base URLs are invalid. Preserve the guidance about joining the
gateway network namespace or using the published port.

In @.claude/skills/quality-vector-tests/scripts/counts.sh:
- Line 62: Replace the `grep -q .` consumers in the source proofs at lines 62,
76, and 81 with a complete-consuming check such as `awk 'END { exit (NR == 0)
}'`. Update each affected pipeline so `find` is fully consumed under `pipefail`,
preserving the existing valid-source detection and `count()` behavior.

In @.claude/skills/stand-api-test/SKILL.md:
- Around line 243-250: Update the case-enumeration step in the Procedure to
require only statuses reachable from the specific handler: include all supported
validation outcomes such as 400 or 422, and add 404, 415, or scope/tenant
refusals only when the handler implements them. Remove the implication that
these statuses are universal while preserving one test per reachable status
code.

In @.claude/skills/stand-scenarios/SCENARIO-COVERAGE.md:
- Around line 169-173: Update the R9-01 scenario guidance to avoid revoking
roles in the shared seeded stand: require execution on an isolated stand with
complete state restoration, or mark the scenario blocked for tests/stand/.
Preserve the two metric-read temporal assertion while ensuring shared
person_roles state is never mutated.
- Around line 149-152: Move R1-01 out of the tests/stand/ scope and document it
under the value-capable metric suite owned by src/ingestion/tests/e2e/,
preserving its requirement to distinguish null from 0. Update the scenario
coverage entry and any implementation references accordingly.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef1d91de-7229-4aed-b365-ac9b23ab879e

📥 Commits

Reviewing files that changed from the base of the PR and between b3a8f0db50658f4dc39d9fcfda9ccd802cfb4f96 and da9d5d6.

📒 Files selected for processing (12)
  • .claude/agents/stand-scenario-designer.md
  • .claude/agents/stand-test-auditor.md
  • .claude/skills/api-test/SKILL.md
  • .claude/skills/insight-stand/SKILL.md
  • .claude/skills/quality-vector-tests/SKILL.md
  • .claude/skills/quality-vector-tests/references/example-lean.md
  • .claude/skills/quality-vector-tests/references/example-port.md
  • .claude/skills/quality-vector-tests/scripts/counts.sh
  • .claude/skills/stand-api-test/SKILL.md
  • .claude/skills/stand-scenarios/SCENARIO-COVERAGE.md
  • .claude/skills/stand-scenarios/SKILL.md
  • .claude/skills/stand-ui-test/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • .claude/skills/quality-vector-tests/references/example-port.md
  • .claude/skills/api-test/SKILL.md
  • .claude/agents/stand-test-auditor.md
  • .claude/skills/quality-vector-tests/references/example-lean.md
  • .claude/agents/stand-scenario-designer.md
  • .claude/skills/stand-scenarios/SKILL.md

| `cannot resolve the stand's base URL — refusing to assume one` (lists what it tried) | no address from any source | `--base-url`, or run from the repo root where the env file lives |
| `gateway is not answering on http://localhost:<port>/` | you aimed `test-stand test` at a stand that is not local | the wrapper preflights localhost — run pytest directly instead |
| Everything skips with "capability … not present" | expected on this stand (`ingestion: no`) | not a failure — read the reason |
| Login loops, then 503 | **base URL is not `localhost`** — see the trap above | join the gateway netns, or use the published port |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit this diagnosis to plain HTTP.

Line 243 conflicts with Lines 108-112. A remote https:// base URL is valid. Only an in-namespace plain-HTTP URL that is not localhost causes the __Host- cookie failure. Update this triage row to state that condition.

Proposed wording
-| Login loops, then 503 | **base URL is not `localhost`** — see the trap above | join the gateway netns, or use the published port |
+| Login loops, then 503 | **in-namespace plain-HTTP base URL is not `localhost`** — see the trap above | use `localhost:<port>` for `--image` mode, or use a trustworthy HTTPS origin |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Login loops, then 503 | **base URL is not `localhost`** — see the trap above | join the gateway netns, or use the published port |
| Login loops, then 503 | **in-namespace plain-HTTP base URL is not `localhost`** — see the trap above | use `localhost:<port>` for `--image` mode, or use a trustworthy HTTPS origin |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/insight-stand/SKILL.md at line 243, Update the “Login loops,
then 503” triage row to limit the diagnosis to in-namespace plain-HTTP base URLs
whose host is not localhost; do not imply that remote HTTPS base URLs are
invalid. Preserve the guidance about joining the gateway network namespace or
using the published port.

count "connectors" \
"src/ingestion/connectors" \
"find src/ingestion/connectors -maxdepth 2 -mindepth 2 -type d | wc -l" \
"find src/ingestion/connectors -maxdepth 2 -mindepth 2 -type d | grep -q ."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -o pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

for i in $(seq 1 10000); do
  mkdir "$tmpdir/d$i"
done

set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -q .
bad_status=$?

find "$tmpdir" -mindepth 1 -maxdepth 1 -type d |
  awk 'END { exit (NR == 0) }'
good_status=$?
set -e

printf 'grep proof status: %s\n' "$bad_status"
printf 'awk proof status: %s\n' "$good_status"

Repository: constructorfabric/insight

Length of output: 241


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf 'pwd=%s\n' "$(pwd)"
printf 'counts_file_exists=%s\n' "$(test -f .claude/skills/quality-vector-tests/scripts/counts.sh && echo yes || echo no)"
if test -f .claude/skills/quality-vector-tests/scripts/counts.sh; then
  wc -l .claude/skills/quality-vector-tests/scripts/counts.sh
  sed -n '1,100p' .claude/skills/quality-vector-tests/scripts/counts.sh | cat -n
fi

printf '\nBash version:\n'
bash --version | head -n 1

printf '\nBehavioral probe 1: early grep under pipefail with large find output:\n'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
dirs=()
for i in $(seq 1 "$((256 * 1000))"); do
  dirs+=("d$i")
done
mkdir -p "$tmpdir"/"${dirs[@]}"
set +e
find "$tmpdir" -min R

Repository: constructorfabric/insight

Length of output: 5054


🏁 Script executed:

#!/usr/bin/env bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

printf '\nBehavioral probe 1: early grep under pipefail with large find output:\n'
for i in $(seq 1 "$((256 * 1000))"); do
  mkdir "$tmpdir/d$i"
done
set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -q .
grep_status=$?
find_exit_status=$?
set -e
printf 'grep -q exit status: %s\n' "$grep_status"
printf 'find exit status in pipeline with bash $? after set +e: %s\n' "$find_exit_status"

printf '\nBehavioral probe 2: small find output under pipefail:\n'
for i in $(seq 1 2); do
  mkdir "$tmpdir/small$i"
done
set +e
find "$tmpdir/small" -mindepth 1 -maxdepth 1 -type d | grep -q .
grep_small_status=$?
set -e
printf 'grep -q small output exit status: %s\n' "$grep_small_status"

printf '\nBehavioral probe 3: complete consumer under pipefail with large find output:\n'
for i in $(seq 1 "$((256 * 1000))"); do
  mkdir "$tmpdir/compl$i"
done
set +e
find "$tmpdir/compl" -mindepth 1 -maxdepth 1 -type d | awk 'END { exit (NR == 0) }'
awk_status=$?
set -e
printf 'awk complete consumer exit status: %s\n' "$awk_status"

Repository: constructorfabric/insight

Length of output: 756


🏁 Script executed:

#!/usr/bin/env bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

printf '\nBehavioral probe 1: early grep under pipefail with large find output:\n'
for i in $(seq 1 "$((256 * 1000))"); do
  mkdir "$tmpdir/d$i"
done
set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -q .
grep_status=$?
set -e
printf 'grep -q exit status: %s\n' "$grep_status"

printf '\nBehavioral probe 2: small find output under pipefail:\n'
for i in $(seq 1 2); do
  mkdir "$tmpdir/small$i"
done
set +e
find "$tmpdir/small" -mindepth 1 -maxdepth 1 -type d | grep -q .
grep_small_status=$?
set -e
printf 'grep -q small output exit status: %s\n' "$grep_small_status"

printf '\nBehavioral probe 3: complete consumer under pipefail with large find output:\n'
for i in $(seq 1 "$((256 * 1000))"); do
  mkdir "$tmpdir/compl$i"
done
set +e
find "$tmpdir/compl" -mindepth 1 -maxdepth 1 -type d | awk 'END { exit (NR == 0) }'
awk_status=$?
set -e
printf 'awk complete consumer exit status: %s\n' "$awk_status"

Repository: constructorfabric/insight

Length of output: 698


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf 'Bash version:\n'
bash --version | head -n 1

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

printf '\nProbe: large find output with early-close consumer under pipefail:\n'
dirs=()
i=0
while true; do
  dirs+=("d$i")
  i=$((i + 1))
  if [ "$i" -ge 60000 ]; then break; fi
done
mkdir -p "$tmpdir"/"${dirs[@]}"
set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -q .
early_status=$?
set -e
printf 'early-close status: %s\npython3 status: %s\n' "$early_status" "$(python3 -c 'print(0 if early_status == 0 else 69)')"

printf '\nProbe: large find output with complete consumer under pipefail:\n'
dirs=()
i=0
while true; do
  dirs+=("c$i")
  i=$((i + 1))
  if [ "$i" -ge 60000 ]; then break; fi
done
mkdir -p "$tmpdir"/"${dirs[@]}"
set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | awk 'END { exit (NR == 0) }'
complete_status=$?
set -e
printf 'complete status: %s\n' "$complete_status"

Repository: constructorfabric/insight

Length of output: 550


🏁 Script executed:

#!/usr/bin/env bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

printf '%s directories\n' 60000
for i in $(seq -w 1 60000); do
  mkdir "$tmpdir/$i"
done

set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -q .
early_status=$?
find_status=$?
set -e
printf 'pipefail behavior: grep status=%s find pipeline status=%s\n' "$early_status" "$find_status"

set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | grep -vq .
awk_status=$?
set -e
printf 'grep-vq empty behavior: status=%s\n' "$awk_status"

set +e
find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | awk 'END { exit (NR == 0) }'
awk_status=$?
set -e
printf 'awk complete consumer behavior: status=%s\n' "$awk_status"

Repository: constructorfabric/insight

Length of output: 348


🏁 Script executed:

#!/usr/bin/env bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir/60000"

set +e
find "$tmpdir" -min R

Repository: constructorfabric/insight

Length of output: 194


🌐 Web query:

GNU find mindepth predicate syntax find -mindepth

💡 Result:

The GNU find command uses the -mindepth predicate to restrict the depth at which tests and actions are applied [1][2]. Syntax: find [path...] -mindepth levels The levels argument must be a non-negative integer [1][2]. When -mindepth is used, the command will not apply any tests or actions to files at levels less than the specified integer [3][4]. Key behavioral notes: -mindepth 0 is the default behavior, meaning it processes the starting-point(s) as well as all files and directories found below them. -mindepth 1 prevents the command from applying tests or actions to the starting-point(s) themselves, processing only the contents within them [1][3]. Example: To find files at least 2 levels below the directory 'dir', you would use: find dir -mindepth 2 This will skip 'dir' and 'dir/subdir' (level 0 and 1) and output files starting from 'dir/subdir/file' [4].

Citations:


Avoid early-closing source proofs under pipefail.

Lines 62, 76, and 81 use grep -q . as the source proof after set -o pipefail. grep -q can stop after the first match and close the pipe while find is still writing, causing the proof to fail and count() to report a valid source as MOVED.

Use a complete consumer for the proof, such as find ... | awk 'END { exit (NR == 0) }', and apply it to all source proofs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/quality-vector-tests/scripts/counts.sh at line 62, Replace
the `grep -q .` consumers in the source proofs at lines 62, 76, and 81 with a
complete-consuming check such as `awk 'END { exit (NR == 0) }'`. Update each
affected pipeline so `find` is fully consumed under `pipefail`, preserving the
existing valid-source detection and `count()` behavior.

Comment on lines +243 to +250
## Procedure

1. Read the handler in `src/backend/services/<service>/src/api/`. Declared codes
are boilerplate; reachable codes are in the code.
2. Add the operation to `operations.py` if it is new.
3. Pick the caller — the choice is usually the test.
4. Enumerate cases: success, each validation 400, path-parse 400, 404 unknown,
415, and the scope/tenant refusals. One test per code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe only reachable status cases.

Line [136] allows off-schema responses of 400 or 422, but Line [249] lists only validation 400. The procedure also presents 404, 415, and scope refusals as universal cases.

Update the step to include each reachable status, such as 400 or 422, and add 404, 415, or scope cases only when the handler supports them.

Suggested wording
-4. Enumerate cases: success, each validation 400, path-parse 400, 404 unknown,
-   415, and the scope/tenant refusals. One test per code.
+4. Enumerate success and each reachable validation status, such as 400 or
+   422, path-parse 400, unknown-resource 404, 415, and scope/tenant refusals.
+   Add one test per reachable status.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/stand-api-test/SKILL.md around lines 243 - 250, Update the
case-enumeration step in the Procedure to require only statuses reachable from
the specific handler: include all supported validation outcomes such as 400 or
422, and add 404, 415, or scope/tenant refusals only when the handler implements
them. Remove the implication that these statuses are universal while preserving
one test per reachable status code.

Comment on lines +149 to +152
**R1-01 — an unmeasured metric answers `null`, never `0`.** The UI half is
covered; the API half is not, and the two are indistinguishable on screen — a
backend returning `0` and a frontend rendering `—` for falsy look identical.
API `POST /v1/metric-results` · P1 · new · `[VERIFIED]`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Move R1-01 to the value-capable suite.

R1-01 must distinguish null from 0. This is a metric-value oracle. .claude/skills/stand-api-test/SKILL.md forbids this assertion because golden_metrics is empty. The same file identifies src/ingestion/tests/e2e/ as the owner for metric-value coverage.

Mark R1-01 out of scope for tests/stand/, or move its implementation to the value-capable suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/stand-scenarios/SCENARIO-COVERAGE.md around lines 149 - 152,
Move R1-01 out of the tests/stand/ scope and document it under the value-capable
metric suite owned by src/ingestion/tests/e2e/, preserving its requirement to
distinguish null from 0. Update the scenario coverage entry and any
implementation references accordingly.

Comment on lines +169 to +173
**R9-01 — revoking a role does not change a past period's figures.** Needs no
value oracle: read the same past window twice, either side of the revoke, and
assert the two reads **agree**. That is the whole claim, and it is the temporal
guarantee the doc leans on in three separate scenarios. API `/v1/person-roles` +
two `/v1/metric-results` reads · P1 · new · `[SUPPORTED]`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not revoke seeded roles in the shared stand.

R9-01 requires revoking a role between two metric reads. The stand API skill defines the deployed stand as read-only and permits mutation only for scratch resources created and deleted through the API.

Run this claim on an isolated stand with full state restoration, or mark it blocked for tests/stand/. Otherwise, the test can change person_roles state for later tests and later runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/stand-scenarios/SCENARIO-COVERAGE.md around lines 169 - 173,
Update the R9-01 scenario guidance to avoid revoking roles in the shared seeded
stand: require execution on an isolated stand with complete state restoration,
or mark the scenario blocked for tests/stand/. Preserve the two metric-read
temporal assertion while ensuring shared person_roles state is never mutated.

@ktursunov
ktursunov added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 5c103c6 Aug 10, 2026
25 checks passed
@ktursunov
ktursunov deleted the test-plan-and-scenarios branch August 10, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants