@carecard/jwt-read 3.11.0 → 3.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.
@@ -3,14 +3,35 @@ name: carecard-workspace-standards
3
3
  description: 'Follow the shared SO_CareCardCa/CareCard workspace coding, testing, repository, dependency, shared package, frontend, database, API response, and security standards. Use before modifying, testing, reviewing, or debugging any ms-*, pkg-*, app-*, website, dashboard, or other CareCard repository in this workspace, especially when choosing validation commands, package boundaries, TypeScript types, dependencies, API contracts, database logic, service patterns, or frontend architecture.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # CareCard Workspace Standards
7
9
 
10
+ Non-negotiable test order invariance rule: Every test must pass independently of which tests run before or after it, and the suite must pass in every execution order. Each test must establish the state it needs, isolate mutable state, and clean up state it owns; it must never rely on another test's setup, mutations, or cleanup. Default test, CI, and Husky commands must use the test framework's ordinary ordering and must not force randomized ordering. Random-order execution is an explicit diagnostic only, and every failure it exposes must be fixed at the root cause.
11
+
12
+ Non-negotiable parallel test execution rule: Run independent test files in parallel with repository-native worker support wherever resource isolation makes parallel execution safe. Tests that share a mutable database, application server, browser state, filesystem fixture, port, or cluster resource must remain in an explicitly isolated serial group until every worker owns a separate resource. Parallel execution must preserve ordinary test selection and must never use randomized ordering, retries, locks, or error suppression to conceal coupling.
13
+
8
14
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
9
15
 
16
+ This requirement is non-negotiable and may be overridden only with the user's
17
+ explicit, direct approval.
18
+
19
+ A pre-existing test—defined as any test present before work on the current task
20
+ begins—must not be deleted, disabled, skipped, weakened, excluded from execution,
21
+ or otherwise removed. A pre-existing test must not be modified without the
22
+ user's explicit approval for the exact proposed change. If changing a
23
+ pre-existing test is believed necessary, stop before making the change and
24
+ request approval. The request must identify every affected test, describe the
25
+ precise proposed modification, provide detailed technical justification, and
26
+ explain all known or reasonably foreseeable regression risks. Until approval is
27
+ granted, leave every pre-existing test unchanged.
28
+
10
29
  Non-negotiable repository isolation rule: Every repository must run its Husky hooks and tests using only files, code, fixtures, dependencies, and services contained within that repository. Tests and Husky scripts must not import, require, read, execute, or otherwise depend on sibling repositories or paths outside the repository root. app-e2e-tests is the only exception because cross-repository end-to-end testing is its explicit responsibility.
11
30
 
12
31
  Non-negotiable error and warning rule: Never suppress, silence, hide, downgrade, filter, ignore, skip, or bypass errors or warnings from code, tests, tools, compilers, linters, or validation. Fix the root cause, then rerun the affected check and require a clean result. Expected error-path tests may assert errors, but must not conceal unexpected failures.
13
32
 
33
+ Non-negotiable TypeScript type rule: Never use the TypeScript type `any`; always use specific domain types, generics, existing project types, or `unknown` with explicit narrowing in all TypeScript-family files (`.ts`, `.tsx`, `.mts`, `.cts`, and `.d.ts`).
34
+
14
35
  Non-negotiable code organization rule: Functions with the same or equivalent behavior must use the same or clearly corresponding descriptive names across CareCard repositories, and equivalent functionality must live in files with the same names within each repository's established architecture. No backward compatibility names, aliases, or duplicate locations are allowed.
15
36
 
16
37
  ## Purpose
@@ -3,6 +3,8 @@ name: github-pr-create-update
3
3
  description: 'Use only when the user explicitly asks for remote Git or GitHub PR work: pushing a branch, creating or updating a PR, or marking a PR ready from the current repository branch into development or main.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # Pull Request Create
7
9
 
8
10
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
@@ -3,6 +3,8 @@ name: github-pr-merge-cleanup
3
3
  description: 'Use only when the user explicitly asks for remote Git or GitHub PR work: pushing a branch, creating a missing PR, reviewing mergeability, validating, merging, deleting, or cleaning up a pull request branch.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # Pull Request Merge Close
7
9
 
8
10
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
@@ -3,6 +3,8 @@ name: pkg-jwt-read-jwt-middleware-library
3
3
  description: 'Use when changing pkg-jwt-read JWT parsing, middleware, visitor tokens, role checks, auth context, package exports, or tests.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # Package JWT Read
7
9
 
8
10
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
@@ -11,6 +13,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
11
13
 
12
14
  Non-negotiable error and warning rule: Never suppress, silence, hide, downgrade, filter, ignore, skip, or bypass errors or warnings from code, tests, tools, compilers, linters, or validation. Fix the root cause, then rerun the affected check and require a clean result. Expected error-path tests may assert errors, but must not conceal unexpected failures.
13
15
 
16
+ Non-negotiable TypeScript type rule: Never use the TypeScript type `any`; always use specific domain types, generics, existing project types, or `unknown` with explicit narrowing in all TypeScript-family files (`.ts`, `.tsx`, `.mts`, `.cts`, and `.d.ts`).
17
+
14
18
  Non-negotiable code organization rule: Functions with the same or equivalent behavior must use the same or clearly corresponding descriptive names across CareCard repositories, and equivalent functionality must live in files with the same names within each repository's established architecture. No backward compatibility names, aliases, or duplicate locations are allowed.
15
19
 
16
20
  ## Purpose
@@ -285,3 +289,21 @@ repository's agents-only Git workflow:
285
289
  Do not commit or push `.agents` guidance changes directly from `development`
286
290
  or `main`. Do not stage unrelated files, generated output, dependency folders,
287
291
  build artifacts, logs, or `.DS_Store`.
292
+
293
+ ## Fail-Closed Test Lifecycle Audit
294
+
295
+ The current package tests own no HTTP listener, database pool, Kafka client,
296
+ background timer, or child process after completion. Mocha's test timeout fails
297
+ a stalled async test, the suites run without bail or forced exit, and npm
298
+ preserves each command's nonzero status. Keep natural process exit as the open
299
+ handle regression check; validation must not hide failures with retries, forced
300
+ success, skipped tests, or output suppression.
301
+
302
+ Do not add unpublished executable validation code to a `pkg-*` repository. If a
303
+ future test owns a long-lived resource or demonstrates a post-suite hang, add a
304
+ contract-tested process watchdog through the coordinated package version,
305
+ publish, and consumer propagation workflow. That watchdog must return
306
+ immediately when no helper remains, allow only a bounded 250 ms settlement
307
+ window for already-stopping helpers, fail persistent descendants, preserve
308
+ failures and output, use exit code `124` only for a real outer deadline, and
309
+ remain a final guard rather than a substitute for explicit cleanup.
@@ -3,6 +3,8 @@ name: pkg-publish
3
3
  description: 'Use when any pkg-* repository has non-Markdown package changes, including source code, public types, tests, scripts, package metadata, lockfiles, dependency behavior, or validation config, and the CareCard packages must be versioned, published in order with just-in-time package pushes, and propagated to pkg-*, ms-*, and app-dashboard consumers. Do not use for Markdown-only changes.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # pkg Publish
7
9
 
8
10
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
@@ -3,6 +3,8 @@ name: software-design-patterns-and-clean-code
3
3
  description: 'Use every time before coding, refactoring, debugging, or reviewing in this repository, alongside all other applicable skills, to apply pragmatic software design patterns, SOLID, Clean Code, and testable architecture.'
4
4
  ---
5
5
 
6
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
7
+
6
8
  # Software Design Patterns And Clean Code
7
9
 
8
10
  Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, add or update the relevant validation check before changing the prose.
@@ -11,6 +13,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
11
13
 
12
14
  Non-negotiable error and warning rule: Never suppress, silence, hide, downgrade, filter, ignore, skip, or bypass errors or warnings from code, tests, tools, compilers, linters, or validation. Fix the root cause, then rerun the affected check and require a clean result. Expected error-path tests may assert errors, but must not conceal unexpected failures.
13
15
 
16
+ Non-negotiable TypeScript type rule: Never use the TypeScript type `any`; always use specific domain types, generics, existing project types, or `unknown` with explicit narrowing in all TypeScript-family files (`.ts`, `.tsx`, `.mts`, `.cts`, and `.d.ts`).
17
+
14
18
  Non-negotiable code organization rule: Functions with the same or equivalent behavior must use the same or clearly corresponding descriptive names across CareCard repositories, and equivalent functionality must live in files with the same names within each repository's established architecture. No backward compatibility names, aliases, or duplicate locations are allowed.
15
19
 
16
20
  ## Purpose
package/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Utility functions for authentication and authorization in the CareCard ecosystem.
3
3
  */
4
4
 
5
- import { NextFunction, Request, Response } from 'express';
5
+ import type { NextFunction, Request, Response } from 'express';
6
6
 
7
7
  export const DEFAULT_USER_AUTHORIZATION_HEADER_NAME: 'X-Authorization-Context';
8
8
  export const DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH: 2048;
@@ -16,7 +16,7 @@ export interface JwtHeader {
16
16
  /** The media type of the JWT. Defaults to 'JWT'. */
17
17
  typ?: string;
18
18
  /** Any other custom header fields. */
19
- [key: string]: any;
19
+ [key: string]: unknown;
20
20
  }
21
21
 
22
22
  /**
@@ -40,7 +40,7 @@ export interface JwtPayload {
40
40
  /** Server-auth session identifier when an opaque server-auth token was used. */
41
41
  sessionId?: string;
42
42
  /** Any other custom payload fields. */
43
- [key: string]: any;
43
+ [key: string]: unknown;
44
44
  }
45
45
 
46
46
  /**
@@ -76,10 +76,10 @@ export interface JwtRequestObject {
76
76
  header: JwtHeader;
77
77
  payload: JwtPayload;
78
78
  age?: number;
79
- jwtClientId: (req?: any) => string | undefined;
79
+ jwtClientId: (req?: JwtRequestContext) => string | undefined;
80
80
  doesJwtUserHasRole: (role: string) => boolean;
81
81
  isJwtExpired: (jwtValiditySeconds?: number) => boolean;
82
- jwtAgeInSeconds: (req?: any) => number;
82
+ jwtAgeInSeconds: (req?: JwtRequestContext) => number;
83
83
  }
84
84
 
85
85
  /**
@@ -88,7 +88,7 @@ export interface JwtRequestObject {
88
88
  export interface VisitorRequestObject {
89
89
  header: JwtHeader;
90
90
  payload: JwtPayload;
91
- visitorClientId: (req?: any) => string | undefined;
91
+ visitorClientId: (req?: JwtRequestContext) => string | undefined;
92
92
  }
93
93
 
94
94
  /**
@@ -99,6 +99,24 @@ export interface UserAuthorizationRequestObject {
99
99
  payload: UserAuthorizationPayload;
100
100
  }
101
101
 
102
+ export interface JwtRequestContext {
103
+ jwt?: {
104
+ header?: JwtHeader;
105
+ payload: JwtPayload;
106
+ age?: number;
107
+ jwtClientId?: JwtRequestObject['jwtClientId'];
108
+ doesJwtUserHasRole?: JwtRequestObject['doesJwtUserHasRole'];
109
+ isJwtExpired?: JwtRequestObject['isJwtExpired'];
110
+ jwtAgeInSeconds?: JwtRequestObject['jwtAgeInSeconds'];
111
+ } | null;
112
+ visitor?: {
113
+ header?: JwtHeader;
114
+ payload: JwtPayload;
115
+ visitorClientId?: VisitorRequestObject['visitorClientId'];
116
+ } | null;
117
+ userAuthorization?: UserAuthorizationRequestObject | null;
118
+ }
119
+
102
120
  export interface UserAuthorizationTokenOptions {
103
121
  publicKey?: string;
104
122
  headerName?: string;
@@ -115,7 +133,7 @@ export interface UserAuthorizationReadOptions {
115
133
  /**
116
134
  * Extended Express Request to include jwt, visitor, and userAuthorization objects.
117
135
  */
118
- export interface AuthenticatedRequest extends Request {
136
+ export interface AuthenticatedRequest extends Request, JwtRequestContext {
119
137
  jwt?: JwtRequestObject | null;
120
138
  visitor?: VisitorRequestObject | null;
121
139
  userAuthorization?: UserAuthorizationRequestObject | null;
@@ -137,7 +155,7 @@ export interface ServerAuthIntrospectionClaims {
137
155
  exp?: number | string;
138
156
  expiresAt?: string;
139
157
  expires_at?: string;
140
- [key: string]: any;
158
+ [key: string]: unknown;
141
159
  }
142
160
 
143
161
  export type ServerAuthIntrospector = (
@@ -214,24 +232,24 @@ export function jwtVerifyVisitorNoThrow(
214
232
  /**
215
233
  * Returns the sub from the extracted JWT in req.jwt.
216
234
  */
217
- export function jwtGetClientId(req?: any): string | undefined;
235
+ export function jwtGetClientId(req?: JwtRequestContext): string | undefined;
218
236
 
219
237
  /**
220
238
  * Returns the sub from the extracted visitor token in req.visitor.
221
239
  */
222
- export function jwtGetVisitorClientId(req?: any): string | undefined;
240
+ export function jwtGetVisitorClientId(req?: JwtRequestContext): string | undefined;
223
241
 
224
242
  /**
225
243
  * Checks if the extracted JWT in req.jwt has expired.
226
244
  */
227
- export function jwtIsExpired(req: any, jwtValiditySeconds?: number): boolean;
245
+ export function jwtIsExpired(req: JwtRequestContext, jwtValiditySeconds?: number): boolean;
228
246
  export function jwtIsExpired(jwtValiditySeconds: number): boolean;
229
247
  export function jwtIsExpired(): boolean;
230
248
 
231
249
  /**
232
250
  * Returns the age of the extracted JWT in seconds.
233
251
  */
234
- export function jwtGetAgeInSeconds(req?: any): number;
252
+ export function jwtGetAgeInSeconds(req?: JwtRequestContext): number;
235
253
 
236
254
  /**
237
255
  * Returns a middleware that verifies the JWT and checks if the user has the required role.
@@ -295,7 +313,7 @@ export interface JwtContext {
295
313
  * Always returns user_id. If the roles array contains 'ad', also returns role: 'super_admin'.
296
314
  * If req.userAuthorization is present, also returns authorizationContext and userAuthorization.
297
315
  */
298
- export function jwtGetContext(req: any): JwtContext;
316
+ export function jwtGetContext(req: JwtRequestContext): JwtContext;
299
317
 
300
318
  /**
301
319
  * Validates the JWT from the Authorization header and extracts it into req.jwt.
@@ -454,19 +472,19 @@ export function verifyVisitorNoThrow(
454
472
  * Returns the sub from the extracted JWT in req.jwt.
455
473
  * @deprecated use jwtGetClientId
456
474
  */
457
- export function jwtClientId(req?: any): string | undefined;
475
+ export function jwtClientId(req?: JwtRequestContext): string | undefined;
458
476
 
459
477
  /**
460
478
  * Returns the sub from the extracted visitor token in req.visitor.
461
479
  * @deprecated use jwtGetVisitorClientId
462
480
  */
463
- export function visitorClientId(req?: any): string | undefined;
481
+ export function visitorClientId(req?: JwtRequestContext): string | undefined;
464
482
 
465
483
  /**
466
484
  * Checks if the extracted JWT in req.jwt has expired.
467
485
  * @deprecated use jwtIsExpired
468
486
  */
469
- export function isJwtExpired(req: any, jwtValiditySeconds?: number): boolean;
487
+ export function isJwtExpired(req: JwtRequestContext, jwtValiditySeconds?: number): boolean;
470
488
  /** @deprecated use jwtIsExpired */
471
489
  export function isJwtExpired(jwtValiditySeconds: number): boolean;
472
490
  /** @deprecated use jwtIsExpired */
@@ -476,7 +494,7 @@ export function isJwtExpired(): boolean;
476
494
  * Returns the age of the extracted JWT in seconds.
477
495
  * @deprecated use jwtGetAgeInSeconds
478
496
  */
479
- export function jwtAgeInSeconds(req?: any): number;
497
+ export function jwtAgeInSeconds(req?: JwtRequestContext): number;
480
498
 
481
499
  /**
482
500
  * Returns a middleware that verifies the JWT and checks if the user has the required role.
@@ -499,7 +517,7 @@ export function throwUsedTokenError(): never;
499
517
  * Checks if the user in the extracted JWT has the specified role.
500
518
  * @deprecated use jwtDoesJwtUserHasRole
501
519
  */
502
- export function doesJwtUserHasRole(req: any, userRole: string): boolean;
520
+ export function doesJwtUserHasRole(req: JwtRequestContext, userRole: string): boolean;
503
521
  /** @deprecated use jwtDoesJwtUserHasRole */
504
522
  export function doesJwtUserHasRole(userRole: string): boolean;
505
523
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/jwt-read",
3
- "version": "3.11.0",
3
+ "version": "3.13.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/CareCard-ca/pkg-jwt-read.git"
@@ -9,9 +9,10 @@
9
9
  "main": "index.js",
10
10
  "types": "index.d.ts",
11
11
  "scripts": {
12
- "test": "mocha --recursive",
13
- "test:types": "tsc --noEmit && mocha -r ts-node/register test/types.test.ts",
14
- "test:coverage": "tsc --noEmit && nyc mocha --recursive -r ts-node/register 'test/**/*.{js,ts}'",
12
+ "test": "npm run test:order && node test/index.test.js",
13
+ "test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testOrder/testOrderPolicy.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/testParallel/parallelTestPolicy.test.mjs",
14
+ "test:types": "npm run test:order && tsc --noEmit && mocha --require ./scripts/testOrder/randomizeTestOrder.cjs -r ts-node/register test/types.test.ts",
15
+ "test:coverage": "npm run test:order && tsc --noEmit && nyc node test/index.test.js",
15
16
  "test:All": "npm run test && npm run test:types",
16
17
  "format": "prettier --write .",
17
18
  "format:check": "prettier --check .",
@@ -41,13 +42,14 @@
41
42
  "typescript": "6.0.3"
42
43
  },
43
44
  "dependencies": {
44
- "@carecard/auth-util": "3.11.0",
45
- "@carecard/common-util": "3.11.0",
46
- "@carecard/validate": "3.11.0"
45
+ "@carecard/auth-util": "3.13.0",
46
+ "@carecard/common-util": "3.13.0",
47
+ "@carecard/validate": "3.13.0"
47
48
  },
48
49
  "overrides": {
49
50
  "diff": "8.0.4",
51
+ "minimatch": "10.2.5",
50
52
  "serialize-javascript": "7.0.5",
51
- "js-yaml": "4.2.0"
53
+ "js-yaml": "4.3.0"
52
54
  }
53
55
  }
package/readme.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @carecard/jwt-read
2
2
 
3
+ Non-negotiable test order invariance rule: Every test must pass independently of which tests run before or after it, and the suite must pass in every execution order. Each test must establish the state it needs, isolate mutable state, and clean up state it owns; it must never rely on another test's setup, mutations, or cleanup. Default test, CI, and Husky commands must use the test framework's ordinary ordering and must not force randomized ordering. Random-order execution is an explicit diagnostic only, and every failure it exposes must be fixed at the root cause.
4
+
5
+ Non-negotiable root-cause solution rule: Always identify and solve the verified root cause, use the stronger solution, and deliver a correct, durable, production-quality result. Never treat a temporary workaround, resource increase, retry, suppression, bypass, or symptom-only patch as completion. Validate the root-cause fix against the real failing workflow and prove the end state.
6
+
3
7
  ![Tests Passing](https://github.com/CareCard-ca/pkg-jwt-read/actions/workflows/ci.yml/badge.svg)
4
8
  ![Coverage](https://img.shields.io/badge/Coverage-80%25-orange)
5
9
 
@@ -16,6 +20,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
16
20
 
17
21
  Non-negotiable error and warning rule: Never suppress, silence, hide, downgrade, filter, ignore, skip, or bypass errors or warnings from code, tests, tools, compilers, linters, or validation. Fix the root cause, then rerun the affected check and require a clean result. Expected error-path tests may assert errors, but must not conceal unexpected failures.
18
22
 
23
+ Non-negotiable TypeScript type rule: Never use the TypeScript type `any`; always use specific domain types, generics, existing project types, or `unknown` with explicit narrowing in all TypeScript-family files (`.ts`, `.tsx`, `.mts`, `.cts`, and `.d.ts`).
24
+
19
25
  Non-negotiable code organization rule: Functions with the same or equivalent behavior must use the same or clearly corresponding descriptive names across CareCard repositories, and equivalent functionality must live in files with the same names within each repository's established architecture. No backward compatibility names, aliases, or duplicate locations are allowed.
20
26
 
21
27
  ## Features
@@ -225,3 +231,21 @@ The package is organized into several modules:
225
231
  - `jwtRoles`: Role mapping between internal codes and names.
226
232
 
227
233
  All modules are exported through the main `index.js`.
234
+
235
+ ## Fail-Closed Test Lifecycle Audit
236
+
237
+ The current package tests own no HTTP listener, database pool, Kafka client,
238
+ background timer, or child process after completion. Mocha's test timeout fails
239
+ a stalled async test, the suites run without bail or forced exit, and npm
240
+ preserves each command's nonzero status. Keep natural process exit as the open
241
+ handle regression check; validation must not hide failures with retries, forced
242
+ success, skipped tests, or output suppression.
243
+
244
+ Do not add unpublished executable validation code to a `pkg-*` repository. If a
245
+ future test owns a long-lived resource or demonstrates a post-suite hang, add a
246
+ contract-tested process watchdog through the coordinated package version,
247
+ publish, and consumer propagation workflow. That watchdog must return
248
+ immediately when no helper remains, allow only a bounded 250 ms settlement
249
+ window for already-stopping helpers, fail persistent descendants, preserve
250
+ failures and output, use exit code `124` only for a real outer deadline, and
251
+ remain a final guard rather than a substitute for explicit cleanup.
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const MAX_TEST_ORDER_SEED = 2_147_483_647;
4
+
5
+ function resolveTestOrderSeed(configuredSeed) {
6
+ if (configuredSeed === undefined) return undefined;
7
+ if (!/^[1-9]\d*$/.test(configuredSeed)) throw new Error('TEST_ORDER_SEED must be a positive 32-bit integer.');
8
+ const seed = Number(configuredSeed);
9
+ if (!Number.isSafeInteger(seed) || seed > MAX_TEST_ORDER_SEED) throw new Error('TEST_ORDER_SEED must be a positive 32-bit integer.');
10
+ return seed;
11
+ }
12
+ function createSeededRandom(seed) {
13
+ let state = seed;
14
+ return function nextRandomValue() {
15
+ state = (state + 0x6d2b79f5) | 0;
16
+ let value = Math.imul(state ^ (state >>> 15), 1 | state);
17
+ value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
18
+ return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
19
+ };
20
+ }
21
+ function shuffleValues(values, random) {
22
+ for (let index = values.length - 1; index > 0; index -= 1) {
23
+ const replacementIndex = Math.floor(random() * (index + 1));
24
+ [values[index], values[replacementIndex]] = [values[replacementIndex], values[index]];
25
+ }
26
+ }
27
+ function shuffleSuiteTree(suite, random) {
28
+ for (const childSuite of suite.suites) shuffleSuiteTree(childSuite, random);
29
+ shuffleValues(suite.tests, random);
30
+ shuffleValues(suite.suites, random);
31
+ }
32
+ const mochaHooks = {
33
+ beforeAll() {
34
+ const seed = resolveTestOrderSeed(process.env.TEST_ORDER_SEED);
35
+ if (seed === undefined) return;
36
+ console.log(`Test order seed: ${seed} (reproduce with TEST_ORDER_SEED=${seed})`);
37
+ shuffleSuiteTree(this.test.parent, createSeededRandom(seed));
38
+ },
39
+ };
40
+ module.exports = { createSeededRandom, mochaHooks, resolveTestOrderSeed, shuffleSuiteTree };
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+
4
+ import testOrderRandomizer from './randomizeTestOrder.cjs';
5
+
6
+ const { createSeededRandom, resolveTestOrderSeed, shuffleSuiteTree } = testOrderRandomizer;
7
+
8
+ function createSuiteTree() {
9
+ return {
10
+ suites: [
11
+ { title: 'alpha', suites: [], tests: [{ title: 'one' }, { title: 'two' }] },
12
+ { title: 'beta', suites: [], tests: [{ title: 'three' }, { title: 'four' }] },
13
+ { title: 'gamma', suites: [], tests: [{ title: 'five' }, { title: 'six' }] },
14
+ ],
15
+ tests: [{ title: 'root one' }, { title: 'root two' }, { title: 'root three' }],
16
+ };
17
+ }
18
+
19
+ test('uses ordinary ordering unless TEST_ORDER_SEED is explicitly supplied', () => {
20
+ assert.strictEqual(resolveTestOrderSeed(undefined), undefined);
21
+ assert.strictEqual(resolveTestOrderSeed('314159'), 314159);
22
+ for (const invalidSeed of ['', '0', '-1', '1.5', 'seed', '2147483648']) {
23
+ assert.throws(() => resolveTestOrderSeed(invalidSeed), /TEST_ORDER_SEED/);
24
+ }
25
+ });
26
+
27
+ test('shuffles nested suites and tests reproducibly', () => {
28
+ const firstTree = createSuiteTree();
29
+ const secondTree = createSuiteTree();
30
+
31
+ shuffleSuiteTree(firstTree, createSeededRandom(314159));
32
+ shuffleSuiteTree(secondTree, createSeededRandom(314159));
33
+
34
+ assert.deepStrictEqual(firstTree, secondTree);
35
+ assert.notDeepStrictEqual(firstTree, createSuiteTree());
36
+ });
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { test } from 'node:test';
5
+
6
+ const TEST_ORDER_INVARIANCE_RULE =
7
+ "Non-negotiable test order invariance rule: Every test must pass independently of which tests run before or after it, and the suite must pass in every execution order. Each test must establish the state it needs, isolate mutable state, and clean up state it owns; it must never rely on another test's setup, mutations, or cleanup. Default test, CI, and Husky commands must use the test framework's ordinary ordering and must not force randomized ordering. Random-order execution is an explicit diagnostic only, and every failure it exposes must be fixed at the root cause.";
8
+
9
+ function listRepositoryFiles() {
10
+ return execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' })
11
+ .trim()
12
+ .split('\n')
13
+ .filter(Boolean);
14
+ }
15
+
16
+ function isRequiredTestGuidance(filePath) {
17
+ return (
18
+ /^readme\.md$/i.test(filePath) ||
19
+ filePath === '.codex/AGENTS.md' ||
20
+ filePath === '.junie/guidelines.md' ||
21
+ filePath === '.agents/skills/carecard-workspace-standards/SKILL.md' ||
22
+ /^\.agents\/skills\/[^/]*(?:test|testing)[^/]*\/(?:SKILL\.md|references\/[^/]*(?:test|testing|coding-principles)[^/]*\.md)$/i.test(
23
+ filePath,
24
+ )
25
+ );
26
+ }
27
+
28
+ test('keeps the non-negotiable test order rule in repository guidance', () => {
29
+ const guidanceFiles = listRepositoryFiles().filter(isRequiredTestGuidance);
30
+ assert.ok(guidanceFiles.length > 0, 'No repository test guidance was found.');
31
+
32
+ for (const guidanceFile of guidanceFiles) {
33
+ const normalizedGuidance = readFileSync(guidanceFile, 'utf8').replace(/\s+/g, ' ').trim();
34
+ assert.ok(
35
+ normalizedGuidance.includes(TEST_ORDER_INVARIANCE_RULE),
36
+ `${guidanceFile} must document the non-negotiable test order invariance rule.`,
37
+ );
38
+ }
39
+ });
40
+
41
+ test('keeps default package scripts on the test framework ordinary ordering', () => {
42
+ const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
43
+
44
+ for (const [scriptName, command] of Object.entries(packageJson.scripts ?? {})) {
45
+ assert.equal(typeof command, 'string', `${scriptName} must be a string command.`);
46
+ assert.doesNotMatch(command, /--test-randomize|--test-random-seed/, `${scriptName} must not force randomized test ordering.`);
47
+ }
48
+ });
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { join, relative, resolve } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const repositoryRoot = resolve(import.meta.dirname, '../..');
9
+ const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
10
+ const testIndexSource = readFileSync(new URL('../../test/index.test.js', import.meta.url), 'utf8');
11
+ const { parallelTestFiles } = require('../../test/index.test.js');
12
+
13
+ function listRuntimeTestFiles(directoryPath) {
14
+ return readdirSync(directoryPath, { withFileTypes: true }).flatMap(entry => {
15
+ const entryPath = join(directoryPath, entry.name);
16
+ if (entry.isDirectory()) return listRuntimeTestFiles(entryPath);
17
+ if (!/\.test\.(?:js|mjs)$/.test(entry.name) || entry.name === 'index.test.js') {
18
+ return [];
19
+ }
20
+ return [relative(repositoryRoot, entryPath)];
21
+ });
22
+ }
23
+
24
+ test('keeps runtime test selection in the index and package scripts short', () => {
25
+ assert.equal(packageJson.scripts.test, 'npm run test:order && node test/index.test.js');
26
+ assert.match(packageJson.scripts['test:coverage'], /nyc node test\/index\.test\.js$/);
27
+ assert.match(testIndexSource, /parallelTestFiles/);
28
+ assert.match(testIndexSource, /runIndexedMochaTests/);
29
+ assert.match(testIndexSource, /if \(require\.main === module\)/);
30
+ });
31
+
32
+ test('runs the parallel execution contract in the test-order gate', () => {
33
+ assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/parallelTestPolicy\.test\.mjs/);
34
+ assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/runIndexedMochaTests\.test\.mjs/);
35
+ });
36
+
37
+ test('selects every runtime test file exactly once', () => {
38
+ assert.deepEqual([...parallelTestFiles].sort(), listRuntimeTestFiles(resolve(repositoryRoot, 'test')).sort());
39
+ });
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { createRequire } = require('node:module');
5
+ const { availableParallelism } = require('node:os');
6
+ const { resolve } = require('node:path');
7
+
8
+ const DEFAULT_MAX_PARALLEL_JOBS = 4;
9
+
10
+ // Pattern: Configuration Boundary - bounds workers without accepting invalid input.
11
+ function resolveParallelJobCount(
12
+ configuredJobCount,
13
+ testFileCount,
14
+ defaultMaximum = DEFAULT_MAX_PARALLEL_JOBS,
15
+ availableJobCount = availableParallelism(),
16
+ ) {
17
+ const requestedJobCount =
18
+ configuredJobCount === undefined ? Math.min(availableJobCount, defaultMaximum) : Number.parseInt(configuredJobCount, 10);
19
+
20
+ if (!Number.isInteger(requestedJobCount) || requestedJobCount < 1) {
21
+ throw new Error('TEST_PARALLEL_JOBS must be a positive integer.');
22
+ }
23
+ return Math.min(requestedJobCount, testFileCount);
24
+ }
25
+
26
+ // Pattern: Command Builder - keeps Mocha worker details out of package metadata.
27
+ function buildMochaArguments(testFiles, jobCount) {
28
+ const requireFromRunner = createRequire(__filename);
29
+ return [
30
+ requireFromRunner.resolve('mocha/bin/mocha.js'),
31
+ '--parallel',
32
+ '--jobs',
33
+ String(jobCount),
34
+ '--require',
35
+ resolve('scripts/testOrder/randomizeTestOrder.cjs'),
36
+ ...testFiles,
37
+ ];
38
+ }
39
+
40
+ // Pattern: Process Adapter - returns the exact test process result to the index.
41
+ function runIndexedMochaTests(testFiles) {
42
+ if (testFiles.length === 0) {
43
+ throw new Error('The package test index must select at least one test file.');
44
+ }
45
+
46
+ const jobCount = resolveParallelJobCount(process.env.TEST_PARALLEL_JOBS, testFiles.length);
47
+ const child = spawn(process.execPath, buildMochaArguments(testFiles, jobCount), {
48
+ env: {
49
+ ...process.env,
50
+ NODE_ENV: 'test',
51
+ },
52
+ stdio: 'inherit',
53
+ });
54
+
55
+ return new Promise((resolveExit, rejectExit) => {
56
+ child.once('error', rejectExit);
57
+ child.once('exit', (code, signal) => {
58
+ if (signal) {
59
+ rejectExit(new Error(`Mocha exited from signal ${signal}.`));
60
+ return;
61
+ }
62
+ resolveExit(code ?? 1);
63
+ });
64
+ });
65
+ }
66
+
67
+ module.exports = {
68
+ buildMochaArguments,
69
+ resolveParallelJobCount,
70
+ runIndexedMochaTests,
71
+ };
@@ -0,0 +1,21 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createRequire } from 'node:module';
3
+ import test from 'node:test';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const { buildMochaArguments, resolveParallelJobCount } = require('./runIndexedMochaTests.cjs');
7
+
8
+ test('uses bounded Mocha file workers without randomized default ordering', () => {
9
+ assert.equal(resolveParallelJobCount(undefined, 8, 4, 12), 4);
10
+ assert.equal(resolveParallelJobCount('2', 8, 4, 12), 2);
11
+
12
+ const argumentsList = buildMochaArguments(['test/example.test.js'], 2);
13
+
14
+ assert.ok(argumentsList.includes('--parallel'));
15
+ assert.deepEqual(argumentsList.slice(argumentsList.indexOf('--jobs'), argumentsList.indexOf('--jobs') + 2), ['--jobs', '2']);
16
+ assert.ok(argumentsList.includes('test/example.test.js'));
17
+ });
18
+
19
+ test('rejects invalid worker configuration instead of changing execution silently', () => {
20
+ assert.throws(() => resolveParallelJobCount('0', 8, 4, 12), /TEST_PARALLEL_JOBS must be a positive integer/);
21
+ });