qa-quality
GitHub提供QA测试场景的质量评分标准,从影响、覆盖、关键性和诊断精度四个维度评估并记录质量分数。用于审查、编写或对比测试场景的价值,辅助决定保留哪些测试。
Trigger Scenarios
Install
npx skills add lgse/strata --skill qa-quality -g -y
SKILL.md
Frontmatter
{
"name": "qa-quality",
"description": "Rubric for scoring the value of Strata QA scenarios (tests\/qa\/scenarios) on impact, coverage, criticality and diagnostic precision, and for recording the optional `quality` header field. Use when scoring or reviewing the value of QA scenarios, writing or checking a scenario's `quality` field, comparing overlapping scenarios, or deciding which tests matter."
}
QA scenario quality rubric
Score each scenario in tests/qa/scenarios/<folder>/*.jsonl on four
dimensions, combine them into a 0–100 score, and record the result in the
scenario's header. QA is Strata's only GUI test suite; every comparison below
is against other QA scenarios and the Rust unit and GTK tests in
src/**/tests.rs.
This rubric produces scores only. It defines no keep/drop threshold, tier or label. The owner sets any cut-offs when applying the scores. Never write a verdict such as "drop" or "keep" into a note.
Supporting files:
- calibration.md: ten scored real scenarios. Read them before scoring anything.
- worksheet.md: a per-scenario worksheet and the batch log format.
- quality.py: computes
score, writes the field byte-exactly, ranks overlapping scenarios and lists recorded scores.
The field
The header is the JSON object on line 1. quality is optional and has a fixed
shape:
"quality":{"impact":4,"coverage":4,"criticality":4,"diagnostic":3,"score":52,"note":"Keep Both via keyboard + undo; only keyboard route through the paste-conflict dialog"}
| Key | Type | Rule |
|---|---|---|
impact |
integer 1–5 | Anchors below |
coverage |
integer 1–5 | Anchors below |
criticality |
integer 1–5 | Anchors below |
diagnostic |
integer 1–5 | Anchors below |
score |
integer 0–100 | Exactly the formula below |
note |
string | One non-empty line: note.splitlines() == [note] and not blank. No \n, \r, \v, \f, \x1c–\x1e, U+0085, U+2028 or U+2029 |
No other keys. The qa-scenarios skill (.agents/skills/qa-scenarios/SKILL.md)
describes the rest of the header.
Impact: how much a user is hurt if the behaviour breaks
Score the worst realistic failure of the behaviour the scenario guards, not the failure of an incidental step. Weigh frequency of use, data loss or safety, recoverability and visibility. A silent failure is worse than an obvious one.
| Score | Anchor | Strata examples |
|---|---|---|
| 5 | Data loss, data corruption or a safety/security breach; unrecoverable or silent. | Dismissing a paste conflict overwrites a file (dismissing-conflict-preserves-both); permanent delete through a symlinked folder removes the wrong tree (permanent-delete-symlinked-parent); an executable runs without confirmation (executable-requires-confirmation); a disconnected provider replays an action (provider-disconnect-never-replays). |
| 4 | A core daily workflow fails with no reasonable workaround, or a recoverable but damaging file outcome (wrong file trashed, undo leaves debris). | Arrow keys stop moving focus (arrow-keys-move-focus); rename does not commit on Enter (rename-commits-on-enter); Keep Both or its undo affects the wrong file (keep-both-then-undo-remaining); settings lost on restart (preferences-sync-windows-restart). |
| 3 | A secondary feature fails, or a core one fails with an obvious workaround; or a whole user group (keyboard-only, screen reader) loses one control. | Sort by size wrong (sorting-by-size); Open With forgets the default (open-with-keeps-default); window shortcuts fire over a modal (modal-dialog-blocks-window-shortcuts); toolbar buttons unnamed for screen readers (toolbar-controls-named). |
| 2 | A visible annoyance on a working path: lost scroll position, wrong preview size, rendering or state glitch the user can recover from at once. | Rename loses the scroll position (visible-rename-preserves-scroll); small images upscaled (small-image-preview-native-size); hover tile renders wrong (icons-hover-baseline); filter text selection ignores the theme (filter-selection-uses-theme). |
| 1 | Cosmetic only: alignment, spacing, centring, a colour shade, tooltip wording. Nothing stops working. | A column title off-centre by a few pixels (the centring assertions in column-header-titles-bounded); tooltip wording. |
Some examples here were removed from the suite for their low score per unit of cost; they remain as scoring examples.
Accessible names, roles and states are product surface: a missing name makes a control unusable for screen-reader users, so score it 3 or higher, never as cosmetic.
Coverage: unique behaviour verified that nothing else verifies
Coverage is marginal: what would go unverified if this scenario were deleted. Breadth counts only where it is unique. Count distinct behaviours, input routes (pointer, keyboard, context menu, drag, shortcut), view modes, and lifecycle paths (startup, restart, relaunch, dismissal, undo, worker retirement) that this scenario alone exercises.
| Score | Anchor | Strata examples |
|---|---|---|
| 5 | Sole guard of a whole behaviour, with several distinct routes, modes or lifecycle paths; no scenario or unit test checks the GUI-level outcome. | dismissing-conflict-preserves-both (Escape, Cancel, the close button, and a move: the file outcome of dismissing a paste conflict is checked nowhere else). |
| 4 | Sole guard of the main behaviour, or of a distinct input route or lifecycle path of it; setup or underlying logic may be shared or unit-tested. | modal-dialog-blocks-window-shortcuts (no other scenario or unit test presses window shortcuts over a modal); keep-both-then-undo-remaining (only keyboard route through the paste-conflict dialog). |
| 3 | The core is covered elsewhere, but one meaningful unique part remains: one route, one view mode with its own code path, one lifecycle step, or the GUI wiring of logic that unit tests cover. | permanent-delete-symlinked-parent (deletion through an alias is unit-tested; resolving the alias pane in the GUI is not); toolbar-controls-named (two of five names checked nowhere else). |
| 2 | Mostly duplicated: the unique part is minor, an equivalent route, another value of an already-tested setting; or every unique assertion is cosmetic. | keep-both-then-undo (only its click on Keep Both is not in keep-both-then-undo-remaining); appearance-menu-switches-view (Icons value of a menu switch the List scenario tests); icons-hover-baseline. |
| 1 | Fully contained: every behaviour and assertion is verified by another scenario or unit test through an equivalent route; or every unique assertion is layout or geometry. | column-header-titles-bounded (only geometry assertions). |
Rules:
- Layout and geometry assertions add no coverage. AGENTS.md "Test value" forbids layout tests: widget positions, sizes, alignment, spacing, wrapping. A scenario whose only unique assertions are layout scores coverage 1.
- Cosmetic assertions cap coverage at 2: colours, theme pixels, visual baselines. A baseline is the only way to see a rendering regression, so it is not 1, but its unique content is visual.
- Behaviour AGENTS.md calls distinct stays distinct: input routing, lifecycle, filesystem safety, error handling and live preferences. A keyboard route and a pointer route to the same action are two behaviours when Strata's code routes them differently (focus, input ownership, key bindings). Activating one GTK button by click or by Return is one route.
- Never mark both sides of a duplicate pair as duplicates. When two scenarios cover the same behaviour, the superset (more routes, assertions, modes) keeps the coverage for the shared part; the subset scores only its remainder. If they are exact mirrors, the one outside the behaviour's own folder, or else the later name alphabetically, takes the low score. Name the other scenario in both notes.
- Unit coverage reduces coverage only for the logic it pins. A unit test of
local_operationsnaming does not cover the dialog, focus, selection or listing refresh that only the GUI path reaches.
Judging overlap
Compare, in this order:
- Fixture: header
tree,setup,images,prefs,env,session_bus,portal, and the firstfssteps. - Input route:
key,type,click(withbutton,count,mods),select,select_with_keyboard,open_menu, drags,wheel. - View mode: header
modes, and any step-level"modes"guard. - Assertions:
expect,wait, andfschecks. Ignoremessagetext.
Two scenarios overlap on a behaviour when fixture, route and mode are equivalent and the assertions on that behaviour are equal or one is a superset.
Criticality: how likely a regression is, and whether only this level catches it
| Score | Anchor | Strata examples |
|---|---|---|
| 5 | Concurrent or external code path (async workers, filesystem operations, races, portals, D-Bus, sandbox helpers) with repeated past fixes and high churn; only the running application shows the failure. | Provider disconnect and replay over D-Bus (provider-disconnect-never-replays); idle sandbox workers exiting and restarting (idle-workers-exit-and-restart). |
| 4 | Complex GUI glue across modules (dialog → operation → listing refresh → focus), or a path with past fix commits or high churn; a unit test cannot reproduce the integration. | Paste-conflict dialogs (src/ui/browser/transfer*, src/adapters/local_operations: ~100 commits, ~55 fixes); modal input routing added by a fix (modal-dialog-blocks-window-shortcuts). |
| 3 | Non-trivial wiring with moderate churn or one prior regression; a refactor could plausibly break it. Visual baselines of widely restyled surfaces. | Appearance menu switching views (src/ui/browser_modes, high churn); filter text selection colour (added with fix #778); baselines refreshed by theming and font-rendering fixes. |
| 2 | Stable, simple code, or logic a unit test could pin as well; regressions unlikely or better caught elsewhere. | Static header geometry (column-header-titles-bounded). |
| 1 | Declarative or effectively frozen: a constant, a label with no history, behaviour fully determined by unit-tested logic. | A fixed string never touched since it was added. |
Measure, don't guess:
- Churn: commits touching the implementing paths in the last 90 days. Roughly ≥ 30 is high, 10–29 medium, < 10 low.
- Past bugs:
fixcommits touching those paths, and especially a fix that introduced the scenario's distinctive assertion (see the recipes below). Several commits are batch fixes ("triage batch N") that touch many areas; count one only when its subject or diff touches the behaviour. - Level: if a Rust unit or GTK test could catch the same regression as well, lower criticality by one. If only real input, real windows, real processes or real buses can show it, do not.
Diagnostic: how precisely a failure points at the cause
Judge what a failing run tells the person reading it: the failing step, its message, and how many modules could be behind it. Read the scenario as if each assertion had just failed.
| Score | Anchor | Strata examples |
|---|---|---|
| 5 | One behaviour, a few assertions, each naming exactly what broke; the failure line alone identifies the regression. | toolbar-controls-named ('Settings' is unnamed); modal-dialog-blocks-window-shortcuts (each check names the shortcut that leaked). |
| 4 | Focused on one behaviour with a short setup; a failure identifies the phase and the outcome, and one area of code is behind it. | dismissing-conflict-preserves-both (per-route variants, file checks); permanent-delete-symlinked-parent; appearance-menu-switches-view. |
| 3 | Several phases chained (dialog, operation, selection, focus, undo), or a heuristic check (a pixel count); a failure names the phase, but several modules could cause it. | keep-both-then-undo-remaining; filter-selection-uses-theme (accent pixel count). |
| 2 | A long walk-through, or many assertions without specific messages (derived geometry, bare expects); a failure needs the artifacts to interpret. |
shortcut-round-trip-keeps-selection (55 steps across three views); column-header-titles-bounded (unlabelled geometry comparisons). |
| 1 | A broad capture or tour where a failure says only that something differs: a percentage of changed pixels across the window. | icons-hover-baseline, list-view-baseline. |
Messages help only when they name the behaviour. "a dialog to appear"
before the behaviour under test is setup, not diagnosis. Diagnostic is
independent of coverage: a broad scenario can cover much and diagnose little.
Scoring formula (normative)
P = 3*(impact - 1) + 3*(criticality - 1) + 2*(diagnostic - 1) # 0..32
score = round_half_up(100 * (coverage - 1)/4 * P/32) # 0..100
= (50*(coverage - 1)*P + 32) // 64 # integer form
scoreis uniqueness × risk:(coverage - 1)/4is the share of the behaviour only this scenario verifies, andP/32is how much that behaviour matters and how well a failure is reported.- Risk weights: impact 3, criticality 3, diagnostic 2.
//is integer division rounding toward negative infinity. Every operand is non-negative, so it equals truncating division. The integer form is exactlyfloor(100*(coverage-1)*P/128 + 1/2): an exact half rounds up.- The runner and
quality.py scoreimplement the integer form. It matches the exact fraction for all 625 combinations of the four dimensions.
Why this shape: deleting a scenario loses only what it alone verifies, so uniqueness multiplies rather than adds. A duplicate is worth nothing however important its behaviour. Impact (harm if broken) and criticality (chance it breaks unseen) are the two halves of risk, so they weigh the same. Diagnostic weighs less: it changes what a failure costs to investigate, not whether the regression is caught.
| impact | coverage | criticality | diagnostic | P | score |
|---|---|---|---|---|---|
| 5 | 5 | 5 | 5 | 32 | 100 |
| 5 | 5 | 4 | 5 | 29 | 91 |
| 4 | 4 | 4 | 4 | 24 | 56 |
| 3 | 5 | 3 | 1 | 12 | 38 |
| 4 | 3 | 4 | 3 | 22 | 34 |
| 3 | 3 | 3 | 3 | 16 | 25 |
| 5 | 2 | 5 | 5 | 32 | 25 |
| 2 | 2 | 2 | 2 | 8 | 6 |
| 5 | 1 | 5 | 5 | 32 | 0 |
| 1 | 5 | 1 | 1 | 0 | 0 |
Reading a score
- Coverage caps the score: coverage 1 always scores 0, 2 at most 25, 3 at most 50, and 4 at most 75. A cosmetic scenario (coverage at most 2) never exceeds 25. A geometry-only scenario (coverage 1) scores 0.
- A score of 0 has two causes. Read the dimensions to see which:
- Coverage 1: the scenario adds nothing that is not verified elsewhere, or its unique part is geometry only. Its behaviour may still matter a great deal. Check that the note names the scenario or unit test that covers it, and that the named one really does, before acting on the 0.
- Impact, criticality and diagnostic all 1: the scenario is unique, but it guards something harmless and frozen, and its failure says little.
- A low score is not a verdict. The rubric only ranks. The dimensions say why a score is low, and the owner decides what to do with it.
The note
One line: the behaviour under test, then the deciding factor. Name the scenario or unit test it duplicates, or the unique route it alone covers.
paste-conflict dismissal by Cancel/Escape/Close keeps both files; only scenario covering dismissal routes
Icons switch via Appearance menu; appearance-menu-switches-view-list covers the same path for List
Keep it under about 140 characters, with no line-break character of any kind (see the field table). Do not restate the numbers or add a keep/drop verdict.
Scoring one scenario
-
Read the scenario. Header (
modes,variants,prefs,tree,setup,skip,known_bug), then every step, noting step-levelmodes,variants,when,ifandunlessguards. List what each variant and mode changes. -
State the behaviour under test in one sentence: the action, the route, and the outcome asserted. Separate the asserted outcome from setup and waits.
-
Find overlapping scenarios. Start with the folder, then the corpus:
ls tests/qa/scenarios/<folder>/ python3 .agents/skills/qa-quality/quality.py similar tests/qa/scenarios/<folder>/<name>.jsonl grep -lF '"name":"Keep Both"' tests/qa/scenarios/*/*.jsonl # same control grep -l '"key":"shift+Delete"' tests/qa/scenarios/*/*.jsonl # same chord grep -l '"accent_pixels"' tests/qa/scenarios/*/*.jsonl # same predicate grep -l '"pref":{"key":"browser_mode"' tests/qa/scenarios/*/*.jsonl # same preference grep -l '"symlink"' tests/qa/scenarios/*/*.jsonl # same fixture shape grep -l '"session_bus":true' tests/qa/scenarios/*/*.jsonl # same environment diff <(tail -n +2 A.jsonl) <(tail -n +2 B.jsonl) # step diff of a pairsimilarcompares literal steps, so it misses pairs that differ in one value (Icons vs List). Always follow up with greps on the controls, chords, predicates and preferences the scenario uses. -
Check unit and GTK test coverage. Find the implementing code from a name or string the scenario uses, then its adjacent tests:
grep -rn '"Keep Both"' src --include=*.rs | grep -v tests.rs grep -rn 'fn .*keep_both\|fn .*conflict' src --include=tests.rs ls src/ui/browser/transfer/tests.rs src/adapters/local_operations/tests/Decide which part of the behaviour those tests pin (logic, dialog contents, focus restoration) and which only the running application shows.
-
Check history for the area.
git log --oneline --since='90 days ago' -- <paths> | wc -l # churn git log --oneline -i --grep='^fix' -- <paths> # past fixes git log --format='%h %ad %s' --date=short -S'<distinctive string>' | tail -1 # originSearch
-Sfor a distinctive accessible name, message or test name from the scenario across the whole tree, not only the scenario file; the scenario file's own history starts at its port. An origin commit typedfixmeans the scenario guards a known regression. -
Score each dimension against the anchors and the calibration examples. When torn between two levels, pick the lower and say why in the note.
-
Compute
score:python3 .agents/skills/qa-quality/quality.py score I C K D(impact, coverage, criticality, diagnostic). -
Write the field.
python3 .agents/skills/qa-quality/quality.py write tests/qa/scenarios/<folder>/<name>.jsonl I C K D "<note>"It replaces an existing
qualityvalue in place, or adds it as the last header key, using compact JSON like the rest of the header, and refuses to write if anything else in the header would change or the note is not one line. It writes a temporary file beside the scenario and renames it over the original, so an interrupted write never leaves a truncated file. Every other byte of the file stays as it was. Editing by hand is allowed under the same rule: change only thequalityvalue on line 1. -
Have an independent grader check it. When an agent wrote or changed the scenario, a separate subagent on a cheap model (Haiku) grades it blind with this rubric: tell it to ignore the existing
qualityfield, to cite the overlapping tests, churn and fix commits behind each dimension, and to edit nothing. Compare its four dimensions with yours. Re-read the anchors and settle any dimension that differs by 2 or more, or ascorethat differs by more than 10; rewrite the field if you change your mind. Report both gradings in the handoff or PR, not in the note.
Applying the rubric to the whole corpus
- Calibrate first. Score the ten scenarios in calibration.md blind, then
compare. Discuss any dimension that differs by 2 or more, or any
scoreoff by more than 10, and re-read the anchors before going on. Every agent or reviewer taking part calibrates separately. - Score folder by folder. Read every file in a folder before scoring any
of them; most duplicates sit in the same folder (
*-remainingnames are usually a split of one behaviour). Settle duplicate pairs inside the folder first, then search the corpus. Fill in the worksheet row as you go. - Cross-folder consistency pass. When every folder is done, run
quality.py report(all folders, or named ones by bare name or path) and sort by each dimension. Rows marked!have a malformedqualityvalue; fix them first. Compare scenarios with the same score across folders: similar harm should have similar impact, and each duplicate pair must have exactly one low-coverage side. Re-score two calibration anchors after every 50 scenarios to catch drift. - Record overrides. A reviewer who disagrees changes the dimensions,
recomputes
scorewithquality.py write, and ends the note with; override: <reason>. Log who overrode what in the issue or PR, not in the tree. - Re-score when the evidence changes:
- the behaviour or its implementation changes, or the scenario's steps do;
- a scenario is added that overlaps it: re-score both;
- an overlapping scenario is removed: the survivor's coverage may rise;
- a unit or GTK test now pins the same logic;
- a bug is found in the area, or escapes past this scenario: raise criticality.
Variants and modes
- A mode or variant adds coverage only when it takes a distinct code path: a different key binding (Right in Icons, Down in List), a different widget (Columns panes vs List rows), a different preference, layout or lifecycle.
- Running view-independent behaviour (a dialog, a file operation after the selection is made) in three modes is one behaviour, not three.
- Look for step-level
"modes"or"variants"guards and mode-specific predicates. Their absence, on behaviour that does not depend on the view, means the extra runs are duplicates. - Variants that change only a value without changing the route
(
short/longnames that hit the same check) add nothing.
Pitfalls
- Don't score length or run count. A 5-step scenario guarding data loss outranks a 55-step menu walk-through. Length matters only through diagnostic, when it blurs what a failure means.
- Don't inflate coverage to avoid a 0. Coverage 1 scoring 0 is the intended reading of a full duplicate; record the duplicate in the note.
- Don't score diagnostic from message count. Judge what a real failure would tell you. Many generic messages diagnose less than one precise one.
- Don't reward duplicate modes or variants. See above.
- Don't penalise slowness. Run cost lives in the header's
costfield and is a separate decision. - Don't confuse flaky with low value. A flaky scenario on a race-prone path often guards the most critical behaviour. Score the behaviour; report the flakiness separately. Flakiness does not lower diagnostic either: score what a genuine failure would say.
- Score a
known_bugorskipscenario on the behaviour it guards, as if it passed. Being skipped is not a reason to lower any dimension. - Don't fold impact into coverage. A fully duplicated data-safety scenario keeps impact 5 and its criticality; coverage 1 alone makes its score 0, and the dimensions still show that the behaviour matters.
- Don't treat accessibility checks as cosmetic, or visual baselines as layout tests. Baselines are the sanctioned way to catch rendering regressions; their coverage cap is 2.
- Don't count waits as assertions. A
waitthat only synchronises (a dialog to appear, a pane to settle) is setup unless the behaviour under test is that it appears.
Version History
- d5ae6aa Current 2026-11-02 10:17


