@superblocksteam/sdk 2.0.123 → 2.0.124-next.1
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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts +37 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +162 -10
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +377 -8
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts +21 -0
- package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs +83 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts +2 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs +51 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs +403 -14
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +33 -2
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-token-priming.test.d.mts +31 -0
- package/dist/cli-replacement/dev-token-priming.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs +87 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts +47 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.d.mts +2 -0
- package/dist/cli-replacement/dev.interception.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev.interception.test.mjs +68 -0
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.mjs +486 -65
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts +180 -0
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.mjs +283 -0
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts +10 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.mjs +582 -0
- package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs +125 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs +260 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts +58 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs +224 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts +11 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs +317 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts +26 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs +148 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +84 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs +26 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +23 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +21 -9
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.d.mts +2 -0
- package/dist/dev-utils/dev-server.status.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.status.test.mjs +41 -0
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -0
- package/dist/dev-utils/token-manager.d.ts +31 -0
- package/dist/dev-utils/token-manager.d.ts.map +1 -1
- package/dist/dev-utils/token-manager.js +34 -0
- package/dist/dev-utils/token-manager.js.map +1 -1
- package/dist/telemetry/local-obs.js +1 -1
- package/dist/telemetry/local-obs.js.map +1 -1
- package/dist/telemetry/util.js +1 -1
- package/dist/types/scoped-jwt-token-payload.d.ts +1 -0
- package/dist/types/scoped-jwt-token-payload.d.ts.map +1 -1
- package/dist/version-control.d.mts.map +1 -1
- package/dist/version-control.mjs +6 -7
- package/dist/version-control.mjs.map +1 -1
- package/package.json +12 -12
- package/src/cli-replacement/automatic-upgrades.test.ts +530 -8
- package/src/cli-replacement/automatic-upgrades.ts +179 -7
- package/src/cli-replacement/dependency-install-classifier.mts +118 -0
- package/src/cli-replacement/dependency-install-classifier.test.mts +72 -0
- package/src/cli-replacement/dev-s3-restore.test.mts +554 -14
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +35 -2
- package/src/cli-replacement/dev-token-priming.test.mts +103 -0
- package/src/cli-replacement/dev.interception.test.mts +80 -0
- package/src/cli-replacement/dev.mts +597 -95
- package/src/cli-replacement/home-npmrc.mts +409 -0
- package/src/cli-replacement/home-npmrc.test.mts +757 -0
- package/src/cli-replacement/install-packages.classify.test.mts +168 -0
- package/src/cli-replacement/install-packages.npm-registry.test.mts +345 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.mts +296 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.test.mts +482 -0
- package/src/cli-replacement/userconfig-env.integration.test.mts +189 -0
- package/src/dev-utils/dev-server-metrics.mts +96 -0
- package/src/dev-utils/dev-server-metrics.test.mts +38 -0
- package/src/dev-utils/dev-server.mts +48 -8
- package/src/dev-utils/dev-server.status.test.mts +58 -0
- package/src/dev-utils/token-manager.ts +36 -0
- package/src/telemetry/local-obs.ts +1 -1
- package/src/telemetry/util.ts +1 -1
- package/src/types/scoped-jwt-token-payload.ts +1 -0
- package/src/version-control.mts +8 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-publish-package.log +0 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the CLI-startup `~/.superblocks/npmrc` writer.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: stand up a tmpdir as a fake `$HOME` and a fake
|
|
5
|
+
* `NpmRegistryClient` whose `getConfig()` returns the canned response
|
|
6
|
+
* under test. Asserts cover the four state transitions plus the 0o400
|
|
7
|
+
* mode bit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
mkdir,
|
|
12
|
+
mkdtemp,
|
|
13
|
+
readFile,
|
|
14
|
+
rm,
|
|
15
|
+
stat,
|
|
16
|
+
writeFile,
|
|
17
|
+
} from "node:fs/promises";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
22
|
+
|
|
23
|
+
import * as npmRegistry from "@superblocksteam/vite-plugin-file-sync/npm-registry";
|
|
24
|
+
import type {
|
|
25
|
+
NpmRegistryClient,
|
|
26
|
+
NpmRegistryFetchResult,
|
|
27
|
+
} from "@superblocksteam/vite-plugin-file-sync/npm-registry";
|
|
28
|
+
|
|
29
|
+
// Wrap the real npm-registry module but make `snapshotInitialNpmrc` a spy
|
|
30
|
+
// that delegates to the genuine implementation by default. Only the
|
|
31
|
+
// APPS-4428 failure test overrides it (to simulate a silent EXDEV-class
|
|
32
|
+
// snapshot failure) so the userconfig-preservation guard can be exercised
|
|
33
|
+
// without crossing filesystems; every other test keeps the real
|
|
34
|
+
// hardlink-backed snapshot behaviour.
|
|
35
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof npmRegistry>(
|
|
37
|
+
"@superblocksteam/vite-plugin-file-sync/npm-registry",
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
snapshotInitialNpmrc: vi.fn(actual.snapshotInitialNpmrc),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
import type { Logger } from "../telemetry/logging.js";
|
|
46
|
+
import {
|
|
47
|
+
HOME_NPMRC_MODE,
|
|
48
|
+
superblocksLogsPath,
|
|
49
|
+
superblocksNpmrcBackupPath,
|
|
50
|
+
superblocksNpmrcPath,
|
|
51
|
+
syncHomeNpmrc,
|
|
52
|
+
} from "./home-npmrc.mjs";
|
|
53
|
+
|
|
54
|
+
/** Typed handle to the mocked snapshot fn (delegates to the real impl by
|
|
55
|
+
* default; overridden only in the failure test). */
|
|
56
|
+
const snapshotInitialNpmrcMock = vi.mocked(npmRegistry.snapshotInitialNpmrc);
|
|
57
|
+
|
|
58
|
+
/** Resolves the userconfig path under the test's tmpdir `homeDir`. Keeps
|
|
59
|
+
* the test free of an inline literal so a future relocation only touches
|
|
60
|
+
* `superblocksNpmrcPath`. */
|
|
61
|
+
function npmrcPath(homeDir: string): string {
|
|
62
|
+
return superblocksNpmrcPath(homeDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type LoggerMock = {
|
|
66
|
+
info: ReturnType<typeof vi.fn>;
|
|
67
|
+
warn: ReturnType<typeof vi.fn>;
|
|
68
|
+
error: ReturnType<typeof vi.fn>;
|
|
69
|
+
debug: ReturnType<typeof vi.fn>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function makeLogger(): LoggerMock {
|
|
73
|
+
return {
|
|
74
|
+
info: vi.fn(),
|
|
75
|
+
warn: vi.fn(),
|
|
76
|
+
error: vi.fn(),
|
|
77
|
+
debug: vi.fn(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function asLogger(mock: LoggerMock): Logger {
|
|
82
|
+
return mock as unknown as Logger;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Minimal stub that satisfies the `NpmRegistryClient` shape
|
|
87
|
+
* `syncHomeNpmrc` actually exercises (only `getConfig`). Casting through
|
|
88
|
+
* `as unknown as NpmRegistryClient` keeps the wider class surface from
|
|
89
|
+
* forcing test churn.
|
|
90
|
+
*/
|
|
91
|
+
function makeClient(impl: () => Promise<NpmRegistryFetchResult>): {
|
|
92
|
+
client: NpmRegistryClient;
|
|
93
|
+
getConfig: ReturnType<typeof vi.fn>;
|
|
94
|
+
} {
|
|
95
|
+
const getConfig = vi.fn(impl);
|
|
96
|
+
const client = {
|
|
97
|
+
getConfig,
|
|
98
|
+
} as unknown as NpmRegistryClient;
|
|
99
|
+
return { client, getConfig };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const CONFIGURED: NpmRegistryFetchResult = {
|
|
103
|
+
source: "configured",
|
|
104
|
+
config: {
|
|
105
|
+
configured: true,
|
|
106
|
+
default: {
|
|
107
|
+
url: "https://artifactory.example.com/api/npm/npm/",
|
|
108
|
+
token: "test-token-abc",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const STALE: NpmRegistryFetchResult = {
|
|
114
|
+
source: "stale",
|
|
115
|
+
config: {
|
|
116
|
+
configured: true,
|
|
117
|
+
default: {
|
|
118
|
+
url: "https://artifactory.example.com/api/npm/npm/",
|
|
119
|
+
token: "test-token-abc",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const NOT_CONFIGURED: NpmRegistryFetchResult = {
|
|
125
|
+
source: "not-configured",
|
|
126
|
+
config: { configured: false },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const UNREACHABLE: NpmRegistryFetchResult = {
|
|
130
|
+
source: "unreachable",
|
|
131
|
+
config: { configured: false },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
describe("syncHomeNpmrc", () => {
|
|
135
|
+
let homeDir: string;
|
|
136
|
+
|
|
137
|
+
beforeEach(async () => {
|
|
138
|
+
homeDir = await mkdtemp(path.join(os.tmpdir(), "home-npmrc-test-"));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
143
|
+
// `mockReset` (not `mockClear`) so the `mockImplementationOnce` queue
|
|
144
|
+
// is also drained. With `mockClear` a queued one-time impl from a
|
|
145
|
+
// test that never invoked the spy (e.g. an early throw) would leak
|
|
146
|
+
// into the next test and produce a hard-to-diagnose cascade failure.
|
|
147
|
+
// In vitest 4 `mockReset` restores the implementation passed to
|
|
148
|
+
// `vi.fn(impl)` — the real `snapshotInitialNpmrc` — so subsequent
|
|
149
|
+
// tests keep delegating to the genuine hardlink-backed snapshot.
|
|
150
|
+
snapshotInitialNpmrcMock.mockReset();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("configured → write", () => {
|
|
154
|
+
it("writes ~/.superblocks/npmrc with the registry config at mode 0o400", async () => {
|
|
155
|
+
const { client } = makeClient(async () => CONFIGURED);
|
|
156
|
+
const logger = makeLogger();
|
|
157
|
+
|
|
158
|
+
const result = await syncHomeNpmrc({
|
|
159
|
+
npmRegistryClient: client,
|
|
160
|
+
logger: asLogger(logger),
|
|
161
|
+
homeDir,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.outcome).toBe("written");
|
|
165
|
+
expect(result.source).toBe("configured");
|
|
166
|
+
expect(result.path).toBe(npmrcPath(homeDir));
|
|
167
|
+
|
|
168
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
169
|
+
expect(content).toContain(
|
|
170
|
+
"registry=https://artifactory.example.com/api/npm/npm/",
|
|
171
|
+
);
|
|
172
|
+
expect(content).toContain(
|
|
173
|
+
"//artifactory.example.com/api/npm/npm/:_authToken=test-token-abc",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const stats = await stat(npmrcPath(homeDir));
|
|
177
|
+
// mask off the file-type bits; only mode matters.
|
|
178
|
+
expect(stats.mode & 0o777).toBe(HOME_NPMRC_MODE);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("creates the ~/.superblocks parent dir when absent (local CLI cold start)", async () => {
|
|
182
|
+
// Pods have /home/node/.superblocks created at image build, but a
|
|
183
|
+
// developer running `superblocks dev` against a fresh $HOME may
|
|
184
|
+
// not. `syncHomeNpmrc` must `mkdir -p` before writing or the
|
|
185
|
+
// atomic-rename inside `writeNpmrc` fails ENOENT.
|
|
186
|
+
const { client } = makeClient(async () => CONFIGURED);
|
|
187
|
+
const logger = makeLogger();
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
stat(path.join(homeDir, ".superblocks")),
|
|
191
|
+
).rejects.toMatchObject({ code: "ENOENT" });
|
|
192
|
+
|
|
193
|
+
const result = await syncHomeNpmrc({
|
|
194
|
+
npmRegistryClient: client,
|
|
195
|
+
logger: asLogger(logger),
|
|
196
|
+
homeDir,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result.outcome).toBe("written");
|
|
200
|
+
const dirStats = await stat(path.join(homeDir, ".superblocks"));
|
|
201
|
+
expect(dirStats.isDirectory()).toBe(true);
|
|
202
|
+
const fileStats = await stat(npmrcPath(homeDir));
|
|
203
|
+
expect(fileStats.mode & 0o777).toBe(HOME_NPMRC_MODE);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("treats `stale` (last-known-good) the same as `configured`", async () => {
|
|
207
|
+
const { client } = makeClient(async () => STALE);
|
|
208
|
+
const logger = makeLogger();
|
|
209
|
+
|
|
210
|
+
const result = await syncHomeNpmrc({
|
|
211
|
+
npmRegistryClient: client,
|
|
212
|
+
logger: asLogger(logger),
|
|
213
|
+
homeDir,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.outcome).toBe("written");
|
|
217
|
+
expect(result.source).toBe("stale");
|
|
218
|
+
const stats = await stat(npmrcPath(homeDir));
|
|
219
|
+
expect(stats.mode & 0o777).toBe(HOME_NPMRC_MODE);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("overwrites a 0o400 file from a previous sync", async () => {
|
|
223
|
+
// The whole point of the atomic-rename pattern is that the target
|
|
224
|
+
// file's mode does not block subsequent re-writes. Regression test
|
|
225
|
+
// for that claim — switching away from atomic rename would EACCES.
|
|
226
|
+
const { client } = makeClient(async () => CONFIGURED);
|
|
227
|
+
const logger = makeLogger();
|
|
228
|
+
|
|
229
|
+
await syncHomeNpmrc({
|
|
230
|
+
npmRegistryClient: client,
|
|
231
|
+
logger: asLogger(logger),
|
|
232
|
+
homeDir,
|
|
233
|
+
});
|
|
234
|
+
const second = await syncHomeNpmrc({
|
|
235
|
+
npmRegistryClient: client,
|
|
236
|
+
logger: asLogger(logger),
|
|
237
|
+
homeDir,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(second.outcome).toBe("written");
|
|
241
|
+
const stats = await stat(npmrcPath(homeDir));
|
|
242
|
+
expect(stats.mode & 0o777).toBe(HOME_NPMRC_MODE);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("overwrites an image-baked default with the server config when set, but preserves the @superblocksteam scope and its auth", async () => {
|
|
246
|
+
// EE pods ship with `/home/node/.superblocks/npmrc` pre-populated
|
|
247
|
+
// with a GHPR scope mapping + token. When the runtime org
|
|
248
|
+
// configures its own private registry:
|
|
249
|
+
// - The server-fetched default registry wins over any baked
|
|
250
|
+
// default (none here, but the assertion pins that
|
|
251
|
+
// `registry=…artifactory…` line lands).
|
|
252
|
+
// - The baked `@superblocksteam:registry=…npm.pkg.github.com/`
|
|
253
|
+
// scope mapping must SURVIVE (it's in `PRESERVE_NPMRC_SCOPES`
|
|
254
|
+
// so `@superblocksteam/*` packages keep flowing through GHPR).
|
|
255
|
+
// - And its matching `//npm.pkg.github.com/:_authToken=…` line
|
|
256
|
+
// must survive with it — otherwise every `@superblocksteam/*`
|
|
257
|
+
// install 401s against GHPR (APPS-4300). The earlier shape of
|
|
258
|
+
// this test asserted the auth line was dropped, which was the
|
|
259
|
+
// bug.
|
|
260
|
+
const baked =
|
|
261
|
+
"//npm.pkg.github.com/:_authToken=ghpr-baked\n" +
|
|
262
|
+
"@superblocksteam:registry=https://npm.pkg.github.com/\n";
|
|
263
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
264
|
+
await writeFile(npmrcPath(homeDir), baked, { mode: 0o600 });
|
|
265
|
+
|
|
266
|
+
const { client } = makeClient(async () => CONFIGURED);
|
|
267
|
+
const logger = makeLogger();
|
|
268
|
+
|
|
269
|
+
const result = await syncHomeNpmrc({
|
|
270
|
+
npmRegistryClient: client,
|
|
271
|
+
logger: asLogger(logger),
|
|
272
|
+
homeDir,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.outcome).toBe("written");
|
|
276
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
277
|
+
expect(content).toContain(
|
|
278
|
+
"registry=https://artifactory.example.com/api/npm/npm/",
|
|
279
|
+
);
|
|
280
|
+
expect(content).toContain(
|
|
281
|
+
"@superblocksteam:registry=https://npm.pkg.github.com/",
|
|
282
|
+
);
|
|
283
|
+
expect(content).toContain("//npm.pkg.github.com/:_authToken=ghpr-baked");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("not-configured → restore image-default (or remove if no backup)", () => {
|
|
288
|
+
it("restores the image-default userconfig from the backup captured on a prior configured boot (APPS-4328)", async () => {
|
|
289
|
+
// Simulates the lifecycle gpoulios-sb flagged on PR #19621:
|
|
290
|
+
// 1. Org is configured → syncHomeNpmrc writes private-registry
|
|
291
|
+
// lines AND captures the image-baked baseline to
|
|
292
|
+
// ~/.superblocks/npmrc.default via hardlink.
|
|
293
|
+
// 2. Org is deconfigured → syncHomeNpmrc must restore the
|
|
294
|
+
// baseline so subsequent npm/pnpm pinned at the userconfig
|
|
295
|
+
// do not keep resolving through the previous org's registry.
|
|
296
|
+
const baked =
|
|
297
|
+
"//npm.pkg.github.com/:_authToken=ghpr-baked\n" +
|
|
298
|
+
"@superblocksteam:registry=https://npm.pkg.github.com/\n";
|
|
299
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
300
|
+
await writeFile(npmrcPath(homeDir), baked, { mode: 0o600 });
|
|
301
|
+
|
|
302
|
+
// Step 1: configured → rewrite + snapshot.
|
|
303
|
+
const configuredClient = makeClient(async () => CONFIGURED).client;
|
|
304
|
+
await syncHomeNpmrc({
|
|
305
|
+
npmRegistryClient: configuredClient,
|
|
306
|
+
logger: asLogger(makeLogger()),
|
|
307
|
+
homeDir,
|
|
308
|
+
});
|
|
309
|
+
const rewritten = await readFile(npmrcPath(homeDir), "utf-8");
|
|
310
|
+
expect(rewritten).toContain(
|
|
311
|
+
"registry=https://artifactory.example.com/api/npm/npm/",
|
|
312
|
+
);
|
|
313
|
+
expect(rewritten).not.toBe(baked);
|
|
314
|
+
expect(await readFile(superblocksNpmrcBackupPath(homeDir), "utf-8")).toBe(
|
|
315
|
+
baked,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Step 2: not-configured → restore image-default.
|
|
319
|
+
const notConfiguredClient = makeClient(async () => NOT_CONFIGURED).client;
|
|
320
|
+
const logger = makeLogger();
|
|
321
|
+
const result = await syncHomeNpmrc({
|
|
322
|
+
npmRegistryClient: notConfiguredClient,
|
|
323
|
+
logger: asLogger(logger),
|
|
324
|
+
homeDir,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.outcome).toBe("restored-not-configured");
|
|
328
|
+
const restored = await readFile(npmrcPath(homeDir), "utf-8");
|
|
329
|
+
expect(restored).toBe(baked);
|
|
330
|
+
expect(restored).not.toContain(
|
|
331
|
+
"registry=https://artifactory.example.com/api/npm/npm/",
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("removes the Superblocks-owned userconfig when no backup exists and the org has been deconfigured", async () => {
|
|
336
|
+
// Cold-boot case: a Superblocks-owned userconfig is on disk from
|
|
337
|
+
// a previous run (or a manual smoke test) but no .npmrc.default
|
|
338
|
+
// backup is alongside it. Leaving the file in place would let
|
|
339
|
+
// long-lived npm/pnpm subprocesses pinned at the userconfig
|
|
340
|
+
// continue resolving through a stale registry. Remove it
|
|
341
|
+
// instead so the next install falls through to public npm.
|
|
342
|
+
const stale =
|
|
343
|
+
"registry=https://artifactory.example.com/api/npm/npm/\n" +
|
|
344
|
+
"//artifactory.example.com/api/npm/npm/:_authToken=stale\n";
|
|
345
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
346
|
+
await writeFile(npmrcPath(homeDir), stale, { mode: 0o600 });
|
|
347
|
+
await expect(
|
|
348
|
+
stat(superblocksNpmrcBackupPath(homeDir)),
|
|
349
|
+
).rejects.toMatchObject({ code: "ENOENT" });
|
|
350
|
+
|
|
351
|
+
const { client } = makeClient(async () => NOT_CONFIGURED);
|
|
352
|
+
const logger = makeLogger();
|
|
353
|
+
|
|
354
|
+
const result = await syncHomeNpmrc({
|
|
355
|
+
npmRegistryClient: client,
|
|
356
|
+
logger: asLogger(logger),
|
|
357
|
+
homeDir,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(result.outcome).toBe("restored-not-configured");
|
|
361
|
+
await expect(stat(npmrcPath(homeDir))).rejects.toMatchObject({
|
|
362
|
+
code: "ENOENT",
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("fails closed on snapshot failure: skips the managed rewrite AND preserves the baked file on later deconfigure (APPS-4428)", async () => {
|
|
367
|
+
// The hole: `snapshotInitialNpmrc` is best-effort and can fail
|
|
368
|
+
// silently (e.g. EXDEV when ~/.superblocks/ and the backup straddle
|
|
369
|
+
// filesystems). When that happens, a real baked-in userconfig
|
|
370
|
+
// exists with NO restore baseline.
|
|
371
|
+
//
|
|
372
|
+
// PR #19690 first try only guarded the unlink on the deconfigure
|
|
373
|
+
// path, but still let `writeNpmrc` rewrite the file with the
|
|
374
|
+
// managed private-registry content. gpoulios-sb caught that this
|
|
375
|
+
// leaves a stale Artifactory token + registry on disk after the
|
|
376
|
+
// org is deconfigured — the exact stale-userconfig leak the
|
|
377
|
+
// restore path is supposed to avoid.
|
|
378
|
+
//
|
|
379
|
+
// Fix: fail closed on the snapshot-failed branch. The configured
|
|
380
|
+
// sync refuses to overwrite the baked userconfig, so the
|
|
381
|
+
// deconfigure path inherits an unchanged baked file (which the
|
|
382
|
+
// snapshot-failure guard then correctly leaves in place).
|
|
383
|
+
const baked =
|
|
384
|
+
"//npm.pkg.github.com/:_authToken=ghpr-baked\n" +
|
|
385
|
+
"@superblocksteam:registry=https://npm.pkg.github.com/\n";
|
|
386
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
387
|
+
await writeFile(npmrcPath(homeDir), baked, { mode: 0o600 });
|
|
388
|
+
|
|
389
|
+
// Step 1: configured → snapshot fails silently. The managed
|
|
390
|
+
// rewrite MUST be skipped; the baked content survives untouched.
|
|
391
|
+
snapshotInitialNpmrcMock.mockImplementationOnce(async () => "failed");
|
|
392
|
+
const configuredClient = makeClient(async () => CONFIGURED).client;
|
|
393
|
+
const configuredLogger = makeLogger();
|
|
394
|
+
const rewriteResult = await syncHomeNpmrc({
|
|
395
|
+
npmRegistryClient: configuredClient,
|
|
396
|
+
logger: asLogger(configuredLogger),
|
|
397
|
+
homeDir,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(rewriteResult.outcome).toBe("skipped-snapshot-failed");
|
|
401
|
+
expect(snapshotInitialNpmrcMock).toHaveBeenCalledWith(
|
|
402
|
+
npmrcPath(homeDir),
|
|
403
|
+
superblocksNpmrcBackupPath(homeDir),
|
|
404
|
+
);
|
|
405
|
+
await expect(
|
|
406
|
+
snapshotInitialNpmrcMock.mock.results[0]?.value,
|
|
407
|
+
).resolves.toBe("failed");
|
|
408
|
+
// The userconfig is still the baked content — no Artifactory
|
|
409
|
+
// registry/token, no managed lines.
|
|
410
|
+
const preserved = await readFile(npmrcPath(homeDir), "utf-8");
|
|
411
|
+
expect(preserved).toBe(baked);
|
|
412
|
+
expect(preserved).not.toContain("artifactory.example.com");
|
|
413
|
+
// And no backup exists (the snapshot failed).
|
|
414
|
+
await expect(
|
|
415
|
+
stat(superblocksNpmrcBackupPath(homeDir)),
|
|
416
|
+
).rejects.toMatchObject({ code: "ENOENT" });
|
|
417
|
+
// The refusal to overwrite is surfaced as a warn for telemetry.
|
|
418
|
+
expect(configuredLogger.warn).toHaveBeenCalledWith(
|
|
419
|
+
expect.stringContaining("userconfig snapshot failed"),
|
|
420
|
+
expect.objectContaining({
|
|
421
|
+
path: npmrcPath(homeDir),
|
|
422
|
+
source: "configured",
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Step 2: not-configured. With no backup and a recorded snapshot
|
|
427
|
+
// failure, the userconfig (still the baked content) must survive.
|
|
428
|
+
// The deconfigure path's destructive unlink is the original
|
|
429
|
+
// APPS-4328 hole; the snapshot-failure guard withholds it here and
|
|
430
|
+
// returns the dedicated `skipped-snapshot-failed` outcome — NOT
|
|
431
|
+
// `restored-not-configured`, since nothing was restored or removed.
|
|
432
|
+
const notConfiguredClient = makeClient(async () => NOT_CONFIGURED).client;
|
|
433
|
+
const deconfigureLogger = makeLogger();
|
|
434
|
+
const result = await syncHomeNpmrc({
|
|
435
|
+
npmRegistryClient: notConfiguredClient,
|
|
436
|
+
logger: asLogger(deconfigureLogger),
|
|
437
|
+
homeDir,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(result.outcome).toBe("skipped-snapshot-failed");
|
|
441
|
+
const survived = await readFile(npmrcPath(homeDir), "utf-8");
|
|
442
|
+
expect(survived).toBe(baked);
|
|
443
|
+
expect(deconfigureLogger.warn).toHaveBeenCalledWith(
|
|
444
|
+
expect.stringContaining("userconfig snapshot failed earlier"),
|
|
445
|
+
expect.objectContaining({ path: npmrcPath(homeDir) }),
|
|
446
|
+
);
|
|
447
|
+
// And no spurious "restored image-default" info to confuse the
|
|
448
|
+
// operator — the warn above already explains the leave-in-place.
|
|
449
|
+
expect(deconfigureLogger.info).not.toHaveBeenCalled();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("does not create ~/.superblocks/npmrc when the file does not exist", async () => {
|
|
453
|
+
const { client } = makeClient(async () => NOT_CONFIGURED);
|
|
454
|
+
const logger = makeLogger();
|
|
455
|
+
|
|
456
|
+
const result = await syncHomeNpmrc({
|
|
457
|
+
npmRegistryClient: client,
|
|
458
|
+
logger: asLogger(logger),
|
|
459
|
+
homeDir,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(result.outcome).toBe("restored-not-configured");
|
|
463
|
+
await expect(stat(npmrcPath(homeDir))).rejects.toMatchObject({
|
|
464
|
+
code: "ENOENT",
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("captures the image-baked userconfig as .npmrc.default on the first configured write", async () => {
|
|
469
|
+
// Regression guard for the capture half of APPS-4328: the
|
|
470
|
+
// hardlink must land BEFORE writeNpmrc rewrites the userconfig
|
|
471
|
+
// or the backup will hold the rewritten (private-registry)
|
|
472
|
+
// content instead of the image baseline. Verified by checking
|
|
473
|
+
// that the captured file equals the original baked content
|
|
474
|
+
// byte-for-byte AFTER the configured rewrite has happened.
|
|
475
|
+
const baked =
|
|
476
|
+
"//npm.pkg.github.com/:_authToken=ghpr-baked\n" +
|
|
477
|
+
"@superblocksteam:registry=https://npm.pkg.github.com/\n";
|
|
478
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
479
|
+
await writeFile(npmrcPath(homeDir), baked, { mode: 0o600 });
|
|
480
|
+
|
|
481
|
+
const { client } = makeClient(async () => CONFIGURED);
|
|
482
|
+
await syncHomeNpmrc({
|
|
483
|
+
npmRegistryClient: client,
|
|
484
|
+
logger: asLogger(makeLogger()),
|
|
485
|
+
homeDir,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const captured = await readFile(
|
|
489
|
+
superblocksNpmrcBackupPath(homeDir),
|
|
490
|
+
"utf-8",
|
|
491
|
+
);
|
|
492
|
+
expect(captured).toBe(baked);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("unreachable → leave file alone", () => {
|
|
497
|
+
it("does NOT delete the userconfig when the server is unreachable with cold cache", async () => {
|
|
498
|
+
// A cold cache + transient outage at startup must not punch a hole
|
|
499
|
+
// in the userconfig right before the CLI auto-upgrade runs. The
|
|
500
|
+
// client surfaces `source: "unreachable"` precisely so we can
|
|
501
|
+
// branch here.
|
|
502
|
+
const { client, getConfig } = makeClient(async () => CONFIGURED);
|
|
503
|
+
const logger = makeLogger();
|
|
504
|
+
await syncHomeNpmrc({
|
|
505
|
+
npmRegistryClient: client,
|
|
506
|
+
logger: asLogger(logger),
|
|
507
|
+
homeDir,
|
|
508
|
+
});
|
|
509
|
+
const before = await readFile(npmrcPath(homeDir), "utf-8");
|
|
510
|
+
|
|
511
|
+
getConfig.mockResolvedValueOnce(UNREACHABLE);
|
|
512
|
+
const result = await syncHomeNpmrc({
|
|
513
|
+
npmRegistryClient: client,
|
|
514
|
+
logger: asLogger(logger),
|
|
515
|
+
homeDir,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
expect(result.outcome).toBe("skipped-unreachable");
|
|
519
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
520
|
+
expect.stringContaining("registry server unreachable"),
|
|
521
|
+
expect.objectContaining({
|
|
522
|
+
path: npmrcPath(homeDir),
|
|
523
|
+
}),
|
|
524
|
+
);
|
|
525
|
+
const after = await readFile(npmrcPath(homeDir), "utf-8");
|
|
526
|
+
expect(after).toBe(before);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("does not create ~/.superblocks/npmrc when the server is unreachable and no file exists", async () => {
|
|
530
|
+
const { client } = makeClient(async () => UNREACHABLE);
|
|
531
|
+
const logger = makeLogger();
|
|
532
|
+
|
|
533
|
+
const result = await syncHomeNpmrc({
|
|
534
|
+
npmRegistryClient: client,
|
|
535
|
+
logger: asLogger(logger),
|
|
536
|
+
homeDir,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
expect(result.outcome).toBe("skipped-unreachable");
|
|
540
|
+
await expect(stat(npmrcPath(homeDir))).rejects.toMatchObject({
|
|
541
|
+
code: "ENOENT",
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// -------------------------------------------------------------------------
|
|
547
|
+
// APPS-4368: ignore-scripts policy baked into ~/.npmrc
|
|
548
|
+
// -------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
describe("ignore-scripts policy (APPS-4368)", () => {
|
|
551
|
+
const NOT_CONFIGURED_SCRIPTS_OFF: NpmRegistryFetchResult = {
|
|
552
|
+
source: "not-configured",
|
|
553
|
+
config: { configured: false, allowInstallScripts: false },
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const CONFIGURED_SCRIPTS_OFF: NpmRegistryFetchResult = {
|
|
557
|
+
source: "configured",
|
|
558
|
+
config: {
|
|
559
|
+
configured: true,
|
|
560
|
+
default: {
|
|
561
|
+
url: "https://artifactory.example.com/api/npm/npm/",
|
|
562
|
+
token: "test-token-abc",
|
|
563
|
+
},
|
|
564
|
+
allowInstallScripts: false,
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
it("writes a policy-only userconfig when not-configured + allowInstallScripts=false", async () => {
|
|
569
|
+
const { client } = makeClient(async () => NOT_CONFIGURED_SCRIPTS_OFF);
|
|
570
|
+
const logger = makeLogger();
|
|
571
|
+
|
|
572
|
+
const result = await syncHomeNpmrc({
|
|
573
|
+
npmRegistryClient: client,
|
|
574
|
+
logger: asLogger(logger),
|
|
575
|
+
homeDir,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
expect(result.outcome).toBe("written");
|
|
579
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
580
|
+
expect(content).toContain("ignore-scripts=true");
|
|
581
|
+
const stats = await stat(npmrcPath(homeDir));
|
|
582
|
+
expect(stats.mode & 0o777).toBe(HOME_NPMRC_MODE);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("includes ignore-scripts=true alongside registry lines when configured + policy=false", async () => {
|
|
586
|
+
const { client } = makeClient(async () => CONFIGURED_SCRIPTS_OFF);
|
|
587
|
+
const logger = makeLogger();
|
|
588
|
+
|
|
589
|
+
const result = await syncHomeNpmrc({
|
|
590
|
+
npmRegistryClient: client,
|
|
591
|
+
logger: asLogger(logger),
|
|
592
|
+
homeDir,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(result.outcome).toBe("written");
|
|
596
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
597
|
+
expect(content).toContain("ignore-scripts=true");
|
|
598
|
+
expect(content).toContain(
|
|
599
|
+
"registry=https://artifactory.example.com/api/npm/npm/",
|
|
600
|
+
);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("cleans up the policy-only userconfig after flipping to scripts-allowed", async () => {
|
|
604
|
+
const { client, getConfig } = makeClient(
|
|
605
|
+
async () => NOT_CONFIGURED_SCRIPTS_OFF,
|
|
606
|
+
);
|
|
607
|
+
const logger = makeLogger();
|
|
608
|
+
// First sync: policy-only write
|
|
609
|
+
await syncHomeNpmrc({
|
|
610
|
+
npmRegistryClient: client,
|
|
611
|
+
logger: asLogger(logger),
|
|
612
|
+
homeDir,
|
|
613
|
+
});
|
|
614
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
615
|
+
expect(content).toContain("ignore-scripts=true");
|
|
616
|
+
|
|
617
|
+
// Flip to not-configured + scripts allowed → restore removes the
|
|
618
|
+
// policy-only file (no backup exists since we never went through
|
|
619
|
+
// the configured path, and unlinkTargetWhenBackupMissing is true).
|
|
620
|
+
getConfig.mockResolvedValueOnce(NOT_CONFIGURED);
|
|
621
|
+
const result = await syncHomeNpmrc({
|
|
622
|
+
npmRegistryClient: client,
|
|
623
|
+
logger: asLogger(logger),
|
|
624
|
+
homeDir,
|
|
625
|
+
});
|
|
626
|
+
expect(result.outcome).toBe("restored-not-configured");
|
|
627
|
+
await expect(stat(npmrcPath(homeDir))).rejects.toMatchObject({
|
|
628
|
+
code: "ENOENT",
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("cleans up after repeated policy syncs then flip (no poisoned backup)", async () => {
|
|
633
|
+
// Regression: on the second sync with policy=false the write path
|
|
634
|
+
// must NOT snapshot the policy file as the backup baseline.
|
|
635
|
+
// Otherwise a later flip to scripts-allowed restores the policy
|
|
636
|
+
// content (both target and backup are identical) → no-op, leaving
|
|
637
|
+
// ignore-scripts=true in place permanently.
|
|
638
|
+
const { client, getConfig } = makeClient(
|
|
639
|
+
async () => NOT_CONFIGURED_SCRIPTS_OFF,
|
|
640
|
+
);
|
|
641
|
+
const logger = makeLogger();
|
|
642
|
+
|
|
643
|
+
// Two consecutive syncs with scripts off.
|
|
644
|
+
await syncHomeNpmrc({
|
|
645
|
+
npmRegistryClient: client,
|
|
646
|
+
logger: asLogger(logger),
|
|
647
|
+
homeDir,
|
|
648
|
+
});
|
|
649
|
+
await syncHomeNpmrc({
|
|
650
|
+
npmRegistryClient: client,
|
|
651
|
+
logger: asLogger(logger),
|
|
652
|
+
homeDir,
|
|
653
|
+
});
|
|
654
|
+
expect(await readFile(npmrcPath(homeDir), "utf-8")).toContain(
|
|
655
|
+
"ignore-scripts=true",
|
|
656
|
+
);
|
|
657
|
+
// Backup must NOT exist (we never went through the configured path).
|
|
658
|
+
await expect(
|
|
659
|
+
stat(superblocksNpmrcBackupPath(homeDir)),
|
|
660
|
+
).rejects.toMatchObject({ code: "ENOENT" });
|
|
661
|
+
|
|
662
|
+
// Flip: scripts allowed → file must be removed.
|
|
663
|
+
getConfig.mockResolvedValueOnce(NOT_CONFIGURED);
|
|
664
|
+
const result = await syncHomeNpmrc({
|
|
665
|
+
npmRegistryClient: client,
|
|
666
|
+
logger: asLogger(logger),
|
|
667
|
+
homeDir,
|
|
668
|
+
});
|
|
669
|
+
expect(result.outcome).toBe("restored-not-configured");
|
|
670
|
+
await expect(stat(npmrcPath(homeDir))).rejects.toMatchObject({
|
|
671
|
+
code: "ENOENT",
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("writes the policy-only userconfig even when a pre-existing file is present (Superblocks-owned dir)", async () => {
|
|
676
|
+
// The userconfig now lives in ~/.superblocks/npmrc (a Superblocks-
|
|
677
|
+
// owned directory), so there is no foreign-file guard — the writer
|
|
678
|
+
// always owns the file.
|
|
679
|
+
const existingContent = "registry=https://registry.npmjs.org/\n";
|
|
680
|
+
await mkdir(path.join(homeDir, ".superblocks"), { recursive: true });
|
|
681
|
+
await writeFile(npmrcPath(homeDir), existingContent, {
|
|
682
|
+
mode: 0o600,
|
|
683
|
+
});
|
|
684
|
+
const { client } = makeClient(async () => NOT_CONFIGURED_SCRIPTS_OFF);
|
|
685
|
+
const logger = makeLogger();
|
|
686
|
+
|
|
687
|
+
const result = await syncHomeNpmrc({
|
|
688
|
+
npmRegistryClient: client,
|
|
689
|
+
logger: asLogger(logger),
|
|
690
|
+
homeDir,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
expect(result.outcome).toBe("written");
|
|
694
|
+
const content = await readFile(npmrcPath(homeDir), "utf-8");
|
|
695
|
+
expect(content).toContain("ignore-scripts=true");
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
describe("error handling", () => {
|
|
700
|
+
it("returns outcome 'error' and logs a warn when the client throws", async () => {
|
|
701
|
+
// RBAC denial / malformed-request branches surface as throws from
|
|
702
|
+
// `NpmRegistryClient`. Dev-server startup must not crash on those
|
|
703
|
+
// — log + leave the userconfig untouched.
|
|
704
|
+
const seedLogger = makeLogger();
|
|
705
|
+
const { client: seedClient } = makeClient(async () => CONFIGURED);
|
|
706
|
+
await syncHomeNpmrc({
|
|
707
|
+
npmRegistryClient: seedClient,
|
|
708
|
+
logger: asLogger(seedLogger),
|
|
709
|
+
homeDir,
|
|
710
|
+
});
|
|
711
|
+
const before = await readFile(npmrcPath(homeDir), "utf-8");
|
|
712
|
+
|
|
713
|
+
const { client } = makeClient(async () => {
|
|
714
|
+
throw new Error("HTTP 403 from npm-registry endpoint");
|
|
715
|
+
});
|
|
716
|
+
const logger = makeLogger();
|
|
717
|
+
|
|
718
|
+
const result = await syncHomeNpmrc({
|
|
719
|
+
npmRegistryClient: client,
|
|
720
|
+
logger: asLogger(logger),
|
|
721
|
+
homeDir,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
expect(result.outcome).toBe("error");
|
|
725
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
726
|
+
expect.stringContaining("client.getConfig() failed"),
|
|
727
|
+
expect.objectContaining({
|
|
728
|
+
path: npmrcPath(homeDir),
|
|
729
|
+
}),
|
|
730
|
+
);
|
|
731
|
+
const after = await readFile(npmrcPath(homeDir), "utf-8");
|
|
732
|
+
expect(after).toBe(before);
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
describe("superblocksNpmrcPath helper", () => {
|
|
737
|
+
it("returns <homeDir>/.superblocks/npmrc", () => {
|
|
738
|
+
expect(superblocksNpmrcPath(homeDir)).toBe(
|
|
739
|
+
path.join(homeDir, ".superblocks", "npmrc"),
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("falls back to os.homedir() when no argument is provided", () => {
|
|
744
|
+
expect(superblocksNpmrcPath()).toBe(
|
|
745
|
+
path.join(os.homedir(), ".superblocks", "npmrc"),
|
|
746
|
+
);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe("superblocksLogsPath helper", () => {
|
|
751
|
+
it("returns <appDir>/.superblocks/logs (app-based, not home-based)", () => {
|
|
752
|
+
expect(superblocksLogsPath("/srv/app")).toBe(
|
|
753
|
+
path.join("/srv/app", ".superblocks", "logs"),
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
});
|