Skip to content

Commit 50695c0

Browse files
committed
fix(e2e): clean live-service fixture tenants [axon-abbb9c3f]
1 parent 7786436 commit 50695c0

6 files changed

Lines changed: 91 additions & 6 deletions

File tree

.ddx/beads.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
{"claimed-at":"2026-04-06T17:30:20Z","claimed-machine":"sindri","claimed-pid":"3138994","created_at":"2026-04-06T12:15:10.930369412Z","description":"Tailscale whois identity. Audit shows node name. Reject non-tailnet. Default role for untagged.","id":"axon-a8e5637e","issue_type":"task","labels":["helix","build","auth"],"owner":"erik","parent":"axon-cf0472fd","priority":2,"spec-id":"FEAT-012","status":"closed","title":"US-043: Authenticate via Tailscale (FEAT-012)","updated_at":"2026-04-06T17:31:42.061896654Z"}
191191
{"acceptance":"Read-role gRPC clients get PermissionDenied on write RPCs; write-role clients get PermissionDenied on admin RPCs; admin clients succeed on all RPCs; parity with HTTP role boundaries","claimed-at":"2026-04-10T21:00:41Z","claimed-machine":"sindri","claimed-pid":"2445000","created_at":"2026-04-10T02:49:30.80530248Z","description":"Wire identity.role into axon-server gRPC (tonic) service handlers in service.rs. Apply the same role boundaries as the HTTP gateway: read ops require Read+, write ops require Write+, admin ops require Admin. Return tonic::Code::PermissionDenied on violation. Identity is already available via the auth interceptor.","id":"axon-a9e2624e","issue_type":"task","labels":["helix","blocked"],"notes":"BLOCKED [2026-04-10T15:27:52-04:00]: intractable after 4 attempts with exponential backoff","owner":"erik","priority":1,"status":"closed","title":"feat(auth): enforce RBAC role checks in gRPC service handlers","updated_at":"2026-04-10T21:50:41.91953084Z"}
192192
{"claimed-at":"2026-04-06T17:32:44Z","claimed-machine":"sindri","claimed-pid":"3188796","created_at":"2026-04-06T12:15:11.019843436Z","description":"Per-collection write policies. Field immutability. Policy storage in __axon_policies__.","id":"axon-aa7c1409","issue_type":"task","labels":["helix","build","auth"],"owner":"erik","parent":"axon-cf0472fd","priority":3,"spec-id":"FEAT-012","status":"closed","title":"US-047: Attribute-based write control (FEAT-012)","updated_at":"2026-04-06T17:33:57.546819769Z"}
193-
{"acceptance":"The live-service Playwright workflow either runs against isolated temporary data paths or tears down all tenants/users it creates; a post-run assertion verifies no known E2E tenant prefixes remain in the local service.","created_at":"2026-04-20T20:11:25.402593111Z","description":"Running the full Playwright suite against an installed persistent local service creates many tenants/users. Manual cleanup currently requires ad hoc API/sqlite cleanup, and tenant deletion gaps can leave rows behind.","id":"axon-abbb9c3f","issue_type":"task","labels":["area:ui","test","e2e"],"priority":2,"status":"open","title":"Live-service Playwright runs should clean up fixture tenants","updated_at":"2026-04-20T20:11:25.402593111Z"}
193+
{"acceptance":"The live-service Playwright workflow either runs against isolated temporary data paths or tears down all tenants/users it creates; a post-run assertion verifies no known E2E tenant prefixes remain in the local service.","claimed-at":"2026-04-20T21:30:39Z","claimed-machine":"sindri","claimed-pid":"3372203","created_at":"2026-04-20T20:11:25.402593111Z","description":"Running the full Playwright suite against an installed persistent local service creates many tenants/users. Manual cleanup currently requires ad hoc API/sqlite cleanup, and tenant deletion gaps can leave rows behind.","execute-loop-heartbeat-at":"2026-04-20T21:30:39.117519456Z","id":"axon-abbb9c3f","issue_type":"task","labels":["area:ui","test","e2e"],"owner":"erik","priority":2,"status":"closed","title":"Live-service Playwright runs should clean up fixture tenants","updated_at":"2026-04-20T21:39:23.807369421Z"}
194194
{"acceptance":"Templates are stored and versioned independently from CollectionSchema. Schema version does not bump when only template changes. Evolution compatibility checker is unaffected by template additions/edits.","claimed-at":"2026-04-08T21:09:26Z","claimed-machine":"sindri","claimed-pid":"1549901","created_at":"2026-04-07T15:56:52.884081682Z","description":"Markdown templates are a presentation concern; CollectionSchema is a validation concern. Storing templates as a field on CollectionSchema causes schema version inflation on cosmetic changes, couples evolution analysis to presentation, and adds a rendering dependency to axon-schema. Design templates as a sibling concept (e.g. CollectionView) with independent versioning.","id":"axon-abf94a4d","issue_type":"epic","labels":["helix","blocked"],"notes":"\u003cmeasure-results\u003e\n \u003ctimestamp\u003e2026-04-08T21:14:33Z\u003c/timestamp\u003e\n \u003cstatus\u003ePASS\u003c/status\u003e\n \u003cacceptance\u003e\n \u003ccriterion name='templates-independent-from-schema' status='pass' evidence='Added CollectionView storage/versioning in axon-schema and axon-storage adapters.'/\u003e\n \u003ccriterion name='schema-version-stable-on-template-change' status='pass' evidence='Conformance tests assert CollectionSchema version remains 1 while CollectionView versions advance.'/\u003e\n \u003ccriterion name='evolution-checker-unaffected' status='pass' evidence='Conformance tests diff unchanged entity schemas after view updates and get MetadataOnly with no changes.'/\u003e\n \u003c/acceptance\u003e\n \u003cgates\u003e\n \u003cgate concern='rust-cargo' command='cargo test -p axon-schema -p axon-storage' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo clippy -p axon-schema -p axon-storage -- -D warnings' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo check -p axon-schema -p axon-storage' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo check' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo test' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo clippy -- -D warnings' status='pass'/\u003e\n \u003cgate concern='rust-cargo' command='cargo fmt --check' status='pass'/\u003e\n \u003c/gates\u003e\n \u003cratchets\u003e\u003c/ratchets\u003e\n\u003c/measure-results\u003e","owner":"erik","priority":1,"status":"closed","title":"FEAT-026: Separate markdown template storage from CollectionSchema","updated_at":"2026-04-08T21:16:07.7940066Z"}
195195
{"claimed-at":"2026-04-06T17:34:56Z","claimed-machine":"sindri","claimed-pid":"3227647","created_at":"2026-04-06T12:15:10.493620381Z","description":"beads.aggregate tool auto-generated. Structured response.","id":"axon-ac66ee66","issue_type":"task","labels":["helix","build","api"],"owner":"erik","parent":"axon-cf0472fd","priority":3,"spec-id":"FEAT-018","status":"closed","title":"US-065: Aggregate via MCP (FEAT-018)","updated_at":"2026-04-06T17:35:56.793245828Z"}
196196
{"claimed-at":"2026-04-05T20:39:15Z","claimed-machine":"sindri","claimed-pid":"1483355","created_at":"2026-04-05T01:57:29.664992664Z","dependencies":[{"issue_id":"axon-accb40b2","depends_on_id":"axon-d03dd28c","type":"blocks","created_at":"2026-04-05T01:57:43Z"},{"issue_id":"axon-accb40b2","depends_on_id":"axon-f48352d5","type":"blocks","created_at":"2026-04-05T01:57:43Z"},{"issue_id":"axon-accb40b2","depends_on_id":"axon-515aa615","type":"blocks","created_at":"2026-04-05T01:57:43Z"}],"description":"Test plan specifies 10 business scenarios grounded in use case research. None implemented. This is the highest priority gap per Principle P1.","id":"axon-accb40b2","issue_type":"task","labels":["helix","phase:test","kind:execution","priority:critical"],"owner":"erik","priority":0,"status":"closed","title":"Implement test plan L2: business scenario tests (SCN-001 through SCN-010)","updated_at":"2026-04-05T20:48:17.672343872Z"}

ui/playwright.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const baseURL = process.env.AXON_E2E_BASE_URL ?? 'http://localhost:4170';
1515
*/
1616
export default defineConfig({
1717
testDir: './tests/e2e',
18+
globalSetup: './tests/e2e/cleanup-fixtures.ts',
19+
globalTeardown: './tests/e2e/cleanup-fixtures.ts',
1820
fullyParallel: false,
1921
forbidOnly: !!process.env.CI,
2022
retries: process.env.CI ? 2 : 0,

ui/tests/e2e/cleanup-fixtures.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { request as playwrightRequest } from '@playwright/test';
2+
import { E2E_FIXTURE_PREFIX } from './helpers';
3+
4+
const baseURL = process.env.AXON_E2E_BASE_URL ?? 'http://localhost:4170';
5+
6+
type Tenant = {
7+
id: string;
8+
name?: string;
9+
db_name?: string;
10+
dbName?: string;
11+
};
12+
13+
type User = {
14+
id: string;
15+
display_name?: string;
16+
displayName?: string;
17+
};
18+
19+
function tenantMatches(tenant: Tenant): boolean {
20+
const values = [tenant.name, tenant.db_name, tenant.dbName].filter(Boolean) as string[];
21+
return values.some((value) => value.startsWith(E2E_FIXTURE_PREFIX));
22+
}
23+
24+
function userMatches(user: User): boolean {
25+
const displayName = user.display_name ?? user.displayName ?? '';
26+
return displayName.startsWith(E2E_FIXTURE_PREFIX);
27+
}
28+
29+
async function cleanupFixtures() {
30+
const context = await playwrightRequest.newContext({ baseURL });
31+
try {
32+
const tenantsResponse = await context.get('/control/tenants');
33+
if (tenantsResponse.ok()) {
34+
const body = await tenantsResponse.json();
35+
const tenants = (body.tenants ?? []) as Tenant[];
36+
for (const tenant of tenants.filter(tenantMatches)) {
37+
const response = await context.delete(`/control/tenants/${encodeURIComponent(tenant.id)}`);
38+
if (!response.ok()) {
39+
throw new Error(
40+
`failed to delete E2E tenant ${tenant.id}: ${response.status()} ${await response.text()}`,
41+
);
42+
}
43+
}
44+
}
45+
46+
const usersResponse = await context.get('/control/users/list');
47+
if (usersResponse.ok()) {
48+
const body = await usersResponse.json();
49+
const users = (body.users ?? []) as User[];
50+
for (const user of users.filter(userMatches)) {
51+
const response = await context.delete(`/control/users/suspend/${encodeURIComponent(user.id)}`);
52+
if (!response.ok()) {
53+
throw new Error(
54+
`failed to suspend E2E user ${user.id}: ${response.status()} ${await response.text()}`,
55+
);
56+
}
57+
}
58+
}
59+
60+
const verifyResponse = await context.get('/control/tenants');
61+
if (verifyResponse.ok()) {
62+
const body = await verifyResponse.json();
63+
const leftovers = ((body.tenants ?? []) as Tenant[]).filter(tenantMatches);
64+
if (leftovers.length > 0) {
65+
throw new Error(
66+
`E2E tenant cleanup left ${leftovers.length} tenant(s): ${leftovers
67+
.map((tenant) => `${tenant.id}:${tenant.name ?? tenant.db_name ?? tenant.dbName ?? ''}`)
68+
.join(', ')}`,
69+
);
70+
}
71+
}
72+
} finally {
73+
await context.dispose();
74+
}
75+
}
76+
77+
export default cleanupFixtures;

ui/tests/e2e/helpers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export type TestDatabase = {
2020
name: string;
2121
};
2222

23+
export const E2E_FIXTURE_PREFIX = 'e2e-';
24+
25+
function withE2eFixturePrefix(value: string): string {
26+
return value.startsWith(E2E_FIXTURE_PREFIX) ? value : `${E2E_FIXTURE_PREFIX}${value}`;
27+
}
28+
2329
async function expectOkResponse(response: APIResponse, label: string) {
2430
if (response.ok()) return;
2531
let body = '';
@@ -37,7 +43,7 @@ export async function createTestTenant(
3743
prefix: string,
3844
): Promise<TestTenant> {
3945
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
40-
const name = `${prefix}${suffix}`;
46+
const name = `${withE2eFixturePrefix(prefix)}-${suffix}`;
4147
const response = await request.post('/control/tenants', {
4248
data: { name },
4349
});
@@ -163,7 +169,7 @@ export async function createTestUser(
163169
displayName?: string,
164170
email?: string | null,
165171
): Promise<TestUser> {
166-
const name = displayName ?? `test-${Date.now().toString(36)}`;
172+
const name = withE2eFixturePrefix(displayName ?? `test-${Date.now().toString(36)}`);
167173
const response = await request.post('/control/users/provision', {
168174
data: { display_name: name, email: email ?? null },
169175
});

ui/tests/e2e/smoke-restructure.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { createTestTenant, tenantUrl } from './helpers';
1616
*/
1717
test.describe('UI restructure smoke', () => {
1818
const unique = Date.now().toString(36);
19-
const tenantName = `smoke${unique}`;
19+
const tenantName = `e2e-smoke-${unique}`;
2020
const dbName = 'first';
2121
const collectionName = `tasks-${unique}`;
2222
const entityId = `task-${unique}`;

ui/tests/e2e/tenant-admin.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ test.describe('Tenant members — add, change role, remove', () => {
137137
});
138138

139139
test.describe('Global users ACL — add, change, remove', () => {
140-
const login = `test-user-${Date.now().toString(36)}`;
141-
const displayName = `Provisioned ${Date.now().toString(36)}`;
140+
const login = `e2e-test-user-${Date.now().toString(36)}`;
141+
const displayName = `e2e-provisioned-${Date.now().toString(36)}`;
142142
const email = `${login}@example.test`;
143143

144144
test('add a user, change role, remove', async ({ page }) => {

0 commit comments

Comments
 (0)