@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,51 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { InitialInstallFailed, classifyInitialInstallError, } from "./dependency-install-classifier.mjs";
|
|
3
|
+
const ERESOLVE_JSON = JSON.stringify({
|
|
4
|
+
error: {
|
|
5
|
+
code: "ERESOLVE",
|
|
6
|
+
summary: "unable to resolve dependency tree",
|
|
7
|
+
detail: 'Found: react-router@undefined\nCould not resolve dependency:\npeer react-router@">=6.4" from @superblocksteam/library@2.0.0-SNAPSHOT',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
describe("classifyInitialInstallError", () => {
|
|
11
|
+
it("classifies ERESOLVE as dependency_conflict and extracts packages", () => {
|
|
12
|
+
const e = classifyInitialInstallError({
|
|
13
|
+
stdout: ERESOLVE_JSON,
|
|
14
|
+
stderr: "npm error code ERESOLVE",
|
|
15
|
+
message: "Command failed",
|
|
16
|
+
}, { hasAnyRegistryConfigured: true, requestedPackages: [] });
|
|
17
|
+
expect(e.type).toBe("dev-server/dependency-install");
|
|
18
|
+
expect(e.category).toBe("dependency_conflict");
|
|
19
|
+
expect(e.npmErrorCode).toBe("ERESOLVE");
|
|
20
|
+
expect(e.packages?.some((p) => p.name === "react-router")).toBe(true);
|
|
21
|
+
expect(e.rawError.length).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
it("maps an E404 registry block to not_in_registry", () => {
|
|
24
|
+
const E404 = JSON.stringify({
|
|
25
|
+
error: {
|
|
26
|
+
code: "E404",
|
|
27
|
+
summary: "Not Found - GET https://reg.corp/foo",
|
|
28
|
+
detail: "'foo@1.0.0' is not in this registry.",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const e = classifyInitialInstallError({ stdout: E404, stderr: "", message: "x" }, { hasAnyRegistryConfigured: true, requestedPackages: [{ name: "foo" }] });
|
|
32
|
+
expect(e.category).toBe("not_in_registry");
|
|
33
|
+
expect(e.registryHost).toBe("reg.corp");
|
|
34
|
+
});
|
|
35
|
+
it("falls back to unknown for an unmapped code (still a DependencyInstallError)", () => {
|
|
36
|
+
const e = classifyInitialInstallError({
|
|
37
|
+
stdout: "not json",
|
|
38
|
+
stderr: "npm error code EWEIRD\nsomething broke",
|
|
39
|
+
message: "x",
|
|
40
|
+
}, {});
|
|
41
|
+
expect(e.category).toBe("unknown");
|
|
42
|
+
expect(e.rawError).toContain("EWEIRD");
|
|
43
|
+
});
|
|
44
|
+
it("InitialInstallFailed carries the classified error", () => {
|
|
45
|
+
const se = classifyInitialInstallError({ stdout: ERESOLVE_JSON, stderr: "", message: "x" }, {});
|
|
46
|
+
const marker = new InitialInstallFailed(se);
|
|
47
|
+
expect(marker).toBeInstanceOf(Error);
|
|
48
|
+
expect(marker.serverError).toBe(se);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=dependency-install-classifier.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-install-classifier.test.mjs","sourceRoot":"","sources":["../../src/cli-replacement/dependency-install-classifier.test.mts"],"names":[],"mappings":"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"}
|
|
@@ -5,9 +5,11 @@ import * as path from "node:path";
|
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { packageJsonSnapshot } from "./package-json-snapshot.mjs";
|
|
7
7
|
const execMock = vi.fn();
|
|
8
|
+
const execFileMock = vi.fn();
|
|
8
9
|
vi.mock("node:child_process", () => ({
|
|
9
|
-
default: { exec: execMock },
|
|
10
|
+
default: { exec: execMock, execFile: execFileMock },
|
|
10
11
|
exec: execMock,
|
|
12
|
+
execFile: execFileMock,
|
|
11
13
|
}));
|
|
12
14
|
vi.mock("node:readline", () => ({
|
|
13
15
|
default: {
|
|
@@ -46,6 +48,9 @@ const mockLogger = {
|
|
|
46
48
|
error: vi.fn(),
|
|
47
49
|
debug: vi.fn(),
|
|
48
50
|
};
|
|
51
|
+
const mockAiServiceState = vi.hoisted(() => ({
|
|
52
|
+
npmRegistryClient: undefined,
|
|
53
|
+
}));
|
|
49
54
|
vi.mock("../telemetry/logging.js", () => ({
|
|
50
55
|
getLogger: () => mockLogger,
|
|
51
56
|
getErrorMeta: vi.fn(() => ({})),
|
|
@@ -61,7 +66,12 @@ vi.mock("@opentelemetry/api", () => ({
|
|
|
61
66
|
vi.mock("colorette", () => ({
|
|
62
67
|
green: (s) => s,
|
|
63
68
|
}));
|
|
64
|
-
vi.mock("@superblocksteam/shared", () => ({
|
|
69
|
+
vi.mock("@superblocksteam/shared", async (importOriginal) => ({
|
|
70
|
+
// dev.mts now transitively loads @superblocksteam/shared (via the npm-error
|
|
71
|
+
// classifier re-exported from vite-plugin-file-sync/npm-registry — APPS-4450),
|
|
72
|
+
// pulling in many named exports (DeploymentTypeEnum, ISocket, …). Spread the
|
|
73
|
+
// real module so the closed overrides below don't have to enumerate them.
|
|
74
|
+
...(await importOriginal()),
|
|
65
75
|
buildGithubSuperblocksSyncWorkflow: vi.fn(),
|
|
66
76
|
buildGithubSuperblocksSyncWorkflowFromBaseUrl: vi.fn(),
|
|
67
77
|
ConflictError: class ConflictError extends Error {
|
|
@@ -82,6 +92,12 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
82
92
|
initialize = vi.fn(async () => undefined);
|
|
83
93
|
removeIntegrationCache = vi.fn(async () => undefined);
|
|
84
94
|
chatSessionStore = { invalidateCache: vi.fn() };
|
|
95
|
+
// `dev.mts` reads this at startup for two paths: `syncHomeNpmrc`
|
|
96
|
+
// (APPS-4231 home `~/.npmrc` writer) and the startup `installPackages`
|
|
97
|
+
// call (APPS-4251 per-org `allow_install_scripts`). Returning undefined
|
|
98
|
+
// keeps the install path on the "no policy resolved" branch and is also
|
|
99
|
+
// valid input for the mocked-out `syncHomeNpmrc`.
|
|
100
|
+
getNpmRegistryClient = vi.fn(() => mockAiServiceState.npmRegistryClient);
|
|
85
101
|
},
|
|
86
102
|
AiServiceFeatureFlags: class {
|
|
87
103
|
static create = vi.fn(() => ({}));
|
|
@@ -92,6 +108,20 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
92
108
|
isSdkApiTemplate: vi.fn(() => false),
|
|
93
109
|
stripResolvedFromLockfile: vi.fn(async () => undefined),
|
|
94
110
|
}));
|
|
111
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", () => ({
|
|
112
|
+
shouldIgnoreInstallScripts: vi.fn(() => false),
|
|
113
|
+
// Used by the dependency-install-classifier reachable from installPackages'
|
|
114
|
+
// catch path (APPS-4450). Previously the only test that hit this codepath
|
|
115
|
+
// (`logs a structured error when the background install fails`) didn't
|
|
116
|
+
// assert classification, so the missing exports surfaced silently as a
|
|
117
|
+
// ReferenceError instead of an InitialInstallFailed. The new
|
|
118
|
+
// restart-vs-degrade test depends on installPackages re-throwing an actual
|
|
119
|
+
// InitialInstallFailed marker, so stub all classifier-required exports.
|
|
120
|
+
scrubSecrets: vi.fn((value) => value),
|
|
121
|
+
parseNpmJsonDiagnostic: vi.fn(() => ""),
|
|
122
|
+
parseNpmJsonError: vi.fn(() => null),
|
|
123
|
+
classifyNpmExecStderr: vi.fn(() => null),
|
|
124
|
+
}));
|
|
95
125
|
vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
96
126
|
createGitService: vi.fn(async () => undefined),
|
|
97
127
|
}));
|
|
@@ -154,6 +184,24 @@ const mockCheckVersions = vi.fn(async () => ({
|
|
|
154
184
|
}));
|
|
155
185
|
vi.mock("./automatic-upgrades.js", () => ({
|
|
156
186
|
checkVersionsAndWritePackageJson: (...args) => mockCheckVersions(...args),
|
|
187
|
+
buildInstallEnv: (userconfigPath) => ({
|
|
188
|
+
...process.env,
|
|
189
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
190
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
191
|
+
}),
|
|
192
|
+
}));
|
|
193
|
+
// The home `~/.npmrc` writer (APPS-4231) is a best-effort step that runs
|
|
194
|
+
// before auto-upgrade. Mock it out here so these tests stay focused on the
|
|
195
|
+
// DBFS / S3 restore behaviour. The `AiService` mock above exposes a
|
|
196
|
+
// minimal `getNpmRegistryClient()` stub so the call site doesn't `TypeError`
|
|
197
|
+
// and silently swallow this mock.
|
|
198
|
+
vi.mock("./home-npmrc.mjs", () => ({
|
|
199
|
+
syncHomeNpmrc: vi.fn(async () => ({
|
|
200
|
+
outcome: "skipped-not-configured",
|
|
201
|
+
path: "/tmp/.superblocks/npmrc",
|
|
202
|
+
})),
|
|
203
|
+
superblocksNpmrcPath: vi.fn(() => "/tmp/.superblocks/npmrc"),
|
|
204
|
+
superblocksLogsPath: vi.fn((appDir) => `${appDir}/.superblocks/logs`),
|
|
157
205
|
}));
|
|
158
206
|
vi.mock("./git-repo-setup.mjs", () => ({
|
|
159
207
|
ensureRemoteHasDefaultBranch: vi.fn(async () => undefined),
|
|
@@ -207,6 +255,8 @@ function buildDevOptions(overrides = {}) {
|
|
|
207
255
|
tokenManager: {
|
|
208
256
|
getToken: vi.fn(() => "test-token"),
|
|
209
257
|
onTokenRefresh: vi.fn(),
|
|
258
|
+
updateToken: vi.fn(),
|
|
259
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
210
260
|
},
|
|
211
261
|
getCurrentToken: vi.fn(() => "test-token"),
|
|
212
262
|
sdk: buildMockSdk(),
|
|
@@ -232,6 +282,7 @@ function expectInstallToHaveRun() {
|
|
|
232
282
|
describe("dev startup after S3 workspace restore", () => {
|
|
233
283
|
beforeEach(async () => {
|
|
234
284
|
vi.clearAllMocks();
|
|
285
|
+
mockAiServiceState.npmRegistryClient = undefined;
|
|
235
286
|
setupExecMock();
|
|
236
287
|
mockCheckVersions.mockResolvedValue({
|
|
237
288
|
cliUpdated: false,
|
|
@@ -454,6 +505,234 @@ describe("dev startup after S3 workspace restore", () => {
|
|
|
454
505
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
455
506
|
}
|
|
456
507
|
});
|
|
508
|
+
it("keeps forced npm install when unchanged restore inputs still need private registry validation", async () => {
|
|
509
|
+
mockAiServiceState.npmRegistryClient = {
|
|
510
|
+
getConfig: vi.fn(async () => ({
|
|
511
|
+
source: "configured",
|
|
512
|
+
config: { configured: true },
|
|
513
|
+
})),
|
|
514
|
+
};
|
|
515
|
+
const unchangedPackageJson = {
|
|
516
|
+
name: "test-app",
|
|
517
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
518
|
+
};
|
|
519
|
+
const { dev } = await import("./dev.mjs");
|
|
520
|
+
await dev(buildDevOptions({
|
|
521
|
+
forcePackageInstall: true,
|
|
522
|
+
packageJsonSnapshotBeforeRestore: packageJsonSnapshot(unchangedPackageJson),
|
|
523
|
+
}));
|
|
524
|
+
expectInstallToHaveRun();
|
|
525
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Package install decision", expect.objectContaining({
|
|
526
|
+
forcePackageInstall: true,
|
|
527
|
+
forcePackageInstallRequested: true,
|
|
528
|
+
packageJsonRequiresInstall: false,
|
|
529
|
+
}));
|
|
530
|
+
});
|
|
531
|
+
it("runs npm install at startup when private registries are configured even without package changes", async () => {
|
|
532
|
+
mockAiServiceState.npmRegistryClient = {
|
|
533
|
+
getConfig: vi.fn(async () => ({
|
|
534
|
+
source: "configured",
|
|
535
|
+
config: { configured: true },
|
|
536
|
+
})),
|
|
537
|
+
};
|
|
538
|
+
const unchangedPackageJson = {
|
|
539
|
+
name: "test-app",
|
|
540
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
541
|
+
};
|
|
542
|
+
const { dev } = await import("./dev.mjs");
|
|
543
|
+
await dev(buildDevOptions({
|
|
544
|
+
forcePackageInstall: false,
|
|
545
|
+
packageJsonSnapshotBeforeRestore: packageJsonSnapshot(unchangedPackageJson),
|
|
546
|
+
}));
|
|
547
|
+
expectInstallToHaveRun();
|
|
548
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Package install decision", expect.objectContaining({
|
|
549
|
+
forcePackageInstall: false,
|
|
550
|
+
forcePackageInstallRequested: false,
|
|
551
|
+
packageJsonRequiresInstall: false,
|
|
552
|
+
privateRegistryRequiresInstallValidation: true,
|
|
553
|
+
}));
|
|
554
|
+
});
|
|
555
|
+
it("does not upload package state when private registry validation install leaves the lockfile unchanged", async () => {
|
|
556
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sdk-dev-private-registry-"));
|
|
557
|
+
const unchangedPackageJson = {
|
|
558
|
+
name: "test-app",
|
|
559
|
+
packageManager: "npm@10.0.0",
|
|
560
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
561
|
+
};
|
|
562
|
+
try {
|
|
563
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
564
|
+
await fs.writeFile(path.join(tmpDir, "package.json"), JSON.stringify(unchangedPackageJson, null, 2));
|
|
565
|
+
await fs.writeFile(path.join(tmpDir, "package-lock.json"), JSON.stringify({ name: "test-app", lockfileVersion: 3 }, null, 2));
|
|
566
|
+
const { readPackage } = await import("read-pkg");
|
|
567
|
+
readPackage.mockImplementation(async ({ cwd }) => JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")));
|
|
568
|
+
mockAiServiceState.npmRegistryClient = {
|
|
569
|
+
getConfig: vi.fn(async () => ({
|
|
570
|
+
source: "configured",
|
|
571
|
+
config: { configured: true },
|
|
572
|
+
})),
|
|
573
|
+
};
|
|
574
|
+
const { dev } = await import("./dev.mjs");
|
|
575
|
+
await dev(buildDevOptions({
|
|
576
|
+
cwd: tmpDir,
|
|
577
|
+
forcePackageInstall: false,
|
|
578
|
+
packageJsonSnapshotBeforeRestore: packageJsonSnapshot(unchangedPackageJson),
|
|
579
|
+
}));
|
|
580
|
+
expectInstallToHaveRun();
|
|
581
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
582
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
it("does NOT upload package state when the validation install only rewrites `resolved` URLs", async () => {
|
|
589
|
+
// The exact churn cycle flagged in review: `stripResolvedFromLockfile`
|
|
590
|
+
// removes every `resolved` URL before the install and npm writes them
|
|
591
|
+
// back, so a byte-level comparison would flag "changed" on EVERY
|
|
592
|
+
// registry-validation boot and re-upload the workspace to DBFS forever.
|
|
593
|
+
// `lockfileComparisonKey` ignores `resolved`, so a resolved-only rewrite
|
|
594
|
+
// must NOT trigger the upload.
|
|
595
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sdk-dev-private-registry-"));
|
|
596
|
+
const unchangedPackageJson = {
|
|
597
|
+
name: "test-app",
|
|
598
|
+
packageManager: "npm@10.0.0",
|
|
599
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
600
|
+
};
|
|
601
|
+
const strippedLockfile = {
|
|
602
|
+
name: "test-app",
|
|
603
|
+
lockfileVersion: 3,
|
|
604
|
+
packages: {
|
|
605
|
+
"node_modules/@superblocksteam/library": { version: "1.0.0" },
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
const rewrittenLockfile = {
|
|
609
|
+
name: "test-app",
|
|
610
|
+
lockfileVersion: 3,
|
|
611
|
+
packages: {
|
|
612
|
+
"node_modules/@superblocksteam/library": {
|
|
613
|
+
version: "1.0.0",
|
|
614
|
+
resolved: "https://registry.example.com/@superblocksteam/library/-/library-1.0.0.tgz",
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
try {
|
|
619
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
620
|
+
await fs.writeFile(path.join(tmpDir, "package.json"), JSON.stringify(unchangedPackageJson, null, 2));
|
|
621
|
+
await fs.writeFile(path.join(tmpDir, "package-lock.json"), JSON.stringify(strippedLockfile, null, 2));
|
|
622
|
+
const { readPackage } = await import("read-pkg");
|
|
623
|
+
readPackage.mockImplementation(async ({ cwd }) => JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")));
|
|
624
|
+
mockAiServiceState.npmRegistryClient = {
|
|
625
|
+
getConfig: vi.fn(async () => ({
|
|
626
|
+
source: "configured",
|
|
627
|
+
config: { configured: true },
|
|
628
|
+
})),
|
|
629
|
+
};
|
|
630
|
+
execMock.mockImplementation((_cmd, opts, cb) => {
|
|
631
|
+
const cwd = typeof opts === "object" ? opts.cwd : tmpDir;
|
|
632
|
+
void fs
|
|
633
|
+
.writeFile(path.join(cwd ?? tmpDir, "package-lock.json"), JSON.stringify(rewrittenLockfile, null, 2))
|
|
634
|
+
.then(() => {
|
|
635
|
+
if (typeof opts === "function") {
|
|
636
|
+
opts(null, { stdout: "installed" });
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
cb?.(null, { stdout: "installed" });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
const { dev } = await import("./dev.mjs");
|
|
644
|
+
await dev(buildDevOptions({
|
|
645
|
+
cwd: tmpDir,
|
|
646
|
+
forcePackageInstall: false,
|
|
647
|
+
packageJsonSnapshotBeforeRestore: packageJsonSnapshot(unchangedPackageJson),
|
|
648
|
+
}));
|
|
649
|
+
expectInstallToHaveRun();
|
|
650
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
651
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
it("uploads package state when private registry validation install rewrites the lockfile", async () => {
|
|
658
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sdk-dev-private-registry-"));
|
|
659
|
+
const unchangedPackageJson = {
|
|
660
|
+
name: "test-app",
|
|
661
|
+
packageManager: "npm@10.0.0",
|
|
662
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
663
|
+
};
|
|
664
|
+
try {
|
|
665
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
666
|
+
await fs.writeFile(path.join(tmpDir, "package.json"), JSON.stringify(unchangedPackageJson, null, 2));
|
|
667
|
+
await fs.writeFile(path.join(tmpDir, "package-lock.json"), JSON.stringify({ name: "test-app", lockfileVersion: 3 }, null, 2));
|
|
668
|
+
const { readPackage } = await import("read-pkg");
|
|
669
|
+
readPackage.mockImplementation(async ({ cwd }) => JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")));
|
|
670
|
+
mockAiServiceState.npmRegistryClient = {
|
|
671
|
+
getConfig: vi.fn(async () => ({
|
|
672
|
+
source: "configured",
|
|
673
|
+
config: { configured: true },
|
|
674
|
+
})),
|
|
675
|
+
};
|
|
676
|
+
execMock.mockImplementation((_cmd, opts, cb) => {
|
|
677
|
+
const cwd = typeof opts === "object" ? opts.cwd : tmpDir;
|
|
678
|
+
void fs
|
|
679
|
+
.writeFile(path.join(cwd ?? tmpDir, "package-lock.json"), JSON.stringify({ name: "test-app", lockfileVersion: 3, packages: {} }, null, 2))
|
|
680
|
+
.then(() => {
|
|
681
|
+
if (typeof opts === "function") {
|
|
682
|
+
opts(null, { stdout: "installed" });
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
cb?.(null, { stdout: "installed" });
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
const { dev } = await import("./dev.mjs");
|
|
690
|
+
await dev(buildDevOptions({
|
|
691
|
+
cwd: tmpDir,
|
|
692
|
+
forcePackageInstall: false,
|
|
693
|
+
packageJsonSnapshotBeforeRestore: packageJsonSnapshot(unchangedPackageJson),
|
|
694
|
+
}));
|
|
695
|
+
expectInstallToHaveRun();
|
|
696
|
+
expect(mockSyncService.uploadDirectory).toHaveBeenCalledWith("cli:sdk");
|
|
697
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).toHaveBeenCalledWith("cli:sdk");
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
it("does NOT force the startup install when the registry config is unreachable (fail closed, APPS-4527)", async () => {
|
|
704
|
+
// `unreachable` with no last-known-good means we could not materialize
|
|
705
|
+
// the right `.npmrc`. Forcing an install would re-resolve a
|
|
706
|
+
// private-registry org's lockfile against whatever registry happens to
|
|
707
|
+
// be on disk — validating against the wrong host and writing its URLs
|
|
708
|
+
// back into the lockfile. Fail closed instead (APPS-4370 principle):
|
|
709
|
+
// keep today's snapshot decision and skip the install.
|
|
710
|
+
mockAiServiceState.npmRegistryClient = {
|
|
711
|
+
getConfig: vi.fn(async () => ({ source: "unreachable" })),
|
|
712
|
+
};
|
|
713
|
+
const { dev } = await import("./dev.mjs");
|
|
714
|
+
await dev(buildDevOptions());
|
|
715
|
+
expect(execMock).not.toHaveBeenCalled();
|
|
716
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Package install decision", expect.objectContaining({
|
|
717
|
+
packageJsonRequiresInstall: false,
|
|
718
|
+
forcePackageInstall: false,
|
|
719
|
+
privateRegistryRequiresInstallValidation: false,
|
|
720
|
+
}));
|
|
721
|
+
});
|
|
722
|
+
it("does NOT force the startup install when registry config resolution throws (fail closed, APPS-4527)", async () => {
|
|
723
|
+
mockAiServiceState.npmRegistryClient = {
|
|
724
|
+
getConfig: vi.fn(async () => {
|
|
725
|
+
throw new Error("registry config fetch failed");
|
|
726
|
+
}),
|
|
727
|
+
};
|
|
728
|
+
const { dev } = await import("./dev.mjs");
|
|
729
|
+
await dev(buildDevOptions());
|
|
730
|
+
expect(execMock).not.toHaveBeenCalled();
|
|
731
|
+
expect(mockLogger.warn).toHaveBeenCalledWith("Could not resolve npm registry config for startup install validation; preserving package snapshot decision", expect.anything());
|
|
732
|
+
expect(mockLogger.info).toHaveBeenCalledWith("Package install decision", expect.objectContaining({
|
|
733
|
+
privateRegistryRequiresInstallValidation: false,
|
|
734
|
+
}));
|
|
735
|
+
});
|
|
457
736
|
});
|
|
458
737
|
// Wires up `execMock` so install calls hang until the test calls
|
|
459
738
|
// `finishInstall`. We stash the promisify callback directly rather than
|
|
@@ -503,6 +782,7 @@ function setupDeferredInstallMock(options = {}) {
|
|
|
503
782
|
describe("dev background package install join semantics", () => {
|
|
504
783
|
beforeEach(async () => {
|
|
505
784
|
vi.clearAllMocks();
|
|
785
|
+
mockAiServiceState.npmRegistryClient = undefined;
|
|
506
786
|
mockCheckVersions.mockResolvedValue({
|
|
507
787
|
cliUpdated: false,
|
|
508
788
|
upgradePromises: [],
|
|
@@ -543,31 +823,140 @@ describe("dev background package install join semantics", () => {
|
|
|
543
823
|
it("logs a structured error when the background install fails", async () => {
|
|
544
824
|
const deferred = setupDeferredInstallMock({ fail: true });
|
|
545
825
|
// Use upgradePromises so the install runs without forcing the upload
|
|
546
|
-
// path — the failure
|
|
547
|
-
//
|
|
548
|
-
//
|
|
826
|
+
// path — the failure propagates through the pre-Vite join. With the
|
|
827
|
+
// dependency-install-classifier fully wired in tests, the rejection is
|
|
828
|
+
// an InitialInstallFailed marker and `handleStartupError` routes it to
|
|
829
|
+
// "degrade" (Vite still starts up, dev() resolves with the error
|
|
830
|
+
// recorded on devServerStatus). The point of THIS test is the
|
|
831
|
+
// independent `.catch` backstop on packageInstallPromise — it must
|
|
832
|
+
// log a structured error regardless of which downstream join consumes
|
|
833
|
+
// the rejection.
|
|
549
834
|
mockCheckVersions.mockResolvedValue({
|
|
550
835
|
cliUpdated: false,
|
|
551
836
|
upgradePromises: [Promise.resolve()],
|
|
552
837
|
});
|
|
553
838
|
const { dev } = await import("./dev.mjs");
|
|
554
|
-
|
|
555
|
-
// the rejection synchronously with promise creation; without this, the
|
|
556
|
-
// .catch backstop's logging counts as a handler but lands on the
|
|
557
|
-
// microtask queue after vitest's check.
|
|
558
|
-
let caughtError;
|
|
559
|
-
const devPromise = dev(buildDevOptions()).catch((err) => {
|
|
560
|
-
caughtError = err;
|
|
561
|
-
});
|
|
839
|
+
const devPromise = dev(buildDevOptions()).catch(() => undefined);
|
|
562
840
|
await deferred.awaitInstallStarted();
|
|
563
841
|
await deferred.finishInstall();
|
|
564
842
|
await devPromise;
|
|
565
|
-
expect(caughtError).toBeDefined();
|
|
566
843
|
// The .catch backstop fires regardless and tags the log with errorId so
|
|
567
844
|
// alert rules can match without needing typed attributes.
|
|
568
845
|
expect(mockLogger.error.mock.calls.some((call) => typeof call[0] === "string" &&
|
|
569
846
|
call[0].includes("Background package install failed") &&
|
|
570
847
|
call[0].includes("errorId=DEV_SERVER_BG_INSTALL_FAILED"))).toBe(true);
|
|
571
848
|
});
|
|
849
|
+
// APPS-4450 review: an InitialInstallFailed observed in the "before upload"
|
|
850
|
+
// join must NOT preempt a successful CLI upgrade restart. Previously the
|
|
851
|
+
// join's rejection was caught by the outer sync/setup catch (→ degrade) and
|
|
852
|
+
// execution never reached the `hasCliUpdated` branch, so the restart was
|
|
853
|
+
// silently dropped. The fix catches the join locally when `hasCliUpdated`
|
|
854
|
+
// and falls through to the restart block.
|
|
855
|
+
it("prioritises CLI restart over install-failure degrade when both fire in the upload path", async () => {
|
|
856
|
+
const deferred = setupDeferredInstallMock({ fail: true });
|
|
857
|
+
// CLI upgrade succeeds + `forcePackageInstall` triggers the upload path.
|
|
858
|
+
mockCheckVersions.mockResolvedValue({
|
|
859
|
+
cliUpdated: true,
|
|
860
|
+
upgradePromises: [Promise.resolve()],
|
|
861
|
+
});
|
|
862
|
+
// Capture `process.exit` synchronously; the production path calls it on
|
|
863
|
+
// the restart with AUTO_UPGRADE_EXIT_CODE (mocked to 99). Track every
|
|
864
|
+
// call and throw a tagged error so the test can assert the exit code
|
|
865
|
+
// without actually terminating the vitest worker. We replace
|
|
866
|
+
// `process.exit` directly instead of `vi.spyOn` — vitest's `process`
|
|
867
|
+
// object is partially shadowed and `vi.spyOn(process, "exit")` swallows
|
|
868
|
+
// calls intermittently here.
|
|
869
|
+
const originalExit = process.exit;
|
|
870
|
+
const exitCalls = [];
|
|
871
|
+
process.exit = (code) => {
|
|
872
|
+
exitCalls.push(code);
|
|
873
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
874
|
+
};
|
|
875
|
+
try {
|
|
876
|
+
const { dev } = await import("./dev.mjs");
|
|
877
|
+
let caughtError;
|
|
878
|
+
const devPromise = dev(buildDevOptions({ forcePackageInstall: true })).catch((err) => {
|
|
879
|
+
caughtError = err;
|
|
880
|
+
});
|
|
881
|
+
await deferred.awaitInstallStarted();
|
|
882
|
+
await deferred.finishInstall();
|
|
883
|
+
await devPromise;
|
|
884
|
+
// First exit call MUST be the CLI-restart with AUTO_UPGRADE_EXIT_CODE
|
|
885
|
+
// (mocked to 99). The throw escapes through the outer catch, which
|
|
886
|
+
// routes the non-InitialInstallFailed thrown wrapper through
|
|
887
|
+
// handleStartupError → exit, so a follow-up `process.exit(1)` is
|
|
888
|
+
// expected and benign. The invariant is the FIRST call.
|
|
889
|
+
expect(exitCalls[0]).toBe(99);
|
|
890
|
+
// devPromise resolved (or rejected) after the chain settled.
|
|
891
|
+
expect(caughtError).toBeDefined();
|
|
892
|
+
// Upload was skipped — install failed before the upload body could run
|
|
893
|
+
// and the local catch routed straight to the restart branch.
|
|
894
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
895
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
896
|
+
// Restart actually got reached (logged + skip-upload reason logged).
|
|
897
|
+
expect(mockLogger.info).toHaveBeenCalledWith("CLI was updated, restarting the dev server…");
|
|
898
|
+
expect(mockLogger.info.mock.calls.some((call) => typeof call[0] === "string" &&
|
|
899
|
+
call[0].includes("Initial package install failed before startup upload"))).toBe(true);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
process.exit = originalExit;
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
// APPS-4450 review (companion to above): the new local try/catch in the
|
|
906
|
+
// upload path must only swallow `InitialInstallFailed`. A non-marker
|
|
907
|
+
// rejection from `joinUpgradeThenInstall("before upload")` (e.g. a failed
|
|
908
|
+
// package upgrade) must rethrow so the outer sync/setup catch routes it to
|
|
909
|
+
// `process.exit(1)` — even when `hasCliUpdated` is true. Otherwise an
|
|
910
|
+
// upgrade rejection could be silently dropped on the upload path.
|
|
911
|
+
it("rethrows non-InitialInstallFailed join errors so the outer catch exits", async () => {
|
|
912
|
+
// Install succeeds instantly — only the UPGRADE rejects, so the join's
|
|
913
|
+
// first await (`joinPackageUpgrade`) is what surfaces.
|
|
914
|
+
execMock.mockImplementation((_cmd, opts, cb) => {
|
|
915
|
+
const callback = typeof opts === "function"
|
|
916
|
+
? opts
|
|
917
|
+
: cb;
|
|
918
|
+
callback?.(null, { stdout: "installed" });
|
|
919
|
+
});
|
|
920
|
+
// hasCliUpdated=true ensures we'd hit the restart branch IF the local
|
|
921
|
+
// catch wrongly swallowed the rejection. A rejecting upgrade promise
|
|
922
|
+
// surfaces a generic Error (not InitialInstallFailed), so the local catch
|
|
923
|
+
// must rethrow.
|
|
924
|
+
mockCheckVersions.mockResolvedValue({
|
|
925
|
+
cliUpdated: true,
|
|
926
|
+
upgradePromises: [Promise.reject(new Error("simulated upgrade failure"))],
|
|
927
|
+
});
|
|
928
|
+
const originalExit = process.exit;
|
|
929
|
+
const exitCalls = [];
|
|
930
|
+
process.exit = (code) => {
|
|
931
|
+
exitCalls.push(code);
|
|
932
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
933
|
+
};
|
|
934
|
+
try {
|
|
935
|
+
const { dev } = await import("./dev.mjs");
|
|
936
|
+
let caughtError;
|
|
937
|
+
await dev(buildDevOptions({ forcePackageInstall: true })).catch((err) => {
|
|
938
|
+
caughtError = err;
|
|
939
|
+
});
|
|
940
|
+
// First exit MUST be the outer-catch exit(1), NOT the
|
|
941
|
+
// AUTO_UPGRADE_EXIT_CODE (99). If the local catch wrongly swallowed
|
|
942
|
+
// the upgrade rejection, execution would fall through to the
|
|
943
|
+
// `if (hasCliUpdated)` restart branch and call exit(99).
|
|
944
|
+
expect(exitCalls[0]).toBe(1);
|
|
945
|
+
expect(exitCalls[0]).not.toBe(99);
|
|
946
|
+
expect(caughtError).toBeDefined();
|
|
947
|
+
// Upload skipped — join rejected before the upload body could run.
|
|
948
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
949
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
950
|
+
// Restart was NOT reached — outer catch exited before that block.
|
|
951
|
+
expect(mockLogger.info).not.toHaveBeenCalledWith("CLI was updated, restarting the dev server…");
|
|
952
|
+
// The skip-upload-for-restart log must NOT fire either; it's gated on
|
|
953
|
+
// the `InitialInstallFailed` branch of the local catch.
|
|
954
|
+
expect(mockLogger.info.mock.calls.some((call) => typeof call[0] === "string" &&
|
|
955
|
+
call[0].includes("Initial package install failed before startup upload"))).toBe(false);
|
|
956
|
+
}
|
|
957
|
+
finally {
|
|
958
|
+
process.exit = originalExit;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
572
961
|
});
|
|
573
962
|
//# sourceMappingURL=dev-s3-restore.test.mjs.map
|