@bongos/core 1.20.6 → 1.20.7
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/.bongos-core.json +23 -13
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/find-untested.js +371 -0
- package/scripts/gds/newcomer-floor.templates.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/find_untested.mjs +137 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.7",
|
|
6
|
+
"core_contract": "1.20.7",
|
|
7
|
+
"source_commit": "9a8e336459336ffc6beebeac8b6496960dec3bdd",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-30T04:
|
|
9
|
+
"built_at": "2026-09-30T04:20:20.175Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 556,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2621,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3203,
|
|
20
|
+
"tree_sha256": "dcbd562f300693a4aac156ab9d3d09aa65f2ec33fe5f0b469846f0eaca1a6005",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2772,7 +2772,7 @@
|
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/module-api-changelog.md",
|
|
2774
2774
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2775
|
+
"sha256": "20bca63270af7e3ccc11037fc0faf76c5d7ed75d8bde9b115c96b1b6f4e985a2"
|
|
2776
2776
|
},
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/modules-contract.md",
|
|
@@ -8852,12 +8852,12 @@
|
|
|
8852
8852
|
{
|
|
8853
8853
|
"path": "package-lock.json",
|
|
8854
8854
|
"mode": "0000644",
|
|
8855
|
-
"sha256": "
|
|
8855
|
+
"sha256": "1822db240575bf86013d014572456d1f007004bfdc29209929938288ba43e3b2"
|
|
8856
8856
|
},
|
|
8857
8857
|
{
|
|
8858
8858
|
"path": "package.json",
|
|
8859
8859
|
"mode": "0000644",
|
|
8860
|
-
"sha256": "
|
|
8860
|
+
"sha256": "1b1b7816c342fed258f9eaa453ce64588f26fe28beb4ab3eb1dfc06ec8f4529e"
|
|
8861
8861
|
},
|
|
8862
8862
|
{
|
|
8863
8863
|
"path": "public-docs/index.html",
|
|
@@ -8877,7 +8877,7 @@
|
|
|
8877
8877
|
{
|
|
8878
8878
|
"path": "release-notes.json",
|
|
8879
8879
|
"mode": "0000644",
|
|
8880
|
-
"sha256": "
|
|
8880
|
+
"sha256": "59dcfa061d238c013470c67e6a82249f559fdbc4509c0c1d4ba8c29c03cc68ab"
|
|
8881
8881
|
},
|
|
8882
8882
|
{
|
|
8883
8883
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -9394,6 +9394,11 @@
|
|
|
9394
9394
|
"mode": "0000644",
|
|
9395
9395
|
"sha256": "b8b1a7ed1d8dd217b24757a3ec9f4b3aa58b26a3e2a7b2cef9de217792e5ea43"
|
|
9396
9396
|
},
|
|
9397
|
+
{
|
|
9398
|
+
"path": "scripts/gds/find-untested.js",
|
|
9399
|
+
"mode": "0000644",
|
|
9400
|
+
"sha256": "6788cb8ea36250fe84236f1a1f1a216f5627a1772d9dad5399b700cd3f984d74"
|
|
9401
|
+
},
|
|
9397
9402
|
{
|
|
9398
9403
|
"path": "scripts/gds/fitness-checks-error-envelope.js",
|
|
9399
9404
|
"mode": "0000644",
|
|
@@ -9727,7 +9732,7 @@
|
|
|
9727
9732
|
{
|
|
9728
9733
|
"path": "scripts/gds/newcomer-floor.templates.json",
|
|
9729
9734
|
"mode": "0000644",
|
|
9730
|
-
"sha256": "
|
|
9735
|
+
"sha256": "0623cc216ae3d20def01ac6d1aaedb4cf25d25fe9a94201cee86b8ff07673c0a"
|
|
9731
9736
|
},
|
|
9732
9737
|
{
|
|
9733
9738
|
"path": "scripts/gds/newcomer-floor.test.js",
|
|
@@ -10982,7 +10987,7 @@
|
|
|
10982
10987
|
{
|
|
10983
10988
|
"path": "src/module-api.js",
|
|
10984
10989
|
"mode": "0000644",
|
|
10985
|
-
"sha256": "
|
|
10990
|
+
"sha256": "37545c18b037b0de0faaba69a716f67b779a48815d0f5e8937a7202ac7bb8f65"
|
|
10986
10991
|
},
|
|
10987
10992
|
{
|
|
10988
10993
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12324,6 +12329,11 @@
|
|
|
12324
12329
|
"mode": "0000644",
|
|
12325
12330
|
"sha256": "815f2fa2ffd6e7a721507852d43b0490115a0b15393670f6035626bdf000e99c"
|
|
12326
12331
|
},
|
|
12332
|
+
{
|
|
12333
|
+
"path": "tests/find_untested.mjs",
|
|
12334
|
+
"mode": "0000644",
|
|
12335
|
+
"sha256": "1a4b0eeee9212776a1a1f159053cd48d5af3041dc0a45c3b32cf0e7706600422"
|
|
12336
|
+
},
|
|
12327
12337
|
{
|
|
12328
12338
|
"path": "tests/first_admin_bootstrap.mjs",
|
|
12329
12339
|
"mode": "0000644",
|
|
@@ -2669,5 +2669,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2669
2669
|
landed since 1.20.4 with no explicit bump. run 36666171516. (task 1002620)
|
|
2670
2670
|
1.20.6 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2671
2671
|
landed since 1.20.5 with no explicit bump. run 36667238912. (task 1002620)
|
|
2672
|
+
1.20.7 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2673
|
+
landed since 1.20.6 with no explicit bump. run 36668328505. (task 1002620)
|
|
2672
2674
|
---------------------------------------------------------------------------
|
|
2673
2675
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.7",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.7",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.7",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8088,5 +8088,11 @@
|
|
|
8088
8088
|
"id": "1004392",
|
|
8089
8089
|
"text": "A goal's checklist item no longer ticks itself off just because some linked work shipped. Once the work is done it now shows \"Awaiting UAT\", and it closes only when someone who did not build it tries it on the live site and up"
|
|
8090
8090
|
}
|
|
8091
|
+
],
|
|
8092
|
+
"1.20.7": [
|
|
8093
|
+
{
|
|
8094
|
+
"id": "1002458",
|
|
8095
|
+
"text": "The starter task that asks a new builder to add a test now finds its own targets from the current code, so it no longer goes out of date every time someone completes it."
|
|
8096
|
+
}
|
|
8091
8097
|
]
|
|
8092
8098
|
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// scripts/gds/find-untested.js — list source modules that no test names, for the
|
|
4
|
+
// newcomer "add a unit test" chore (task 1002458, from idea 1000574).
|
|
5
|
+
//
|
|
6
|
+
// WHY. The evergreen newcomer task `unit-test-pass` (scripts/gds/newcomer-floor.templates.json)
|
|
7
|
+
// used to carry a HARDCODED list of "untested" helpers. Every newcomer who completed it made
|
|
8
|
+
// the list a little more wrong, and a blanket rename broke three of its five paths outright.
|
|
9
|
+
// This script is the METHOD instead of the list: run it and it names candidates from the live
|
|
10
|
+
// tree, so the task cannot decay.
|
|
11
|
+
//
|
|
12
|
+
// THE DEFINITION (decided at claim time; recorded on the task). A module is a candidate when
|
|
13
|
+
// NO TEST FILE NAMES IT — no test requires/imports it, builds its path with path.join, or
|
|
14
|
+
// spawns it. That is the "no dedicated test" definition. The stricter one, "unreachable from
|
|
15
|
+
// any test at all", is reported alongside as the `reach` column rather than used as the
|
|
16
|
+
// filter: measured in this repo it leaves only a handful of modules, most of them DB pollers,
|
|
17
|
+
// so filtering on it would hand newcomers an empty list. `--unreachable` applies it anyway.
|
|
18
|
+
//
|
|
19
|
+
// THE TWO NAIVE SCANS THIS AVOIDS (both measured before building):
|
|
20
|
+
// - Matching on BASENAME ("does settings.js appear in any test?") falsely drops common-word
|
|
21
|
+
// modules — quality.js, settings.js, hierarchy.js match dozens of unrelated tests. So a
|
|
22
|
+
// reference only counts when it resolves to the module's full repo-relative path.
|
|
23
|
+
// - Scanning only literal `require('…')` misses the ~65 tests that reach their module
|
|
24
|
+
// through `require(path.join(ROOT, 'scripts', 'gds', 'x.js'))`, a `const GOV = path.join(…)`
|
|
25
|
+
// prefix, or a spawned `node scripts/gds/x.js`; that scan wrongly flagged the well-tested
|
|
26
|
+
// memory-map.js. So every path.join/resolve call is evaluated (through the file's own
|
|
27
|
+
// path constants) and every path-shaped string literal counts.
|
|
28
|
+
//
|
|
29
|
+
// PURITY IS A HEURISTIC, and the output says so. A module is marked impure when it, or a
|
|
30
|
+
// module it statically requires, reaches Postgres (`pg` or a db*.js module), a subprocess or
|
|
31
|
+
// the network — the propagation is what catches a helper that reaches the database through a
|
|
32
|
+
// module doorway rather than a literal `pg` require. Filesystem use is shown but does not
|
|
33
|
+
// exclude: a module may read files in its CLI half and still export pure helpers. A newcomer
|
|
34
|
+
// still checks the helper they pick.
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
|
|
39
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
40
|
+
|
|
41
|
+
// Where candidate modules live, and the directories never scanned for them.
|
|
42
|
+
const SOURCE_ROOTS = ['scripts', 'src', 'modules'];
|
|
43
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'migrations', 'fixtures', 'vendor', 'public']);
|
|
44
|
+
const CODE_EXT = /\.(?:c|m)?js$/;
|
|
45
|
+
|
|
46
|
+
// ---- pure helpers ----------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** Is this repo-relative path a test file? A file under any `tests/` directory, or a
|
|
49
|
+
* `*.test.js` beside its module (scripts/gds/newcomer-floor.test.js is one). */
|
|
50
|
+
function isTestPath(rel) {
|
|
51
|
+
const p = String(rel).split(path.sep).join('/');
|
|
52
|
+
return /(^|\/)tests\//.test(p) || /\.test\.(?:c|m)?js$/.test(p);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Normalize a path to repo-relative posix form, clamping any climb above the root to the
|
|
56
|
+
* root itself. Returns '' for the root. */
|
|
57
|
+
function normRel(p) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const seg of String(p).split(/[\\/]+/)) {
|
|
60
|
+
if (!seg || seg === '.') continue;
|
|
61
|
+
if (seg === '..') out.pop();
|
|
62
|
+
else out.push(seg);
|
|
63
|
+
}
|
|
64
|
+
return out.join('/');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Directory of a repo-relative file path ('' at the root). */
|
|
68
|
+
function dirOf(rel) {
|
|
69
|
+
const i = rel.lastIndexOf('/');
|
|
70
|
+
return i < 0 ? '' : rel.slice(0, i);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A single argument of a path.join/resolve call, evaluated to a repo-relative string, or
|
|
74
|
+
// null when it cannot be known. `first` lets an unknown leading identifier stand for the
|
|
75
|
+
// repo root — a `ROOT`/`REPO` constant imported from a helper is the overwhelming case.
|
|
76
|
+
function evalArg(arg, env, fileDir, first) {
|
|
77
|
+
const a = arg.trim();
|
|
78
|
+
const lit = /^(['"`])((?:\\.|(?!\1)[^\\])*)\1$/.exec(a);
|
|
79
|
+
if (lit) return lit[2].includes('${') ? null : lit[2];
|
|
80
|
+
if (a === '__dirname' || a === 'HERE' || a === '__here') return '/' + fileDir;
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(env, a)) return env[a] == null ? null : '/' + env[a];
|
|
82
|
+
if (first && /^[A-Za-z_$][\w$]*$/.test(a)) return '/';
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Evaluate the argument text of one path.join/resolve call to a repo-relative path.
|
|
87
|
+
function evalJoin(argText, env, fileDir) {
|
|
88
|
+
const args = argText.split(',');
|
|
89
|
+
let acc = null;
|
|
90
|
+
for (let i = 0; i < args.length; i++) {
|
|
91
|
+
const v = evalArg(args[i], env, fileDir, i === 0);
|
|
92
|
+
if (v == null) return null;
|
|
93
|
+
if (v.startsWith('/')) acc = v.slice(1);
|
|
94
|
+
else acc = acc == null ? v : acc + '/' + v;
|
|
95
|
+
}
|
|
96
|
+
return acc == null ? null : normRel(acc);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const JOIN_RE = /path\.(?:join|resolve)\s*\(([^()]*)\)/g;
|
|
100
|
+
const CONST_JOIN_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*path\.(?:join|resolve)\s*\(([^()]*)\)/g;
|
|
101
|
+
const CONST_URL_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:path\.dirname\s*\(\s*)?fileURLToPath\s*\(\s*(?:new URL\s*\(\s*(['"])([^'"]*)\2\s*,\s*)?import\.meta\.url/g;
|
|
102
|
+
const URL_RE = /new URL\s*\(\s*(['"])([^'"]+)\1\s*,\s*import\.meta\.url\s*\)/g;
|
|
103
|
+
const STRING_RE = /(['"`])((?:\\.|(?!\1)[^\\\n])*)\1/g;
|
|
104
|
+
|
|
105
|
+
/** Every repo-relative path a source text names: evaluated path.join/resolve calls (through
|
|
106
|
+
* the file's own path constants), `new URL(…, import.meta.url)`, and path-shaped string
|
|
107
|
+
* literals (relative ones resolved from the file's directory). Pure — it reads no files.
|
|
108
|
+
* This is deliberately BROAD: it is how a test's reach is measured, and a test reaches its
|
|
109
|
+
* module through any of these shapes. */
|
|
110
|
+
function extractPathRefs(src, fileRel) {
|
|
111
|
+
const text = String(src);
|
|
112
|
+
const fileDir = dirOf(normRel(fileRel));
|
|
113
|
+
const env = {};
|
|
114
|
+
const refs = new Set();
|
|
115
|
+
let m;
|
|
116
|
+
CONST_URL_RE.lastIndex = 0;
|
|
117
|
+
while ((m = CONST_URL_RE.exec(text))) {
|
|
118
|
+
// fileURLToPath(import.meta.url) is the file itself; with dirname or a relative URL it
|
|
119
|
+
// is a directory. Either way the directory is the useful prefix.
|
|
120
|
+
env[m[1]] = m[3] ? normRel(fileDir + '/' + m[3]) : fileDir;
|
|
121
|
+
}
|
|
122
|
+
CONST_JOIN_RE.lastIndex = 0;
|
|
123
|
+
while ((m = CONST_JOIN_RE.exec(text))) env[m[1]] = evalJoin(m[2], env, fileDir);
|
|
124
|
+
JOIN_RE.lastIndex = 0;
|
|
125
|
+
while ((m = JOIN_RE.exec(text))) {
|
|
126
|
+
const v = evalJoin(m[1], env, fileDir);
|
|
127
|
+
if (v) refs.add(v);
|
|
128
|
+
}
|
|
129
|
+
URL_RE.lastIndex = 0;
|
|
130
|
+
while ((m = URL_RE.exec(text))) refs.add(normRel(fileDir + '/' + m[2]));
|
|
131
|
+
STRING_RE.lastIndex = 0;
|
|
132
|
+
while ((m = STRING_RE.exec(text))) {
|
|
133
|
+
const s = m[2];
|
|
134
|
+
if (!s || s.includes('${') || !/\.(?:c|m)?js$|\//.test(s) || /\s/.test(s) || /^[a-z]+:/i.test(s)) continue;
|
|
135
|
+
refs.add(s.startsWith('.') ? normRel(fileDir + '/' + s) : normRel(s));
|
|
136
|
+
}
|
|
137
|
+
return refs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const REQUIRE_RE = /(?:require|import)\s*\(\s*(['"])([^'"]+)\1\s*\)|\bfrom\s+(['"])([^'"]+)\3/g;
|
|
141
|
+
const REQUIRE_JOIN_RE = /require\s*\(\s*path\.(?:join|resolve)\s*\(([^()]*)\)\s*\)/g;
|
|
142
|
+
|
|
143
|
+
/** The module specifiers a SOURCE file statically loads: bare package names as-is, and
|
|
144
|
+
* in-repo paths (relative, or built with path.join from __dirname) as repo-relative paths
|
|
145
|
+
* WITHOUT extension resolution. Narrower than extractPathRefs on purpose — this is the
|
|
146
|
+
* require graph, and a path merely mentioned in a comment is not an edge. */
|
|
147
|
+
function extractRequires(src, fileRel) {
|
|
148
|
+
const text = String(src);
|
|
149
|
+
const fileDir = dirOf(normRel(fileRel));
|
|
150
|
+
const env = {};
|
|
151
|
+
const out = { local: new Set(), packages: new Set() };
|
|
152
|
+
let m;
|
|
153
|
+
CONST_JOIN_RE.lastIndex = 0;
|
|
154
|
+
while ((m = CONST_JOIN_RE.exec(text))) env[m[1]] = evalJoin(m[2], env, fileDir);
|
|
155
|
+
REQUIRE_RE.lastIndex = 0;
|
|
156
|
+
while ((m = REQUIRE_RE.exec(text))) {
|
|
157
|
+
const spec = m[2] || m[4];
|
|
158
|
+
if (spec.startsWith('.')) out.local.add(normRel(fileDir + '/' + spec));
|
|
159
|
+
else out.packages.add(spec.replace(/^node:/, '').split('/')[0]);
|
|
160
|
+
}
|
|
161
|
+
REQUIRE_JOIN_RE.lastIndex = 0;
|
|
162
|
+
while ((m = REQUIRE_JOIN_RE.exec(text))) {
|
|
163
|
+
const v = evalJoin(m[1], env, fileDir);
|
|
164
|
+
if (v) out.local.add(v);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Resolve a reference against the set of known module paths the way Node would for a
|
|
170
|
+
* local specifier: exact, then with .js/.mjs/.cjs, then as a directory's index.js.
|
|
171
|
+
* Returns the module path or null. Full-path matching is what keeps a common-word
|
|
172
|
+
* basename like settings.js from matching an unrelated module. */
|
|
173
|
+
function resolveRef(ref, modules) {
|
|
174
|
+
if (!ref) return null;
|
|
175
|
+
for (const c of [ref, ref + '.js', ref + '.mjs', ref + '.cjs', ref + '/index.js']) {
|
|
176
|
+
if (modules.has(c)) return c;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Does this source export anything a test could call? CommonJS or ESM. */
|
|
182
|
+
function hasExports(src) {
|
|
183
|
+
return /\bmodule\.exports\b|\bexports\.[A-Za-z_$]|^\s*export\s/m.test(String(src));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Rough count of exported names — a size hint for picking a newcomer-sized module. */
|
|
187
|
+
function countExports(src) {
|
|
188
|
+
const text = String(src);
|
|
189
|
+
const names = new Set();
|
|
190
|
+
let m;
|
|
191
|
+
const objRe = /module\.exports\s*=\s*\{([\s\S]*?)\};?/g;
|
|
192
|
+
while ((m = objRe.exec(text))) {
|
|
193
|
+
for (const part of m[1].split(',')) {
|
|
194
|
+
const k = /^\s*(?:\.\.\.)?([A-Za-z_$][\w$]*)/.exec(part.replace(/\/\/.*$/gm, ''));
|
|
195
|
+
if (k) names.add(k[1]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const dotRe = /\b(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g;
|
|
199
|
+
while ((m = dotRe.exec(text))) names.add(m[1]);
|
|
200
|
+
const esmRe = /^\s*export\s+(?:async\s+)?(?:function\*?|const|let|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
201
|
+
while ((m = esmRe.exec(text))) names.add(m[1]);
|
|
202
|
+
return names.size;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const NETWORK_PKGS = new Set(['http', 'https', 'net', 'tls', 'undici', 'ws', 'dgram']);
|
|
206
|
+
|
|
207
|
+
/** The impurity markers a single source shows on its own: 'postgres', 'subprocess',
|
|
208
|
+
* 'network' (hard — the module cannot be tested DB-free and offline) and 'filesystem'
|
|
209
|
+
* (soft). `requires` is extractRequires' output for the same text. */
|
|
210
|
+
function ownImpurity(src, requires) {
|
|
211
|
+
const text = String(src);
|
|
212
|
+
const hard = new Set();
|
|
213
|
+
const pk = requires.packages;
|
|
214
|
+
if (pk.has('pg')) hard.add('postgres');
|
|
215
|
+
if (pk.has('child_process')) hard.add('subprocess');
|
|
216
|
+
for (const p of pk) if (NETWORK_PKGS.has(p)) hard.add('network');
|
|
217
|
+
if (/(?:^|[^\w.])fetch\s*\(/.test(text)) hard.add('network');
|
|
218
|
+
for (const r of requires.local) if (/(?:^|\/|-)db(?:-[\w-]+)?(?:\.js)?$/.test(r)) hard.add('postgres');
|
|
219
|
+
return { hard, fs: pk.has('fs') || pk.has('fs/promises') };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Rank candidates for a newcomer: unreachable before indirectly-covered, pure before
|
|
223
|
+
* filesystem-touching, then smallest first (a newcomer-sized module). */
|
|
224
|
+
function rankCandidates(rows) {
|
|
225
|
+
return rows.slice().sort((a, b) =>
|
|
226
|
+
(a.reach === 'unreachable' ? 0 : 1) - (b.reach === 'unreachable' ? 0 : 1)
|
|
227
|
+
|| (a.fs ? 1 : 0) - (b.fs ? 1 : 0)
|
|
228
|
+
|| a.lines - b.lines
|
|
229
|
+
|| a.path.localeCompare(b.path));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- tree scan -------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
function walk(relDir, out) {
|
|
235
|
+
let entries;
|
|
236
|
+
try { entries = fs.readdirSync(path.join(ROOT, relDir), { withFileTypes: true }); } catch { return; }
|
|
237
|
+
for (const e of entries) {
|
|
238
|
+
const rel = relDir ? relDir + '/' + e.name : e.name;
|
|
239
|
+
if (e.isDirectory()) {
|
|
240
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('public-') && !e.name.startsWith('.')) walk(rel, out);
|
|
241
|
+
} else if (CODE_EXT.test(e.name)) out.push(rel);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Scan the live tree and return every exported source module with its coverage, reach and
|
|
246
|
+
* purity: [{ path, lines, exports, tested, reach, impure: [..], fs }]. `tested` is true
|
|
247
|
+
* when some test file names the module directly; `reach` is 'direct' | 'indirect' |
|
|
248
|
+
* 'unreachable' (indirect = only loaded through another module a test names). */
|
|
249
|
+
function scanTree() {
|
|
250
|
+
const files = [];
|
|
251
|
+
for (const r of SOURCE_ROOTS) walk(r, files);
|
|
252
|
+
walk('tests', files);
|
|
253
|
+
const tests = files.filter((f) => isTestPath(f) && !f.includes('/fixtures/'));
|
|
254
|
+
const sources = files.filter((f) => !isTestPath(f) && !f.includes('/fixtures/'));
|
|
255
|
+
const text = new Map();
|
|
256
|
+
for (const f of files) text.set(f, fs.readFileSync(path.join(ROOT, f), 'utf8'));
|
|
257
|
+
const modules = new Set(sources);
|
|
258
|
+
|
|
259
|
+
// The require graph and each module's own impurity.
|
|
260
|
+
const edges = new Map();
|
|
261
|
+
const own = new Map();
|
|
262
|
+
for (const f of sources) {
|
|
263
|
+
const req = extractRequires(text.get(f), f);
|
|
264
|
+
const local = new Set();
|
|
265
|
+
for (const r of req.local) { const t = resolveRef(r, modules); if (t) local.add(t); }
|
|
266
|
+
edges.set(f, local);
|
|
267
|
+
own.set(f, ownImpurity(text.get(f), req));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Hard impurity propagates along the require graph (the "module doorway" case).
|
|
271
|
+
const impure = new Map();
|
|
272
|
+
function impurityOf(f, seen) {
|
|
273
|
+
if (impure.has(f)) return impure.get(f);
|
|
274
|
+
if (seen.has(f)) return own.get(f).hard;
|
|
275
|
+
seen.add(f);
|
|
276
|
+
const acc = new Set(own.get(f).hard);
|
|
277
|
+
for (const d of edges.get(f)) for (const x of impurityOf(d, seen)) acc.add(x);
|
|
278
|
+
seen.delete(f);
|
|
279
|
+
impure.set(f, acc);
|
|
280
|
+
return acc;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Direct test references, then everything reachable from them.
|
|
284
|
+
const direct = new Set();
|
|
285
|
+
for (const t of tests) {
|
|
286
|
+
for (const r of extractPathRefs(text.get(t), t)) {
|
|
287
|
+
const hit = resolveRef(r, modules);
|
|
288
|
+
if (hit) direct.add(hit);
|
|
289
|
+
}
|
|
290
|
+
const req = extractRequires(text.get(t), t);
|
|
291
|
+
for (const r of req.local) { const hit = resolveRef(r, modules); if (hit) direct.add(hit); }
|
|
292
|
+
}
|
|
293
|
+
const reached = new Set(direct);
|
|
294
|
+
const queue = [...direct];
|
|
295
|
+
while (queue.length) {
|
|
296
|
+
for (const d of edges.get(queue.pop()) || []) if (!reached.has(d)) { reached.add(d); queue.push(d); }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const rows = [];
|
|
300
|
+
for (const f of sources) {
|
|
301
|
+
const src = text.get(f);
|
|
302
|
+
if (!hasExports(src)) continue;
|
|
303
|
+
rows.push({
|
|
304
|
+
path: f,
|
|
305
|
+
lines: src.split('\n').length,
|
|
306
|
+
exports: countExports(src),
|
|
307
|
+
tested: direct.has(f),
|
|
308
|
+
reach: direct.has(f) ? 'direct' : reached.has(f) ? 'indirect' : 'unreachable',
|
|
309
|
+
impure: [...impurityOf(f, new Set())].sort(),
|
|
310
|
+
fs: own.get(f).fs,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return rows;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---- CLI -------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const USAGE = `usage: node scripts/gds/find-untested.js [--unreachable] [--all] [--limit N] [--json]
|
|
319
|
+
|
|
320
|
+
Lists source modules (scripts/, src/, modules/) that NO test file names, for the newcomer
|
|
321
|
+
"add a unit test" task. Candidates come first when no test reaches them at all, then when
|
|
322
|
+
they look pure, then smallest first.
|
|
323
|
+
|
|
324
|
+
--unreachable only modules no test reaches even indirectly (the strict definition)
|
|
325
|
+
--all include modules that look impure (Postgres, subprocess, network)
|
|
326
|
+
--limit N how many to print (default 25; 0 = all)
|
|
327
|
+
--json machine-readable rows
|
|
328
|
+
|
|
329
|
+
Purity is a heuristic: check the helper you pick really is DB-free before testing it.`;
|
|
330
|
+
|
|
331
|
+
function main(argv) {
|
|
332
|
+
if (argv.includes('--help') || argv.includes('-h')) { console.log(USAGE); return 0; }
|
|
333
|
+
const li = argv.indexOf('--limit');
|
|
334
|
+
const limit = li >= 0 ? parseInt(argv[li + 1], 10) : 25;
|
|
335
|
+
if (li >= 0 && !(limit >= 0)) { console.error('find-untested: --limit needs a number'); return 2; }
|
|
336
|
+
const all = scanTree();
|
|
337
|
+
let rows = all.filter((r) => !r.tested);
|
|
338
|
+
if (argv.includes('--unreachable')) rows = rows.filter((r) => r.reach === 'unreachable');
|
|
339
|
+
if (!argv.includes('--all')) rows = rows.filter((r) => r.impure.length === 0);
|
|
340
|
+
rows = rankCandidates(rows);
|
|
341
|
+
const shown = limit ? rows.slice(0, limit) : rows;
|
|
342
|
+
if (argv.includes('--json')) { console.log(JSON.stringify(shown, null, 2)); return 0; }
|
|
343
|
+
const untested = all.filter((r) => !r.tested).length;
|
|
344
|
+
console.log(`find-untested: ${all.length} exported modules scanned; ${untested} have no test naming them; `
|
|
345
|
+
+ `${rows.length} match the filters.`);
|
|
346
|
+
if (!shown.length) { console.log(' (none — try --all, or strengthen a thin existing test instead)'); return 0; }
|
|
347
|
+
const w = Math.max(...shown.map((r) => r.path.length));
|
|
348
|
+
console.log(` ${'module'.padEnd(w)} lines exports reach notes`);
|
|
349
|
+
for (const r of shown) {
|
|
350
|
+
const notes = [r.impure.length ? 'impure: ' + r.impure.join('+') : 'looks pure', r.fs ? 'uses fs' : ''].filter(Boolean).join(', ');
|
|
351
|
+
console.log(` ${r.path.padEnd(w)} ${String(r.lines).padStart(5)} ${String(r.exports).padStart(7)} ${r.reach.padEnd(11)} ${notes}`);
|
|
352
|
+
}
|
|
353
|
+
if (rows.length > shown.length) console.log(` … ${rows.length - shown.length} more (--limit 0 for all)`);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (require.main === module) process.exitCode = main(process.argv.slice(2));
|
|
358
|
+
|
|
359
|
+
module.exports = {
|
|
360
|
+
isTestPath,
|
|
361
|
+
normRel,
|
|
362
|
+
extractPathRefs,
|
|
363
|
+
extractRequires,
|
|
364
|
+
resolveRef,
|
|
365
|
+
hasExports,
|
|
366
|
+
countExports,
|
|
367
|
+
ownImpurity,
|
|
368
|
+
rankCandidates,
|
|
369
|
+
scanTree,
|
|
370
|
+
main,
|
|
371
|
+
};
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"credits_reward": 15,
|
|
11
11
|
"floor": 2,
|
|
12
12
|
"title": "Add a unit test for an untested pure exported helper (assert its real behaviour)",
|
|
13
|
-
"description": "## What you're doing\n\nThe repo's safety net is its **unit suite** — every `tests/*.mjs` file, run by `scripts/gds/run-unit-tests.js` in CI on every PR. New pure tests are **auto-discovered** (there is no allowlist to edit), so the moment you add one it joins the gate. Your first ship as an engineer: pick ONE exported helper that has thin or no test coverage and add a focused, DB-free unit test for it.\n\nIt is identical for every newcomer and **always valuable** — coverage is never \"done\", the codebase has hundreds of small pure helpers, and a good test both guards behaviour and teaches you how a corner of the system works. It is also **safe to auto-ship**: you add ONE file under `tests/`, which is not a protected surface, so a clean test merges on green with no owner approval.\n\n## How to pick a target (a PURE helper)\n\nPick a **pure** function — deterministic, with no database, network, filesystem, or live-server dependency — so the test runs standalone.
|
|
13
|
+
"description": "## What you're doing\n\nThe repo's safety net is its **unit suite** — every `tests/*.mjs` file, run by `scripts/gds/run-unit-tests.js` in CI on every PR. New pure tests are **auto-discovered** (there is no allowlist to edit), so the moment you add one it joins the gate. Your first ship as an engineer: pick ONE exported helper that has thin or no test coverage and add a focused, DB-free unit test for it.\n\nIt is identical for every newcomer and **always valuable** — coverage is never \"done\", the codebase has hundreds of small pure helpers, and a good test both guards behaviour and teaches you how a corner of the system works. It is also **safe to auto-ship**: you add ONE file under `tests/`, which is not a protected surface, so a clean test merges on green with no owner approval.\n\n## How to pick a target (a PURE helper)\n\nPick a **pure** function — deterministic, with no database, network, filesystem, or live-server dependency — so the test runs standalone. Do not hunt by hand: run the finder, which reads the live tree every time (so this task never goes stale the way a written list does):\n\n```\nnode scripts/gds/find-untested.js\n```\n\nIt lists modules that **no test file names**, best candidates first — ones no test reaches at all, then ones that look pure, then the smallest. Its purity column is a heuristic: open the module and confirm the helper you pick really needs no database, network or files. If the list is empty, run it with `--all` and pick a module that still exports a helper you can test DB-free.\n\n## How to write it (mirror an existing test — there is no framework)\n\nThese tests are plain Node scripts using `node:assert`; there is no test runner. Open a small existing one as your model — for example `tests/criterion_ref_parse.mjs` or `tests/task_classifier_vocab.mjs` — and copy the shape:\n1. `import` / `require` the helper from its module.\n2. Assert **several** cases, including at least one **edge / boundary** case (empty input, null, a tricky value).\n3. Print a one-line success message at the end (an assert that throws exits non-zero, which is how the gate sees a failure).\n\nName the file `tests/<helper-or-module>.mjs`.\n\n## Verify + ship\n\nRun BOTH and confirm both are green:\n- `node tests/<your-file>.mjs` — your test alone, exits 0.\n- `node scripts/gds/run-unit-tests.js` — the whole gate, with your test auto-included.\n\nThen `/builder-ship`. The ship notes must name the helper and file you tested.\n\n## Stay safe (off-limits)\n\nAdd exactly ONE new file under `tests/`. Do NOT edit `scripts/gds/run-unit-tests.js` (it auto-discovers your test, and it is a protected file you may not touch), and do NOT change the helper you are testing. Your test must be **DB-free**: anything needing Postgres, the network, or a live server belongs in the integration gate and will fail standalone here. Touch no permission, pipeline, migration, or infra file.",
|
|
14
14
|
"done_when": [
|
|
15
15
|
"A NEW tests/<name>.mjs file was added (it did not exist before) that unit-tests a PURE exported helper — no Postgres, network, filesystem, or live server.",
|
|
16
16
|
"`node tests/<name>.mjs` exits 0 and the test makes real assertions about the helper's behaviour — at least two cases including one edge/boundary case — not a trivial always-true assertion.",
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.7'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/find_untested.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for scripts/gds/find-untested.js (task 1002458) — the finder the newcomer
|
|
4
|
+
// "add a unit test" chore runs instead of a hardcoded target list. The cases pin the two
|
|
5
|
+
// failures that made the naive scans unusable, measured before it was built:
|
|
6
|
+
// - BASENAME matching falsely marks a common-word module (settings.js) as tested because
|
|
7
|
+
// some unrelated test mentions a different settings.js;
|
|
8
|
+
// - LITERAL-require-only scanning misses tests that reach a module through
|
|
9
|
+
// path.join(ROOT, …), a `const GOV = path.join(…)` prefix, or a spawned script — which is
|
|
10
|
+
// how it once flagged the well-tested memory-map.js as untested.
|
|
11
|
+
// Plus the purity heuristic, the ranking, and one pass over the live tree. Run:
|
|
12
|
+
// node tests/find_untested.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const fu = require('../scripts/gds/find-untested.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function test(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` ok ${name}`); } catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MODS = new Set([
|
|
27
|
+
'modules/builder-settings/settings.js',
|
|
28
|
+
'modules/hall-ui/settings.js',
|
|
29
|
+
'modules/government/db.js',
|
|
30
|
+
'modules/government/board.js',
|
|
31
|
+
'scripts/gds/memory-map.js',
|
|
32
|
+
'scripts/gds/grade-smoke.js',
|
|
33
|
+
'modules/lifecycle/index.js',
|
|
34
|
+
]);
|
|
35
|
+
const hits = (src, file) => [...fu.extractPathRefs(src, file)].map((r) => fu.resolveRef(r, MODS)).filter(Boolean).sort();
|
|
36
|
+
|
|
37
|
+
test('a basename alone never resolves to a module (the common-word false drop)', () => {
|
|
38
|
+
assert.deepEqual(hits("const s = require('./settings.js'); // settings.js", 'tests/x.mjs'), []);
|
|
39
|
+
assert.deepEqual(hits("import x from 'settings.js'", 'tests/x.mjs'), []);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a full or relative path resolves to exactly that module, not its namesake', () => {
|
|
43
|
+
assert.deepEqual(hits("require('../modules/hall-ui/settings.js')", 'tests/x.mjs'), ['modules/hall-ui/settings.js']);
|
|
44
|
+
assert.deepEqual(hits("require('modules/builder-settings/settings')", 'tests/x.mjs'), ['modules/builder-settings/settings.js']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('path.join(ROOT, …) segments, and a const prefix, are evaluated (the memory-map miss)', () => {
|
|
48
|
+
const src = [
|
|
49
|
+
"const ROOT = path.resolve(__dirname, '..');",
|
|
50
|
+
"const GOV = path.join(ROOT, 'modules', 'government');",
|
|
51
|
+
"const db = require(path.join(GOV, 'db.js'));",
|
|
52
|
+
"const mm = require(path.join(ROOT, 'scripts', 'gds', 'memory-map.js'));",
|
|
53
|
+
].join('\n');
|
|
54
|
+
assert.deepEqual(hits(src, 'tests/x.mjs'), ['modules/government/db.js', 'scripts/gds/memory-map.js']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('an unknown leading identifier stands for the repo root; one in the middle does not', () => {
|
|
58
|
+
assert.deepEqual(hits("path.join(REPO, 'modules/government/board.js')", 'tests/x.mjs'), ['modules/government/board.js']);
|
|
59
|
+
assert.deepEqual(hits("path.join('modules', someVar, 'board.js')", 'tests/x.mjs'), []);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a spawned script, new URL(…, import.meta.url) and a directory index all count', () => {
|
|
63
|
+
assert.deepEqual(hits("spawnSync(process.execPath, ['scripts/gds/grade-smoke.js', '--dry'])", 'tests/x.mjs'), ['scripts/gds/grade-smoke.js']);
|
|
64
|
+
assert.deepEqual(hits("await import(new URL('../scripts/gds/memory-map.js', import.meta.url))", 'tests/x.mjs'), ['scripts/gds/memory-map.js']);
|
|
65
|
+
assert.deepEqual(hits("require('../modules/lifecycle')", 'tests/x.mjs'), ['modules/lifecycle/index.js']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('template literals with interpolation and URLs are ignored, not mis-resolved', () => {
|
|
69
|
+
assert.deepEqual(hits('require(`../modules/${k}/settings.js`)', 'tests/x.mjs'), []);
|
|
70
|
+
assert.deepEqual(hits("fetch('https://example.com/scripts/gds/memory-map.js')", 'tests/x.mjs'), []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('extractRequires separates packages from local edges and ignores comment mentions', () => {
|
|
74
|
+
const r = fu.extractRequires("const pg = require('pg');\nconst fs = require('node:fs');\nconst b = require('./board');\n// see ./db.js", 'modules/government/x.js');
|
|
75
|
+
assert.deepEqual([...r.packages].sort(), ['fs', 'pg']);
|
|
76
|
+
assert.deepEqual([...r.local], ['modules/government/board']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('ownImpurity: pg, a db module, child_process and fetch are hard; fs is soft', () => {
|
|
80
|
+
const imp = (src, file = 'modules/a/x.js') => fu.ownImpurity(src, fu.extractRequires(src, file));
|
|
81
|
+
assert.deepEqual([...imp("require('pg')").hard], ['postgres']);
|
|
82
|
+
assert.deepEqual([...imp("require('./db-claims.js')").hard], ['postgres']);
|
|
83
|
+
assert.deepEqual([...imp("require('node:child_process')").hard], ['subprocess']);
|
|
84
|
+
assert.deepEqual([...imp('const r = await fetch(url)').hard], ['network']);
|
|
85
|
+
const f = imp("require('fs')");
|
|
86
|
+
assert.equal(f.hard.size, 0);
|
|
87
|
+
assert.equal(f.fs, true);
|
|
88
|
+
// A name that merely contains "db" is not a database module.
|
|
89
|
+
assert.equal(imp("require('./feedback.js')").hard.size, 0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('isTestPath covers every test root and *.test.js siblings, and nothing else', () => {
|
|
93
|
+
assert.equal(fu.isTestPath('tests/a.mjs'), true);
|
|
94
|
+
assert.equal(fu.isTestPath('modules/lifecycle/tests/a.mjs'), true);
|
|
95
|
+
assert.equal(fu.isTestPath('scripts/gds/newcomer-floor.test.js'), true);
|
|
96
|
+
assert.equal(fu.isTestPath('scripts/gds/latest.js'), false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('hasExports / countExports read CommonJS and ESM', () => {
|
|
100
|
+
assert.equal(fu.hasExports('function a(){}'), false);
|
|
101
|
+
assert.equal(fu.countExports('module.exports = {\n a,\n b, // note\n c: 1,\n};'), 3);
|
|
102
|
+
assert.equal(fu.countExports('exports.x = 1; module.exports.y = 2;'), 2);
|
|
103
|
+
assert.equal(fu.countExports('export function f(){}\nexport const g = 1;'), 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('normRel clamps a climb above the root instead of escaping it', () => {
|
|
107
|
+
assert.equal(fu.normRel('tests/../../../scripts/gds/x.js'), 'scripts/gds/x.js');
|
|
108
|
+
assert.equal(fu.normRel('./a//b/./c'), 'a/b/c');
|
|
109
|
+
assert.equal(fu.normRel('..'), '');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('rankCandidates: unreachable first, then no-fs, then smallest', () => {
|
|
113
|
+
const rows = [
|
|
114
|
+
{ path: 'c', reach: 'indirect', fs: false, lines: 10 },
|
|
115
|
+
{ path: 'b', reach: 'unreachable', fs: true, lines: 500 },
|
|
116
|
+
{ path: 'a', reach: 'indirect', fs: true, lines: 5 },
|
|
117
|
+
{ path: 'd', reach: 'indirect', fs: false, lines: 90 },
|
|
118
|
+
];
|
|
119
|
+
assert.deepEqual(fu.rankCandidates(rows).map((r) => r.path), ['b', 'c', 'd', 'a']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('the live tree: tested modules the naive scans got wrong are NOT candidates', () => {
|
|
123
|
+
const rows = fu.scanTree();
|
|
124
|
+
const by = new Map(rows.map((r) => [r.path, r]));
|
|
125
|
+
// A scan that finds nothing looks exactly like a clean tree, so assert it found a lot.
|
|
126
|
+
assert.ok(rows.length > 300, `expected hundreds of exported modules, scanned ${rows.length}`);
|
|
127
|
+
for (const p of ['scripts/gds/memory-map.js', 'modules/grading/grader-workers/worker-verdict.js', 'scripts/gds/find-untested.js']) {
|
|
128
|
+
assert.ok(by.has(p), `${p} should be scanned`);
|
|
129
|
+
assert.equal(by.get(p).tested, true, `${p} has a test naming it`);
|
|
130
|
+
}
|
|
131
|
+
const untested = rows.filter((r) => !r.tested);
|
|
132
|
+
assert.ok(untested.length > 0 && untested.length < rows.length / 2,
|
|
133
|
+
`untested should be a minority, got ${untested.length} of ${rows.length}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(`\nfind-untested (task 1002458) tests: ${passed} passed, ${failed} failed`);
|
|
137
|
+
process.exit(failed > 0 ? 1 : 0);
|