@syncerauk/test-rules 1.0.8 → 1.0.9
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/README.md +11 -2
- package/package.json +1 -1
- package/test-rules.mjs +33 -9
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ Both in the repo's `scripts/test-rules/`:
|
|
|
56
56
|
`pins` (the kept pins, below),
|
|
57
57
|
`reads` (the reads register: `"comment"`, or
|
|
58
58
|
`{ "json": "<path>" }` for the map under `"tests"` in that file, `"key": "<name>"` for another
|
|
59
|
-
key, `"key": null` when the file itself is the map
|
|
59
|
+
key, `"key": null` when the file itself is the map; `"runs": "<name>"` (1.0.9) names a second
|
|
60
|
+
map in that file of what each test only runs). The defaults are `CONFIG_DEFAULTS` in the
|
|
60
61
|
script.
|
|
61
62
|
- `listed.json`: the breaches the repo had on the day the check arrived. Written once by
|
|
62
63
|
`test-rules --init`. An optional `moved` map, `{ "<new test file>": "<old test file>" }`,
|
|
@@ -123,7 +124,7 @@ when the PR does not raise the version.
|
|
|
123
124
|
| Version | Detections it added |
|
|
124
125
|
|---|---|
|
|
125
126
|
| 1.0.0 | every rule: `T1:name`, `T1:hub`, `R7:lines`, `T4:source-text` (a declared read, a raw import, a source path built from the test's place), `T6:timer`, `T6:clock`, `T6:outside`, `T6:env` |
|
|
126
|
-
| 1.0.1, 1.0.4, 1.0.5, 1.0.6 | none |
|
|
127
|
+
| 1.0.1, 1.0.4, 1.0.5, 1.0.6, 1.0.9 | none |
|
|
127
128
|
| 1.0.2 | `T4:source-text`: a path that starts with a dot-folder; a JSON register that is the whole file |
|
|
128
129
|
| 1.0.3 | `T4:source-text`: the built-path scan under a JSON register |
|
|
129
130
|
| 1.0.7 | `T4:pin`: a `pins` entry that does not hold (never listed, so never re-listed) |
|
|
@@ -131,6 +132,14 @@ when the PR does not raise the version.
|
|
|
131
132
|
|
|
132
133
|
## Versions
|
|
133
134
|
|
|
135
|
+
- **1.0.9** — Run, not read, under a JSON register: a register row the test also names as run
|
|
136
|
+
is not a `T4:source-text` breach. A test says what it runs on a `// runs:` line, as the
|
|
137
|
+
comment-form repos already do, or in the register file's runs map, named by
|
|
138
|
+
`"reads": { …, "runs": "<name>" }`. That covers a script the test runs, the migrations it
|
|
139
|
+
replays, or a checker's input paths (TESTING.md, 6 Nov 2026). A runs-map row also declares a
|
|
140
|
+
built path, as a `// runs:` line does. `**` covers nothing; a missing or malformed runs map
|
|
141
|
+
that the config names cannot run (exit 3). It only drops breaches: listed entries it clears come off
|
|
142
|
+
the list in the PR that bumps the pin.
|
|
134
143
|
- **1.0.8** — Reads made through a helper: a source path a test hands to a helper that reads
|
|
135
144
|
the file system at that argument is a `T4:source-text` read, as a `readFileSync` in the test
|
|
136
145
|
is. A helper is a function of the test's own or one the test imports by a relative path
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncerauk/test-rules",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "The static check of Syncera's test-writing rules (T1, T4, T6 and the line limit of R7). One file, no dependency. Published to npm by the orchestrator's publish-test-rules workflow; each repo pins an exact version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
package/test-rules.mjs
CHANGED
|
@@ -64,6 +64,9 @@
|
|
|
64
64
|
// A test on the config's `pins` list is a kept pin (R4) and not a breach (1.0.7).
|
|
65
65
|
// A source path the test hands to a helper of its own or of the repo's, at a
|
|
66
66
|
// parameter the helper reads the file system at, is a read too (1.0.8; below).
|
|
67
|
+
// A JSON register row the test also names as run (a `// runs:` line, or the
|
|
68
|
+
// register's `runs` map) is run, not read, and is not a breach; `**` covers
|
|
69
|
+
// nothing (1.0.9).
|
|
67
70
|
// T4:pin a `pins` entry that does not hold: no test file, or one that is not a test
|
|
68
71
|
// file here; no rule it guards, or a rule another entry already pins (one pin
|
|
69
72
|
// per rule); no recorded mutation, or no record of its red-before (1.0.7)
|
|
@@ -104,7 +107,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
|
|
|
104
107
|
wording: [], // the one list of what a person reads (a built page, a legal text): files, globs, folders
|
|
105
108
|
style_contract: {}, // { "<test file>": ["<.css/.scss path or glob>", …] }: the one test that may read those stylesheets
|
|
106
109
|
pins: [], // [{ file, rule, mutation, red_before }]: the kept pins (R4), one per rule, each with the mutation that turns it red
|
|
107
|
-
reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" } (key null: the file itself is the map)
|
|
110
|
+
reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests", runs: "runs" } (key null: the file itself is the map; runs, 1.0.9: the map of what each test only runs)
|
|
108
111
|
source_ext: ['mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'sql', 'css', 'scss', 'html', 'sh', 'yml', 'yaml', 'md', 'mdx'],
|
|
109
112
|
max_lines: 1500,
|
|
110
113
|
task_names: ['(?:^|[^a-z0-9])w-?\\d+', '(?:^|[^a-z0-9])(?:fs|ops|trk|ci|app|leg|mig|bank|push|fun)-\\d+', '(?:^|[^a-z0-9])s\\d+(?:[^a-z0-9]|$)'],
|
|
@@ -605,7 +608,7 @@ function helperArgsOf(path, view, ctx) {
|
|
|
605
608
|
}
|
|
606
609
|
|
|
607
610
|
/** T4: the line of the first read of a source file as text, or 0. */
|
|
608
|
-
function sourceTextLine(view, { config, register, tracked, as, helpers }) {
|
|
611
|
+
function sourceTextLine(view, { config, register, runs = null, tracked, as, helpers }) {
|
|
609
612
|
const { path, text, code, bare, strings } = view;
|
|
610
613
|
const on = (id) => detects(id, as);
|
|
611
614
|
const isSource = (p) => config.source_ext.includes(String(p).split('.').pop().toLowerCase());
|
|
@@ -617,8 +620,15 @@ function sourceTextLine(view, { config, register, tracked, as, helpers }) {
|
|
|
617
620
|
|
|
618
621
|
const declared = declaredIn(text);
|
|
619
622
|
const json = register && typeof register === 'object';
|
|
623
|
+
const wide = (p) => /^\*\*\/?$/.test(p); // `**`: the whole repo is never "run"
|
|
624
|
+
const mapped = (runs?.[path] ?? []).filter((p) => !wide(p));
|
|
625
|
+
const ran = [...declared.runs.map((r) => r.pattern).filter((p) => !wide(p)), ...mapped];
|
|
620
626
|
if (on('T4:declared') && json) {
|
|
621
|
-
|
|
627
|
+
// A register row the test also names as run, on a `// runs:` line or in the register's runs
|
|
628
|
+
// map, is run, not read as text (1.0.9): a script it runs, migrations it replays, or a
|
|
629
|
+
// checker's inputs (TESTING.md, 6 Nov), never the whole repo.
|
|
630
|
+
const run = (p) => ran.some((q) => q === p || globRe(q).test(p));
|
|
631
|
+
if ((register[path] ?? []).some((p) => offends(p) && !run(p))) return 1;
|
|
622
632
|
} else if (on('T4:declared') && config.reads === 'comment') {
|
|
623
633
|
const hit = declared.reads.find((r) => offends(r.pattern));
|
|
624
634
|
if (hit) return hit.line;
|
|
@@ -635,7 +645,7 @@ function sourceTextLine(view, { config, register, tracked, as, helpers }) {
|
|
|
635
645
|
// system, that the test does not declare: `// runs:` when it is only run. A JSON register does
|
|
636
646
|
// not silence this (1.0.3): a register can miss a read, and a source file it names returned above.
|
|
637
647
|
if (!tracked) return 0;
|
|
638
|
-
const said = [...declared.reads, ...declared.runs].map((r) => r.pattern);
|
|
648
|
+
const said = [...[...declared.reads, ...declared.runs].map((r) => r.pattern), ...mapped];
|
|
639
649
|
const breach = (target) => target !== path && tracked.has(target) && !allowed(target) && !said.some((p) => globRe(p).test(target));
|
|
640
650
|
// `./x` and `../x` are relative to the test; `.github/x` is a repo path like any other (1.0.2;
|
|
641
651
|
// before it, any leading dot was read as beside the test).
|
|
@@ -724,7 +734,7 @@ export function pinsOf(pins, testPaths) {
|
|
|
724
734
|
* its first line: [{ file, rule, line, say }], sorted. Pure: no disk, no git. `as`: a version
|
|
725
735
|
* (x.y.z) to run only the detectors it had (DETECTORS), for the re-list; null runs them all.
|
|
726
736
|
*/
|
|
727
|
-
export function breachesOf({ files, config, register = null, tracked = null, as = null, readText = null }) {
|
|
737
|
+
export function breachesOf({ files, config, register = null, runs = null, tracked = null, as = null, readText = null }) {
|
|
728
738
|
const out = [];
|
|
729
739
|
const add = (file, rule, line) => out.push({ file, rule, line, say: RULES[rule] });
|
|
730
740
|
const on = (id) => detects(id, as);
|
|
@@ -740,7 +750,7 @@ export function breachesOf({ files, config, register = null, tracked = null, as
|
|
|
740
750
|
const base = path.split('/').pop();
|
|
741
751
|
if (on('T1:name') && names.some((re) => re.test(base))) add(path, 'T1:name', 1);
|
|
742
752
|
if (on('R7:lines') && countLines(text) > config.max_lines) add(path, 'R7:lines', config.max_lines + 1);
|
|
743
|
-
const t4 = sourceTextLine(view, { config, register, tracked, as, helpers });
|
|
753
|
+
const t4 = sourceTextLine(view, { config, register, runs, tracked, as, helpers });
|
|
744
754
|
if (t4 && !pins.pinned.has(path)) add(path, 'T4:source-text', t4);
|
|
745
755
|
const faked = fakes.some((re) => re.test(view.bare));
|
|
746
756
|
const timer = faked || !on('T6:timer') ? 0 : timerLine(view);
|
|
@@ -878,8 +888,9 @@ export function withDefaults(raw) {
|
|
|
878
888
|
}
|
|
879
889
|
const r = config.reads;
|
|
880
890
|
const json = r && typeof r === 'object' && !Array.isArray(r) && typeof r.json === 'string'
|
|
881
|
-
&& Object.keys(r).every((k) => k === 'json' || k === 'key') && (r.key === undefined || r.key === null || typeof r.key === 'string')
|
|
882
|
-
|
|
891
|
+
&& Object.keys(r).every((k) => k === 'json' || k === 'key' || k === 'runs') && (r.key === undefined || r.key === null || typeof r.key === 'string')
|
|
892
|
+
&& (r.runs === undefined || (typeof r.runs === 'string' && r.runs !== ''));
|
|
893
|
+
if (!(r === null || r === 'comment' || json)) throw fail(EXIT.FAILED, `${CONFIG_FILE}: reads must be null, "comment" or { "json": "<path>", "key": "<name>" | null, "runs": "<name>" (optional) }`);
|
|
883
894
|
return config;
|
|
884
895
|
}
|
|
885
896
|
|
|
@@ -899,6 +910,18 @@ export function registerOf(file, reads, as = null) {
|
|
|
899
910
|
return map;
|
|
900
911
|
}
|
|
901
912
|
|
|
913
|
+
/**
|
|
914
|
+
* The runs map beside the reads register (1.0.9), when `reads.runs` names one: what each test only
|
|
915
|
+
* runs (scripts, migrations it replays, a checker's inputs). No map there cannot run (exit 3).
|
|
916
|
+
*/
|
|
917
|
+
export function runsOf(file, reads) {
|
|
918
|
+
if (!reads || typeof reads !== 'object' || reads.runs === undefined) return null;
|
|
919
|
+
const map = file?.[reads.runs];
|
|
920
|
+
const isMap = map && typeof map === 'object' && !Array.isArray(map) && Object.values(map).every((v) => Array.isArray(v));
|
|
921
|
+
if (!isMap) throw fail(EXIT.FAILED, `${reads.json}: no "${reads.runs}" map of { "<test file>": ["<pattern>", …] } (named by "runs")`);
|
|
922
|
+
return map;
|
|
923
|
+
}
|
|
924
|
+
|
|
902
925
|
// ---------------------------------------------------------------------------------------------
|
|
903
926
|
// The command line: the only part that touches disk and git.
|
|
904
927
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -1004,13 +1027,14 @@ export function main(argv = [], { log = console.log, error = console.error, runn
|
|
|
1004
1027
|
const files = testPaths.map((path) => ({ path, text: readFileSync(join(dir, path), 'utf8') }));
|
|
1005
1028
|
const registerFile = config.reads && typeof config.reads === 'object' ? readJson(dir, config.reads.json) : null;
|
|
1006
1029
|
const register = registerFile ? registerOf(registerFile, config.reads) : null;
|
|
1030
|
+
const runs = registerFile ? runsOf(registerFile, config.reads) : null;
|
|
1007
1031
|
// A helper a test imports is read from the worktree, once (1.0.8).
|
|
1008
1032
|
const texts = new Map(files.map((f) => [f.path, f.text]));
|
|
1009
1033
|
const readText = (rel) => {
|
|
1010
1034
|
if (!texts.has(rel)) { try { texts.set(rel, tracked.has(rel) ? readFileSync(join(dir, rel), 'utf8') : null); } catch { texts.set(rel, null); } }
|
|
1011
1035
|
return texts.get(rel);
|
|
1012
1036
|
};
|
|
1013
|
-
const all = breachesOf({ files, config, register, tracked, readText });
|
|
1037
|
+
const all = breachesOf({ files, config, register, runs, tracked, readText });
|
|
1014
1038
|
|
|
1015
1039
|
if (opts.init) {
|
|
1016
1040
|
if (existsSync(join(dir, LISTED_FILE))) throw fail(EXIT.USAGE, `${LISTED_FILE} exists: the list is written once and only shrinks`);
|