@carecard/auth-util 3.18.0 → 3.19.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.
@@ -57,7 +57,11 @@ const isNonEmptyString = value => {
57
57
  };
58
58
 
59
59
  const audienceIsNonEmptyStringArray = audience => {
60
- return Array.isArray(audience) && audience.length > 0 && audience.every(audienceValue => isNonEmptyString(audienceValue));
60
+ return (
61
+ Array.isArray(audience) &&
62
+ audience.length > 0 &&
63
+ audience.every(audienceValue => isNonEmptyString(audienceValue))
64
+ );
61
65
  };
62
66
 
63
67
  const isNonEmptyAudience = audience => {
@@ -65,7 +69,9 @@ const isNonEmptyAudience = audience => {
65
69
  };
66
70
 
67
71
  const normalizeSeconds = value => {
68
- if (!Number.isFinite(value)) return null;
72
+ if (!Number.isFinite(value)) {
73
+ return null;
74
+ }
69
75
  return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
70
76
  };
71
77
 
@@ -77,7 +83,9 @@ const normalizeSeconds = value => {
77
83
  * @return {string|null}
78
84
  */
79
85
  const createSignedJwtFromObject = (headerObject, payloadObject, privateKey) => {
80
- if (!privateKey) return null;
86
+ if (!privateKey) {
87
+ return null;
88
+ }
81
89
 
82
90
  const header = { ...headerObject };
83
91
  header.alg = header.alg || 'EdDSA';
@@ -116,7 +124,9 @@ function createServiceJwt({
116
124
  }
117
125
 
118
126
  const iat = normalizeSeconds(issuedAt);
119
- if (!Number.isInteger(iat)) return null;
127
+ if (!Number.isInteger(iat)) {
128
+ return null;
129
+ }
120
130
 
121
131
  // Service-to-service tokens use registered JWT claims so receivers can
122
132
  // validate sender, intended audience, subject, and lifetime consistently.
@@ -147,9 +157,13 @@ function createServiceAuthorizationHeader(options = {}) {
147
157
  */
148
158
  const verifyJwtSignature = (jwt, publicKey) => {
149
159
  try {
150
- if (!jwt || !publicKey) return false;
160
+ if (!jwt || !publicKey) {
161
+ return false;
162
+ }
151
163
  const splitJWT = jwt.split('.');
152
- if (splitJWT.length !== 3) return false;
164
+ if (splitJWT.length !== 3) {
165
+ return false;
166
+ }
153
167
 
154
168
  const [header, payload, signature] = splitJWT;
155
169
  const token = header + '.' + payload;
@@ -157,7 +171,9 @@ const verifyJwtSignature = (jwt, publicKey) => {
157
171
 
158
172
  return _verify(token, signature, headerObject.alg, publicKey);
159
173
  } catch (error) {
160
- if (!(error instanceof SyntaxError)) throw error;
174
+ if (!(error instanceof SyntaxError)) {
175
+ throw error;
176
+ }
161
177
  return false;
162
178
  }
163
179
  };
@@ -169,16 +185,22 @@ const verifyJwtSignature = (jwt, publicKey) => {
169
185
  */
170
186
  const getHeaderPayloadFromJwt = jwt => {
171
187
  try {
172
- if (typeof jwt !== 'string') return null;
188
+ if (typeof jwt !== 'string') {
189
+ return null;
190
+ }
173
191
  const splitJWT = jwt.split('.');
174
- if (splitJWT.length !== 3) return null;
192
+ if (splitJWT.length !== 3) {
193
+ return null;
194
+ }
175
195
 
176
196
  const headerObject = _decode(splitJWT[0]);
177
197
  const payloadObject = _decode(splitJWT[1]);
178
198
 
179
199
  return { header: headerObject, payload: payloadObject };
180
200
  } catch (error) {
181
- if (!(error instanceof SyntaxError)) throw error;
201
+ if (!(error instanceof SyntaxError)) {
202
+ throw error;
203
+ }
182
204
  return null;
183
205
  }
184
206
  };
@@ -22,10 +22,14 @@ const createPasswordHashWithRandomSalt = (password, secret, algorithm) => {
22
22
  * @return {string}
23
23
  */
24
24
  const createPasswordHashBasedOnSavedAlgorithmSalt = (password, savedPasswordHash, secret) => {
25
- if (typeof savedPasswordHash !== 'string') return null;
25
+ if (typeof savedPasswordHash !== 'string') {
26
+ return null;
27
+ }
26
28
 
27
29
  const splitStringArray = savedPasswordHash.split('$');
28
- if (splitStringArray.length !== 6) return null;
30
+ if (splitStringArray.length !== 6) {
31
+ return null;
32
+ }
29
33
 
30
34
  const algBase64 = splitStringArray[2];
31
35
  const salt = splitStringArray[4];
@@ -60,7 +60,9 @@ const base64ToAscii = codedString => {
60
60
  */
61
61
  const dollarSignConnectedStringToAlgorithmHashSalt = passwordHash => {
62
62
  const splitStringArray = passwordHash.split('$');
63
- if (splitStringArray.length !== 6) return null;
63
+ if (splitStringArray.length !== 6) {
64
+ return null;
65
+ }
64
66
  return {
65
67
  version: splitStringArray[1],
66
68
  alg: splitStringArray[2],
@@ -76,9 +78,13 @@ const dollarSignConnectedStringToAlgorithmHashSalt = passwordHash => {
76
78
  * @deprecated Use native Buffer methods or other modern alternatives.
77
79
  */
78
80
  const dotConnectedStringToHeaderPayloadSignature = jwt => {
79
- if (typeof jwt !== 'string') return null;
81
+ if (typeof jwt !== 'string') {
82
+ return null;
83
+ }
80
84
  const splitJWT = jwt.split('.');
81
- if (splitJWT.length !== 3) return null;
85
+ if (splitJWT.length !== 3) {
86
+ return null;
87
+ }
82
88
  return {
83
89
  header: splitJWT[0],
84
90
  payload: splitJWT[1],
@@ -0,0 +1,36 @@
1
+ import { ESLint } from 'eslint';
2
+
3
+ const eslint = new ESLint();
4
+
5
+ // Pattern: Pure Function - builds one deterministic command without shell interpolation.
6
+ const createCommand = (command, filePaths) =>
7
+ `${command} ${filePaths.map(filePath => JSON.stringify(filePath)).join(' ')}`;
8
+
9
+ // Pattern: Adapter - derives lint-staged input from ESLint's authoritative ignore rules.
10
+ const removeEslintIgnoredFiles = async filePaths => {
11
+ const ignoredFileStates = await Promise.all(
12
+ filePaths.map(filePath => eslint.isPathIgnored(filePath)),
13
+ );
14
+
15
+ return filePaths.flatMap((filePath, index) => (ignoredFileStates[index] ? [] : [filePath]));
16
+ };
17
+
18
+ // Pattern: Pipeline - preserves ESLint-before-Prettier ordering for staged code.
19
+ const createJavaScriptTasks = async filePaths => {
20
+ const lintableFilePaths = await removeEslintIgnoredFiles(filePaths);
21
+ const tasks = [];
22
+
23
+ if (lintableFilePaths.length > 0) {
24
+ tasks.push(createCommand('eslint --fix --max-warnings 0', lintableFilePaths));
25
+ }
26
+
27
+ tasks.push(createCommand('prettier --write', filePaths));
28
+ return tasks;
29
+ };
30
+
31
+ const lintStagedConfig = {
32
+ '*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}': createJavaScriptTasks,
33
+ '*.{json,jsonc,md,mdx,css,scss,yaml,yml}': ['prettier --write'],
34
+ };
35
+
36
+ export default lintStagedConfig;
package/package.json CHANGED
@@ -1,21 +1,23 @@
1
1
  {
2
2
  "name": "@carecard/auth-util",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "repository": "https://github.com/CareCard-ca/pkg-auth-util.git",
5
5
  "description": "Auth utility functions",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {
9
9
  "test": "node scripts/runPackageTask.mjs test",
10
- "test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testOrder/testOrderPolicy.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/testParallel/parallelTestPolicy.test.mjs scripts/packageTaskRunner.test.mjs",
10
+ "test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/packageTaskRunner.test.mjs scripts/canonicalTestCommand.test.mjs",
11
11
  "test:types": "node scripts/runPackageTask.mjs test:types",
12
12
  "test:coverage": "node scripts/runPackageTask.mjs test:coverage",
13
13
  "test:All": "node scripts/runPackageTask.mjs test:All",
14
- "format": "prettier --write .",
15
- "format:check": "prettier --check .",
14
+ "validate:audits": "node scripts/runPackageTask.mjs validate:audits",
15
+ "format": "prettier --write \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
16
+ "format:check": "prettier --check \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
17
+ "lint-staged": "lint-staged",
16
18
  "prepare": "husky",
17
- "lint:fix": "eslint --fix",
18
- "lint": "eslint"
19
+ "lint:fix": "eslint . --fix --max-warnings 0",
20
+ "lint": "eslint . --max-warnings 0"
19
21
  },
20
22
  "keywords": [
21
23
  "auth",
@@ -26,16 +28,19 @@
26
28
  "author": "CareCard team",
27
29
  "license": "ISC",
28
30
  "devDependencies": {
31
+ "@eslint/js": "9.39.5",
29
32
  "@istanbuljs/nyc-config-typescript": "1.0.2",
30
33
  "@types/mocha": "10.0.10",
31
34
  "@types/node": "25.9.3",
32
- "eslint": "9.39.4",
35
+ "@typescript-eslint/parser": "8.67.0",
36
+ "eslint": "9.39.5",
33
37
  "express": "5.2.1",
38
+ "globals": "17.7.0",
34
39
  "husky": "9.1.7",
35
- "lint-staged": "17.0.7",
40
+ "lint-staged": "17.2.0",
36
41
  "mocha": "11.7.6",
37
42
  "nyc": "18.0.0",
38
- "prettier": "3.8.4",
43
+ "prettier": "3.9.6",
39
44
  "source-map-support": "0.5.21",
40
45
  "supertest": "7.2.2",
41
46
  "ts-node": "10.9.2",
@@ -49,8 +54,9 @@
49
54
  "overrides": {
50
55
  "diff": "8.0.4",
51
56
  "glob": "13.0.6",
52
- "minimatch": "10.2.5",
57
+ "brace-expansion": "5.0.9",
58
+ "minimatch": "10.2.6",
53
59
  "serialize-javascript": "7.0.5",
54
- "js-yaml": "4.3.0"
60
+ "js-yaml": "4.3.1"
55
61
  }
56
62
  }
package/readme.md CHANGED
@@ -11,7 +11,9 @@ Utility package for authentication and authorization in the CareCard ecosystem.
11
11
 
12
12
  ## Development Rule
13
13
 
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.
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, run the relevant focused non-test
15
+ validation before changing the prose; do not add automated tests that inspect
16
+ prose, files, or repository structure.
15
17
 
16
18
  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.
17
19
 
@@ -172,3 +174,22 @@ immediately when no helper remains, allow only a bounded 250 ms settlement
172
174
  window for already-stopping helpers, fail persistent descendants, preserve
173
175
  failures and output, use exit code `124` only for a real outer deadline, and
174
176
  remain a final guard rather than a substitute for explicit cleanup.
177
+
178
+ ## TDD And Validation
179
+
180
+ Test Driven Development is a non-negotiable requirement.
181
+
182
+ The sole purpose of automated tests is to verify observable functionality and externally visible behavior.
183
+ Tests must validate what the system does through its public interfaces and expected outcomes.
184
+
185
+ Tests must not assert, inspect, or depend on implementation details, including but not limited to:
186
+
187
+ - The existence of specific lines of code, statements, functions, classes, files, or modules.
188
+ - Specific algorithms, control flow, variable names, method calls, code snippets, or internal implementation choices.
189
+ - Any internal structure that can change without changing externally observable behavior.
190
+
191
+ A correct implementation may be completely rewritten or refactored without requiring changes to functional tests, provided its externally observable behavior remains unchanged.
192
+
193
+ Any test that fails solely because the implementation changed while the externally observable behavior remained correct is incorrectly designed and must be rewritten or removed.
194
+
195
+ This requirement is mandatory for all new tests and must be applied whenever existing tests are modified.
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { runPackageTask } from './runPackageTask.mjs';
5
+
6
+ test('the complete test command runs each validation category exactly once', () => {
7
+ const executedSteps = [];
8
+
9
+ const exitCode = runPackageTask('test', taskStep => {
10
+ executedSteps.push([taskStep.command, ...(taskStep.arguments ?? [])].join(' '));
11
+ return 0;
12
+ });
13
+
14
+ assert.equal(exitCode, 0);
15
+ assert.deepEqual(executedSteps, [
16
+ 'npm run validate:audits',
17
+ 'npm run test:order',
18
+ 'tsc --noEmit',
19
+ 'mocha --require ./scripts/testOrder/randomizeTestOrder.cjs -r ts-node/register test/types.test.ts',
20
+ 'nyc node test/index.test.js',
21
+ ]);
22
+ });
23
+
24
+ test('the legacy aggregate command delegates to the complete test command once', () => {
25
+ const executedSteps = [];
26
+
27
+ runPackageTask('test:All', taskStep => {
28
+ executedSteps.push([taskStep.command, ...(taskStep.arguments ?? [])].join(' '));
29
+ return 0;
30
+ });
31
+
32
+ assert.deepEqual(executedSteps, ['npm test']);
33
+ });
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
+ const composedCommandPattern = /&&|\|\||\bsh -c\b|\bnode -e\b/u;
7
+ const runnerCommandPattern = /^node scripts\/runPackageTask\.mjs ([A-Za-z0-9:_-]+)$/u;
8
+
9
+ async function loadPackageTaskRunner() {
10
+ return import(new URL('./runPackageTask.mjs', import.meta.url));
11
+ }
12
+
13
+ test('delegates composed package tasks to the repository runner', async () => {
14
+ const runnerCommands = Object.entries(packageJson.scripts ?? {}).filter(([, command]) =>
15
+ runnerCommandPattern.test(command),
16
+ );
17
+
18
+ for (const [scriptName, command] of Object.entries(packageJson.scripts ?? {})) {
19
+ assert.doesNotMatch(
20
+ command,
21
+ composedCommandPattern,
22
+ `${scriptName} must delegate composition to runPackageTask.mjs`,
23
+ );
24
+ }
25
+
26
+ assert.ok(runnerCommands.length > 0, 'at least one package task must use the runner');
27
+ const { packageTasks } = await loadPackageTaskRunner();
28
+ for (const [scriptName] of runnerCommands) {
29
+ assert.ok(packageTasks[scriptName], `${scriptName} must have a runner task`);
30
+ }
31
+ });
32
+
33
+ test('does not suppress command execution errors at the CLI boundary', () => {
34
+ const runnerSource = readFileSync(new URL('./runPackageTask.mjs', import.meta.url), 'utf8');
35
+
36
+ assert.doesNotMatch(runnerSource, /catch\s*\{/u);
37
+ });
@@ -1,29 +1,10 @@
1
1
  import assert from 'node:assert/strict';
2
- import { readFileSync } from 'node:fs';
3
2
  import test from 'node:test';
4
3
 
5
- const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
- const composedCommandPattern = /&&|\|\||\bsh -c\b|\bnode -e\b/u;
7
- const runnerCommandPattern = /^node scripts\/runPackageTask\.mjs ([A-Za-z0-9:_-]+)$/u;
8
-
9
4
  async function loadPackageTaskRunner() {
10
5
  return import(new URL('./runPackageTask.mjs', import.meta.url));
11
6
  }
12
7
 
13
- test('delegates composed package tasks to the repository runner', async () => {
14
- const runnerCommands = Object.entries(packageJson.scripts ?? {}).filter(([, command]) => runnerCommandPattern.test(command));
15
-
16
- for (const [scriptName, command] of Object.entries(packageJson.scripts ?? {})) {
17
- assert.doesNotMatch(command, composedCommandPattern, `${scriptName} must delegate composition to runPackageTask.mjs`);
18
- }
19
-
20
- assert.ok(runnerCommands.length > 0, 'at least one package task must use the runner');
21
- const { packageTasks } = await loadPackageTaskRunner();
22
- for (const [scriptName] of runnerCommands) {
23
- assert.ok(packageTasks[scriptName], `${scriptName} must have a runner task`);
24
- }
25
- });
26
-
27
8
  test('runs task steps in order and stops at the first failure', async () => {
28
9
  const { runPackageTask } = await loadPackageTaskRunner();
29
10
  const executedCommands = [];
@@ -65,17 +46,14 @@ test('runs conditional steps only when their required output is missing', async
65
46
  assert.deepEqual(executedCommands, ['execute']);
66
47
  });
67
48
 
68
- test('does not suppress command execution errors at the CLI boundary', () => {
69
- const runnerSource = readFileSync(new URL('./runPackageTask.mjs', import.meta.url), 'utf8');
70
-
71
- assert.doesNotMatch(runnerSource, /catch\s*\{/u);
72
- });
73
-
74
49
  test('merges task environment overrides without mutating inherited values', async () => {
75
50
  const { createTaskEnvironment } = await loadPackageTaskRunner();
76
51
  const inheritedEnvironment = { NODE_ENV: 'test', PATH: '/bin' };
77
52
 
78
- const environment = createTaskEnvironment({ NODE_ENV: 'production', DB_ENV: 'privileged' }, inheritedEnvironment);
53
+ const environment = createTaskEnvironment(
54
+ { NODE_ENV: 'production', DB_ENV: 'privileged' },
55
+ inheritedEnvironment,
56
+ );
79
57
 
80
58
  assert.deepEqual(environment, {
81
59
  NODE_ENV: 'production',
@@ -4,27 +4,51 @@ import { resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  export const packageTasks = Object.freeze({
7
+ 'validate:audits': [
8
+ { command: 'node', arguments: ['scripts/packageTaskRunner.audit.mjs'] },
9
+ { command: 'node', arguments: ['scripts/testOrder/testOrderPolicy.audit.mjs'] },
10
+ { command: 'node', arguments: ['scripts/testParallel/parallelTestPolicy.audit.mjs'] },
11
+ { command: 'mocha', arguments: ['test/config/repositoryIsolation.audit.js'] },
12
+ { command: 'mocha', arguments: ['test/config/tddGuidanceDocs.audit.js'] },
13
+ { command: 'mocha', arguments: ['test/dependencyOverrides.audit.js'] },
14
+ ],
7
15
  test: [
16
+ { command: 'npm', arguments: ['run', 'validate:audits'] },
8
17
  { command: 'npm', arguments: ['run', 'test:order'] },
9
- { command: 'node', arguments: ['test/index.test.js'] },
18
+ { command: 'tsc', arguments: ['--noEmit'] },
19
+ {
20
+ command: 'mocha',
21
+ arguments: [
22
+ '--require',
23
+ './scripts/testOrder/randomizeTestOrder.cjs',
24
+ '-r',
25
+ 'ts-node/register',
26
+ 'test/types.test.ts',
27
+ ],
28
+ },
29
+ { command: 'nyc', arguments: ['node', 'test/index.test.js'] },
10
30
  ],
11
31
  'test:types': [
12
32
  { command: 'npm', arguments: ['run', 'test:order'] },
13
33
  { command: 'tsc', arguments: ['--noEmit'] },
14
34
  {
15
35
  command: 'mocha',
16
- arguments: ['--require', './scripts/testOrder/randomizeTestOrder.cjs', '-r', 'ts-node/register', 'test/types.test.ts'],
36
+ arguments: [
37
+ '--require',
38
+ './scripts/testOrder/randomizeTestOrder.cjs',
39
+ '-r',
40
+ 'ts-node/register',
41
+ 'test/types.test.ts',
42
+ ],
17
43
  },
18
44
  ],
19
45
  'test:coverage': [
46
+ { command: 'npm', arguments: ['run', 'validate:audits'] },
20
47
  { command: 'npm', arguments: ['run', 'test:order'] },
21
48
  { command: 'tsc', arguments: ['--noEmit'] },
22
49
  { command: 'nyc', arguments: ['node', 'test/index.test.js'] },
23
50
  ],
24
- 'test:All': [
25
- { command: 'npm', arguments: ['run', 'test'] },
26
- { command: 'npm', arguments: ['run', 'test:types'] },
27
- ],
51
+ 'test:All': [{ command: 'npm', arguments: ['test'] }],
28
52
  });
29
53
 
30
54
  export function createTaskEnvironment(overrides = {}, inheritedEnvironment = process.env) {
@@ -32,10 +56,18 @@ export function createTaskEnvironment(overrides = {}, inheritedEnvironment = pro
32
56
  }
33
57
 
34
58
  function getTaskExitCode(result) {
35
- if (result.error) throw result.error;
36
- if (typeof result.status === 'number') return result.status;
37
- if (result.signal === 'SIGINT') return 130;
38
- if (result.signal === 'SIGTERM') return 143;
59
+ if (result.error) {
60
+ throw result.error;
61
+ }
62
+ if (typeof result.status === 'number') {
63
+ return result.status;
64
+ }
65
+ if (result.signal === 'SIGINT') {
66
+ return 130;
67
+ }
68
+ if (result.signal === 'SIGTERM') {
69
+ return 143;
70
+ }
39
71
  return 1;
40
72
  }
41
73
 
@@ -57,20 +89,33 @@ function shouldRunTaskStep(taskStep, pathExists) {
57
89
  return !taskStep.whenMissing || !pathExists(taskStep.whenMissing);
58
90
  }
59
91
 
60
- export function runPackageTask(taskName, executeTask = executeTaskStep, taskDefinitions = packageTasks, pathExists = existsSync) {
92
+ export function runPackageTask(
93
+ taskName,
94
+ executeTask = executeTaskStep,
95
+ taskDefinitions = packageTasks,
96
+ pathExists = existsSync,
97
+ ) {
61
98
  const taskSteps = taskDefinitions[taskName];
62
- if (!Array.isArray(taskSteps)) throw new Error('Unknown package task.');
99
+ if (!Array.isArray(taskSteps)) {
100
+ throw new Error('Unknown package task.');
101
+ }
63
102
 
64
103
  for (const taskStep of taskSteps) {
65
- if (!shouldRunTaskStep(taskStep, pathExists)) continue;
104
+ if (!shouldRunTaskStep(taskStep, pathExists)) {
105
+ continue;
106
+ }
66
107
  const exitCode = executeTask(taskStep);
67
- if (exitCode !== 0) return exitCode;
108
+ if (exitCode !== 0) {
109
+ return exitCode;
110
+ }
68
111
  }
69
112
  return 0;
70
113
  }
71
114
 
72
115
  function isDirectExecution() {
73
- if (!process.argv[1]) return false;
116
+ if (!process.argv[1]) {
117
+ return false;
118
+ }
74
119
  return resolve(process.argv[1]) === fileURLToPath(import.meta.url);
75
120
  }
76
121
 
@@ -85,4 +130,6 @@ function runCommandLineTask() {
85
130
  process.exitCode = runPackageTask(taskName);
86
131
  }
87
132
 
88
- if (isDirectExecution()) runCommandLineTask();
133
+ if (isDirectExecution()) {
134
+ runCommandLineTask();
135
+ }
@@ -3,10 +3,16 @@
3
3
  const MAX_TEST_ORDER_SEED = 2_147_483_647;
4
4
 
5
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.');
6
+ if (configuredSeed === undefined) {
7
+ return undefined;
8
+ }
9
+ if (!/^[1-9]\d*$/.test(configuredSeed)) {
10
+ throw new Error('TEST_ORDER_SEED must be a positive 32-bit integer.');
11
+ }
8
12
  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.');
13
+ if (!Number.isSafeInteger(seed) || seed > MAX_TEST_ORDER_SEED) {
14
+ throw new Error('TEST_ORDER_SEED must be a positive 32-bit integer.');
15
+ }
10
16
  return seed;
11
17
  }
12
18
  function createSeededRandom(seed) {
@@ -25,14 +31,18 @@ function shuffleValues(values, random) {
25
31
  }
26
32
  }
27
33
  function shuffleSuiteTree(suite, random) {
28
- for (const childSuite of suite.suites) shuffleSuiteTree(childSuite, random);
34
+ for (const childSuite of suite.suites) {
35
+ shuffleSuiteTree(childSuite, random);
36
+ }
29
37
  shuffleValues(suite.tests, random);
30
38
  shuffleValues(suite.suites, random);
31
39
  }
32
40
  const mochaHooks = {
33
41
  beforeAll() {
34
42
  const seed = resolveTestOrderSeed(process.env.TEST_ORDER_SEED);
35
- if (seed === undefined) return;
43
+ if (seed === undefined) {
44
+ return;
45
+ }
36
46
  console.log(`Test order seed: ${seed} (reproduce with TEST_ORDER_SEED=${seed})`);
37
47
  shuffleSuiteTree(this.test.parent, createSeededRandom(seed));
38
48
  },
@@ -7,7 +7,9 @@ const TEST_ORDER_INVARIANCE_RULE =
7
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
8
 
9
9
  function listRepositoryFiles() {
10
- return execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' })
10
+ return execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
11
+ encoding: 'utf8',
12
+ })
11
13
  .trim()
12
14
  .split('\n')
13
15
  .filter(Boolean);
@@ -43,6 +45,10 @@ test('keeps default package scripts on the test framework ordinary ordering', ()
43
45
 
44
46
  for (const [scriptName, command] of Object.entries(packageJson.scripts ?? {})) {
45
47
  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.`);
48
+ assert.doesNotMatch(
49
+ command,
50
+ /--test-randomize|--test-random-seed/,
51
+ `${scriptName} must not force randomized test ordering.`,
52
+ );
47
53
  }
48
54
  });
@@ -6,15 +6,22 @@ import test from 'node:test';
6
6
 
7
7
  const require = createRequire(import.meta.url);
8
8
  const repositoryRoot = resolve(import.meta.dirname, '../..');
9
- const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
10
- const packageTaskRunnerSource = readFileSync(new URL('../runPackageTask.mjs', import.meta.url), 'utf8');
9
+ const packageJson = JSON.parse(
10
+ readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
11
+ );
12
+ const packageTaskRunnerSource = readFileSync(
13
+ new URL('../runPackageTask.mjs', import.meta.url),
14
+ 'utf8',
15
+ );
11
16
  const testIndexSource = readFileSync(new URL('../../test/index.test.js', import.meta.url), 'utf8');
12
17
  const { parallelTestFiles } = require('../../test/index.test.js');
13
18
 
14
19
  function listRuntimeTestFiles(directoryPath) {
15
20
  return readdirSync(directoryPath, { withFileTypes: true }).flatMap(entry => {
16
21
  const entryPath = join(directoryPath, entry.name);
17
- if (entry.isDirectory()) return listRuntimeTestFiles(entryPath);
22
+ if (entry.isDirectory()) {
23
+ return listRuntimeTestFiles(entryPath);
24
+ }
18
25
  if (!/\.test\.(?:js|mjs)$/.test(entry.name) || entry.name === 'index.test.js') {
19
26
  return [];
20
27
  }
@@ -24,20 +31,32 @@ function listRuntimeTestFiles(directoryPath) {
24
31
 
25
32
  test('keeps runtime test selection in the index and package scripts short', () => {
26
33
  assert.equal(packageJson.scripts.test, 'node scripts/runPackageTask.mjs test');
27
- assert.equal(packageJson.scripts['test:coverage'], 'node scripts/runPackageTask.mjs test:coverage');
34
+ assert.equal(
35
+ packageJson.scripts['test:coverage'],
36
+ 'node scripts/runPackageTask.mjs test:coverage',
37
+ );
28
38
  assert.match(packageTaskRunnerSource, /arguments: \['run', 'test:order'\]/);
29
- assert.match(packageTaskRunnerSource, /arguments: \['test\/index\.test\.js'\]/);
39
+ assert.match(packageTaskRunnerSource, /arguments: \['node', 'test\/index\.test\.js'\]/);
30
40
  assert.match(packageTaskRunnerSource, /command: 'nyc'/);
31
41
  assert.match(testIndexSource, /parallelTestFiles/);
32
42
  assert.match(testIndexSource, /runIndexedMochaTests/);
33
43
  assert.match(testIndexSource, /if \(require\.main === module\)/);
34
44
  });
35
45
 
36
- test('runs the parallel execution contract in the test-order gate', () => {
37
- assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/parallelTestPolicy\.test\.mjs/);
38
- assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/runIndexedMochaTests\.test\.mjs/);
46
+ test('keeps parallel behavior tests in the test-order gate and static policy checks in the audit gate', () => {
47
+ assert.match(
48
+ packageJson.scripts['test:order'],
49
+ /scripts\/testParallel\/runIndexedMochaTests\.test\.mjs/,
50
+ );
51
+ assert.equal(
52
+ packageJson.scripts['validate:audits'],
53
+ 'node scripts/runPackageTask.mjs validate:audits',
54
+ );
39
55
  });
40
56
 
41
57
  test('selects every runtime test file exactly once', () => {
42
- assert.deepEqual([...parallelTestFiles].sort(), listRuntimeTestFiles(resolve(repositoryRoot, 'test')).sort());
58
+ assert.deepEqual(
59
+ [...parallelTestFiles].sort(),
60
+ listRuntimeTestFiles(resolve(repositoryRoot, 'test')).sort(),
61
+ );
43
62
  });