Skip to content

Commit 70815db

Browse files
committed
merge: land axon-10629702 user-story tagging + coverage check [axon-10629702]
2 parents 978beed + 1bebf4e commit 70815db

9 files changed

Lines changed: 111 additions & 13 deletions

ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"test:e2e": "cd .. && AXON_E2E_STORAGE=postgres scripts/test-ui-e2e-docker.sh",
1414
"test:e2e:real": "cd .. && scripts/test-ui-e2e-docker.sh",
1515
"test:e2e:postgres": "cd .. && AXON_E2E_STORAGE=postgres scripts/test-ui-e2e-docker.sh",
16-
"test:e2e:sqlite": "cd .. && AXON_E2E_STORAGE=sqlite scripts/test-ui-e2e-docker.sh"
16+
"test:e2e:sqlite": "cd .. && AXON_E2E_STORAGE=sqlite scripts/test-ui-e2e-docker.sh",
17+
"check:story-coverage": "bun scripts/check-story-coverage.ts"
1718
},
1819
"devDependencies": {
1920
"@biomejs/biome": "^1.9.4",

ui/scripts/check-story-coverage.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* check-story-coverage.ts
4+
*
5+
* Verifies that every FEAT-031 user story (US-113..US-119) has at least one
6+
* Playwright test or describe block tagged with `@US-NNN` in ui/tests/e2e/.
7+
*
8+
* Usage:
9+
* bun scripts/check-story-coverage.ts
10+
*
11+
* Exits 0 if all stories are covered, non-zero otherwise.
12+
*/
13+
14+
import { readdirSync, readFileSync } from 'node:fs';
15+
import { join } from 'node:path';
16+
17+
const REQUIRED_STORIES = [113, 114, 115, 116, 117, 118, 119] as const;
18+
19+
const E2E_DIR = join(import.meta.dir, '..', 'tests', 'e2e');
20+
21+
// Collect all spec files
22+
const specFiles = readdirSync(E2E_DIR).filter((f) => f.endsWith('.spec.ts'));
23+
24+
// Map: US number -> array of spec filenames that contain a tag
25+
const coverage = new Map<number, string[]>();
26+
for (const story of REQUIRED_STORIES) {
27+
coverage.set(story, []);
28+
}
29+
30+
const tagPattern = /@US-(\d+)/g;
31+
32+
for (const file of specFiles) {
33+
const content = readFileSync(join(E2E_DIR, file), 'utf-8');
34+
const found = new Set<number>();
35+
let match: RegExpExecArray | null;
36+
tagPattern.lastIndex = 0;
37+
while ((match = tagPattern.exec(content)) !== null) {
38+
const num = parseInt(match[1], 10);
39+
if (REQUIRED_STORIES.includes(num as (typeof REQUIRED_STORIES)[number])) {
40+
found.add(num);
41+
}
42+
}
43+
for (const num of found) {
44+
coverage.get(num)?.push(file);
45+
}
46+
}
47+
48+
// Report
49+
console.log('Story coverage report (FEAT-031 / FEAT-015 / FEAT-016 / FEAT-029 / FEAT-030):');
50+
console.log('');
51+
52+
let allCovered = true;
53+
for (const story of REQUIRED_STORIES) {
54+
const specs = coverage.get(story) ?? [];
55+
if (specs.length === 0) {
56+
console.error(` MISSING US-${story} — no tagged test found`);
57+
allCovered = false;
58+
} else {
59+
console.log(` COVERED US-${story}${specs.join(', ')}`);
60+
}
61+
}
62+
63+
console.log('');
64+
65+
if (!allCovered) {
66+
const uncovered = REQUIRED_STORIES.filter((s) => (coverage.get(s) ?? []).length === 0);
67+
const label = uncovered.map((s) => `US-${s}`).join(', ');
68+
console.error(`uncovered: ${label}`);
69+
process.exit(1);
70+
}
71+
72+
console.log('All FEAT-031 stories have at least one live-server workflow test tag.');

ui/tests/e2e/approval-inbox.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {
44
TASK_COLLECTION,
55
activateProposedPolicy,
66
approveIntent,
7+
captureDataPlaneRequests,
78
createBudgetRecord,
89
dbIntentUrl,
910
dbIntentsUrl,
11+
expectGraphqlPrimaryDataPlane,
1012
patchBudgetRecordAs,
1113
previewBudgetIntent,
1214
proposedPolicyDraftDenyHigh,
@@ -21,12 +23,13 @@ async function selectStatus(page: Page, status: string) {
2123
}
2224

2325
test.describe('Approval inbox', () => {
24-
test('lists scoped intents across review states and opens detail', async ({ page, request }) => {
26+
test('lists scoped intents across review states and opens detail @US-117', async ({ page, request }) => {
2527
const db = await seedApprovalCollections(request, 'approval-inbox');
2628
const ids = await seedIntentStates(request, db);
2729
const foreignDb = await seedApprovalCollections(request, 'approval-foreign');
2830
await createBudgetRecord(request, foreignDb, TASK_COLLECTION, 'task-foreign');
2931
const foreign = await previewBudgetIntent(request, foreignDb, 'task-foreign', 20_000);
32+
const requests = captureDataPlaneRequests(page, db);
3033

3134
await routeGraphqlAs(page, 'finance-approver');
3235
await page.goto(dbIntentsUrl(db));
@@ -64,6 +67,8 @@ test.describe('Approval inbox', () => {
6467
await expect(page.getByTestId('intent-audit-trail')).toContainText('intent.approve');
6568
await expect(page.getByTestId('intent-audit-trail')).toContainText('approved');
6669
await expect(page.getByTestId('intent-deep-links')).toContainText('Open audit log');
70+
71+
expectGraphqlPrimaryDataPlane(requests, 'approval inbox route should stay GraphQL-primary');
6772
});
6873

6974
test('supports dense filters, keyboard selection, and inline review without leaving inbox', async ({
@@ -182,7 +187,7 @@ test.describe('Approval inbox', () => {
182187
await expect(page.getByTestId('intent-reason')).toHaveValue('lost role attempt');
183188
});
184189

185-
test('shows disabled action states for rejected, expired, committed, and stale intents', async ({
190+
test('shows disabled action states for rejected, expired, committed, and stale intents @US-118', async ({
186191
page,
187192
request,
188193
}) => {

ui/tests/e2e/graphql-policy-console.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
SCN017_COLLECTIONS,
44
SCN017_ROLES,
55
SCN017_SUBJECTS,
6+
captureDataPlaneRequests,
7+
expectGraphqlPrimaryDataPlane,
68
routeGraphqlAs,
79
seedScn017PolicyUiFixture,
810
} from './helpers';
@@ -12,9 +14,10 @@ function escapeRegExp(value: string): string {
1214
}
1315

1416
test.describe('GraphQL policy console', () => {
15-
test('opens an effectivePolicy preset from the policy workspace', async ({ page, request }) => {
17+
test('opens an effectivePolicy preset from the policy workspace @US-113', async ({ page, request }) => {
1618
const fixture = await seedScn017PolicyUiFixture(request, 'graphql-policy-console-effective');
1719
const policiesUrl = `/ui/tenants/${encodeURIComponent(fixture.tenant.db_name)}/databases/${encodeURIComponent(fixture.db.name)}/policies`;
20+
const requests = captureDataPlaneRequests(page, fixture.db);
1821

1922
await routeGraphqlAs(page, SCN017_SUBJECTS.contractor);
2023
await page.goto(policiesUrl);
@@ -46,6 +49,8 @@ test.describe('GraphQL policy console', () => {
4649
await expect(page.getByTestId('graphql-response')).toContainText('"canRead": true');
4750
await expect(page.getByTestId('graphql-response')).toContainText('amount_cents');
4851
await expect(page.getByTestId('graphql-response')).toContainText('commercial_terms');
52+
53+
expectGraphqlPrimaryDataPlane(requests, 'graphql policy console route should stay GraphQL-primary');
4954
});
5055

5156
test('opens an explainPolicy preset from the policy workspace', async ({ page, request }) => {

ui/tests/e2e/intent-audit-lineage.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
TASK_COLLECTION,
77
activateProposedPolicy,
88
approveIntent,
9+
captureDataPlaneRequests,
910
commitIntent,
1011
createBudgetRecord,
1112
dbAuditUrl,
1213
dbIntentUrl,
1314
dbIntentsUrl,
15+
expectGraphqlPrimaryDataPlane,
1416
graphqlPath,
1517
patchBudgetRecordAs,
1618
previewBudgetIntent,
@@ -22,12 +24,13 @@ import {
2224
} from './helpers';
2325

2426
test.describe('Intent audit lineage', () => {
25-
test('shows delegated MCP intent metadata in the inbox and detail panels', async ({
27+
test('shows delegated MCP intent metadata in the inbox and detail panels @US-119', async ({
2628
page,
2729
request,
2830
}) => {
2931
const db = await seedApprovalCollections(request, 'intent-audit-lineage');
3032
const ids = await seedIntentStates(request, db);
33+
const requests = captureDataPlaneRequests(page, db);
3134

3235
await routeGraphqlAs(page, 'finance-approver');
3336
await page.goto(dbIntentsUrl(db));
@@ -68,9 +71,11 @@ test.describe('Intent audit lineage', () => {
6871
await expect(page.getByTestId('intent-tool-arguments')).not.toContainText('23000');
6972
await expect(page.getByTestId('intent-structured-outcome')).toContainText('needs_approval');
7073
await expect(page.getByTestId('intent-structured-outcome')).toContainText('pending');
74+
75+
expectGraphqlPrimaryDataPlane(requests, 'intent audit lineage route should stay GraphQL-primary');
7176
});
7277

73-
test('shows conflict outcomes for stale MCP-originated intent commits', async ({
78+
test('shows conflict outcomes for stale MCP-originated intent commits @US-118', async ({
7479
page,
7580
request,
7681
}) => {
@@ -148,7 +153,7 @@ test.describe('Intent audit lineage', () => {
148153
});
149154

150155
test.describe('Intent audit deep link and lineage panel', () => {
151-
test('deep link filters /audit by intent ID and pre-populates filter', async ({
156+
test('deep link filters /audit by intent ID and pre-populates filter @US-116', async ({
152157
page,
153158
request,
154159
}) => {

ui/tests/e2e/mcp-envelope-preview.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function escapeRegExp(value: string): string {
1717
}
1818

1919
test.describe('MCP envelope preview', () => {
20-
test('mirrors explainPolicy outcomes for read, needs_approval, and denied flows', async ({
20+
test('mirrors explainPolicy outcomes for read, needs_approval, and denied flows @US-119', async ({
2121
page,
2222
request,
2323
}) => {

ui/tests/e2e/mutation-intents.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
TASK_COLLECTION,
55
type TestDatabase,
66
activateProposedPolicy,
7+
captureDataPlaneRequests,
78
dbCollectionUrl,
89
dbIntentUrl,
10+
expectGraphqlPrimaryDataPlane,
911
patchBudgetRecordAs,
1012
proposedPolicyDraftDenyHigh,
1113
routeGraphqlAs,
@@ -52,11 +54,12 @@ async function previewIntent(page: Page): Promise<PreviewPayload> {
5254
}
5355

5456
test.describe('Mutation intents', () => {
55-
test('renders and commits an allowed mutation intent without showing the token', async ({
57+
test('renders and commits an allowed mutation intent without showing the token @US-116', async ({
5658
page,
5759
request,
5860
}) => {
5961
const db = await seedIntentCollection(request, 'intent-allow');
62+
const requests = captureDataPlaneRequests(page, db);
6063
await routeGraphqlAs(page, 'finance-agent');
6164
await openEntityEditor(page, db);
6265
await fillJsonField(page, 'budget_cents', '6000');
@@ -77,6 +80,8 @@ test.describe('Mutation intents', () => {
7780
await page.getByTestId('intent-commit').click();
7881
await expect(page.getByText('Saved v2.')).toBeVisible({ timeout: 10_000 });
7982
await expect(modal).toContainText('committed');
83+
84+
expectGraphqlPrimaryDataPlane(requests, 'mutation intents route should stay GraphQL-primary');
8085
});
8186

8287
test('renders a needs-approval preview with approval route details', async ({
@@ -119,7 +124,7 @@ test.describe('Mutation intents', () => {
119124
await expect(page.getByTestId('intent-commit')).toBeDisabled();
120125
});
121126

122-
test('renders stale pre-image conflict details after preview drift', async ({
127+
test('renders stale pre-image conflict details after preview drift @US-118', async ({
123128
page,
124129
request,
125130
}) => {

ui/tests/e2e/policy-authoring.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ async function routeGraphqlWithDraft(
144144
}
145145

146146
test.describe('Policy authoring', () => {
147-
test('runs read, patch, and transaction policy evaluations from the workspace', async ({
147+
test('runs read, patch, and transaction policy evaluations from the workspace @US-113 @US-114', async ({
148148
page,
149149
request,
150150
}) => {
@@ -612,7 +612,7 @@ test.describe('Policy authoring (transaction-row delta)', () => {
612612
});
613613

614614
test.describe('Policy authoring (schemas tab)', () => {
615-
test('compile + fixture dry-run + activate updates the persisted policy', async ({
615+
test('compile + fixture dry-run + activate updates the persisted policy @US-114', async ({
616616
page,
617617
request,
618618
}) => {

ui/tests/e2e/policy-enforcement.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
SCN017_COLLECTIONS,
55
SCN017_SUBJECTS,
66
activateProposedPolicy,
7+
captureDataPlaneRequests,
78
createTestEntity,
89
createTestLink,
910
dbAuditUrl,
1011
dbCollectionUrl,
1112
dbCollectionsUrl,
13+
expectGraphqlPrimaryDataPlane,
1214
proposedPolicyDraftDenyHigh,
1315
routeGraphqlAs,
1416
seedScn017PolicyUiFixture,
@@ -57,14 +59,15 @@ function captureSubscribeFrame(page: Page): Promise<unknown> {
5759
*/
5860

5961
test.describe('Policy enforcement (UI redaction)', () => {
60-
test('contractor sees redacted commercial_terms across list, detail, and audit views', async ({
62+
test('contractor sees redacted commercial_terms across list, detail, and audit views @US-115', async ({
6163
page,
6264
request,
6365
}) => {
6466
const fixture = await seedScn017PolicyUiFixture(request, 'policy-enforcement-redaction');
6567
const sensitiveCommercialTerms = 'net-15 expedited infrastructure terms';
6668
const collectionUrl = dbCollectionUrl(fixture.db, SCN017_COLLECTIONS.invoices);
6769
const auditUrl = dbAuditUrl(fixture.db);
70+
const requests = captureDataPlaneRequests(page, fixture.db);
6871

6972
await routeGraphqlAs(page, SCN017_SUBJECTS.contractor);
7073
await page.goto(collectionUrl);
@@ -112,6 +115,8 @@ test.describe('Policy enforcement (UI redaction)', () => {
112115
.click({ trial: false });
113116
const auditHtml = await page.content();
114117
expect(auditHtml).not.toContain(sensitiveCommercialTerms);
118+
119+
expectGraphqlPrimaryDataPlane(requests, 'policy enforcement route should stay GraphQL-primary');
115120
});
116121

117122
test('denied delete surfaces stable code, reason, and policy explanation', async ({

0 commit comments

Comments
 (0)