@carecard/jwt-read 3.12.0 → 3.14.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.
- package/.agents/skills/carecard-workspace-standards/SKILL.md +13 -3
- package/.agents/skills/pkg-jwt-read-coding-standards-and-best-practices/SKILL.md +162 -0
- package/.agents/skills/pkg-jwt-read-coding-standards-and-best-practices/agents/openai.yaml +4 -0
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/SKILL.md +23 -3
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +2 -0
- package/AGENTS.md +15 -0
- package/index.d.ts +36 -20
- package/lib/jwtLib.js +6 -6
- package/package.json +10 -8
- package/readme.md +25 -3
- package/scripts/testOrder/randomizeTestOrder.cjs +40 -0
- package/scripts/testOrder/randomizeTestOrder.test.mjs +36 -0
- package/scripts/testOrder/testOrderPolicy.test.mjs +48 -0
- package/scripts/testParallel/parallelTestPolicy.test.mjs +39 -0
- package/scripts/testParallel/runIndexedMochaTests.cjs +71 -0
- package/scripts/testParallel/runIndexedMochaTests.test.mjs +21 -0
|
@@ -7,6 +7,14 @@ Non-negotiable root-cause solution rule: Always identify and solve the verified
|
|
|
7
7
|
|
|
8
8
|
# CareCard Workspace Standards
|
|
9
9
|
|
|
10
|
+
Mandatory companion: load
|
|
11
|
+
`$pkg-jwt-read-coding-standards-and-best-practices` before this skill for every
|
|
12
|
+
task in this repository.
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
10
18
|
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
19
|
|
|
12
20
|
This requirement is non-negotiable and may be overridden only with the user's
|
|
@@ -26,6 +34,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
|
|
|
26
34
|
|
|
27
35
|
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.
|
|
28
36
|
|
|
37
|
+
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`).
|
|
38
|
+
|
|
29
39
|
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.
|
|
30
40
|
|
|
31
41
|
## Purpose
|
|
@@ -406,9 +416,9 @@ the authenticated dashboard.
|
|
|
406
416
|
may map that role to `super_admin`, but backend auth RLS must not require a
|
|
407
417
|
separate database role row for that bypass.
|
|
408
418
|
- Public auth flows such as registration, login, server-auth session create and
|
|
409
|
-
introspection,
|
|
419
|
+
introspection, recovery, visitor creation, and service user
|
|
410
420
|
lookup must use narrow system contexts (`system_create`, `system_login`,
|
|
411
|
-
`
|
|
421
|
+
`system_recovery`, `system_visitor`, `system_service`)
|
|
412
422
|
instead of privileged runtime queries.
|
|
413
423
|
|
|
414
424
|
- `ms-auth` controller exports use concise action names such as `loginUser`,
|
|
@@ -419,7 +429,7 @@ the authenticated dashboard.
|
|
|
419
429
|
|
|
420
430
|
## Security Requirements
|
|
421
431
|
|
|
422
|
-
- Treat authentication, authorization, JWT, password, email
|
|
432
|
+
- Treat authentication, authorization, JWT, password, email verification,
|
|
423
433
|
recovery, file upload, CORS, rate limits, and error response behavior as
|
|
424
434
|
security-sensitive.
|
|
425
435
|
- Never log or return secrets, tokens, passwords, credentials, private keys,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pkg-jwt-read-coding-standards-and-best-practices
|
|
3
|
+
description: 'Mandatory for every pkg-jwt-read task, including analysis, clarification, planning, implementation, review, debugging, documentation, public API work, skill maintenance, and validation. Use before every narrower skill.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pkg JWT Read Coding Standards And Best Practices
|
|
7
|
+
|
|
8
|
+
Non-negotiable root-cause solution rule: Always identify and solve the verified
|
|
9
|
+
root cause, use the stronger solution, and deliver a correct, durable,
|
|
10
|
+
production-quality result. Never treat a temporary workaround, resource
|
|
11
|
+
increase, retry, suppression, bypass, or symptom-only patch as completion.
|
|
12
|
+
Validate the root-cause fix against the real failing workflow and prove the end
|
|
13
|
+
state.
|
|
14
|
+
|
|
15
|
+
Non-negotiable error and warning rule: Never suppress, silence, hide, downgrade,
|
|
16
|
+
filter, ignore, skip, or bypass errors or warnings from code, tests, tools,
|
|
17
|
+
compilers, linters, or validation. Fix the root cause, then rerun the affected
|
|
18
|
+
check and require a clean result. Expected error-path tests may assert errors,
|
|
19
|
+
but must not conceal unexpected failures.
|
|
20
|
+
|
|
21
|
+
Non-negotiable repository isolation rule: Every repository must run its Husky
|
|
22
|
+
hooks and tests using only files, code, fixtures, dependencies, and services
|
|
23
|
+
contained within that repository. Tests and Husky scripts must not import,
|
|
24
|
+
require, read, execute, or otherwise depend on sibling repositories or paths
|
|
25
|
+
outside the repository root. app-e2e-tests is the only exception because
|
|
26
|
+
cross-repository end-to-end testing is its explicit responsibility.
|
|
27
|
+
|
|
28
|
+
## Mandatory Use And Authorities
|
|
29
|
+
|
|
30
|
+
Load this skill before doing any work in `pkg-jwt-read`, including read-only
|
|
31
|
+
and documentation-only work. Then load every narrower skill that owns the
|
|
32
|
+
affected package contract.
|
|
33
|
+
|
|
34
|
+
Use these existing authorities instead of duplicating them:
|
|
35
|
+
|
|
36
|
+
- `$carecard-workspace-standards` for TDD, root-cause solutions, dependencies,
|
|
37
|
+
errors, isolation, and repository workflow;
|
|
38
|
+
- `$software-design-patterns-and-clean-code` for design, DRY, KISS,
|
|
39
|
+
testability, and clean-code details;
|
|
40
|
+
- `$pkg-jwt-read-jwt-middleware-library` for JWT middleware, auth context,
|
|
41
|
+
exports, types, security, and validation; and
|
|
42
|
+
- `$pkg-publish` only when runtime package artifacts or consumer versions must
|
|
43
|
+
be published and propagated.
|
|
44
|
+
|
|
45
|
+
This skill adds the function-evolution, direct-contract, composition, and
|
|
46
|
+
completion rules below without weakening those companion skills.
|
|
47
|
+
|
|
48
|
+
## Requirement Judgment
|
|
49
|
+
|
|
50
|
+
1. Read the complete request and inspect implementation, public exports,
|
|
51
|
+
declarations, middleware call sites, tests, documentation, and consumers
|
|
52
|
+
before deciding how to change the package.
|
|
53
|
+
2. Translate the request into a coherent technical contract. Do not apply
|
|
54
|
+
wording mechanically when it is contradictory, unsafe, impossible, or
|
|
55
|
+
incompatible with authentication or package architecture.
|
|
56
|
+
3. Make low-risk, reversible assumptions only when they preserve requested
|
|
57
|
+
behavior and scope.
|
|
58
|
+
4. Ask for clarification when an unresolved choice would materially change a
|
|
59
|
+
public API, authentication or authorization behavior, security, consumer
|
|
60
|
+
behavior, destructive scope, or the repositories that must change.
|
|
61
|
+
5. Explain architectural tradeoffs before a major API, middleware, type,
|
|
62
|
+
authentication, module, package, or dependency change.
|
|
63
|
+
|
|
64
|
+
## Scope And Quality
|
|
65
|
+
|
|
66
|
+
- Treat every workspace repository as independent and validate it from its own
|
|
67
|
+
root.
|
|
68
|
+
- Update every skill, document, source, runtime test, type test, export,
|
|
69
|
+
declaration, consumer, and package version genuinely required for a coherent
|
|
70
|
+
task.
|
|
71
|
+
- Do not broaden the task into unrelated cleanup.
|
|
72
|
+
- Preserve CommonJS, middleware, request-context, role, declaration, naming,
|
|
73
|
+
and test conventions unless the task explicitly replaces them.
|
|
74
|
+
- Prefer Node core and existing package helpers over new dependencies.
|
|
75
|
+
- Prefer readable direct implementation over clever compression or a temporary
|
|
76
|
+
workaround.
|
|
77
|
+
- Use meaningful names that describe authentication and authorization intent.
|
|
78
|
+
|
|
79
|
+
## Function Evolution
|
|
80
|
+
|
|
81
|
+
Before changing a function's behavior or signature, inventory every direct,
|
|
82
|
+
indirect, test, exported, middleware, callback, configuration-driven, and
|
|
83
|
+
dynamic consumer.
|
|
84
|
+
|
|
85
|
+
- If exactly one consumer is proven, change the function only when required.
|
|
86
|
+
- If two or more consumers exist, do not change the shared function's behavior.
|
|
87
|
+
Create a new purpose-named function and migrate only intended consumers.
|
|
88
|
+
- If every consumer needs the new contract, migrate all consumers and delete
|
|
89
|
+
the old function after proving it unused.
|
|
90
|
+
- Treat every exported, declared, public, middleware, callback, or dynamically
|
|
91
|
+
discovered function as shared unless single use is conclusively proven.
|
|
92
|
+
- Do not add caller branches, mode flags, or optional parameters merely to
|
|
93
|
+
make one shared function serve incompatible contracts.
|
|
94
|
+
- Cover the new function, public surface, types, and every migrated consumer
|
|
95
|
+
through TDD.
|
|
96
|
+
|
|
97
|
+
## Direct Contract Without Backward Compatibility
|
|
98
|
+
|
|
99
|
+
When the active task replaces a contract, implement the requested end state
|
|
100
|
+
directly. Do not add legacy aliases, deprecated wrappers, compatibility
|
|
101
|
+
overloads, duplicate exports, dual auth paths, transitional names, or fallback
|
|
102
|
+
behavior solely to preserve the superseded contract.
|
|
103
|
+
|
|
104
|
+
Delete obsolete functions and exports after all intended consumers have
|
|
105
|
+
migrated and repository-native search, runtime tests, and type tests prove them
|
|
106
|
+
unused. This does not authorize unrelated API removal. If an existing
|
|
107
|
+
published or security contract requires compatibility and the request does not
|
|
108
|
+
clearly supersede it, explain the conflict and ask first.
|
|
109
|
+
|
|
110
|
+
## TDD And Root-Cause Gate
|
|
111
|
+
|
|
112
|
+
Follow `$carecard-workspace-standards` and
|
|
113
|
+
`$pkg-jwt-read-jwt-middleware-library` for the complete failing-test-first and
|
|
114
|
+
root-cause workflow. Documentation and skill changes require a focused
|
|
115
|
+
structural validation before prose changes. Do not accept retries, suppressed
|
|
116
|
+
diagnostics, weakened types, disabled tests, forced success, compatibility
|
|
117
|
+
patches, or symptom-only workarounds as completion.
|
|
118
|
+
|
|
119
|
+
## Function Size
|
|
120
|
+
|
|
121
|
+
Every new or materially changed function or middleware body must contain at most 25 logical code lines.
|
|
122
|
+
|
|
123
|
+
- Count executable statements, branches, loop headers, side-effecting calls,
|
|
124
|
+
returns, and throws.
|
|
125
|
+
- Exclude signatures, type-only declarations, blank lines, comments, and
|
|
126
|
+
isolated braces.
|
|
127
|
+
- Extract cohesive purpose-named helpers and compose them when needed.
|
|
128
|
+
- Keep parsing, verification, attachment, role evaluation, and error behavior
|
|
129
|
+
explicit rather than hiding them in one long function.
|
|
130
|
+
- Avoid meaningless forwarding wrappers and do not refactor untouched
|
|
131
|
+
functions solely to satisfy this limit.
|
|
132
|
+
|
|
133
|
+
## UI Composition
|
|
134
|
+
|
|
135
|
+
This package does not own UI. If a package contract requires UI changes, make
|
|
136
|
+
them in the owning app repository. There, create focused components, compose
|
|
137
|
+
existing and new components, and delete obsolete components only after proving
|
|
138
|
+
them unused and replaced.
|
|
139
|
+
|
|
140
|
+
## Skills, Documentation, And Database Boundaries
|
|
141
|
+
|
|
142
|
+
- Update affected skills, README guidance, examples, exports, and declarations
|
|
143
|
+
with behavior or validation changes.
|
|
144
|
+
- Reference existing authoritative skills instead of copying their details.
|
|
145
|
+
- If work reaches an `ms-*` database, change SQL only in the owning repository
|
|
146
|
+
using its database-migration-ownership skill. For first-create work, edit the
|
|
147
|
+
existing migration and matching rollback directly rather than adding a
|
|
148
|
+
compatibility migration.
|
|
149
|
+
- Keep persistence and service behavior in their owning repositories.
|
|
150
|
+
|
|
151
|
+
## Completion
|
|
152
|
+
|
|
153
|
+
1. Review each changed repository's diff and status independently.
|
|
154
|
+
2. Run focused runtime and type tests, then all broader checks required by
|
|
155
|
+
local skills.
|
|
156
|
+
3. If every changed file in a repository is Markdown (`*.md`), skip Husky and
|
|
157
|
+
run only focused Markdown validation.
|
|
158
|
+
4. If any changed file is not Markdown, run every direct `.husky` script. If
|
|
159
|
+
none exists, run the strongest repository-native focused validation.
|
|
160
|
+
5. Fix every in-scope failure at its root cause and rerun the exact command.
|
|
161
|
+
6. Report exact commands, results, limitations, and remaining risk.
|
|
162
|
+
7. Do not perform remote Git or GitHub operations unless explicitly requested.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: 'Pkg JWT Read Coding Standards'
|
|
3
|
+
short_description: 'Apply mandatory JWT package standards'
|
|
4
|
+
default_prompt: 'Use $pkg-jwt-read-coding-standards-and-best-practices before every pkg-jwt-read task and apply all narrower skills that the task requires.'
|
|
@@ -13,6 +13,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
|
|
|
13
13
|
|
|
14
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.
|
|
15
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
|
+
|
|
16
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.
|
|
17
19
|
|
|
18
20
|
## Purpose
|
|
@@ -124,9 +126,9 @@ depend on those folders being present.
|
|
|
124
126
|
- Server-auth request attachment behavior that normalizes introspected claims
|
|
125
127
|
into `req.jwt.payload` with `authMode: "server-auth"` and
|
|
126
128
|
`auth_mode: "server-auth"`.
|
|
127
|
-
- Server-auth email
|
|
128
|
-
`emailVerified
|
|
129
|
-
|
|
129
|
+
- Server-auth email verification claims are copied only when present. The
|
|
130
|
+
`emailVerified` and `email_verified` names retain their exact values, and
|
|
131
|
+
omission remains omission.
|
|
130
132
|
- Integration with `@carecard/common-util` for standardized login and
|
|
131
133
|
authorization errors.
|
|
132
134
|
|
|
@@ -287,3 +289,21 @@ repository's agents-only Git workflow:
|
|
|
287
289
|
Do not commit or push `.agents` guidance changes directly from `development`
|
|
288
290
|
or `main`. Do not stage unrelated files, generated output, dependency folders,
|
|
289
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.
|
|
@@ -13,6 +13,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
|
|
|
13
13
|
|
|
14
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.
|
|
15
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
|
+
|
|
16
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.
|
|
17
19
|
|
|
18
20
|
## Purpose
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Codex Instructions
|
|
2
|
+
|
|
3
|
+
## Non-negotiable Codex banked-reset requirement
|
|
4
|
+
|
|
5
|
+
- Never use or consume a banked Codex rate-limit reset automatically.
|
|
6
|
+
- Before using any banked Codex rate-limit reset, stop, ask the user for explicit, direct approval for that specific reset, and wait for their reply.
|
|
7
|
+
- Never treat earlier approval, a standing instruction, silence, urgency, an unfinished task, or a request to continue as approval for a future reset.
|
|
8
|
+
- Do not invoke `/usage` redemption, a reset-consumption action, a reset API or tool, or any equivalent mechanism unless the user explicitly approved that specific reset.
|
|
9
|
+
- If a Codex limit is reached without that approval, pause and let the user reset it manually. Never consume a banked reset to keep working.
|
|
10
|
+
|
|
11
|
+
## Repository validation contracts
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
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.
|
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]:
|
|
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]:
|
|
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?:
|
|
79
|
+
jwtClientId: (req?: JwtRequestContext) => string | undefined;
|
|
80
80
|
doesJwtUserHasRole: (role: string) => boolean;
|
|
81
81
|
isJwtExpired: (jwtValiditySeconds?: number) => boolean;
|
|
82
|
-
jwtAgeInSeconds: (req?:
|
|
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?:
|
|
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;
|
|
@@ -129,15 +147,13 @@ export interface ServerAuthIntrospectionClaims {
|
|
|
129
147
|
email?: string;
|
|
130
148
|
emailVerified?: boolean;
|
|
131
149
|
email_verified?: boolean;
|
|
132
|
-
emailConfirmed?: boolean;
|
|
133
|
-
email_confirmed?: boolean;
|
|
134
150
|
roles?: string[];
|
|
135
151
|
sessionId?: string;
|
|
136
152
|
session_id?: string;
|
|
137
153
|
exp?: number | string;
|
|
138
154
|
expiresAt?: string;
|
|
139
155
|
expires_at?: string;
|
|
140
|
-
[key: string]:
|
|
156
|
+
[key: string]: unknown;
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
export type ServerAuthIntrospector = (
|
|
@@ -214,24 +230,24 @@ export function jwtVerifyVisitorNoThrow(
|
|
|
214
230
|
/**
|
|
215
231
|
* Returns the sub from the extracted JWT in req.jwt.
|
|
216
232
|
*/
|
|
217
|
-
export function jwtGetClientId(req?:
|
|
233
|
+
export function jwtGetClientId(req?: JwtRequestContext): string | undefined;
|
|
218
234
|
|
|
219
235
|
/**
|
|
220
236
|
* Returns the sub from the extracted visitor token in req.visitor.
|
|
221
237
|
*/
|
|
222
|
-
export function jwtGetVisitorClientId(req?:
|
|
238
|
+
export function jwtGetVisitorClientId(req?: JwtRequestContext): string | undefined;
|
|
223
239
|
|
|
224
240
|
/**
|
|
225
241
|
* Checks if the extracted JWT in req.jwt has expired.
|
|
226
242
|
*/
|
|
227
|
-
export function jwtIsExpired(req:
|
|
243
|
+
export function jwtIsExpired(req: JwtRequestContext, jwtValiditySeconds?: number): boolean;
|
|
228
244
|
export function jwtIsExpired(jwtValiditySeconds: number): boolean;
|
|
229
245
|
export function jwtIsExpired(): boolean;
|
|
230
246
|
|
|
231
247
|
/**
|
|
232
248
|
* Returns the age of the extracted JWT in seconds.
|
|
233
249
|
*/
|
|
234
|
-
export function jwtGetAgeInSeconds(req?:
|
|
250
|
+
export function jwtGetAgeInSeconds(req?: JwtRequestContext): number;
|
|
235
251
|
|
|
236
252
|
/**
|
|
237
253
|
* Returns a middleware that verifies the JWT and checks if the user has the required role.
|
|
@@ -295,7 +311,7 @@ export interface JwtContext {
|
|
|
295
311
|
* Always returns user_id. If the roles array contains 'ad', also returns role: 'super_admin'.
|
|
296
312
|
* If req.userAuthorization is present, also returns authorizationContext and userAuthorization.
|
|
297
313
|
*/
|
|
298
|
-
export function jwtGetContext(req:
|
|
314
|
+
export function jwtGetContext(req: JwtRequestContext): JwtContext;
|
|
299
315
|
|
|
300
316
|
/**
|
|
301
317
|
* Validates the JWT from the Authorization header and extracts it into req.jwt.
|
|
@@ -454,19 +470,19 @@ export function verifyVisitorNoThrow(
|
|
|
454
470
|
* Returns the sub from the extracted JWT in req.jwt.
|
|
455
471
|
* @deprecated use jwtGetClientId
|
|
456
472
|
*/
|
|
457
|
-
export function jwtClientId(req?:
|
|
473
|
+
export function jwtClientId(req?: JwtRequestContext): string | undefined;
|
|
458
474
|
|
|
459
475
|
/**
|
|
460
476
|
* Returns the sub from the extracted visitor token in req.visitor.
|
|
461
477
|
* @deprecated use jwtGetVisitorClientId
|
|
462
478
|
*/
|
|
463
|
-
export function visitorClientId(req?:
|
|
479
|
+
export function visitorClientId(req?: JwtRequestContext): string | undefined;
|
|
464
480
|
|
|
465
481
|
/**
|
|
466
482
|
* Checks if the extracted JWT in req.jwt has expired.
|
|
467
483
|
* @deprecated use jwtIsExpired
|
|
468
484
|
*/
|
|
469
|
-
export function isJwtExpired(req:
|
|
485
|
+
export function isJwtExpired(req: JwtRequestContext, jwtValiditySeconds?: number): boolean;
|
|
470
486
|
/** @deprecated use jwtIsExpired */
|
|
471
487
|
export function isJwtExpired(jwtValiditySeconds: number): boolean;
|
|
472
488
|
/** @deprecated use jwtIsExpired */
|
|
@@ -476,7 +492,7 @@ export function isJwtExpired(): boolean;
|
|
|
476
492
|
* Returns the age of the extracted JWT in seconds.
|
|
477
493
|
* @deprecated use jwtGetAgeInSeconds
|
|
478
494
|
*/
|
|
479
|
-
export function jwtAgeInSeconds(req?:
|
|
495
|
+
export function jwtAgeInSeconds(req?: JwtRequestContext): number;
|
|
480
496
|
|
|
481
497
|
/**
|
|
482
498
|
* Returns a middleware that verifies the JWT and checks if the user has the required role.
|
|
@@ -499,7 +515,7 @@ export function throwUsedTokenError(): never;
|
|
|
499
515
|
* Checks if the user in the extracted JWT has the specified role.
|
|
500
516
|
* @deprecated use jwtDoesJwtUserHasRole
|
|
501
517
|
*/
|
|
502
|
-
export function doesJwtUserHasRole(req:
|
|
518
|
+
export function doesJwtUserHasRole(req: JwtRequestContext, userRole: string): boolean;
|
|
503
519
|
/** @deprecated use jwtDoesJwtUserHasRole */
|
|
504
520
|
export function doesJwtUserHasRole(userRole: string): boolean;
|
|
505
521
|
|
package/lib/jwtLib.js
CHANGED
|
@@ -5,7 +5,7 @@ const { throwLoginRequiredError, throwNotAuthorizedError } = require('@carecard/
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_USER_AUTHORIZATION_HEADER_NAME = 'X-Authorization-Context';
|
|
7
7
|
const DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH = 2048;
|
|
8
|
-
const
|
|
8
|
+
const EMAIL_VERIFICATION_CLAIM_NAMES = ['emailVerified', 'email_verified'];
|
|
9
9
|
|
|
10
10
|
function jwtClientId(req) {
|
|
11
11
|
const jwtObj = req?.jwt || this;
|
|
@@ -436,22 +436,22 @@ function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
|
436
436
|
_attachJwtMethods(req.jwt);
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
// Pattern: Projection - copies only authoritative
|
|
440
|
-
function
|
|
439
|
+
// Pattern: Projection - copies only authoritative verification names and preserves their exact values.
|
|
440
|
+
function pickEmailVerificationClaims(claims) {
|
|
441
441
|
return Object.fromEntries(
|
|
442
|
-
|
|
442
|
+
EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName => Object.prototype.hasOwnProperty.call(claims, claimName)).map(claimName => [
|
|
443
443
|
claimName,
|
|
444
444
|
claims[claimName],
|
|
445
445
|
]),
|
|
446
446
|
);
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// Pattern: Mapper - attaches server-auth metadata while preserving authoritative
|
|
449
|
+
// Pattern: Mapper - attaches server-auth metadata while preserving authoritative verification claims.
|
|
450
450
|
function createServerAuthPayload(claims) {
|
|
451
451
|
return {
|
|
452
452
|
sub: claims.sub || claims.userId || claims.user_id,
|
|
453
453
|
email: claims.email || '',
|
|
454
|
-
...
|
|
454
|
+
...pickEmailVerificationClaims(claims),
|
|
455
455
|
roles: Array.isArray(claims.roles) ? claims.roles : [],
|
|
456
456
|
authMode: 'server-auth',
|
|
457
457
|
auth_mode: 'server-auth',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/jwt-read",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.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": "
|
|
13
|
-
"test:
|
|
14
|
-
"test:
|
|
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.
|
|
45
|
-
"@carecard/common-util": "3.
|
|
46
|
-
"@carecard/validate": "3.
|
|
45
|
+
"@carecard/auth-util": "3.14.0",
|
|
46
|
+
"@carecard/common-util": "3.14.0",
|
|
47
|
+
"@carecard/validate": "3.14.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.
|
|
53
|
+
"js-yaml": "4.3.0"
|
|
52
54
|
}
|
|
53
55
|
}
|
package/readme.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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
|
+
|
|
3
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.
|
|
4
6
|
|
|
5
7
|

|
|
@@ -18,6 +20,8 @@ Non-negotiable repository isolation rule: Every repository must run its Husky ho
|
|
|
18
20
|
|
|
19
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.
|
|
20
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
|
+
|
|
21
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.
|
|
22
26
|
|
|
23
27
|
## Features
|
|
@@ -148,9 +152,9 @@ those claims onto `req.jwt.payload` with `authMode: "server-auth"` and
|
|
|
148
152
|
`auth_mode: "server-auth"` so services can keep their existing JWT-backed
|
|
149
153
|
database context and role checks.
|
|
150
154
|
|
|
151
|
-
Server-auth email
|
|
152
|
-
`emailVerified
|
|
153
|
-
|
|
155
|
+
Server-auth email verification claims are copied only when present. The
|
|
156
|
+
`emailVerified` and `email_verified` names retain their exact values, and
|
|
157
|
+
omission remains omission.
|
|
154
158
|
|
|
155
159
|
### Scoped User Authorization Context
|
|
156
160
|
|
|
@@ -227,3 +231,21 @@ The package is organized into several modules:
|
|
|
227
231
|
- `jwtRoles`: Role mapping between internal codes and names.
|
|
228
232
|
|
|
229
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
|
+
});
|