leerness 1.36.302 → 1.36.304

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.
Files changed (177) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +4 -4
  4. package/bin/leerness.js +1 -1
  5. package/package.json +1 -1
  6. package/scripts/agents-bench-output-probe.js +1 -0
  7. package/scripts/api-skill-match-probe.js +1 -0
  8. package/scripts/audit-env-text-probe.js +1 -0
  9. package/scripts/audit-readme-text-probe.js +1 -0
  10. package/scripts/audit-text-probe.js +1 -0
  11. package/scripts/authoring-language-probe.js +1 -0
  12. package/scripts/auto-update-hooks-probe.js +1 -0
  13. package/scripts/brief-text-probe.js +1 -0
  14. package/scripts/bugfix-areas-probe.js +1 -0
  15. package/scripts/ci-shard-probe.js +114 -0
  16. package/scripts/ci-shard.js +37 -2
  17. package/scripts/claim-runtime-cache-probe.js +1 -0
  18. package/scripts/claim-sweep-git-probe.js +1 -0
  19. package/scripts/claims-baseline-concurrency-probe.js +1 -0
  20. package/scripts/claims-baseline-probe.js +1 -0
  21. package/scripts/command-flags-probe.js +1 -0
  22. package/scripts/context-current-probe.js +1 -0
  23. package/scripts/context-reading-probe.js +1 -0
  24. package/scripts/copy-tree-probe.js +1 -0
  25. package/scripts/crud-check-probe.js +1 -0
  26. package/scripts/dead-flags-probe.js +1 -0
  27. package/scripts/done-status-probe.js +1 -0
  28. package/scripts/dry-run-refusal-probe.js +1 -0
  29. package/scripts/e2e-child-diagnostics-probe.js +1 -0
  30. package/scripts/e2e-command-surface.js +1 -0
  31. package/scripts/e2e-concurrency.js +1 -0
  32. package/scripts/e2e-core.js +1 -0
  33. package/scripts/e2e-lived-in-harness.js +1 -0
  34. package/scripts/e2e-repo-guard.js +1 -0
  35. package/scripts/e2e-temp-scope-probe.js +1 -0
  36. package/scripts/e2e.js +5 -0
  37. package/scripts/encoding-diagnostics-probe.js +1 -0
  38. package/scripts/encoding-selftest-probe.js +1 -0
  39. package/scripts/env-snapshot-text-probe.js +1 -0
  40. package/scripts/env-sync-text-probe.js +1 -0
  41. package/scripts/execution-ledger-sync-probe.js +1 -0
  42. package/scripts/false-claim-probe.js +1 -0
  43. package/scripts/feature-graph-text-probe.js +1 -0
  44. package/scripts/file-lease-probe.js +1 -0
  45. package/scripts/fix-evidence-language-probe.js +1 -0
  46. package/scripts/free-text-args-probe.js +1 -0
  47. package/scripts/generation-text-probe.js +1 -0
  48. package/scripts/git-resolution-cache-probe.js +1 -0
  49. package/scripts/github-login-probe.js +1 -0
  50. package/scripts/handoff-history-text-probe.js +1 -0
  51. package/scripts/handoff-i18n-probe.js +1 -0
  52. package/scripts/handoff-preservation-probe.js +1 -0
  53. package/scripts/handoff-readonly-probe.js +1 -0
  54. package/scripts/handoff-text-probe.js +1 -0
  55. package/scripts/harness-text-probe.js +1 -0
  56. package/scripts/history-retention-probe.js +1 -0
  57. package/scripts/history-retention-write-probe.js +1 -0
  58. package/scripts/i18n-next-cluster-probe.js +1 -0
  59. package/scripts/i18n-priority-surface-probe.js +1 -0
  60. package/scripts/install-text-probe.js +1 -0
  61. package/scripts/installed-cleanroom-probe.js +1 -0
  62. package/scripts/installed-handoff-lifecycle-probe.js +1 -0
  63. package/scripts/lazy-auto-track-probe.js +1 -0
  64. package/scripts/legacy-runtime-negative-probe.js +1 -0
  65. package/scripts/lock-heartbeat-probe.js +1 -0
  66. package/scripts/lock-probe.js +1 -0
  67. package/scripts/managed-text-probe.js +1 -0
  68. package/scripts/mcp-presence-probe.js +1 -0
  69. package/scripts/memory-archive-check-probe.js +1 -0
  70. package/scripts/memory-archive-retention-probe.js +1 -0
  71. package/scripts/memory-arena-cleanup-probe.js +1 -0
  72. package/scripts/memory-checked-input-probe.js +1 -0
  73. package/scripts/memory-guard-order-probe.js +1 -0
  74. package/scripts/memory-input-preflight-probe.js +1 -0
  75. package/scripts/memory-input-unit-probe.js +1 -0
  76. package/scripts/memory-language-probe.js +1 -0
  77. package/scripts/memory-legacy-retention-probe.js +1 -0
  78. package/scripts/memory-migration-concurrency-probe.js +1 -0
  79. package/scripts/memory-migration-test-support.js +11 -28
  80. package/scripts/memory-migration-transport-probe.js +1 -0
  81. package/scripts/memory-output-parent-race-probe.js +1 -0
  82. package/scripts/memory-output-posix-probe.js +1 -0
  83. package/scripts/memory-output-publication-probe.js +1 -0
  84. package/scripts/memory-output-unit-probe.js +1 -0
  85. package/scripts/memory-projection-retention-probe.js +1 -0
  86. package/scripts/memory-recovery-external-probe.js +1 -0
  87. package/scripts/memory-recovery-receipt-probe.js +1 -0
  88. package/scripts/memory-restore-canonical-probe.js +1 -0
  89. package/scripts/memory-utf8-admission-probe.js +1 -0
  90. package/scripts/memory-write-budget-meter.js +1 -0
  91. package/scripts/migration-compat.js +1 -0
  92. package/scripts/mode-text-probe.js +1 -0
  93. package/scripts/mutation-integrity-probe.js +1 -0
  94. package/scripts/next-action-suggestion-probe.js +1 -0
  95. package/scripts/openai-response-retrieval-probe.js +1 -0
  96. package/scripts/openai-ui-review-dispatch-probe.js +1 -0
  97. package/scripts/openai-ui-review-observation-probe.js +1 -0
  98. package/scripts/openai-ui-review-receipt-probe.js +1 -0
  99. package/scripts/parent-detect-selftest-probe.js +1 -0
  100. package/scripts/plain-language-reporting-probe.js +1 -0
  101. package/scripts/plan-text-probe.js +1 -0
  102. package/scripts/platform-smoke.js +1 -0
  103. package/scripts/provider-capacity-probe.js +1 -0
  104. package/scripts/race-overlap-probe.js +1 -0
  105. package/scripts/release-bump-probe.js +1 -0
  106. package/scripts/release-runtime-probe.js +1 -0
  107. package/scripts/reminder-text-probe.js +1 -0
  108. package/scripts/review-checkpoint-transport-probe.js +1 -0
  109. package/scripts/review-control-probe.js +1 -0
  110. package/scripts/review-feedback-probe.js +1 -0
  111. package/scripts/review-request-labels-probe.js +1 -0
  112. package/scripts/review-request-signals-probe.js +1 -0
  113. package/scripts/review-snapshot-transport-probe.js +1 -0
  114. package/scripts/role-agent-schema-probe.js +1 -0
  115. package/scripts/role-fallback-probe.js +1 -0
  116. package/scripts/role-file-identity-probe.js +1 -0
  117. package/scripts/role-store-loader-probe.js +1 -0
  118. package/scripts/rule-applicability-probe.js +1 -0
  119. package/scripts/rule-text-probe.js +1 -0
  120. package/scripts/rule-verification-probe.js +1 -0
  121. package/scripts/runtime-admission-probe.js +1 -0
  122. package/scripts/runtime-cost-probe.js +1 -0
  123. package/scripts/runtime-descriptor-identity-probe.js +1 -0
  124. package/scripts/runtime-layout-probe.js +1 -0
  125. package/scripts/runtime-repl-probe.js +1 -0
  126. package/scripts/runtime-replacement-probe.js +1 -0
  127. package/scripts/runtime-write-probe.js +1 -0
  128. package/scripts/secret-scan-scope-probe.js +1 -0
  129. package/scripts/selftest-cleanup-probe.js +1 -0
  130. package/scripts/session-close-text-probe.js +1 -0
  131. package/scripts/shared-state-lock-probe.js +1 -0
  132. package/scripts/skill-archive-probe.js +1 -0
  133. package/scripts/smoke.js +1 -0
  134. package/scripts/state-inspect-cli-probe.js +1 -0
  135. package/scripts/state-scopes-probe.js +1 -0
  136. package/scripts/store-diagnostic-identity-probe.js +1 -0
  137. package/scripts/store-diagnostic-probe.js +1 -0
  138. package/scripts/store-values-probe.js +1 -0
  139. package/scripts/task-sync-roundtrip-probe.js +1 -0
  140. package/scripts/teams-text-probe.js +1 -0
  141. package/scripts/temp-cleanup-probe.js +433 -0
  142. package/scripts/temp-cleanup.js +182 -1
  143. package/scripts/test-execution-plan-probe.js +1 -0
  144. package/scripts/test-firestore-probe.js +1 -0
  145. package/scripts/test-http-probe.js +1 -0
  146. package/scripts/test-node-runner-probe.js +1 -0
  147. package/scripts/test-npm-script-probe.js +1 -0
  148. package/scripts/test-npm-script-runner-probe.js +1 -0
  149. package/scripts/test-prerequisite-probe.js +1 -0
  150. package/scripts/test-suite-list-runner-probe.js +1 -0
  151. package/scripts/title-args-probe.js +1 -0
  152. package/scripts/tracker-write-language-probe.js +1 -0
  153. package/scripts/ui-boundary-browser-probe.js +1 -0
  154. package/scripts/ui-boundary-contracts-probe.js +1 -0
  155. package/scripts/ui-boundary-dashboard-probe.js +1 -0
  156. package/scripts/ui-copy-boundary-probe.js +1 -0
  157. package/scripts/ui-review-attempt-probe.js +1 -0
  158. package/scripts/ui-review-authority-floor-probe.js +1 -0
  159. package/scripts/ui-review-authority-host-probe.js +1 -0
  160. package/scripts/ui-review-authorization-probe.js +1 -0
  161. package/scripts/ui-review-ledger-probe.js +1 -0
  162. package/scripts/ui-review-observation-probe.js +1 -0
  163. package/scripts/ui-review-proof-use-probe.js +1 -0
  164. package/scripts/ui-review-publication-probe.js +1 -0
  165. package/scripts/ui-review-recovery-probe.js +1 -0
  166. package/scripts/ui-review-result-probe.js +1 -0
  167. package/scripts/ui-review-run-probe.js +1 -0
  168. package/scripts/ui-review-target-probe.js +1 -0
  169. package/scripts/ui-review-transport-probe.js +1 -0
  170. package/scripts/verify-code-cross-runtime-probe.js +1 -0
  171. package/scripts/which-shim-probe.js +1 -0
  172. package/scripts/workspace-dir-lock-order-probe.js +1 -0
  173. package/scripts/workspace-dir-migration-probe.js +1 -0
  174. package/scripts/workspace-migration-heartbeat-probe.js +1 -0
  175. package/scripts/workspace-selection-probe.js +1 -0
  176. package/scripts/write-path-language-probe.js +1 -0
  177. package/scripts/write-primitive-language-probe.js +1 -0
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ require('./temp-cleanup').trackTemp(module); // T-0357: records this run's temp folders; the runner that started it removes them after a clean exit
2
3
 
3
4
  // removeTree waits out a short hold that fs.rmSync's maxRetries does not, keeps rm semantics, and stays bounded.
4
5
  const fs = require('fs');
@@ -68,6 +69,438 @@ try {
68
69
  assert.ok(waited >= 600 && waited < 2500, `waited ${waited} ms`);
69
70
  } finally { child.kill(); }
70
71
  });
72
+
73
+ // T-0357 (1.36.304): the test process only records the folders it makes, and the runner that sees its exit status
74
+ // removes them (scripts/ci-shard.js runNpm). 1.36.302 and 1.36.303 decided inside the process at exit; eight review
75
+ // rounds kept finding a later piece of JavaScript that changed the verdict. A child here is a script file run as
76
+ // the main module (trackTemp(module)), with its temp directory in this arena and, unless the test says otherwise,
77
+ // LEERNESS_TEMP_RECORD set to a new directory.
78
+ const T = require('./temp-cleanup');
79
+ const TC = JSON.stringify(path.join(__dirname, 'temp-cleanup.js'));
80
+ const childTemp = path.join(arena, 'child-temp');
81
+ const outside = path.join(arena, 'outside');
82
+ fs.mkdirSync(childTemp); fs.mkdirSync(outside);
83
+ fs.writeFileSync(path.join(outside, 'sentinel.txt'), 'kept');
84
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
85
+ let seq = 0;
86
+ const newRecordDir = () => { const d = path.join(arena, `record-${++seq}`); fs.mkdirSync(d); return d; };
87
+ const readRecords = dir => fs.readdirSync(dir).sort().flatMap(f => fs.readFileSync(path.join(dir, f), 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)));
88
+ const childEnv = dir => {
89
+ const env = { ...process.env, TEMP: childTemp, TMP: childTemp, TMPDIR: childTemp };
90
+ for (const k of Object.keys(env)) if (k.toUpperCase() === T.RECORD_ENV) delete env[k];
91
+ if (dir) env[T.RECORD_ENV] = dir;
92
+ return env;
93
+ };
94
+ const tracked = (body, { before = '', record = true, start = 'const tracker = T.trackTemp(module);', nodeArgs = [] } = {}) => {
95
+ const dir = record ? newRecordDir() : null;
96
+ const file = path.join(arena, `child-${++seq}.js`);
97
+ fs.writeFileSync(file, [
98
+ `const T = require(${TC});`,
99
+ "const fs = require('fs'), os = require('os'), path = require('path');",
100
+ `const OUTSIDE = ${JSON.stringify(outside)}, TYPE = ${JSON.stringify(linkType)}, ARENA = ${JSON.stringify(arena)}, CHILD_TEMP = ${JSON.stringify(childTemp)};`,
101
+ before, start,
102
+ 'const made = [];',
103
+ "const mk = parent => { const d = fs.mkdtempSync(path.join(parent, 'leerness-track-')); made.push(d); return d; };",
104
+ "process.on('exit', () => console.log('MADE ' + JSON.stringify(made)));",
105
+ body, ''].join('\n'));
106
+ const r = cp.spawnSync(process.execPath, [...nodeArgs, file], { encoding: 'utf8', windowsHide: true, timeout: 60000, env: childEnv(dir) });
107
+ const line = r.stdout.split(/\r?\n/).find(l => l.startsWith('MADE '));
108
+ assert.ok(line, r.stdout + r.stderr);
109
+ return { r, made: JSON.parse(line.slice(5)), dir, records: dir ? readRecords(dir) : [] };
110
+ };
111
+ const out = (r, tag) => JSON.parse((r.stdout.split(/\r?\n/).find(l => l.startsWith(tag + ' ')) || `${tag} null`).slice(tag.length + 1));
112
+ const sentinelKept = () => assert.strictEqual(fs.readFileSync(path.join(outside, 'sentinel.txt'), 'utf8'), 'kept');
113
+ const cleanup = dirs => { for (const d of dirs) removeTree(d, { recursive: true, force: true }); };
114
+ const SAME = "console.log('SAME ' + JSON.stringify([fs.mkdtempSync === S.sync, fs.mkdtemp === S.cb, fs.promises.mkdtemp === S.p, tracker.active]));";
115
+ const SAVE = 'const S = { sync: fs.mkdtempSync, cb: fs.mkdtemp, p: fs.promises.mkdtemp };';
116
+
117
+ // --- what the test process does ---
118
+ test('trackTemp without LEERNESS_TEMP_RECORD changes nothing: the functions stay, nothing is recorded or removed', () => {
119
+ const { r, made } = tracked(`mk(os.tmpdir()); ${SAME}`, { record: false, before: SAVE });
120
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
121
+ assert.deepStrictEqual(out(r, 'SAME'), [true, true, true, false]);
122
+ assert.strictEqual(fs.existsSync(made[0]), true, 'a run without the variable removed its folder');
123
+ cleanup(made);
124
+ });
125
+ test('with LEERNESS_TEMP_RECORD set, fs.mkdtempSync records its folder in the process\'s own file, and the process removes nothing', () => {
126
+ const { r, made, dir, records } = tracked('mk(os.tmpdir()); mk(os.tmpdir());');
127
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
128
+ for (const d of made) assert.strictEqual(fs.existsSync(d), true, `the process removed ${d} itself`);
129
+ assert.deepStrictEqual(records.map(x => x.path), made);
130
+ const files = fs.readdirSync(dir);
131
+ assert.strictEqual(files.length, 1); assert.ok(/^\d+-[0-9a-f]{8}\.jsonl$/.test(files[0]), files[0]);
132
+ const temp = fs.realpathSync.native(childTemp);
133
+ for (const x of records) {
134
+ const st = fs.lstatSync(x.path, { bigint: true });
135
+ assert.deepStrictEqual([x.dev, x.ino, x.real, x.temp], [String(st.dev), String(st.ino), fs.realpathSync.native(x.path), temp]);
136
+ }
137
+ cleanup(made);
138
+ });
139
+ // 1.36.304 review round 1: an asynchronous form completes later, so another folder can be put in its place before the
140
+ // completion is handled, and that folder was recorded. Only the synchronous form is recorded; the asynchronous ones
141
+ // are not wrapped and their folders are left, like those of a process that does not inherit the variable.
142
+ test('the asynchronous mkdtemp forms are not wrapped and record nothing, also when the folder is swapped before completion', () => {
143
+ for (const form of ['callback', 'promise']) {
144
+ const W = path.join(childTemp, `async-${form}`);
145
+ const call = form === 'callback' ? "fs.mkdtemp(path.join(W, 'leerness-async-'), () => {})" : "fs.promises.mkdtemp(path.join(W, 'leerness-async-'))";
146
+ try {
147
+ const { r, made, dir, records } = tracked(`
148
+ const W = ${JSON.stringify(W)}; fs.mkdirSync(W);
149
+ ${SAME}
150
+ ${call};
151
+ let name; const t0 = Date.now();
152
+ while (!(name = fs.readdirSync(W).find(n => n.startsWith('leerness-async-')))) if (Date.now() - t0 > 20000) throw new Error('mkdtemp did not finish');
153
+ const d = path.join(W, name), other = path.join(W, 'unrelated');
154
+ fs.mkdirSync(other); fs.writeFileSync(path.join(other, 'u.txt'), 'u');
155
+ fs.renameSync(d, d + '-aside'); fs.renameSync(other, d);
156
+ require('util').promisify(fs.mkdtemp)(path.join(W, 'leerness-promisify-'));
157
+ require('fs/promises').mkdtemp(path.join(W, 'leerness-fsp-'));
158
+ mk(os.tmpdir());
159
+ console.log('SWAP ' + JSON.stringify(d));`, { before: SAVE });
160
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
161
+ assert.deepStrictEqual(out(r, 'SAME'), [false, true, true, true], form);
162
+ assert.deepStrictEqual(records.map(x => x.path), made, `${form}: recorded an asynchronous folder, or not the control`);
163
+ T.removeRecorded(dir, { tempRoot: childTemp });
164
+ assert.strictEqual(fs.readFileSync(path.join(out(r, 'SWAP'), 'u.txt'), 'utf8'), 'u', `${form}: the folder swapped in was removed`);
165
+ } finally { cleanup([W]); }
166
+ }
167
+ });
168
+ // 1.36.304 review round 2: a preload that wrapped fs.mkdtempSync before the tracker started called the original, put a
169
+ // folder that already existed in place of the new one, and returned the path; that folder was recorded. A folder
170
+ // mkdtempSync just made is empty, so only an empty folder is recorded: a folder with something in it is left. An
171
+ // empty folder swapped in is recorded and goes with the run (named in the stop rule; it holds nothing to lose).
172
+ test('a folder that is not empty when fs.mkdtempSync returns is not recorded (a wrapper installed before the tracker swapped it in)', () => {
173
+ const pre = path.join(arena, 'preload-swap.js');
174
+ fs.writeFileSync(pre, ["const fs = require('fs');", 'const orig = fs.mkdtempSync;',
175
+ 'fs.mkdtempSync = function (...a) { const d = orig.apply(this, a); const from = process.env.SWAP_FROM; if (from) { fs.renameSync(d, d + "-aside"); fs.renameSync(from, d); } return d; };', ''].join('\n'));
176
+ const { r, made, dir, records } = tracked(`
177
+ const swap = (content) => { const u = path.join(CHILD_TEMP, 'unrelated-' + content); fs.mkdirSync(u); if (content === 'full') fs.writeFileSync(path.join(u, 'u.txt'), 'u');
178
+ process.env.SWAP_FROM = u; const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-swap-')); delete process.env.SWAP_FROM; return d; };
179
+ const full = swap('full'), empty = swap('empty');
180
+ mk(os.tmpdir());
181
+ console.log('SWAP ' + JSON.stringify({ full, empty }));`, { nodeArgs: ['--require', pre] });
182
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
183
+ const swapped = out(r, 'SWAP');
184
+ assert.deepStrictEqual(records.map(x => x.path), [swapped.empty, ...made], 'recorded the folder with something in it, or not the empty one and the control');
185
+ T.removeRecorded(dir, { tempRoot: childTemp });
186
+ assert.strictEqual(fs.readFileSync(path.join(swapped.full, 'u.txt'), 'utf8'), 'u', 'the folder swapped in with a file was removed');
187
+ cleanup([swapped.full, swapped.full + '-aside', swapped.empty + '-aside']);
188
+ });
189
+ // 1.36.304 review round 3: with byte FF in a Buffer prefix, POSIX makes the folder with that byte, and its lossy
190
+ // decoding names a sibling with U+FFFD. A wrapper preloaded before the tracker plants that sibling from the
191
+ // returned Buffer; without the round-trip check the sibling was recorded. Windows refuses such a prefix (ENOENT),
192
+ // so there this case is a control.
193
+ test('a Buffer prefix that is not UTF-8 records nothing, also when a wrapper planted the sibling its lossy decoding names', () => {
194
+ const pre = path.join(arena, 'preload-ff.js');
195
+ fs.writeFileSync(pre, ["const fs = require('fs');", 'const orig = fs.mkdtempSync;',
196
+ 'fs.mkdtempSync = function (prefix, ...rest) { const r = orig.call(fs, prefix, ...rest); if (Buffer.isBuffer(prefix) && prefix.includes(0xff) && Buffer.isBuffer(r)) fs.mkdirSync(r.toString("utf8")); return r; };', ''].join('\n'));
197
+ const { r, made, records } = tracked(`
198
+ let ff = 'made';
199
+ try { fs.mkdtempSync(Buffer.concat([Buffer.from(path.join(os.tmpdir(), 'leerness-ffa-')), Buffer.from([0xff])]), { encoding: 'buffer' }); } catch (e) { ff = e.code; }
200
+ mk(os.tmpdir());
201
+ console.log('FF ' + JSON.stringify(ff));`, { nodeArgs: ['--require', pre] });
202
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
203
+ assert.strictEqual(out(r, 'FF'), process.platform === 'win32' ? 'ENOENT' : 'made');
204
+ assert.deepStrictEqual(records.map(x => x.path), made, 'recorded the sibling a lossy decoding names, or not the control');
205
+ cleanup(made);
206
+ });
207
+ // 1.36.304 review round 4: the hex case did not distinguish the "prefix plus six characters" check, because nothing
208
+ // existed at the path a hex result names. With encoding 'latin1' a prefix holding a non-ASCII letter comes back
209
+ // mangled; a wrapper preloaded before the tracker knows the six characters by then and makes the mangled path, empty,
210
+ // before returning. Without the check that folder was recorded, and with it the file the test then puts there.
211
+ test('a result that is not the prefix plus six characters is not recorded, also when a wrapper made the folder it names', () => {
212
+ // latin1 mangles every non-ASCII letter of the returned path, the temp directory's part included. If the child's
213
+ // temp path (the string its os.tmpdir() returns, which the prefix starts with) holds one, the mangled result names
214
+ // another parent: normally one that does not exist, so the wrapper cannot make the folder, and a folder made
215
+ // elsewhere would lie outside the temp path, where the temp check alone rejects it. The case is skipped there; this
216
+ // gives up the setup where a link of the mangled name leads back into the temp path (1.36.304 review round 6).
217
+ if ([...childTemp].some(ch => ch.charCodeAt(0) > 127)) { console.log(' skipped: the temp path holds non-ASCII letters'); return; }
218
+ const pre = path.join(arena, 'preload-latin1.js');
219
+ fs.writeFileSync(pre, ["const fs = require('fs');", 'const orig = fs.mkdtempSync;',
220
+ 'fs.mkdtempSync = function (prefix, options) { const r = orig.call(fs, prefix, options); if (options && options.encoding === "latin1") fs.mkdirSync(r); return r; };', ''].join('\n'));
221
+ const { r, made, dir, records } = tracked(`
222
+ const alias = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-' + String.fromCharCode(0xe9) + '-'), { encoding: 'latin1' });
223
+ fs.writeFileSync(path.join(alias, 'sentinel.txt'), 's');
224
+ mk(os.tmpdir());
225
+ console.log('ALIAS ' + JSON.stringify(alias));`, { nodeArgs: ['--require', pre] });
226
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
227
+ const alias = out(r, 'ALIAS');
228
+ assert.ok(alias && fs.existsSync(path.join(alias, 'sentinel.txt')), r.stdout);
229
+ assert.deepStrictEqual(records.map(x => x.path), made, 'recorded the folder a mangled result names, or not the control');
230
+ T.removeRecorded(dir, { tempRoot: childTemp });
231
+ assert.strictEqual(fs.readFileSync(path.join(alias, 'sentinel.txt'), 'utf8'), 's', 'the folder a mangled result names was removed');
232
+ cleanup([alias, ...fs.readdirSync(childTemp).filter(n => n.startsWith('leerness-' + String.fromCharCode(0xe9) + '-')).map(n => path.join(childTemp, n))]);
233
+ });
234
+ test('a record is written when the call returns, so a run that then fails or throws keeps an accurate record', () => {
235
+ for (const end of ['process.exitCode = 1;', 'throw new Error("planned");', 'process.exit(3);']) {
236
+ const { r, made, records } = tracked(`mk(os.tmpdir()); ${end}`);
237
+ assert.notStrictEqual(r.status, 0, r.stdout + r.stderr);
238
+ assert.deepStrictEqual(records.map(x => x.path), made, end);
239
+ assert.strictEqual(fs.existsSync(made[0]), true);
240
+ cleanup(made);
241
+ }
242
+ });
243
+ // From the 1.36.303 rounds: a prefix that names a folder only through the working directory or links, a result in
244
+ // another encoding, a Buffer that does not survive decoding, and a folder outside the temp directory of the call
245
+ // are not recorded; their folders are left. The controls are recorded.
246
+ test('not recorded: a relative or dotted prefix, an encoded result, a lossy Buffer prefix, a folder outside the temp directory', () => {
247
+ const { r, records } = tracked(`
248
+ const W = path.join(os.tmpdir(), 'w'); fs.mkdirSync(path.join(W, 'x'), { recursive: true }); process.chdir(W);
249
+ const left = [];
250
+ left.push(path.resolve(fs.mkdtempSync('leerness-rel-')));
251
+ left.push(fs.mkdtempSync(W + path.sep + 'x' + path.sep + '..' + path.sep + 'leerness-dot-'));
252
+ const hex = fs.mkdtempSync(path.join(W, 'leerness-hex-'), { encoding: 'hex' }); left.push(Buffer.from(hex, 'hex').toString());
253
+ let ff = 'made';
254
+ try { left.push(fs.mkdtempSync(Buffer.concat([Buffer.from(path.join(W, 'leerness-ff-')), Buffer.from([0xff])]), { encoding: 'buffer' }).toString()); } catch (e) { ff = e.code; }
255
+ left.push(fs.mkdtempSync(path.join(ARENA, 'leerness-outside-')));
256
+ const kept = [mk(os.tmpdir()), fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-buf-'), { encoding: 'buffer' }).toString(),
257
+ fs.mkdtempSync(require('url').pathToFileURL(path.join(os.tmpdir(), 'leerness-url-')))];
258
+ console.log('SETS ' + JSON.stringify({ left, kept, ff }));`);
259
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
260
+ const sets = out(r, 'SETS');
261
+ // POSIX makes a folder from a prefix holding byte FF; Windows refuses it (ENOENT).
262
+ assert.strictEqual(sets.ff, process.platform === 'win32' ? 'ENOENT' : 'made');
263
+ assert.deepStrictEqual(records.map(x => x.path), sets.kept, 'recorded the wrong folders');
264
+ cleanup([path.join(childTemp, 'w'), ...sets.left.filter(p => p.startsWith(arena)), ...sets.kept]);
265
+ });
266
+ // Found by the first full run of this version: lib/runtime-writes.js intercepts fs writes, and the memory recovery
267
+ // bundles it makes while writes are latched were not recorded, because the record went through fs.appendFileSync,
268
+ // which calls the intercepted fs.openSync. Records are written and identities read with functions taken when the
269
+ // tracker starts, so replacing fs functions afterwards does not stop or change a record.
270
+ test('a record is written and read with the fs functions of the start, even after the test replaces them', () => {
271
+ const { r, made, records } = tracked(`
272
+ const saved = {}; const block = () => { throw Object.assign(new Error('blocked'), { code: 'EBLOCKED' }); };
273
+ for (const k of ['openSync', 'writeSync', 'closeSync', 'appendFileSync', 'writeFileSync', 'lstatSync', 'statSync']) { saved[k] = fs[k]; fs[k] = block; }
274
+ saved.native = fs.realpathSync.native; fs.realpathSync.native = block;
275
+ let d;
276
+ try { d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-track-blocked-')); }
277
+ finally { for (const k of Object.keys(saved)) if (k !== 'native') fs[k] = saved[k]; fs.realpathSync.native = saved.native; }
278
+ made.push(d);`);
279
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
280
+ assert.deepStrictEqual(records.map(x => x.path), made, 'a folder made while fs functions were replaced was not recorded');
281
+ const st = fs.lstatSync(made[0], { bigint: true });
282
+ assert.deepStrictEqual([records[0].dev, records[0].ino, records[0].real], [String(st.dev), String(st.ino), fs.realpathSync.native(made[0])]);
283
+ cleanup(made);
284
+ });
285
+ test('with recording on, the exit, emit and exception machinery is untouched', () => {
286
+ const { r } = tracked(`console.log('HOOKS ' + JSON.stringify([process.reallyExit === H.reallyExit, process.exit === H.exit, process.emit === H.emit,
287
+ ...['exit', 'beforeExit', 'uncaughtExceptionMonitor', 'newListener'].map(e => process.listenerCount(e) - (e === 'exit' ? 1 : 0) === H.counts[e]), tracker.active]));`,
288
+ { before: "const H = { reallyExit: process.reallyExit, exit: process.exit, emit: process.emit, counts: Object.fromEntries(['exit', 'beforeExit', 'uncaughtExceptionMonitor', 'newListener'].map(e => [e, process.listenerCount(e)])) };" });
289
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
290
+ // the harness adds one exit listener after the tracker (to print MADE); the counts are taken before the tracker.
291
+ assert.deepStrictEqual(out(r, 'HOOKS'), [true, true, true, true, true, true, true, true]);
292
+ });
293
+ // Where it records: only code the main script loaded, named by its module, in the main thread (1.36.302 rounds 1-3 for
294
+ // the preload rules; 1.36.304 round 1: without a module argument the ancestry check was skipped, so a preload that
295
+ // called trackTemp() recorded).
296
+ test('trackTemp() without a module records nothing: in a main script, in a --require preload, and in node -e', () => {
297
+ const noArg = tracked('mk(os.tmpdir()); console.log("ACTIVE " + JSON.stringify(tracker.active));', { start: 'const tracker = T.trackTemp();' });
298
+ assert.strictEqual(noArg.r.status, 0, noArg.r.stdout + noArg.r.stderr);
299
+ assert.strictEqual(out(noArg.r, 'ACTIVE'), false); assert.deepStrictEqual(noArg.records, []);
300
+ cleanup(noArg.made);
301
+ const pre = path.join(arena, 'preload-noarg.js');
302
+ fs.writeFileSync(pre, `require(${TC}).trackTemp();\nconst fs = require('fs'), os = require('os'), path = require('path');\nconsole.log('MADE ' + fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pre-noarg-')));\n`);
303
+ for (const args of [['--require', pre, '-e', '0'], ['-e', `require(${TC}).trackTemp(); const fs = require('fs'), os = require('os'), path = require('path'); console.log('MADE ' + fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-eval-noarg-')));`]]) {
304
+ const dir = newRecordDir();
305
+ const r = cp.spawnSync(process.execPath, args, { encoding: 'utf8', windowsHide: true, timeout: 60000, env: childEnv(dir) });
306
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
307
+ assert.deepStrictEqual(readRecords(dir), [], `recorded without a module: ${args[0]}`);
308
+ cleanup(r.stdout.split(/\r?\n/).filter(l => l.startsWith('MADE ')).map(l => l.slice(5)));
309
+ }
310
+ });
311
+ test('trackTemp(module) records nothing in a script loaded with --require, a module it loads, or through createRequire()', () => {
312
+ const support = path.join(arena, 'preload-support.js'), script = path.join(arena, 'preloaded.js'), viaCreate = path.join(arena, 'preload-create.js');
313
+ const body = `require(${TC}).trackTemp(module);
314
+ const fs = require('fs'), os = require('os'), path = require('path');
315
+ console.log('MADE ' + fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-track-pre-')));`;
316
+ fs.writeFileSync(support, `'use strict';\n${body}\n`);
317
+ fs.writeFileSync(script, `'use strict';\n${body}\nif (process.env.LOAD_SUPPORT) require(${JSON.stringify(support)});\n`);
318
+ fs.writeFileSync(viaCreate, `require('node:module').createRequire(__filename)(${JSON.stringify(script)});\n`);
319
+ const runIt = (args, extra = {}) => {
320
+ const dir = newRecordDir();
321
+ const r = cp.spawnSync(process.execPath, args, { encoding: 'utf8', windowsHide: true, timeout: 60000, env: { ...childEnv(dir), ...extra } });
322
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
323
+ return { madeHere: r.stdout.split(/\r?\n/).filter(l => l.startsWith('MADE ')).map(l => l.slice(5)), records: readRecords(dir) };
324
+ };
325
+ for (const args of [['--require', script, '-e', '0'], ['--require', viaCreate, '-e', '0']]) {
326
+ const { madeHere, records } = runIt(args, { LOAD_SUPPORT: '1' });
327
+ assert.ok(madeHere.length >= 1, args.join(' '));
328
+ assert.deepStrictEqual(records, [], `a preload recorded: ${args.join(' ')}`);
329
+ cleanup(madeHere);
330
+ }
331
+ const viaOptions = runIt(['-e', '0'], { NODE_OPTIONS: `--require "${script.replace(/\\/g, '/')}"` });
332
+ assert.deepStrictEqual(viaOptions.records, []); cleanup(viaOptions.madeHere);
333
+ const asMain = runIt([script], { LOAD_SUPPORT: '1' });
334
+ assert.strictEqual(asMain.madeHere.length, 2);
335
+ assert.deepStrictEqual(asMain.records.map(x => x.path).sort(), asMain.madeHere.slice().sort(), 'run as the main script, it did not record');
336
+ cleanup(asMain.madeHere);
337
+ const helperPreloaded = runIt(['--require', path.join(__dirname, 'temp-cleanup.js'), script]);
338
+ assert.deepStrictEqual(helperPreloaded.records.map(x => x.path), helperPreloaded.madeHere, 'with temp-cleanup.js preloaded, the main script did not record');
339
+ cleanup(helperPreloaded.madeHere);
340
+ });
341
+ test('trackTemp(module) records nothing 60 requires below a --require preload, and does 60 requires below the main script', () => {
342
+ const chain = path.join(arena, 'chain');
343
+ fs.mkdirSync(chain);
344
+ for (let i = 0; i < 60; i++) {
345
+ fs.writeFileSync(path.join(chain, `m${i}.js`), i < 59 ? `require('./m${i + 1}.js');\n`
346
+ : `require(${TC}).trackTemp(module);\n`
347
+ + `const fs = require('fs'), os = require('os'), path = require('path');\n`
348
+ + `console.log('MADE ' + fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-track-chain-')));\n`);
349
+ }
350
+ for (const [args, want] of [[['--require', path.join(chain, 'm0.js'), '-e', '0'], 0], [[path.join(chain, 'm0.js')], 1]]) {
351
+ const dir = newRecordDir();
352
+ const r = cp.spawnSync(process.execPath, args, { encoding: 'utf8', windowsHide: true, timeout: 60000, env: childEnv(dir) });
353
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
354
+ assert.strictEqual(readRecords(dir).length, want, args.join(' '));
355
+ cleanup(r.stdout.split(/\r?\n/).filter(l => l.startsWith('MADE ')).map(l => l.slice(5)));
356
+ }
357
+ });
358
+ test('trackTemp(module) records nothing in a worker thread started from a script file', () => {
359
+ const worker = path.join(arena, 'worker-script.js');
360
+ fs.writeFileSync(worker, `const T = require(${TC});\nif (T.trackTemp(module).active) throw new Error('active in a worker');\n`
361
+ + "const fs = require('fs'), os = require('os'), path = require('path');\n"
362
+ + "require('worker_threads').parentPort.postMessage(fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-track-worker-')));\n");
363
+ const { r, made, records } = tracked(`
364
+ const w = new (require('worker_threads').Worker)(${JSON.stringify(worker)});
365
+ w.on('message', d => console.log('WORKER ' + JSON.stringify(d)));
366
+ w.on('error', e => { console.log('worker error ' + e.message); process.exitCode = 2; });
367
+ mk(os.tmpdir());`);
368
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
369
+ assert.deepStrictEqual(records.map(x => x.path), made, 'the worker recorded, or the main thread did not');
370
+ cleanup([...made, out(r, 'WORKER')]);
371
+ });
372
+ test('one tracker per process; pending() counts what it recorded', () => {
373
+ const { r, made, records } = tracked(`
374
+ if (T.trackTemp(module) !== tracker) throw new Error('a second tracker');
375
+ mk(os.tmpdir()); mk(os.tmpdir());
376
+ console.log('PENDING ' + tracker.pending());`);
377
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
378
+ assert.strictEqual(out(r, 'PENDING'), 2);
379
+ assert.strictEqual(records.length, 2);
380
+ cleanup(made);
381
+ });
382
+
383
+ // --- what the runner does with the records (removeRecorded) ---
384
+ test('removeRecorded removes the recorded folders, nested ones too, and a link out leaves its target alone', () => {
385
+ const { r, made, dir } = tracked(`
386
+ const a = mk(os.tmpdir()), b = mk(a);
387
+ fs.writeFileSync(path.join(b, 'file.txt'), 'x');
388
+ fs.symlinkSync(OUTSIDE, path.join(a, 'link-out'), TYPE);`);
389
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
390
+ const result = T.removeRecorded(dir, { tempRoot: childTemp });
391
+ for (const d of made) assert.strictEqual(fs.existsSync(d), false, `${d} was kept`);
392
+ assert.deepStrictEqual(result, { removed: 2, gone: 0, left: 0, skipped: 0 }, 'newest first: the nested folder, then its parent');
393
+ sentinelKept();
394
+ });
395
+ test('removeRecorded leaves a folder replaced by a link (and the link) or by another folder, and skips one already gone', () => {
396
+ const { r, made, dir } = tracked(`
397
+ const a = mk(os.tmpdir()), b = mk(os.tmpdir()), c = mk(os.tmpdir()), d = mk(os.tmpdir());
398
+ fs.rmdirSync(b);
399
+ fs.renameSync(c, c + '-aside'); fs.symlinkSync(OUTSIDE, c, TYPE);
400
+ fs.renameSync(d, d + '-aside'); fs.mkdirSync(d); fs.writeFileSync(path.join(d, 'other.txt'), 'other');`);
401
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
402
+ const result = T.removeRecorded(dir, { tempRoot: childTemp });
403
+ assert.deepStrictEqual(result, { removed: 1, gone: 1, left: 2, skipped: 0 });
404
+ assert.strictEqual(fs.existsSync(made[0]), false);
405
+ assert.strictEqual(fs.lstatSync(made[2]).isSymbolicLink(), true, 'the link in place of a folder was removed');
406
+ assert.strictEqual(fs.readFileSync(path.join(made[3], 'other.txt'), 'utf8'), 'other', 'the folder now at that path was removed');
407
+ sentinelKept();
408
+ try { fs.unlinkSync(made[2]); } catch { fs.rmdirSync(made[2]); }
409
+ cleanup([made[2] + '-aside', made[3], made[3] + '-aside']);
410
+ });
411
+ test('removeRecorded leaves a folder outside the runner\'s temp directory and skips lines it cannot use', () => {
412
+ const { r, made, dir } = tracked('mk(os.tmpdir());');
413
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
414
+ fs.writeFileSync(path.join(dir, 'zz-bad.jsonl'), 'not json\n{"path":"x"}\n' + JSON.stringify({ path: outside, dev: '1', ino: '1', real: outside, temp: arena }) + '\n');
415
+ const elsewhere = T.removeRecorded(dir, { tempRoot: outside });
416
+ assert.deepStrictEqual(elsewhere, { removed: 0, gone: 0, left: 2, skipped: 2 });
417
+ assert.strictEqual(fs.existsSync(made[0]), true, 'removed outside the runner\'s temp directory');
418
+ sentinelKept();
419
+ const here = T.removeRecorded(dir, { tempRoot: childTemp });
420
+ assert.strictEqual(here.removed, 1); assert.strictEqual(fs.existsSync(made[0]), false);
421
+ sentinelKept();
422
+ });
423
+ // Ablation of the first version: with the check against the temp directory recorded with a folder removed, no test
424
+ // failed. A record whose real path is not below its own temp directory is not one the tracker writes; it is left.
425
+ test('removeRecorded leaves a folder whose record names a temp directory it is not below', () => {
426
+ const { r, made, dir } = tracked('mk(os.tmpdir());');
427
+ assert.strictEqual(r.status, 0, r.stdout + r.stderr);
428
+ const file = path.join(dir, fs.readdirSync(dir)[0]);
429
+ const x = JSON.parse(fs.readFileSync(file, 'utf8').trim());
430
+ fs.writeFileSync(file, JSON.stringify({ ...x, temp: outside }) + '\n');
431
+ assert.deepStrictEqual(T.removeRecorded(dir, { tempRoot: childTemp }), { removed: 0, gone: 0, left: 1, skipped: 0 });
432
+ assert.strictEqual(fs.existsSync(made[0]), true, 'removed a folder outside the temp directory of its record');
433
+ cleanup(made);
434
+ });
435
+ test('removeRecorded of a directory that does not exist or holds no records removes nothing', () => {
436
+ assert.deepStrictEqual(T.removeRecorded(path.join(arena, 'no-such-record-dir'), { tempRoot: childTemp }), { removed: 0, gone: 0, left: 0, skipped: 0 });
437
+ assert.deepStrictEqual(T.removeRecorded(newRecordDir(), { tempRoot: childTemp }), { removed: 0, gone: 0, left: 0, skipped: 0 });
438
+ });
439
+ // The tracker statement must be the script's first statement: before it only a shebang, blank lines, comments and
440
+ // the 'use strict' directive. So it is not inside a block comment or a string, and it runs before anything from
441
+ // lib/ or bin/ is loaded. (Review round 1: a column-0 match inside a block comment passed the first version.)
442
+ const TRACKER = /^(?:const [\w$]+ = )?require\('\.\/temp-cleanup'\)\.trackTemp\(module\);/;
443
+ function installedFirst(text) {
444
+ // Every JavaScript line terminator ends a line comment: CRLF, CR, LF, U+2028 and U+2029 (review round 2: a lone CR
445
+ // ended the comment for Node but not for this check). The characters are named by number, not written here.
446
+ const raw = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
447
+ const s = [String.fromCharCode(0x2028), String.fromCharCode(0x2029), '\r'].reduce((t, end) => t.split(end).join('\n'), raw.split('\r\n').join('\n'));
448
+ let i = s.startsWith('#!') ? s.indexOf('\n') + 1 : 0;
449
+ if (i === 0 && s.startsWith('#!')) return false;
450
+ for (;;) {
451
+ const rest = s.slice(i);
452
+ let m;
453
+ if ((m = /^[ \t\n]+/.exec(rest))) { i += m[0].length; continue; }
454
+ if (rest.startsWith('//')) { const nl = rest.indexOf('\n'); i += nl < 0 ? rest.length : nl + 1; continue; }
455
+ if (rest.startsWith('/*')) { const end = rest.indexOf('*/', 2); if (end < 0) return false; i += end + 2; continue; }
456
+ if ((m = /^(['"])use strict\1;?/.exec(rest))) { i += m[0].length; continue; }
457
+ break;
458
+ }
459
+ return (i === 0 || s[i - 1] === '\n') && TRACKER.test(s.slice(i));
460
+ }
461
+ test('the ratchet accepts the tracker only as the first statement, not in a comment, a string or after code', () => {
462
+ const T = "require('./temp-cleanup').trackTemp(module); // T-0357";
463
+ const cases = [
464
+ [`'use strict';\n${T}\nconst fs = require('fs');\n`, true],
465
+ [`#!/usr/bin/env node\r\n// header\r\n/* block\r\n comment */\r\n'use strict';\r\n\r\n${T}\r\n`, true],
466
+ [`// no directive\n${T}\n`, true],
467
+ [`'use strict';\n/*\n${T}\n*/\nconst fs = require('fs');\n`, false],
468
+ [`'use strict';\n// ${T}\n`, false],
469
+ [`'use strict';\nconst s = \`\n${T}\n\`;\n`, false],
470
+ [`'use strict';\nconst io = require('../lib/io');\n${T}\n`, false],
471
+ [`'use strict';\nconst fs = require('fs');\n`, false],
472
+ [`'use strict';\n/* never closed\n${T}\n`, false],
473
+ // Review round 2: a lone CR ends a line comment in JavaScript, and so do U+2028 and U+2029.
474
+ [`// header\r/*\n${T}\n*/\nconst fs = require('fs');\n`, false],
475
+ [`// header${String.fromCharCode(0x2028)}/*\n${T}\n*/\n`, false],
476
+ [`// header${String.fromCharCode(0x2029)}/*\n${T}\n*/\n`, false],
477
+ [`'use strict';\r${T}\r`, true],
478
+ ];
479
+ for (const [text, want] of cases) assert.strictEqual(installedFirst(text), want, JSON.stringify(text));
480
+ });
481
+ // Which scripts must have the tracker: every one that names mkdtemp at all, in any of its forms (1.36.302 review round
482
+ // 3: `fs.mkdtempSync (p)`, with a space, escaped a `mkdtempSync(` selection). All forms are tracked at run time.
483
+ const namesMkdtemp = text => /\bmkdtemp/.test(text);
484
+ test('the ratchet selects every script that names mkdtemp, in any form', () => {
485
+ for (const text of ['fs.mkdtempSync(p)', 'fs.mkdtempSync (p)', 'fs\n .mkdtempSync(p)', 'const { mkdtempSync } = fs;', "fs['mkdtempSync'](p)",
486
+ 'fs.mkdtemp(p, cb)', 'await fs.promises.mkdtemp(p)', "require('fs/promises').mkdtemp(p)", 'const { mkdtemp: mk } = fs;']) {
487
+ assert.strictEqual(namesMkdtemp(text), true, JSON.stringify(text));
488
+ }
489
+ assert.strictEqual(namesMkdtemp('fs.mkdirSync(p)'), false);
490
+ });
491
+ test('every script that names mkdtemp has the tracker as its first statement', () => {
492
+ const notFirst = [];
493
+ let checked = 0;
494
+ for (const name of fs.readdirSync(__dirname).filter(f => f.endsWith('.js')).sort()) {
495
+ if (name === 'temp-cleanup.js') continue;
496
+ const text = fs.readFileSync(path.join(__dirname, name), 'utf8');
497
+ if (!namesMkdtemp(text)) continue;
498
+ checked++;
499
+ if (!installedFirst(text)) notFirst.push(name);
500
+ }
501
+ assert.ok(checked >= 150, `only ${checked} scripts make temp folders`);
502
+ assert.deepStrictEqual(notFirst, []);
503
+ });
71
504
  } finally {
72
505
  sleep(300);
73
506
  removeTree(arena, { recursive: true, force: true });