@dzhechkov/p-replicator 1.13.3 → 1.13.4

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.
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The case table IS the contract — feature `repo-sweep-honesty`, T4 (FR-3, FR-4, ADR-001).
5
+ *
6
+ * This is the CommonJS half of a twin pair. It drives `tests/npm-resolver-cases.json`, which is a
7
+ * byte-identical copy of the table that harness-cli's TypeScript twin drives; the two copies are
8
+ * held in step by a guard in harness-core. Neither implementation may grow a behaviour the other
9
+ * lacks, because neither test decides what to check — the table does.
10
+ *
11
+ * The placeholders are resolved WITHOUT the resolver under test (npm through `npm root -g`, pnpm
12
+ * through `which`), so a bug in the resolver cannot also supply its own fixture.
13
+ */
14
+
15
+ const { test } = require('node:test');
16
+ const assert = require('node:assert/strict');
17
+ const { execFileSync } = require('node:child_process');
18
+ const NodeModule = require('node:module');
19
+ const fs = require('node:fs');
20
+ const os = require('node:os');
21
+ const path = require('node:path');
22
+
23
+ const {
24
+ NPM_CLI_RELATIVE_PATH,
25
+ NPM_IDENTITY_MODULE,
26
+ ambientGlobalRoots,
27
+ announceNpmSkip,
28
+ defaultGlobalRoots,
29
+ isNpmCli,
30
+ npmConfigPrefix,
31
+ npmPackageRoot,
32
+ resolveNpmCli,
33
+ resolveNpmCliPath,
34
+ } = require('../npm-cli-resolver.js');
35
+
36
+ const TABLE = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'npm-resolver-cases.json'), 'utf8'));
37
+
38
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-npm-resolver-'));
39
+ const emptyRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-npm-empty-root-'));
40
+ const decoyCli = path.join(scratch, 'decoy-cli.js');
41
+ fs.writeFileSync(decoyCli,
42
+ '// An existing file that is not npm. An existsSync check accepts it; identity does not.\nmodule.exports = {};\n');
43
+ const missingPath = path.join(scratch, 'definitely-absent', NPM_CLI_RELATIVE_PATH);
44
+
45
+ process.on('exit', () => {
46
+ fs.rmSync(scratch, { recursive: true, force: true });
47
+ fs.rmSync(emptyRoot, { recursive: true, force: true });
48
+ });
49
+
50
+ /** npm, located independently of the code under test. */
51
+ function locateRealNpmCli() {
52
+ try {
53
+ const globalRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
54
+ const candidate = path.join(globalRoot, NPM_CLI_RELATIVE_PATH);
55
+ return fs.existsSync(candidate) ? candidate : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** pnpm, located independently of the code under test — an existing file that is NOT npm. */
62
+ function locateRealPnpmCli() {
63
+ try {
64
+ const which = process.platform === 'win32' ? 'where' : 'which';
65
+ const found = execFileSync(which, ['pnpm'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
66
+ .split(/\r?\n/)[0];
67
+ if (!found || found.trim() === '') return null;
68
+ const real = fs.realpathSync(found.trim());
69
+ return fs.existsSync(real) ? real : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * A file that is NOT npm, sitting in a tree where `libnpmpack` DOES resolve — the fixture a
77
+ * resolution-only identity check accepts (review r1, HIGH-5), built without touching the real npm.
78
+ */
79
+ function buildIntruder() {
80
+ const tree = path.join(scratch, 'intruder-tree');
81
+ const fake = path.join(tree, 'node_modules', NPM_IDENTITY_MODULE);
82
+ fs.mkdirSync(fake, { recursive: true });
83
+ fs.writeFileSync(path.join(fake, 'package.json'), '{"name":"libnpmpack","version":"0.0.0","main":"index.js"}\n');
84
+ fs.writeFileSync(path.join(fake, 'index.js'), 'module.exports = {};\n');
85
+ const intruder = path.join(tree, 'intruder-cli.js');
86
+ fs.writeFileSync(intruder, '// Not npm. Only its NEIGHBOURHOOD looks like npm.\nmodule.exports = {};\n');
87
+ return intruder;
88
+ }
89
+
90
+ /** A symlink to the real npm-cli.js — how a genuine npm is normally reached. */
91
+ function buildNpmSymlink(realNpm) {
92
+ if (realNpm === null) return null;
93
+ const link = path.join(scratch, 'npm-link.js');
94
+ try {
95
+ fs.symlinkSync(realNpm, link);
96
+ } catch {
97
+ return null;
98
+ }
99
+ return link;
100
+ }
101
+
102
+ /** A PATH-style bin directory whose `npm` entry is a symlink to the real CLI. */
103
+ function buildNpmBinDir(realNpm) {
104
+ if (realNpm === null) return null;
105
+ const dir = path.join(scratch, 'fake-bin');
106
+ try {
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ fs.symlinkSync(realNpm, path.join(dir, 'npm'));
109
+ } catch {
110
+ return null;
111
+ }
112
+ return dir;
113
+ }
114
+
115
+ /** npm's install prefix, derived from `npm root -g` — again, without the resolver under test. */
116
+ function locateNpmPrefix() {
117
+ try {
118
+ const globalRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
119
+ if (globalRoot === '') return null;
120
+ const parent = path.dirname(globalRoot);
121
+ return path.basename(globalRoot) === 'node_modules' && path.basename(parent) === 'lib'
122
+ ? path.dirname(parent)
123
+ : parent;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /** A HOME directory whose `.npmrc` carries `prefix=<npmPrefix>` — npm's own user config. */
130
+ function buildNpmrcHome(prefix) {
131
+ if (prefix === null) return null;
132
+ const home = path.join(scratch, 'npmrc-home');
133
+ fs.mkdirSync(home, { recursive: true });
134
+ fs.writeFileSync(path.join(home, '.npmrc'), `; a user config, exactly as npm writes one\nprefix=${prefix}\n`);
135
+ return home;
136
+ }
137
+
138
+ const realNpmCli = locateRealNpmCli();
139
+ const npmPrefix = locateNpmPrefix();
140
+
141
+ const PLACEHOLDERS = {
142
+ npmCli: realNpmCli,
143
+ pnpmCli: locateRealPnpmCli(),
144
+ decoyCli,
145
+ intruderCli: buildIntruder(),
146
+ npmCliSymlink: buildNpmSymlink(realNpmCli),
147
+ npmPrefix,
148
+ npmrcHome: buildNpmrcHome(npmPrefix),
149
+ npmBinDir: buildNpmBinDir(realNpmCli),
150
+ missingPath,
151
+ emptyRoot,
152
+ };
153
+
154
+ function expand(name) {
155
+ if (!(name in PLACEHOLDERS)) throw new Error(`case table uses an unknown placeholder '${name}'`);
156
+ if (PLACEHOLDERS[name] === null) throw new Error(`placeholder '${name}' is not available on this machine`);
157
+ return PLACEHOLDERS[name];
158
+ }
159
+
160
+ function missingRequirements(entry) {
161
+ return entry.requires.filter((name) => !(name in PLACEHOLDERS) || PLACEHOLDERS[name] === null);
162
+ }
163
+
164
+ test('the table is a real table: it has cases, and every placeholder it names is documented', () => {
165
+ assert.ok(TABLE.cases.length > 0, 'the shared case table is empty');
166
+ for (const entry of TABLE.cases) {
167
+ assert.ok(entry.why, `case ${entry.id} must say why it exists`);
168
+ for (const name of entry.requires) {
169
+ assert.ok(name in TABLE.placeholders, `case ${entry.id} requires undocumented placeholder ${name}`);
170
+ }
171
+ if (entry.given.npmExecPath !== null) {
172
+ assert.ok(entry.given.npmExecPath in PLACEHOLDERS,
173
+ `case ${entry.id} names a placeholder this twin cannot build: ${entry.given.npmExecPath}`);
174
+ }
175
+ }
176
+ });
177
+
178
+ for (const entry of TABLE.cases) {
179
+ test(`${entry.id} — ${entry.why}`, (t) => {
180
+ const missing = missingRequirements(entry);
181
+ if (missing.length > 0) {
182
+ // FR-4: the skip is audible and names the tool that could not be found.
183
+ console.log(`SKIPPED — case '${entry.id}' needs ${missing.join(', ')}, which this machine does not`
184
+ + ` provide (looked for ${NPM_CLI_RELATIVE_PATH} / pnpm).`);
185
+ t.skip(`missing ${missing.join(', ')}`);
186
+ return;
187
+ }
188
+
189
+ const env = {};
190
+ if (entry.given.npmExecPath !== null) env.npm_execpath = expand(entry.given.npmExecPath);
191
+ for (const [name, placeholder] of Object.entries(entry.given.env || {})) {
192
+ env[name] = expand(placeholder);
193
+ }
194
+ const options = entry.given.globalRoots === 'defaultRoots'
195
+ ? {}
196
+ : { globalRoots: entry.given.globalRoots.map(expand) };
197
+
198
+ // LOW-7: the null-argument case exercises the SIGNATURE, so it must not be handed an object.
199
+ const result = entry.given.nullArguments === true ? resolveNpmCli(null, null) : resolveNpmCli(env, options);
200
+
201
+ assert.equal(result.kind, entry.expect.kind, `case ${entry.id}: ${JSON.stringify(result)}`);
202
+ if (result.kind === 'npm-cli') {
203
+ if (entry.expect.source !== undefined) {
204
+ assert.equal(result.source, entry.expect.source, `case ${entry.id} source`);
205
+ }
206
+ if (entry.expect.path !== undefined) assert.equal(result.path, expand(entry.expect.path));
207
+ // Whatever was chosen must actually BE npm — the property, re-checked at the outcome.
208
+ assert.equal(isNpmCli(result.path), true, `case ${entry.id}: chosen path is not npm`);
209
+ } else {
210
+ for (const fragment of entry.expect.reasonIncludes || []) {
211
+ assert.ok(result.reason.includes(fragment),
212
+ `case ${entry.id}: reason does not name '${fragment}': ${result.reason}`);
213
+ }
214
+ }
215
+ });
216
+ }
217
+
218
+ test('the identity check rejects an existing non-npm file that an existence check would accept', () => {
219
+ assert.equal(fs.existsSync(decoyCli), true);
220
+ assert.equal(isNpmCli(decoyCli), false);
221
+ });
222
+
223
+ test('global roots are derived from process.execPath, not only Module.globalPaths', () => {
224
+ // MEASURED 2026-09-18: Module.globalPaths here omits /usr/lib/node_modules, where npm lives.
225
+ assert.equal(ambientGlobalRoots('/usr/bin/node', {})[0], '/usr/lib/node_modules');
226
+ // The composed list keeps the ambient roots first and adds the env-derived ones after.
227
+ assert.equal(defaultGlobalRoots('/usr/bin/node', {})[0], '/usr/lib/node_modules');
228
+ });
229
+
230
+ test('the identity check rejects an intruder whose NEIGHBOURHOOD resolves libnpmpack', () => {
231
+ // review r1, HIGH-5. The fixture genuinely passed the old check: libnpmpack really resolves from
232
+ // here. What it is not is npm.
233
+ const intruder = PLACEHOLDERS.intruderCli;
234
+ assert.ok(intruder, 'the intruder fixture was not built');
235
+ assert.doesNotThrow(() => NodeModule.createRequire(intruder).resolve(NPM_IDENTITY_MODULE));
236
+ assert.equal(isNpmCli(intruder), false);
237
+ });
238
+
239
+ test('a symlink to npm is dereferenced before it is judged, and the REAL path is returned', (t) => {
240
+ const link = PLACEHOLDERS.npmCliSymlink;
241
+ const real = PLACEHOLDERS.npmCli;
242
+ if (link === null || real === null) {
243
+ console.log('SKIPPED — no installed npm to symlink on this machine; nothing to dereference.');
244
+ t.skip('no npm to symlink');
245
+ return;
246
+ }
247
+ assert.throws(() => NodeModule.createRequire(link).resolve(NPM_IDENTITY_MODULE));
248
+ assert.equal(isNpmCli(link), true);
249
+ assert.equal(resolveNpmCliPath(link), real);
250
+ });
251
+
252
+ test('npmPackageRoot names npm own package directory, and refuses anything else', (t) => {
253
+ const real = PLACEHOLDERS.npmCli;
254
+ if (real === null) {
255
+ console.log('SKIPPED — no installed npm on this machine to take a package root from.');
256
+ t.skip('no npm installed');
257
+ return;
258
+ }
259
+ assert.equal(npmPackageRoot(real), path.join(path.dirname(real), '..'));
260
+ assert.equal(npmPackageRoot(decoyCli), null);
261
+ });
262
+
263
+ test('npm configured prefix is read from the environment, never from the ambient home directory', () => {
264
+ assert.equal(npmConfigPrefix({}), undefined);
265
+ assert.equal(npmConfigPrefix({ npm_config_prefix: '/opt/somewhere' }), '/opt/somewhere');
266
+ if (PLACEHOLDERS.npmrcHome !== null) {
267
+ assert.equal(npmConfigPrefix({ HOME: PLACEHOLDERS.npmrcHome }), PLACEHOLDERS.npmPrefix);
268
+ }
269
+ });
270
+
271
+ test('explicit null arguments behave like omitted ones (twin parity, LOW-7)', () => {
272
+ assert.doesNotThrow(() => resolveNpmCli(null, null));
273
+ assert.doesNotThrow(() => resolveNpmCli(undefined, undefined));
274
+ });
275
+
276
+ test('FR-4 — an unavailable npm is announced with a line naming the missing tool', () => {
277
+ const unavailable = resolveNpmCli({ npm_execpath: decoyCli }, { globalRoots: [emptyRoot] });
278
+ assert.equal(unavailable.kind, 'unavailable');
279
+ const lines = [];
280
+ const line = announceNpmSkip(unavailable, (value) => lines.push(value));
281
+ assert.deepEqual(lines, [line]);
282
+ assert.ok(line.includes('SKIPPED'), line);
283
+ assert.ok(line.includes('npm'), line);
284
+ assert.ok(line.includes(NPM_IDENTITY_MODULE), line);
285
+ assert.ok(line.includes(NPM_CLI_RELATIVE_PATH), line);
286
+ });
287
+
288
+ test('FR-4 — an explicit null writer behaves exactly like an omitted one (LOW-7)', () => {
289
+ const unavailable = resolveNpmCli({ npm_execpath: decoyCli }, { globalRoots: [emptyRoot] });
290
+ const seen = [];
291
+ const original = console.log;
292
+ console.log = (...args) => { seen.push(args.map(String).join(' ')); };
293
+ try {
294
+ announceNpmSkip(unavailable, null);
295
+ } finally {
296
+ console.log = original;
297
+ }
298
+ assert.equal(seen.length, 1);
299
+ assert.ok(seen[0].includes('SKIPPED'), seen[0]);
300
+ });
301
+
302
+ test('FR-4 — the announcement writes somewhere by default, so omission cannot silence it', () => {
303
+ const unavailable = resolveNpmCli({}, { globalRoots: [emptyRoot] });
304
+ const seen = [];
305
+ const original = console.log;
306
+ console.log = (...args) => { seen.push(args.map(String).join(' ')); };
307
+ try {
308
+ announceNpmSkip(unavailable);
309
+ } finally {
310
+ console.log = original;
311
+ }
312
+ assert.equal(seen.length, 1);
313
+ assert.ok(seen[0].includes('npm not found'), seen[0]);
314
+ });
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dzhechko
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.