@aria-framework/testkit 0.3.0 → 0.4.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/index.js +4 -0
- package/installedMatchesLockfile.js +99 -0
- package/package.json +22 -21
package/index.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* in scope (the silently-empty-list-in-a-catch class).
|
|
10
10
|
* assertVersionSingleSource({...}) — package.json vs derived version files vs the lockfile's
|
|
11
11
|
* two copies.
|
|
12
|
+
* assertInstalledMatchesLockfile({...}) — what is in node_modules is what `npm ci` would install.
|
|
13
|
+
* The hand-copied-package class: green locally, different
|
|
14
|
+
* code on the server, no error either side.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
'use strict';
|
|
@@ -18,6 +21,7 @@ module.exports = Object.assign(
|
|
|
18
21
|
require('./harness'),
|
|
19
22
|
require('./importsInScope'),
|
|
20
23
|
require('./versionSync'),
|
|
24
|
+
require('./installedMatchesLockfile'),
|
|
21
25
|
// copyDatabase / assertIsCopy / sweep — a private copy of the app's database for one test run,
|
|
22
26
|
// which clears whatever an earlier run with this pid left behind and takes itself away
|
|
23
27
|
// afterwards. See tempDb.js for the flake that made it necessary.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assertInstalledMatchesLockfile — what is ON DISK is what `npm ci` would install.
|
|
3
|
+
*
|
|
4
|
+
* ── THE FAILURE THIS EXISTS FOR ─────────────────────────────────────────────────────────────────
|
|
5
|
+
* A framework package is edited in its own checkout and hand-copied into an app's node_modules.
|
|
6
|
+
* Everything local goes green against it. The lockfile still pins the PUBLISHED version, and the
|
|
7
|
+
* documented deploy is `git pull && npm ci --omit=dev` — which installs the lockfile's version, not
|
|
8
|
+
* the one every test just passed against.
|
|
9
|
+
*
|
|
10
|
+
* It happened, it was not small, and it was invisible: @aria-framework/ai sat at 0.18.1 in
|
|
11
|
+
* node_modules and 0.18.0 in the lockfile, and the ONLY file that differed between them was
|
|
12
|
+
* untrusted.js — the prompt-injection fence the consuming app's hardening was built on. 0.18.0's
|
|
13
|
+
* rule pointed the model at the wrong text. Production would have run a fence that says "below"
|
|
14
|
+
* about a block placed above, while a suite asserting the opposite passed on every machine that
|
|
15
|
+
* had the copy.
|
|
16
|
+
*
|
|
17
|
+
* ── WHY A TEST AND NOT A NOTE ───────────────────────────────────────────────────────────────────
|
|
18
|
+
* Because the failure is silent in the direction that matters. Nothing errors: the app runs, the
|
|
19
|
+
* tests pass, and the difference appears only on the server, in behaviour, with no message. A note
|
|
20
|
+
* describing exactly this already existed in one app's project memory, written after the previous
|
|
21
|
+
* time it happened — and it happened again anyway. A note that has been ignored once is a test that
|
|
22
|
+
* had not been written yet.
|
|
23
|
+
*
|
|
24
|
+
* ── WHAT IT DOES NOT CHECK ──────────────────────────────────────────────────────────────────────
|
|
25
|
+
* Whether the lockfile's version exists on the registry. That needs the network, and a check that
|
|
26
|
+
* fails on a train is a check people learn to skip. This compares two files on disk, which is
|
|
27
|
+
* enough to catch the hand-copy — the only way the two can disagree.
|
|
28
|
+
*
|
|
29
|
+
* Sibling of assertVersionSingleSource: same class of problem (a second copy nothing validates),
|
|
30
|
+
* same shape of answer.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{
|
|
40
|
+
* root: string, app root (absolute)
|
|
41
|
+
* scope?: string, package scope to check (default '@aria-framework')
|
|
42
|
+
* lockfile?: string lockfile name (default 'package-lock.json')
|
|
43
|
+
* }} opts
|
|
44
|
+
* @returns {{checked: number}} how many packages were compared — ASSERT ON THIS. A misspelled
|
|
45
|
+
* scope matches nothing and passes, and a check that inspects nothing is also a claim.
|
|
46
|
+
* @throws Error naming every package whose installed version differs from the pin
|
|
47
|
+
*/
|
|
48
|
+
function assertInstalledMatchesLockfile(opts = {}) {
|
|
49
|
+
const root = opts.root;
|
|
50
|
+
if (!root) throw new Error('assertInstalledMatchesLockfile({ root }): the app root is required');
|
|
51
|
+
const scope = opts.scope || '@aria-framework';
|
|
52
|
+
const lockPath = path.join(root, opts.lockfile || 'package-lock.json');
|
|
53
|
+
|
|
54
|
+
let lock;
|
|
55
|
+
try {
|
|
56
|
+
lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw new Error(`could not read ${lockPath}: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// BOTH SHAPES OF NAME. A scope is `@aria-framework`, whose keys are `node_modules/@scope/name`;
|
|
62
|
+
// a bare package name is its own key, `node_modules/left-pad`, with nothing after it. Matching
|
|
63
|
+
// only the first form made this return `{ checked: 0 }` and PASS for the second — inspecting
|
|
64
|
+
// nothing while reporting success, which is worse than no check because it is also a claim.
|
|
65
|
+
const nested = `node_modules/${scope}/`;
|
|
66
|
+
const exact = `node_modules/${scope}`;
|
|
67
|
+
const wrong = [];
|
|
68
|
+
let checked = 0;
|
|
69
|
+
|
|
70
|
+
for (const [key, entry] of Object.entries(lock.packages || {})) {
|
|
71
|
+
// STARTSWITH FROM THE FRONT, NOT INCLUDES. A nested copy —
|
|
72
|
+
// node_modules/x/node_modules/@scope/y — is a different resolution with its own pin, and
|
|
73
|
+
// comparing it against the top-level install would report a disagreement that is not one.
|
|
74
|
+
if (!key.startsWith(nested) && key !== exact) continue;
|
|
75
|
+
const name = key.slice('node_modules/'.length);
|
|
76
|
+
checked += 1;
|
|
77
|
+
|
|
78
|
+
let installed;
|
|
79
|
+
try {
|
|
80
|
+
installed = JSON.parse(fs.readFileSync(path.join(root, key, 'package.json'), 'utf8')).version;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
wrong.push(`${name}: pinned ${entry.version}, NOT INSTALLED`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (installed !== entry.version) {
|
|
86
|
+
wrong.push(`${name}: lockfile ${entry.version}, installed ${installed}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (wrong.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'node_modules does not match the lockfile, so `npm ci` on the server installs something these '
|
|
93
|
+
+ 'tests never ran against. Publish the package and `npm install` it, or reinstall the pinned '
|
|
94
|
+
+ 'version — do NOT hand-copy a package into node_modules:\n ' + wrong.join('\n '));
|
|
95
|
+
}
|
|
96
|
+
return { checked };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { assertInstalledMatchesLockfile };
|
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@aria-framework/testkit",
|
|
3
|
-
"description": "Aria App Framework
|
|
4
|
-
"version": "0.
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"private": false,
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"main": "index.js",
|
|
11
|
-
"files": [
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"tempDb.js"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@aria-framework/testkit",
|
|
3
|
+
"description": "Aria App Framework — test infrastructure. The ✓/✗ micro test-harness (createHarness, with the async-callback guard that stops a suite silently passing), assertImportsInScope (every consumer of a module has every function it calls in scope), and assertVersionSingleSource (package.json vs derived version files vs the lockfile's two copies). Dev-dependency only: nothing here belongs in a production install.",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"harness.js",
|
|
13
|
+
"importsInScope.js",
|
|
14
|
+
"index.js",
|
|
15
|
+
"installedMatchesLockfile.js",
|
|
16
|
+
"tempDb.js",
|
|
17
|
+
"versionSync.js"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/smoke.js && node test/tempDb.js && node test/installedMatchesLockfile.js"
|
|
21
|
+
}
|
|
22
|
+
}
|