@fuzdev/fuz_app 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/actions/action_codegen.d.ts.map +1 -1
  2. package/dist/actions/action_event.d.ts.map +1 -1
  3. package/dist/actions/action_event.js +6 -0
  4. package/dist/actions/action_event_data.d.ts.map +1 -1
  5. package/dist/actions/action_event_helpers.d.ts +1 -1
  6. package/dist/actions/action_event_helpers.d.ts.map +1 -1
  7. package/dist/actions/action_event_types.d.ts.map +1 -1
  8. package/dist/actions/action_peer.d.ts.map +1 -1
  9. package/dist/actions/request_tracker.svelte.d.ts.map +1 -1
  10. package/dist/actions/transports.d.ts.map +1 -1
  11. package/dist/actions/transports_http.d.ts.map +1 -1
  12. package/dist/actions/transports_ws.d.ts.map +1 -1
  13. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  14. package/dist/auth/account_routes.d.ts +30 -0
  15. package/dist/auth/account_routes.d.ts.map +1 -1
  16. package/dist/auth/account_routes.js +44 -9
  17. package/dist/auth/admin_routes.d.ts.map +1 -1
  18. package/dist/auth/admin_routes.js +33 -2
  19. package/dist/auth/audit_log_routes.d.ts +2 -1
  20. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  21. package/dist/auth/audit_log_routes.js +11 -2
  22. package/dist/auth/audit_log_schema.d.ts +1 -1
  23. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  24. package/dist/auth/audit_log_schema.js +3 -1
  25. package/dist/auth/permit_queries.d.ts +19 -0
  26. package/dist/auth/permit_queries.d.ts.map +1 -1
  27. package/dist/auth/permit_queries.js +21 -0
  28. package/dist/auth/request_context.d.ts +10 -0
  29. package/dist/auth/request_context.d.ts.map +1 -1
  30. package/dist/auth/request_context.js +14 -0
  31. package/dist/hono_context.d.ts +7 -0
  32. package/dist/hono_context.d.ts.map +1 -1
  33. package/dist/realtime/sse.d.ts +0 -2
  34. package/dist/realtime/sse.d.ts.map +1 -1
  35. package/dist/realtime/sse_auth_guard.d.ts +23 -3
  36. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  37. package/dist/realtime/sse_auth_guard.js +38 -2
  38. package/dist/realtime/subscriber_registry.d.ts +62 -17
  39. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  40. package/dist/realtime/subscriber_registry.js +64 -21
  41. package/dist/server/validate_nginx.d.ts.map +1 -1
  42. package/dist/server/validate_nginx.js +61 -7
  43. package/dist/testing/admin_integration.d.ts.map +1 -1
  44. package/dist/testing/admin_integration.js +8 -8
  45. package/dist/testing/app_server.d.ts +9 -0
  46. package/dist/testing/app_server.d.ts.map +1 -1
  47. package/dist/testing/app_server.js +4 -3
  48. package/dist/testing/data_exposure.d.ts.map +1 -1
  49. package/dist/testing/data_exposure.js +1 -20
  50. package/dist/testing/error_coverage.d.ts +93 -27
  51. package/dist/testing/error_coverage.d.ts.map +1 -1
  52. package/dist/testing/error_coverage.js +160 -67
  53. package/dist/testing/integration.d.ts.map +1 -1
  54. package/dist/testing/integration.js +6 -6
  55. package/dist/testing/integration_helpers.d.ts +17 -5
  56. package/dist/testing/integration_helpers.d.ts.map +1 -1
  57. package/dist/testing/integration_helpers.js +37 -1
  58. package/dist/testing/round_trip.d.ts.map +1 -1
  59. package/dist/testing/round_trip.js +41 -55
  60. package/dist/testing/sse_round_trip.d.ts +64 -0
  61. package/dist/testing/sse_round_trip.d.ts.map +1 -0
  62. package/dist/testing/sse_round_trip.js +241 -0
  63. package/dist/ui/AdminOverview.svelte +1 -0
  64. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  65. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAomCF,CAAC"}
1
+ {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAwnCF,CAAC"}
@@ -130,9 +130,9 @@ export const describe_standard_admin_integration_tests = (options) => {
130
130
  headers: test_app.create_session_headers(),
131
131
  });
132
132
  assert.strictEqual(res.status, 403);
133
- error_collector.record(test_app.route_specs, 'GET', accounts_route.path, 403);
134
- const body = await res.json();
133
+ const body = await res.clone().json();
135
134
  assert.strictEqual(body.error, 'insufficient_permissions');
135
+ await error_collector.assert_and_record(test_app.route_specs, 'GET', accounts_route.path, res);
136
136
  });
137
137
  });
138
138
  // --- 2. Permit grant lifecycle ---
@@ -167,9 +167,9 @@ export const describe_standard_admin_integration_tests = (options) => {
167
167
  body: JSON.stringify({ role: ROLE_KEEPER }),
168
168
  });
169
169
  assert.strictEqual(res.status, 403);
170
- error_collector.record(test_app.route_specs, 'POST', grant_route.path, 403);
171
- const body = await res.json();
170
+ const body = await res.clone().json();
172
171
  assert.strictEqual(body.error, 'role_not_web_grantable');
172
+ await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
173
173
  });
174
174
  test('granting same role twice is idempotent (returns same permit)', async () => {
175
175
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -227,9 +227,9 @@ export const describe_standard_admin_integration_tests = (options) => {
227
227
  body: JSON.stringify({ role: grantable_role }),
228
228
  });
229
229
  assert.strictEqual(res.status, 404);
230
- error_collector.record(test_app.route_specs, 'POST', grant_route.path, 404);
231
- const body = await res.json();
230
+ const body = await res.clone().json();
232
231
  assert.strictEqual(body.error, 'account_not_found');
232
+ await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
233
233
  });
234
234
  test('admin can revoke a permit', async () => {
235
235
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -309,9 +309,9 @@ export const describe_standard_admin_integration_tests = (options) => {
309
309
  headers: test_app.create_session_headers(),
310
310
  });
311
311
  assert.strictEqual(second.status, 404);
312
- error_collector.record(test_app.route_specs, 'POST', revoke_route.path, 404);
313
- const body = await second.json();
312
+ const body = await second.clone().json();
314
313
  assert.strictEqual(body.error, 'permit_not_found');
314
+ await error_collector.assert_and_record(test_app.route_specs, 'POST', revoke_route.path, second);
315
315
  });
316
316
  });
317
317
  // --- 3. Admin session management ---
@@ -17,6 +17,7 @@ import { type Keyring } from '../auth/keyring.js';
17
17
  import type { Db, DbType } from '../db/db.js';
18
18
  import type { PasswordHashDeps } from '../auth/password.js';
19
19
  import { type SessionOptions } from '../auth/session_cookie.js';
20
+ import type { AuditLogEvent } from '../auth/audit_log_schema.js';
20
21
  import type { AppBackend } from '../server/app_backend.js';
21
22
  import { type AppServerOptions, type AppServerContext } from '../server/app_server.js';
22
23
  import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
@@ -100,6 +101,14 @@ export interface TestAppServerOptions {
100
101
  password_value?: string;
101
102
  /** Roles to grant. Default: `[ROLE_KEEPER]`. */
102
103
  roles?: Array<string>;
104
+ /**
105
+ * Backend audit event callback — wired into `backend.deps.on_audit_event`.
106
+ * When `audit_log_sse: true` is passed to `create_app_server`, this runs
107
+ * after the audit SSE broadcast (composed downstream by app_server).
108
+ * Use to wire consumer SSE auth guards in tests.
109
+ * Default: no-op.
110
+ */
111
+ on_audit_event?: (event: AuditLogEvent) => void;
103
112
  }
104
113
  export declare const create_test_app_server: (options: TestAppServerOptions) => Promise<TestAppServer>;
105
114
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAUrD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAIhC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AASjD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,2BAA2B,KAClC,OAAO,CAAC;IACV,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB,CAyCA,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAChD,gCAAgC;IAChC,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,uCAAuC;IACvC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kGAAkG;IAClG,EAAE,CAAC,EAAE,EAAE,CAAC;IACR,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yHAAyH;IACzH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAChD;AAqBD,eAAO,MAAM,sBAAsB,GAClC,SAAS,oBAAoB,KAC3B,OAAO,CAAC,aAAa,CAuFvB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IACjE,yEAAyE;IACzE,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IACpE,gHAAgH;IAChH,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,8DAA8D;IAC9D,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,cAAc,CAAC;IAC7B,OAAO,EAAE,UAAU,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9B,kEAAkE;IAClE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,gEAAgE;IAChE,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClF,iEAAiE;IACjE,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxF,qDAAqD;IACrD,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KACtB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,oBAAoB,KAAG,OAAO,CAAC,OAAO,CAkGpF,CAAC"}
@@ -88,7 +88,8 @@ export const bootstrap_test_account = async (options) => {
88
88
  /** Silent logger for tests — suppresses all output. */
89
89
  const test_log = new Logger('test', { level: 'off' });
90
90
  export const create_test_app_server = async (options) => {
91
- const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER], } = options;
91
+ const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER], on_audit_event = () => { }, // eslint-disable-line @typescript-eslint/no-empty-function
92
+ } = options;
92
93
  // Keyring from test secret
93
94
  const keyring_result = create_validated_keyring(TEST_COOKIE_SECRET);
94
95
  if (!keyring_result.ok) {
@@ -117,7 +118,7 @@ export const create_test_app_server = async (options) => {
117
118
  password,
118
119
  db: existing_db,
119
120
  log: test_log,
120
- on_audit_event: () => { }, // eslint-disable-line @typescript-eslint/no-empty-function
121
+ on_audit_event,
121
122
  ...fs_stubs,
122
123
  },
123
124
  };
@@ -137,7 +138,7 @@ export const create_test_app_server = async (options) => {
137
138
  password,
138
139
  db,
139
140
  log: test_log,
140
- on_audit_event: () => { }, // eslint-disable-line @typescript-eslint/no-empty-function
141
+ on_audit_event,
141
142
  ...fs_stubs,
142
143
  },
143
144
  };
@@ -1 +1 @@
1
- {"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAc9D;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAcF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,wCAAwC;IACxC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4CAA4C;IAC5C,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAmC/E,CAAC"}
1
+ {"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAe9D;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAcF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,wCAAwC;IACxC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4CAA4C;IAC5C,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAmC/E,CAAC"}
@@ -18,7 +18,7 @@ import { resolve_valid_path, generate_valid_body } from './schema_generators.js'
18
18
  import { run_migrations } from '../db/migrate.js';
19
19
  import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
20
20
  import { is_null_schema, is_strict_object_schema } from '../http/schema_helpers.js';
21
- import { SENSITIVE_FIELD_BLOCKLIST, ADMIN_ONLY_FIELD_BLOCKLIST, assert_no_sensitive_fields_in_json, } from './integration_helpers.js';
21
+ import { SENSITIVE_FIELD_BLOCKLIST, ADMIN_ONLY_FIELD_BLOCKLIST, assert_no_sensitive_fields_in_json, pick_auth_headers, } from './integration_helpers.js';
22
22
  // --- Schema introspection ---
23
23
  /**
24
24
  * Recursively collect all property names from a JSON Schema.
@@ -275,22 +275,3 @@ const describe_data_exposure_runtime_tests = (options) => {
275
275
  });
276
276
  }
277
277
  };
278
- /**
279
- * Pick auth headers matching a route spec's auth requirement.
280
- */
281
- const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
282
- switch (spec.auth.type) {
283
- case 'none':
284
- return { host: 'localhost', origin: 'http://localhost:5173' };
285
- case 'authenticated':
286
- return authed_account.create_session_headers();
287
- case 'role':
288
- if (spec.auth.role === ROLE_ADMIN) {
289
- return admin_account.create_session_headers();
290
- }
291
- // keeper role uses the bootstrapped account
292
- return test_app.create_session_headers();
293
- case 'keeper':
294
- return test_app.create_daemon_token_headers();
295
- }
296
- };
@@ -1,52 +1,116 @@
1
1
  import './assert_dev_env.js';
2
+ /**
3
+ * Error reachability coverage tracking.
4
+ *
5
+ * Tracks which declared error statuses (and specific error codes) are
6
+ * actually exercised in tests. `ErrorCoverageCollector` records status
7
+ * codes (optionally with body `error` codes) observed during test runs,
8
+ * then `assert_error_coverage` compares against declared error schemas
9
+ * to find uncovered error paths — reporting per-code when the declared
10
+ * schema is a literal or enum, per-status otherwise.
11
+ *
12
+ * @module
13
+ */
14
+ import { z } from 'zod';
2
15
  import type { RouteSpec } from '../http/route_spec.js';
3
16
  /**
4
- * Tracks which route × status combinations have been exercised in tests.
17
+ * Extract declared error code values from an error response schema.
18
+ *
19
+ * Recognizes schemas shaped like `z.object({error: z.literal(...)})` or
20
+ * `z.object({error: z.enum([...])})` (incl. `looseObject`/`strictObject`).
21
+ * Returns the set of declared code values, or `null` if the schema doesn't
22
+ * expose a literal/enum `error` field (e.g., bare `ApiError` with `z.string()`).
5
23
  *
6
- * Use `record()` to log an observed status, or `assert_and_record()` to
7
- * combine response validation with tracking. After all tests, call
8
- * `uncovered()` to find declared error statuses never exercised.
24
+ * Used by coverage reporting to split a single declared status into per-code
25
+ * rows when the route's error schema names specific codes.
26
+ */
27
+ export declare const extract_declared_error_codes: (schema: z.ZodType) => Array<string> | null;
28
+ /** Uncovered entry — either a status-level row (no `code`) or a specific-code row. */
29
+ export interface UncoveredEntry {
30
+ method: string;
31
+ path: string;
32
+ status: number;
33
+ /** Declared code value missing, when the status's error schema names specific codes. */
34
+ code?: string;
35
+ }
36
+ /** Options controlling which routes/statuses are considered for coverage. */
37
+ export interface CoverageFilterOptions {
38
+ /** Routes to skip, in `'METHOD /path'` format. */
39
+ ignore_routes?: Array<string>;
40
+ /** HTTP status codes to skip. */
41
+ ignore_statuses?: Array<number>;
42
+ }
43
+ /**
44
+ * Tracks which route × status (and route × status × code) combinations have
45
+ * been exercised in tests.
46
+ *
47
+ * Use `record()` to log an observed status (optionally with the body's `error`
48
+ * code), or `assert_and_record()` to combine response validation with tracking
49
+ * (auto-extracts `body.error` from the response when present).
50
+ * After all tests, call `uncovered()` to find declared error paths never
51
+ * exercised.
52
+ *
53
+ * An observation recorded without a code still satisfies "any-code" coverage
54
+ * requirements for the same status — i.e., if a caller records just the status,
55
+ * all declared codes for that status are considered covered. Per-code tracking
56
+ * is additive: callers who know the body's `error` value should pass it to get
57
+ * precise per-code gap reporting on routes with literal/enum error schemas.
9
58
  */
10
59
  export declare class ErrorCoverageCollector {
11
- /** Observed route × status keys: `"METHOD /spec-path:STATUS"`. */
60
+ /**
61
+ * Observed keys: `"METHOD /spec-path:STATUS"` or `"METHOD /spec-path:STATUS:CODE"`.
62
+ *
63
+ * Both shapes coexist — the code-less key marks the status as covered at any
64
+ * code; a code-bearing key adds per-code precision.
65
+ */
12
66
  readonly observed: Set<string>;
13
67
  /**
14
- * Record an observed error status for a route.
68
+ * Record an observed error status (optionally with the body's `error` code) for a route.
15
69
  *
16
70
  * Resolves the concrete request path back to the spec template path
17
- * (e.g., `/api/accounts/abc` → `/api/accounts/:id`).
71
+ * (e.g., `/api/accounts/abc` → `/api/accounts/:id`). When `code` is provided,
72
+ * it is stored alongside the status for per-code coverage tracking.
18
73
  *
19
74
  * @param route_specs - route specs for path resolution
20
75
  * @param method - HTTP method
21
76
  * @param path - request path (may be concrete)
22
77
  * @param status - observed HTTP status code
78
+ * @param code - observed body `error` code (pass when the route's error
79
+ * schema declares specific codes via `z.literal` or `z.enum`)
23
80
  */
24
- record(route_specs: Array<RouteSpec>, method: string, path: string, status: number): void;
81
+ record(route_specs: Array<RouteSpec>, method: string, path: string, status: number, code?: string): void;
25
82
  /**
26
83
  * Validate a response against its route spec and record the status.
27
84
  *
28
- * Wraps `assert_response_matches_spec` and records the status code.
85
+ * Wraps `assert_response_matches_spec` and records the status code. For
86
+ * error responses, auto-extracts `body.error` from the JSON body (via a
87
+ * cloned response, so the original stream stays usable) and records it
88
+ * for per-code coverage. Pass an explicit `code` to override the
89
+ * auto-extracted value or when the body was already consumed.
29
90
  *
30
91
  * @param route_specs - route specs for schema lookup and path resolution
31
92
  * @param method - HTTP method
32
93
  * @param path - request path
33
94
  * @param response - the Response to validate and record
95
+ * @param code - observed body `error` code (override; if omitted and the
96
+ * response body is a JSON object with a string `error` field, that value
97
+ * is auto-extracted)
34
98
  */
35
- assert_and_record(route_specs: Array<RouteSpec>, method: string, path: string, response: Response): Promise<void>;
99
+ assert_and_record(route_specs: Array<RouteSpec>, method: string, path: string, response: Response, code?: string): Promise<void>;
36
100
  /**
37
- * Find declared error statuses that were never observed.
101
+ * Find declared error paths that were never observed.
38
102
  *
39
- * Computes the declared set from `merge_error_schemas` for each route spec,
40
- * then subtracts observed keys.
103
+ * Computes the declared set from `merge_error_schemas` for each route spec.
104
+ * For statuses whose error schema names specific codes (via `z.literal` or
105
+ * `z.enum`), reports per-code rows; otherwise reports one row per status.
106
+ * A status-only observation (no code) satisfies all declared codes for that
107
+ * status — the "any-code" rule.
41
108
  *
42
109
  * @param route_specs - route specs to check coverage against
43
- * @returns uncovered entries with method, path, and status
110
+ * @param options - exclusion configuration (skip routes or statuses)
111
+ * @returns uncovered entries with method, path, status, and optional code
44
112
  */
45
- uncovered(route_specs: Array<RouteSpec>): Array<{
46
- method: string;
47
- path: string;
48
- status: number;
49
- }>;
113
+ uncovered(route_specs: Array<RouteSpec>, options?: CoverageFilterOptions): Array<UncoveredEntry>;
50
114
  }
51
115
  /**
52
116
  * Default minimum error coverage threshold for the standard integration
@@ -55,20 +119,22 @@ export declare class ErrorCoverageCollector {
55
119
  */
56
120
  export declare const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
57
121
  /** Options for `assert_error_coverage`. */
58
- export interface ErrorCoverageOptions {
122
+ export interface ErrorCoverageOptions extends CoverageFilterOptions {
59
123
  /** Minimum coverage ratio (0–1). Default `0` (informational only). */
60
124
  min_coverage?: number;
61
- /** Routes to skip, in `'METHOD /path'` format. */
62
- ignore_routes?: Array<string>;
63
- /** HTTP status codes to skip. */
64
- ignore_statuses?: Array<number>;
65
125
  }
66
126
  /**
67
127
  * Assert error coverage meets a minimum threshold.
68
128
  *
69
- * Computes the ratio of exercised error statuses to total declared error
70
- * statuses. When `min_coverage` is 0 (default), logs coverage info without
71
- * failing. When > 0, fails if coverage is below the threshold.
129
+ * Computes the ratio of exercised error paths to total declared error paths.
130
+ * For routes whose status error schema names specific codes (`z.literal` or
131
+ * `z.enum`), each declared code counts as one coverage path; for schemas
132
+ * without declared codes (`ApiError`/`z.string()`), the status counts as one
133
+ * path. A status-only observation covers all declared codes for that status
134
+ * (the "any-code" rule).
135
+ *
136
+ * When `min_coverage` is 0 (default), logs coverage info without failing.
137
+ * When > 0, fails if coverage is below the threshold.
72
138
  *
73
139
  * @param collector - the coverage collector with recorded observations
74
140
  * @param route_specs - route specs to check coverage against
@@ -1 +1 @@
1
- {"version":3,"file":"error_coverage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/error_coverage.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAe7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAIrD;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAClC,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IAE3C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAMzF;;;;;;;;;OASG;IACG,iBAAiB,CACtB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,IAAI,CAAC;IAKhB;;;;;;;;OAQG;IACH,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;CAe/F;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACpC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,iCAAiC;IACjC,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GACjC,WAAW,sBAAsB,EACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,UAAU,oBAAoB,KAC5B,IA6CF,CAAC"}
1
+ {"version":3,"file":"error_coverage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/error_coverage.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAIrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,MAAM,CAAC,GAAG,IAWhF,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACrC,kDAAkD;IAClD,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,iCAAiC;IACjC,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAChC;AAqDD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,sBAAsB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IAE3C;;;;;;;;;;;;;OAaG;IACH,MAAM,CACL,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,GACX,IAAI;IAUP;;;;;;;;;;;;;;;;OAgBG;IACG,iBAAiB,CACtB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,IAAI,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAgBhB;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC,cAAc,CAAC;CAKhG;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,2CAA2C;AAC3C,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IAClE,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GACjC,WAAW,sBAAsB,EACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,UAAU,oBAAoB,KAC5B,IAqBF,CAAC"}
@@ -2,80 +2,183 @@ import './assert_dev_env.js';
2
2
  /**
3
3
  * Error reachability coverage tracking.
4
4
  *
5
- * Tracks which declared error statuses are actually exercised in tests.
6
- * `ErrorCoverageCollector` records status codes observed during test runs,
5
+ * Tracks which declared error statuses (and specific error codes) are
6
+ * actually exercised in tests. `ErrorCoverageCollector` records status
7
+ * codes (optionally with body `error` codes) observed during test runs,
7
8
  * then `assert_error_coverage` compares against declared error schemas
8
- * to find uncovered error paths.
9
+ * to find uncovered error paths — reporting per-code when the declared
10
+ * schema is a literal or enum, per-status otherwise.
9
11
  *
10
12
  * @module
11
13
  */
14
+ import { z } from 'zod';
12
15
  import { assert } from 'vitest';
13
16
  import { merge_error_schemas } from '../http/schema_helpers.js';
14
17
  import { find_route_spec, assert_response_matches_spec } from './integration_helpers.js';
15
18
  /**
16
- * Tracks which route × status combinations have been exercised in tests.
19
+ * Extract declared error code values from an error response schema.
17
20
  *
18
- * Use `record()` to log an observed status, or `assert_and_record()` to
19
- * combine response validation with tracking. After all tests, call
20
- * `uncovered()` to find declared error statuses never exercised.
21
+ * Recognizes schemas shaped like `z.object({error: z.literal(...)})` or
22
+ * `z.object({error: z.enum([...])})` (incl. `looseObject`/`strictObject`).
23
+ * Returns the set of declared code values, or `null` if the schema doesn't
24
+ * expose a literal/enum `error` field (e.g., bare `ApiError` with `z.string()`).
25
+ *
26
+ * Used by coverage reporting to split a single declared status into per-code
27
+ * rows when the route's error schema names specific codes.
28
+ */
29
+ export const extract_declared_error_codes = (schema) => {
30
+ if (!(schema instanceof z.ZodObject))
31
+ return null;
32
+ const error_field = schema.shape.error;
33
+ if (!error_field)
34
+ return null;
35
+ if (error_field instanceof z.ZodLiteral) {
36
+ return [...error_field.values].map(String);
37
+ }
38
+ if (error_field instanceof z.ZodEnum) {
39
+ return error_field.options.map(String);
40
+ }
41
+ return null;
42
+ };
43
+ /**
44
+ * Shared walk over declared error paths.
45
+ *
46
+ * Single source of truth for the route → status → code traversal used by
47
+ * both `uncovered()` and `assert_error_coverage`. Yields one entry per
48
+ * declared coverage path with a `covered` flag, applying the "any-code"
49
+ * rule (status-only observation covers all declared codes).
50
+ */
51
+ const walk_coverage = (collector, route_specs, options) => {
52
+ const ignore_routes = new Set(options?.ignore_routes);
53
+ const ignore_statuses = new Set(options?.ignore_statuses);
54
+ const entries = [];
55
+ for (const spec of route_specs) {
56
+ const route_key = `${spec.method} ${spec.path}`;
57
+ if (ignore_routes.has(route_key))
58
+ continue;
59
+ const merged = merge_error_schemas(spec);
60
+ if (!merged)
61
+ continue;
62
+ for (const status_str of Object.keys(merged)) {
63
+ const status = Number(status_str);
64
+ if (ignore_statuses.has(status))
65
+ continue;
66
+ const error_schema = merged[status];
67
+ if (!error_schema)
68
+ continue;
69
+ const status_key = `${spec.method} ${spec.path}:${status}`;
70
+ const status_observed = collector.observed.has(status_key);
71
+ const codes = extract_declared_error_codes(error_schema);
72
+ if (codes && codes.length > 0) {
73
+ for (const code of codes) {
74
+ const covered = status_observed || collector.observed.has(`${status_key}:${code}`);
75
+ entries.push({ method: spec.method, path: spec.path, status, code, covered });
76
+ }
77
+ }
78
+ else {
79
+ entries.push({ method: spec.method, path: spec.path, status, covered: status_observed });
80
+ }
81
+ }
82
+ }
83
+ return entries;
84
+ };
85
+ /**
86
+ * Tracks which route × status (and route × status × code) combinations have
87
+ * been exercised in tests.
88
+ *
89
+ * Use `record()` to log an observed status (optionally with the body's `error`
90
+ * code), or `assert_and_record()` to combine response validation with tracking
91
+ * (auto-extracts `body.error` from the response when present).
92
+ * After all tests, call `uncovered()` to find declared error paths never
93
+ * exercised.
94
+ *
95
+ * An observation recorded without a code still satisfies "any-code" coverage
96
+ * requirements for the same status — i.e., if a caller records just the status,
97
+ * all declared codes for that status are considered covered. Per-code tracking
98
+ * is additive: callers who know the body's `error` value should pass it to get
99
+ * precise per-code gap reporting on routes with literal/enum error schemas.
21
100
  */
22
101
  export class ErrorCoverageCollector {
23
- /** Observed route × status keys: `"METHOD /spec-path:STATUS"`. */
102
+ /**
103
+ * Observed keys: `"METHOD /spec-path:STATUS"` or `"METHOD /spec-path:STATUS:CODE"`.
104
+ *
105
+ * Both shapes coexist — the code-less key marks the status as covered at any
106
+ * code; a code-bearing key adds per-code precision.
107
+ */
24
108
  observed = new Set();
25
109
  /**
26
- * Record an observed error status for a route.
110
+ * Record an observed error status (optionally with the body's `error` code) for a route.
27
111
  *
28
112
  * Resolves the concrete request path back to the spec template path
29
- * (e.g., `/api/accounts/abc` → `/api/accounts/:id`).
113
+ * (e.g., `/api/accounts/abc` → `/api/accounts/:id`). When `code` is provided,
114
+ * it is stored alongside the status for per-code coverage tracking.
30
115
  *
31
116
  * @param route_specs - route specs for path resolution
32
117
  * @param method - HTTP method
33
118
  * @param path - request path (may be concrete)
34
119
  * @param status - observed HTTP status code
120
+ * @param code - observed body `error` code (pass when the route's error
121
+ * schema declares specific codes via `z.literal` or `z.enum`)
35
122
  */
36
- record(route_specs, method, path, status) {
123
+ record(route_specs, method, path, status, code) {
37
124
  const spec = find_route_spec(route_specs, method, path);
38
125
  const spec_path = spec ? spec.path : path;
39
- this.observed.add(`${method} ${spec_path}:${status}`);
126
+ const base_key = `${method} ${spec_path}:${status}`;
127
+ this.observed.add(base_key);
128
+ if (code !== undefined) {
129
+ this.observed.add(`${base_key}:${code}`);
130
+ }
40
131
  }
41
132
  /**
42
133
  * Validate a response against its route spec and record the status.
43
134
  *
44
- * Wraps `assert_response_matches_spec` and records the status code.
135
+ * Wraps `assert_response_matches_spec` and records the status code. For
136
+ * error responses, auto-extracts `body.error` from the JSON body (via a
137
+ * cloned response, so the original stream stays usable) and records it
138
+ * for per-code coverage. Pass an explicit `code` to override the
139
+ * auto-extracted value or when the body was already consumed.
45
140
  *
46
141
  * @param route_specs - route specs for schema lookup and path resolution
47
142
  * @param method - HTTP method
48
143
  * @param path - request path
49
144
  * @param response - the Response to validate and record
145
+ * @param code - observed body `error` code (override; if omitted and the
146
+ * response body is a JSON object with a string `error` field, that value
147
+ * is auto-extracted)
50
148
  */
51
- async assert_and_record(route_specs, method, path, response) {
149
+ async assert_and_record(route_specs, method, path, response, code) {
52
150
  await assert_response_matches_spec(route_specs, method, path, response);
53
- this.record(route_specs, method, path, response.status);
151
+ let resolved_code = code;
152
+ if (resolved_code === undefined && !response.ok && !response.bodyUsed) {
153
+ try {
154
+ const body = await response.clone().json();
155
+ if (body && typeof body.error === 'string') {
156
+ resolved_code = body.error;
157
+ }
158
+ }
159
+ catch {
160
+ // non-JSON body — no code to extract
161
+ }
162
+ }
163
+ this.record(route_specs, method, path, response.status, resolved_code);
54
164
  }
55
165
  /**
56
- * Find declared error statuses that were never observed.
166
+ * Find declared error paths that were never observed.
57
167
  *
58
- * Computes the declared set from `merge_error_schemas` for each route spec,
59
- * then subtracts observed keys.
168
+ * Computes the declared set from `merge_error_schemas` for each route spec.
169
+ * For statuses whose error schema names specific codes (via `z.literal` or
170
+ * `z.enum`), reports per-code rows; otherwise reports one row per status.
171
+ * A status-only observation (no code) satisfies all declared codes for that
172
+ * status — the "any-code" rule.
60
173
  *
61
174
  * @param route_specs - route specs to check coverage against
62
- * @returns uncovered entries with method, path, and status
175
+ * @param options - exclusion configuration (skip routes or statuses)
176
+ * @returns uncovered entries with method, path, status, and optional code
63
177
  */
64
- uncovered(route_specs) {
65
- const missing = [];
66
- for (const spec of route_specs) {
67
- const merged = merge_error_schemas(spec);
68
- if (!merged)
69
- continue;
70
- for (const status_str of Object.keys(merged)) {
71
- const status = Number(status_str);
72
- const key = `${spec.method} ${spec.path}:${status}`;
73
- if (!this.observed.has(key)) {
74
- missing.push({ method: spec.method, path: spec.path, status });
75
- }
76
- }
77
- }
78
- return missing;
178
+ uncovered(route_specs, options) {
179
+ return walk_coverage(this, route_specs, options)
180
+ .filter((entry) => !entry.covered)
181
+ .map(({ method, path, status, code }) => ({ method, path, status, ...(code && { code }) }));
79
182
  }
80
183
  }
81
184
  /**
@@ -84,12 +187,25 @@ export class ErrorCoverageCollector {
84
187
  * in the composable suites. Consumers should increase as their test suites mature.
85
188
  */
86
189
  export const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
190
+ /**
191
+ * Format an uncovered entry for human-readable log output.
192
+ *
193
+ * Uses `status (code)` — spaces around the code make `:` unambiguous as
194
+ * the route_key / status separator.
195
+ */
196
+ const format_uncovered = (entry) => `${entry.method} ${entry.path} → ${entry.status}${entry.code ? ` (${entry.code})` : ''}`;
87
197
  /**
88
198
  * Assert error coverage meets a minimum threshold.
89
199
  *
90
- * Computes the ratio of exercised error statuses to total declared error
91
- * statuses. When `min_coverage` is 0 (default), logs coverage info without
92
- * failing. When > 0, fails if coverage is below the threshold.
200
+ * Computes the ratio of exercised error paths to total declared error paths.
201
+ * For routes whose status error schema names specific codes (`z.literal` or
202
+ * `z.enum`), each declared code counts as one coverage path; for schemas
203
+ * without declared codes (`ApiError`/`z.string()`), the status counts as one
204
+ * path. A status-only observation covers all declared codes for that status
205
+ * (the "any-code" rule).
206
+ *
207
+ * When `min_coverage` is 0 (default), logs coverage info without failing.
208
+ * When > 0, fails if coverage is below the threshold.
93
209
  *
94
210
  * @param collector - the coverage collector with recorded observations
95
211
  * @param route_specs - route specs to check coverage against
@@ -97,39 +213,16 @@ export const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
97
213
  */
98
214
  export const assert_error_coverage = (collector, route_specs, options) => {
99
215
  const min_coverage = options?.min_coverage ?? 0;
100
- const ignore_routes = new Set(options?.ignore_routes);
101
- const ignore_statuses = new Set(options?.ignore_statuses);
102
- let total = 0;
103
- let covered = 0;
104
- const uncovered_entries = [];
105
- for (const spec of route_specs) {
106
- const route_key = `${spec.method} ${spec.path}`;
107
- if (ignore_routes.has(route_key))
108
- continue;
109
- const merged = merge_error_schemas(spec);
110
- if (!merged)
111
- continue;
112
- for (const status_str of Object.keys(merged)) {
113
- const status = Number(status_str);
114
- if (ignore_statuses.has(status))
115
- continue;
116
- total++;
117
- const key = `${spec.method} ${spec.path}:${status}`;
118
- if (collector.observed.has(key)) {
119
- covered++;
120
- }
121
- else {
122
- uncovered_entries.push(`${route_key} → ${status}`);
123
- }
124
- }
125
- }
216
+ const entries = walk_coverage(collector, route_specs, options);
217
+ const total = entries.length;
218
+ const uncovered_entries = entries.filter((e) => !e.covered);
219
+ const covered = total - uncovered_entries.length;
220
+ const uncovered_lines = uncovered_entries.map(format_uncovered);
126
221
  const ratio = total > 0 ? covered / total : 1;
127
222
  console.log(`[error coverage] ${covered}/${total} (${(ratio * 100).toFixed(1)}%)` +
128
- (uncovered_entries.length > 0
129
- ? `\n uncovered:\n ${uncovered_entries.join('\n ')}`
130
- : ''));
223
+ (uncovered_lines.length > 0 ? `\n uncovered:\n ${uncovered_lines.join('\n ')}` : ''));
131
224
  if (min_coverage > 0) {
132
225
  assert.ok(ratio >= min_coverage, `Error coverage ${(ratio * 100).toFixed(1)}% below threshold ${(min_coverage * 100).toFixed(1)}%` +
133
- `\n uncovered:\n ${uncovered_entries.join('\n ')}`);
226
+ `\n uncovered:\n ${uncovered_lines.join('\n ')}`);
134
227
  }
135
228
  };