@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
|
@@ -8,9 +8,11 @@ import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
|
8
8
|
import { packageJsonSnapshot } from "./package-json-snapshot.mjs";
|
|
9
9
|
|
|
10
10
|
const execMock = vi.fn();
|
|
11
|
+
const execFileMock = vi.fn();
|
|
11
12
|
vi.mock("node:child_process", () => ({
|
|
12
|
-
default: { exec: execMock },
|
|
13
|
+
default: { exec: execMock, execFile: execFileMock },
|
|
13
14
|
exec: execMock,
|
|
15
|
+
execFile: execFileMock,
|
|
14
16
|
}));
|
|
15
17
|
|
|
16
18
|
vi.mock("node:readline", () => ({
|
|
@@ -56,6 +58,12 @@ const mockLogger = {
|
|
|
56
58
|
debug: vi.fn(),
|
|
57
59
|
};
|
|
58
60
|
|
|
61
|
+
const mockAiServiceState = vi.hoisted(() => ({
|
|
62
|
+
npmRegistryClient: undefined as
|
|
63
|
+
| { getConfig: () => Promise<Record<string, unknown>> }
|
|
64
|
+
| undefined,
|
|
65
|
+
}));
|
|
66
|
+
|
|
59
67
|
vi.mock("../telemetry/logging.js", () => ({
|
|
60
68
|
getLogger: () => mockLogger,
|
|
61
69
|
getErrorMeta: vi.fn(() => ({})),
|
|
@@ -83,7 +91,12 @@ vi.mock("colorette", () => ({
|
|
|
83
91
|
green: (s: string) => s,
|
|
84
92
|
}));
|
|
85
93
|
|
|
86
|
-
vi.mock("@superblocksteam/shared", () => ({
|
|
94
|
+
vi.mock("@superblocksteam/shared", async (importOriginal) => ({
|
|
95
|
+
// dev.mts now transitively loads @superblocksteam/shared (via the npm-error
|
|
96
|
+
// classifier re-exported from vite-plugin-file-sync/npm-registry — APPS-4450),
|
|
97
|
+
// pulling in many named exports (DeploymentTypeEnum, ISocket, …). Spread the
|
|
98
|
+
// real module so the closed overrides below don't have to enumerate them.
|
|
99
|
+
...((await importOriginal()) as Record<string, unknown>),
|
|
87
100
|
buildGithubSuperblocksSyncWorkflow: vi.fn(),
|
|
88
101
|
buildGithubSuperblocksSyncWorkflowFromBaseUrl: vi.fn(),
|
|
89
102
|
ConflictError: class ConflictError extends Error {},
|
|
@@ -104,6 +117,12 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
104
117
|
initialize = vi.fn(async () => undefined);
|
|
105
118
|
removeIntegrationCache = vi.fn(async () => undefined);
|
|
106
119
|
chatSessionStore = { invalidateCache: vi.fn() };
|
|
120
|
+
// `dev.mts` reads this at startup for two paths: `syncHomeNpmrc`
|
|
121
|
+
// (APPS-4231 home `~/.npmrc` writer) and the startup `installPackages`
|
|
122
|
+
// call (APPS-4251 per-org `allow_install_scripts`). Returning undefined
|
|
123
|
+
// keeps the install path on the "no policy resolved" branch and is also
|
|
124
|
+
// valid input for the mocked-out `syncHomeNpmrc`.
|
|
125
|
+
getNpmRegistryClient = vi.fn(() => mockAiServiceState.npmRegistryClient);
|
|
107
126
|
},
|
|
108
127
|
AiServiceFeatureFlags: class {
|
|
109
128
|
static create = vi.fn(() => ({}));
|
|
@@ -115,6 +134,21 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
115
134
|
stripResolvedFromLockfile: vi.fn(async () => undefined),
|
|
116
135
|
}));
|
|
117
136
|
|
|
137
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", () => ({
|
|
138
|
+
shouldIgnoreInstallScripts: vi.fn(() => false),
|
|
139
|
+
// Used by the dependency-install-classifier reachable from installPackages'
|
|
140
|
+
// catch path (APPS-4450). Previously the only test that hit this codepath
|
|
141
|
+
// (`logs a structured error when the background install fails`) didn't
|
|
142
|
+
// assert classification, so the missing exports surfaced silently as a
|
|
143
|
+
// ReferenceError instead of an InitialInstallFailed. The new
|
|
144
|
+
// restart-vs-degrade test depends on installPackages re-throwing an actual
|
|
145
|
+
// InitialInstallFailed marker, so stub all classifier-required exports.
|
|
146
|
+
scrubSecrets: vi.fn((value: string) => value),
|
|
147
|
+
parseNpmJsonDiagnostic: vi.fn(() => ""),
|
|
148
|
+
parseNpmJsonError: vi.fn(() => null),
|
|
149
|
+
classifyNpmExecStderr: vi.fn(() => null),
|
|
150
|
+
}));
|
|
151
|
+
|
|
118
152
|
vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
119
153
|
createGitService: vi.fn(async () => undefined),
|
|
120
154
|
}));
|
|
@@ -187,6 +221,25 @@ vi.mock("./automatic-upgrades.js", () => ({
|
|
|
187
221
|
checkVersionsAndWritePackageJson: (
|
|
188
222
|
...args: Parameters<typeof mockCheckVersions>
|
|
189
223
|
) => mockCheckVersions(...args),
|
|
224
|
+
buildInstallEnv: (userconfigPath: string) => ({
|
|
225
|
+
...process.env,
|
|
226
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
227
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
228
|
+
}),
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
// The home `~/.npmrc` writer (APPS-4231) is a best-effort step that runs
|
|
232
|
+
// before auto-upgrade. Mock it out here so these tests stay focused on the
|
|
233
|
+
// DBFS / S3 restore behaviour. The `AiService` mock above exposes a
|
|
234
|
+
// minimal `getNpmRegistryClient()` stub so the call site doesn't `TypeError`
|
|
235
|
+
// and silently swallow this mock.
|
|
236
|
+
vi.mock("./home-npmrc.mjs", () => ({
|
|
237
|
+
syncHomeNpmrc: vi.fn(async () => ({
|
|
238
|
+
outcome: "skipped-not-configured",
|
|
239
|
+
path: "/tmp/.superblocks/npmrc",
|
|
240
|
+
})),
|
|
241
|
+
superblocksNpmrcPath: vi.fn(() => "/tmp/.superblocks/npmrc"),
|
|
242
|
+
superblocksLogsPath: vi.fn((appDir: string) => `${appDir}/.superblocks/logs`),
|
|
190
243
|
}));
|
|
191
244
|
|
|
192
245
|
vi.mock("./git-repo-setup.mjs", () => ({
|
|
@@ -246,6 +299,8 @@ function buildDevOptions(overrides: Record<string, unknown> = {}) {
|
|
|
246
299
|
tokenManager: {
|
|
247
300
|
getToken: vi.fn(() => "test-token"),
|
|
248
301
|
onTokenRefresh: vi.fn(),
|
|
302
|
+
updateToken: vi.fn(),
|
|
303
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
249
304
|
},
|
|
250
305
|
getCurrentToken: vi.fn(() => "test-token"),
|
|
251
306
|
sdk: buildMockSdk(),
|
|
@@ -282,6 +337,7 @@ function expectInstallToHaveRun() {
|
|
|
282
337
|
describe("dev startup after S3 workspace restore", () => {
|
|
283
338
|
beforeEach(async () => {
|
|
284
339
|
vi.clearAllMocks();
|
|
340
|
+
mockAiServiceState.npmRegistryClient = undefined;
|
|
285
341
|
setupExecMock();
|
|
286
342
|
mockCheckVersions.mockResolvedValue({
|
|
287
343
|
cliUpdated: false,
|
|
@@ -597,6 +653,342 @@ describe("dev startup after S3 workspace restore", () => {
|
|
|
597
653
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
598
654
|
}
|
|
599
655
|
});
|
|
656
|
+
|
|
657
|
+
it("keeps forced npm install when unchanged restore inputs still need private registry validation", async () => {
|
|
658
|
+
mockAiServiceState.npmRegistryClient = {
|
|
659
|
+
getConfig: vi.fn(async () => ({
|
|
660
|
+
source: "configured",
|
|
661
|
+
config: { configured: true },
|
|
662
|
+
})),
|
|
663
|
+
};
|
|
664
|
+
const unchangedPackageJson = {
|
|
665
|
+
name: "test-app",
|
|
666
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
667
|
+
};
|
|
668
|
+
const { dev } = await import("./dev.mjs");
|
|
669
|
+
|
|
670
|
+
await dev(
|
|
671
|
+
buildDevOptions({
|
|
672
|
+
forcePackageInstall: true,
|
|
673
|
+
packageJsonSnapshotBeforeRestore:
|
|
674
|
+
packageJsonSnapshot(unchangedPackageJson),
|
|
675
|
+
}) as any,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
expectInstallToHaveRun();
|
|
679
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
680
|
+
"Package install decision",
|
|
681
|
+
expect.objectContaining({
|
|
682
|
+
forcePackageInstall: true,
|
|
683
|
+
forcePackageInstallRequested: true,
|
|
684
|
+
packageJsonRequiresInstall: false,
|
|
685
|
+
}),
|
|
686
|
+
);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("runs npm install at startup when private registries are configured even without package changes", async () => {
|
|
690
|
+
mockAiServiceState.npmRegistryClient = {
|
|
691
|
+
getConfig: vi.fn(async () => ({
|
|
692
|
+
source: "configured",
|
|
693
|
+
config: { configured: true },
|
|
694
|
+
})),
|
|
695
|
+
};
|
|
696
|
+
const unchangedPackageJson = {
|
|
697
|
+
name: "test-app",
|
|
698
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
699
|
+
};
|
|
700
|
+
const { dev } = await import("./dev.mjs");
|
|
701
|
+
|
|
702
|
+
await dev(
|
|
703
|
+
buildDevOptions({
|
|
704
|
+
forcePackageInstall: false,
|
|
705
|
+
packageJsonSnapshotBeforeRestore:
|
|
706
|
+
packageJsonSnapshot(unchangedPackageJson),
|
|
707
|
+
}) as any,
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
expectInstallToHaveRun();
|
|
711
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
712
|
+
"Package install decision",
|
|
713
|
+
expect.objectContaining({
|
|
714
|
+
forcePackageInstall: false,
|
|
715
|
+
forcePackageInstallRequested: false,
|
|
716
|
+
packageJsonRequiresInstall: false,
|
|
717
|
+
privateRegistryRequiresInstallValidation: true,
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("does not upload package state when private registry validation install leaves the lockfile unchanged", async () => {
|
|
723
|
+
const tmpDir = await fs.mkdtemp(
|
|
724
|
+
path.join(os.tmpdir(), "sdk-dev-private-registry-"),
|
|
725
|
+
);
|
|
726
|
+
const unchangedPackageJson = {
|
|
727
|
+
name: "test-app",
|
|
728
|
+
packageManager: "npm@10.0.0",
|
|
729
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
734
|
+
await fs.writeFile(
|
|
735
|
+
path.join(tmpDir, "package.json"),
|
|
736
|
+
JSON.stringify(unchangedPackageJson, null, 2),
|
|
737
|
+
);
|
|
738
|
+
await fs.writeFile(
|
|
739
|
+
path.join(tmpDir, "package-lock.json"),
|
|
740
|
+
JSON.stringify({ name: "test-app", lockfileVersion: 3 }, null, 2),
|
|
741
|
+
);
|
|
742
|
+
const { readPackage } = await import("read-pkg");
|
|
743
|
+
(readPackage as Mock).mockImplementation(async ({ cwd }) =>
|
|
744
|
+
JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")),
|
|
745
|
+
);
|
|
746
|
+
mockAiServiceState.npmRegistryClient = {
|
|
747
|
+
getConfig: vi.fn(async () => ({
|
|
748
|
+
source: "configured",
|
|
749
|
+
config: { configured: true },
|
|
750
|
+
})),
|
|
751
|
+
};
|
|
752
|
+
const { dev } = await import("./dev.mjs");
|
|
753
|
+
|
|
754
|
+
await dev(
|
|
755
|
+
buildDevOptions({
|
|
756
|
+
cwd: tmpDir,
|
|
757
|
+
forcePackageInstall: false,
|
|
758
|
+
packageJsonSnapshotBeforeRestore:
|
|
759
|
+
packageJsonSnapshot(unchangedPackageJson),
|
|
760
|
+
}) as any,
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
expectInstallToHaveRun();
|
|
764
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
765
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
766
|
+
} finally {
|
|
767
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("does NOT upload package state when the validation install only rewrites `resolved` URLs", async () => {
|
|
772
|
+
// The exact churn cycle flagged in review: `stripResolvedFromLockfile`
|
|
773
|
+
// removes every `resolved` URL before the install and npm writes them
|
|
774
|
+
// back, so a byte-level comparison would flag "changed" on EVERY
|
|
775
|
+
// registry-validation boot and re-upload the workspace to DBFS forever.
|
|
776
|
+
// `lockfileComparisonKey` ignores `resolved`, so a resolved-only rewrite
|
|
777
|
+
// must NOT trigger the upload.
|
|
778
|
+
const tmpDir = await fs.mkdtemp(
|
|
779
|
+
path.join(os.tmpdir(), "sdk-dev-private-registry-"),
|
|
780
|
+
);
|
|
781
|
+
const unchangedPackageJson = {
|
|
782
|
+
name: "test-app",
|
|
783
|
+
packageManager: "npm@10.0.0",
|
|
784
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
785
|
+
};
|
|
786
|
+
const strippedLockfile = {
|
|
787
|
+
name: "test-app",
|
|
788
|
+
lockfileVersion: 3,
|
|
789
|
+
packages: {
|
|
790
|
+
"node_modules/@superblocksteam/library": { version: "1.0.0" },
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
const rewrittenLockfile = {
|
|
794
|
+
name: "test-app",
|
|
795
|
+
lockfileVersion: 3,
|
|
796
|
+
packages: {
|
|
797
|
+
"node_modules/@superblocksteam/library": {
|
|
798
|
+
version: "1.0.0",
|
|
799
|
+
resolved:
|
|
800
|
+
"https://registry.example.com/@superblocksteam/library/-/library-1.0.0.tgz",
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
807
|
+
await fs.writeFile(
|
|
808
|
+
path.join(tmpDir, "package.json"),
|
|
809
|
+
JSON.stringify(unchangedPackageJson, null, 2),
|
|
810
|
+
);
|
|
811
|
+
await fs.writeFile(
|
|
812
|
+
path.join(tmpDir, "package-lock.json"),
|
|
813
|
+
JSON.stringify(strippedLockfile, null, 2),
|
|
814
|
+
);
|
|
815
|
+
const { readPackage } = await import("read-pkg");
|
|
816
|
+
(readPackage as Mock).mockImplementation(async ({ cwd }) =>
|
|
817
|
+
JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")),
|
|
818
|
+
);
|
|
819
|
+
mockAiServiceState.npmRegistryClient = {
|
|
820
|
+
getConfig: vi.fn(async () => ({
|
|
821
|
+
source: "configured",
|
|
822
|
+
config: { configured: true },
|
|
823
|
+
})),
|
|
824
|
+
};
|
|
825
|
+
execMock.mockImplementation(
|
|
826
|
+
(
|
|
827
|
+
_cmd: string,
|
|
828
|
+
opts:
|
|
829
|
+
| { cwd?: string }
|
|
830
|
+
| ((err: null, result: { stdout: string }) => void),
|
|
831
|
+
cb?: (err: null, result: { stdout: string }) => void,
|
|
832
|
+
) => {
|
|
833
|
+
const cwd = typeof opts === "object" ? opts.cwd : tmpDir;
|
|
834
|
+
void fs
|
|
835
|
+
.writeFile(
|
|
836
|
+
path.join(cwd ?? tmpDir, "package-lock.json"),
|
|
837
|
+
JSON.stringify(rewrittenLockfile, null, 2),
|
|
838
|
+
)
|
|
839
|
+
.then(() => {
|
|
840
|
+
if (typeof opts === "function") {
|
|
841
|
+
opts(null, { stdout: "installed" });
|
|
842
|
+
} else {
|
|
843
|
+
cb?.(null, { stdout: "installed" });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
},
|
|
847
|
+
);
|
|
848
|
+
const { dev } = await import("./dev.mjs");
|
|
849
|
+
|
|
850
|
+
await dev(
|
|
851
|
+
buildDevOptions({
|
|
852
|
+
cwd: tmpDir,
|
|
853
|
+
forcePackageInstall: false,
|
|
854
|
+
packageJsonSnapshotBeforeRestore:
|
|
855
|
+
packageJsonSnapshot(unchangedPackageJson),
|
|
856
|
+
}) as any,
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
expectInstallToHaveRun();
|
|
860
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
861
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
862
|
+
} finally {
|
|
863
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("uploads package state when private registry validation install rewrites the lockfile", async () => {
|
|
868
|
+
const tmpDir = await fs.mkdtemp(
|
|
869
|
+
path.join(os.tmpdir(), "sdk-dev-private-registry-"),
|
|
870
|
+
);
|
|
871
|
+
const unchangedPackageJson = {
|
|
872
|
+
name: "test-app",
|
|
873
|
+
packageManager: "npm@10.0.0",
|
|
874
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
try {
|
|
878
|
+
await fs.mkdir(path.join(tmpDir, "node_modules"));
|
|
879
|
+
await fs.writeFile(
|
|
880
|
+
path.join(tmpDir, "package.json"),
|
|
881
|
+
JSON.stringify(unchangedPackageJson, null, 2),
|
|
882
|
+
);
|
|
883
|
+
await fs.writeFile(
|
|
884
|
+
path.join(tmpDir, "package-lock.json"),
|
|
885
|
+
JSON.stringify({ name: "test-app", lockfileVersion: 3 }, null, 2),
|
|
886
|
+
);
|
|
887
|
+
const { readPackage } = await import("read-pkg");
|
|
888
|
+
(readPackage as Mock).mockImplementation(async ({ cwd }) =>
|
|
889
|
+
JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8")),
|
|
890
|
+
);
|
|
891
|
+
mockAiServiceState.npmRegistryClient = {
|
|
892
|
+
getConfig: vi.fn(async () => ({
|
|
893
|
+
source: "configured",
|
|
894
|
+
config: { configured: true },
|
|
895
|
+
})),
|
|
896
|
+
};
|
|
897
|
+
execMock.mockImplementation(
|
|
898
|
+
(
|
|
899
|
+
_cmd: string,
|
|
900
|
+
opts:
|
|
901
|
+
| { cwd?: string }
|
|
902
|
+
| ((err: null, result: { stdout: string }) => void),
|
|
903
|
+
cb?: (err: null, result: { stdout: string }) => void,
|
|
904
|
+
) => {
|
|
905
|
+
const cwd = typeof opts === "object" ? opts.cwd : tmpDir;
|
|
906
|
+
void fs
|
|
907
|
+
.writeFile(
|
|
908
|
+
path.join(cwd ?? tmpDir, "package-lock.json"),
|
|
909
|
+
JSON.stringify(
|
|
910
|
+
{ name: "test-app", lockfileVersion: 3, packages: {} },
|
|
911
|
+
null,
|
|
912
|
+
2,
|
|
913
|
+
),
|
|
914
|
+
)
|
|
915
|
+
.then(() => {
|
|
916
|
+
if (typeof opts === "function") {
|
|
917
|
+
opts(null, { stdout: "installed" });
|
|
918
|
+
} else {
|
|
919
|
+
cb?.(null, { stdout: "installed" });
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
},
|
|
923
|
+
);
|
|
924
|
+
const { dev } = await import("./dev.mjs");
|
|
925
|
+
|
|
926
|
+
await dev(
|
|
927
|
+
buildDevOptions({
|
|
928
|
+
cwd: tmpDir,
|
|
929
|
+
forcePackageInstall: false,
|
|
930
|
+
packageJsonSnapshotBeforeRestore:
|
|
931
|
+
packageJsonSnapshot(unchangedPackageJson),
|
|
932
|
+
}) as any,
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expectInstallToHaveRun();
|
|
936
|
+
expect(mockSyncService.uploadDirectory).toHaveBeenCalledWith("cli:sdk");
|
|
937
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).toHaveBeenCalledWith(
|
|
938
|
+
"cli:sdk",
|
|
939
|
+
);
|
|
940
|
+
} finally {
|
|
941
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("does NOT force the startup install when the registry config is unreachable (fail closed, APPS-4527)", async () => {
|
|
946
|
+
// `unreachable` with no last-known-good means we could not materialize
|
|
947
|
+
// the right `.npmrc`. Forcing an install would re-resolve a
|
|
948
|
+
// private-registry org's lockfile against whatever registry happens to
|
|
949
|
+
// be on disk — validating against the wrong host and writing its URLs
|
|
950
|
+
// back into the lockfile. Fail closed instead (APPS-4370 principle):
|
|
951
|
+
// keep today's snapshot decision and skip the install.
|
|
952
|
+
mockAiServiceState.npmRegistryClient = {
|
|
953
|
+
getConfig: vi.fn(async () => ({ source: "unreachable" })),
|
|
954
|
+
};
|
|
955
|
+
const { dev } = await import("./dev.mjs");
|
|
956
|
+
|
|
957
|
+
await dev(buildDevOptions() as any);
|
|
958
|
+
|
|
959
|
+
expect(execMock).not.toHaveBeenCalled();
|
|
960
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
961
|
+
"Package install decision",
|
|
962
|
+
expect.objectContaining({
|
|
963
|
+
packageJsonRequiresInstall: false,
|
|
964
|
+
forcePackageInstall: false,
|
|
965
|
+
privateRegistryRequiresInstallValidation: false,
|
|
966
|
+
}),
|
|
967
|
+
);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("does NOT force the startup install when registry config resolution throws (fail closed, APPS-4527)", async () => {
|
|
971
|
+
mockAiServiceState.npmRegistryClient = {
|
|
972
|
+
getConfig: vi.fn(async () => {
|
|
973
|
+
throw new Error("registry config fetch failed");
|
|
974
|
+
}),
|
|
975
|
+
};
|
|
976
|
+
const { dev } = await import("./dev.mjs");
|
|
977
|
+
|
|
978
|
+
await dev(buildDevOptions() as any);
|
|
979
|
+
|
|
980
|
+
expect(execMock).not.toHaveBeenCalled();
|
|
981
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
982
|
+
"Could not resolve npm registry config for startup install validation; preserving package snapshot decision",
|
|
983
|
+
expect.anything(),
|
|
984
|
+
);
|
|
985
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
986
|
+
"Package install decision",
|
|
987
|
+
expect.objectContaining({
|
|
988
|
+
privateRegistryRequiresInstallValidation: false,
|
|
989
|
+
}),
|
|
990
|
+
);
|
|
991
|
+
});
|
|
600
992
|
});
|
|
601
993
|
|
|
602
994
|
// Wires up `execMock` so install calls hang until the test calls
|
|
@@ -661,6 +1053,7 @@ function setupDeferredInstallMock(
|
|
|
661
1053
|
describe("dev background package install join semantics", () => {
|
|
662
1054
|
beforeEach(async () => {
|
|
663
1055
|
vi.clearAllMocks();
|
|
1056
|
+
mockAiServiceState.npmRegistryClient = undefined;
|
|
664
1057
|
mockCheckVersions.mockResolvedValue({
|
|
665
1058
|
cliUpdated: false,
|
|
666
1059
|
upgradePromises: [],
|
|
@@ -718,29 +1111,26 @@ describe("dev background package install join semantics", () => {
|
|
|
718
1111
|
it("logs a structured error when the background install fails", async () => {
|
|
719
1112
|
const deferred = setupDeferredInstallMock({ fail: true });
|
|
720
1113
|
// Use upgradePromises so the install runs without forcing the upload
|
|
721
|
-
// path — the failure
|
|
722
|
-
//
|
|
723
|
-
//
|
|
1114
|
+
// path — the failure propagates through the pre-Vite join. With the
|
|
1115
|
+
// dependency-install-classifier fully wired in tests, the rejection is
|
|
1116
|
+
// an InitialInstallFailed marker and `handleStartupError` routes it to
|
|
1117
|
+
// "degrade" (Vite still starts up, dev() resolves with the error
|
|
1118
|
+
// recorded on devServerStatus). The point of THIS test is the
|
|
1119
|
+
// independent `.catch` backstop on packageInstallPromise — it must
|
|
1120
|
+
// log a structured error regardless of which downstream join consumes
|
|
1121
|
+
// the rejection.
|
|
724
1122
|
mockCheckVersions.mockResolvedValue({
|
|
725
1123
|
cliUpdated: false,
|
|
726
1124
|
upgradePromises: [Promise.resolve()],
|
|
727
1125
|
});
|
|
728
1126
|
const { dev } = await import("./dev.mjs");
|
|
729
1127
|
|
|
730
|
-
|
|
731
|
-
// the rejection synchronously with promise creation; without this, the
|
|
732
|
-
// .catch backstop's logging counts as a handler but lands on the
|
|
733
|
-
// microtask queue after vitest's check.
|
|
734
|
-
let caughtError: unknown;
|
|
735
|
-
const devPromise = dev(buildDevOptions() as any).catch((err: unknown) => {
|
|
736
|
-
caughtError = err;
|
|
737
|
-
});
|
|
1128
|
+
const devPromise = dev(buildDevOptions() as any).catch(() => undefined);
|
|
738
1129
|
|
|
739
1130
|
await deferred.awaitInstallStarted();
|
|
740
1131
|
await deferred.finishInstall();
|
|
741
1132
|
await devPromise;
|
|
742
1133
|
|
|
743
|
-
expect(caughtError).toBeDefined();
|
|
744
1134
|
// The .catch backstop fires regardless and tags the log with errorId so
|
|
745
1135
|
// alert rules can match without needing typed attributes.
|
|
746
1136
|
expect(
|
|
@@ -752,4 +1142,154 @@ describe("dev background package install join semantics", () => {
|
|
|
752
1142
|
),
|
|
753
1143
|
).toBe(true);
|
|
754
1144
|
});
|
|
1145
|
+
|
|
1146
|
+
// APPS-4450 review: an InitialInstallFailed observed in the "before upload"
|
|
1147
|
+
// join must NOT preempt a successful CLI upgrade restart. Previously the
|
|
1148
|
+
// join's rejection was caught by the outer sync/setup catch (→ degrade) and
|
|
1149
|
+
// execution never reached the `hasCliUpdated` branch, so the restart was
|
|
1150
|
+
// silently dropped. The fix catches the join locally when `hasCliUpdated`
|
|
1151
|
+
// and falls through to the restart block.
|
|
1152
|
+
it("prioritises CLI restart over install-failure degrade when both fire in the upload path", async () => {
|
|
1153
|
+
const deferred = setupDeferredInstallMock({ fail: true });
|
|
1154
|
+
// CLI upgrade succeeds + `forcePackageInstall` triggers the upload path.
|
|
1155
|
+
mockCheckVersions.mockResolvedValue({
|
|
1156
|
+
cliUpdated: true,
|
|
1157
|
+
upgradePromises: [Promise.resolve()],
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Capture `process.exit` synchronously; the production path calls it on
|
|
1161
|
+
// the restart with AUTO_UPGRADE_EXIT_CODE (mocked to 99). Track every
|
|
1162
|
+
// call and throw a tagged error so the test can assert the exit code
|
|
1163
|
+
// without actually terminating the vitest worker. We replace
|
|
1164
|
+
// `process.exit` directly instead of `vi.spyOn` — vitest's `process`
|
|
1165
|
+
// object is partially shadowed and `vi.spyOn(process, "exit")` swallows
|
|
1166
|
+
// calls intermittently here.
|
|
1167
|
+
const originalExit = process.exit;
|
|
1168
|
+
const exitCalls: Array<number | undefined> = [];
|
|
1169
|
+
(process as any).exit = (code?: number) => {
|
|
1170
|
+
exitCalls.push(code);
|
|
1171
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
try {
|
|
1175
|
+
const { dev } = await import("./dev.mjs");
|
|
1176
|
+
|
|
1177
|
+
let caughtError: unknown;
|
|
1178
|
+
const devPromise = dev(
|
|
1179
|
+
buildDevOptions({ forcePackageInstall: true }) as any,
|
|
1180
|
+
).catch((err: unknown) => {
|
|
1181
|
+
caughtError = err;
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
await deferred.awaitInstallStarted();
|
|
1185
|
+
await deferred.finishInstall();
|
|
1186
|
+
await devPromise;
|
|
1187
|
+
|
|
1188
|
+
// First exit call MUST be the CLI-restart with AUTO_UPGRADE_EXIT_CODE
|
|
1189
|
+
// (mocked to 99). The throw escapes through the outer catch, which
|
|
1190
|
+
// routes the non-InitialInstallFailed thrown wrapper through
|
|
1191
|
+
// handleStartupError → exit, so a follow-up `process.exit(1)` is
|
|
1192
|
+
// expected and benign. The invariant is the FIRST call.
|
|
1193
|
+
expect(exitCalls[0]).toBe(99);
|
|
1194
|
+
// devPromise resolved (or rejected) after the chain settled.
|
|
1195
|
+
expect(caughtError).toBeDefined();
|
|
1196
|
+
// Upload was skipped — install failed before the upload body could run
|
|
1197
|
+
// and the local catch routed straight to the restart branch.
|
|
1198
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
1199
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
1200
|
+
// Restart actually got reached (logged + skip-upload reason logged).
|
|
1201
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
1202
|
+
"CLI was updated, restarting the dev server…",
|
|
1203
|
+
);
|
|
1204
|
+
expect(
|
|
1205
|
+
mockLogger.info.mock.calls.some(
|
|
1206
|
+
(call) =>
|
|
1207
|
+
typeof call[0] === "string" &&
|
|
1208
|
+
call[0].includes(
|
|
1209
|
+
"Initial package install failed before startup upload",
|
|
1210
|
+
),
|
|
1211
|
+
),
|
|
1212
|
+
).toBe(true);
|
|
1213
|
+
} finally {
|
|
1214
|
+
(process as any).exit = originalExit;
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// APPS-4450 review (companion to above): the new local try/catch in the
|
|
1219
|
+
// upload path must only swallow `InitialInstallFailed`. A non-marker
|
|
1220
|
+
// rejection from `joinUpgradeThenInstall("before upload")` (e.g. a failed
|
|
1221
|
+
// package upgrade) must rethrow so the outer sync/setup catch routes it to
|
|
1222
|
+
// `process.exit(1)` — even when `hasCliUpdated` is true. Otherwise an
|
|
1223
|
+
// upgrade rejection could be silently dropped on the upload path.
|
|
1224
|
+
it("rethrows non-InitialInstallFailed join errors so the outer catch exits", async () => {
|
|
1225
|
+
// Install succeeds instantly — only the UPGRADE rejects, so the join's
|
|
1226
|
+
// first await (`joinPackageUpgrade`) is what surfaces.
|
|
1227
|
+
execMock.mockImplementation(
|
|
1228
|
+
(
|
|
1229
|
+
_cmd: string,
|
|
1230
|
+
opts: unknown,
|
|
1231
|
+
cb?: (err: Error | null, result?: { stdout: string }) => void,
|
|
1232
|
+
) => {
|
|
1233
|
+
const callback =
|
|
1234
|
+
typeof opts === "function"
|
|
1235
|
+
? (opts as (err: Error | null, result?: { stdout: string }) => void)
|
|
1236
|
+
: cb;
|
|
1237
|
+
callback?.(null, { stdout: "installed" });
|
|
1238
|
+
},
|
|
1239
|
+
);
|
|
1240
|
+
// hasCliUpdated=true ensures we'd hit the restart branch IF the local
|
|
1241
|
+
// catch wrongly swallowed the rejection. A rejecting upgrade promise
|
|
1242
|
+
// surfaces a generic Error (not InitialInstallFailed), so the local catch
|
|
1243
|
+
// must rethrow.
|
|
1244
|
+
mockCheckVersions.mockResolvedValue({
|
|
1245
|
+
cliUpdated: true,
|
|
1246
|
+
upgradePromises: [Promise.reject(new Error("simulated upgrade failure"))],
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const originalExit = process.exit;
|
|
1250
|
+
const exitCalls: Array<number | undefined> = [];
|
|
1251
|
+
(process as any).exit = (code?: number) => {
|
|
1252
|
+
exitCalls.push(code);
|
|
1253
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
try {
|
|
1257
|
+
const { dev } = await import("./dev.mjs");
|
|
1258
|
+
|
|
1259
|
+
let caughtError: unknown;
|
|
1260
|
+
await dev(buildDevOptions({ forcePackageInstall: true }) as any).catch(
|
|
1261
|
+
(err: unknown) => {
|
|
1262
|
+
caughtError = err;
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
// First exit MUST be the outer-catch exit(1), NOT the
|
|
1267
|
+
// AUTO_UPGRADE_EXIT_CODE (99). If the local catch wrongly swallowed
|
|
1268
|
+
// the upgrade rejection, execution would fall through to the
|
|
1269
|
+
// `if (hasCliUpdated)` restart branch and call exit(99).
|
|
1270
|
+
expect(exitCalls[0]).toBe(1);
|
|
1271
|
+
expect(exitCalls[0]).not.toBe(99);
|
|
1272
|
+
expect(caughtError).toBeDefined();
|
|
1273
|
+
// Upload skipped — join rejected before the upload body could run.
|
|
1274
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
1275
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
1276
|
+
// Restart was NOT reached — outer catch exited before that block.
|
|
1277
|
+
expect(mockLogger.info).not.toHaveBeenCalledWith(
|
|
1278
|
+
"CLI was updated, restarting the dev server…",
|
|
1279
|
+
);
|
|
1280
|
+
// The skip-upload-for-restart log must NOT fire either; it's gated on
|
|
1281
|
+
// the `InitialInstallFailed` branch of the local catch.
|
|
1282
|
+
expect(
|
|
1283
|
+
mockLogger.info.mock.calls.some(
|
|
1284
|
+
(call) =>
|
|
1285
|
+
typeof call[0] === "string" &&
|
|
1286
|
+
call[0].includes(
|
|
1287
|
+
"Initial package install failed before startup upload",
|
|
1288
|
+
),
|
|
1289
|
+
),
|
|
1290
|
+
).toBe(false);
|
|
1291
|
+
} finally {
|
|
1292
|
+
(process as any).exit = originalExit;
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
755
1295
|
});
|