@superblocksteam/sdk 2.0.123-next.0 → 2.0.124-next.0
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 +170 -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 +36 -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 +396 -62
- 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 +210 -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 +495 -92
- 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", () => ({
|
|
@@ -83,7 +85,12 @@ vi.mock("colorette", () => ({
|
|
|
83
85
|
green: (s: string) => s,
|
|
84
86
|
}));
|
|
85
87
|
|
|
86
|
-
vi.mock("@superblocksteam/shared", () => ({
|
|
88
|
+
vi.mock("@superblocksteam/shared", async (importOriginal) => ({
|
|
89
|
+
// dev.mts now transitively loads @superblocksteam/shared (via the npm-error
|
|
90
|
+
// classifier re-exported from vite-plugin-file-sync/npm-registry — APPS-4450),
|
|
91
|
+
// pulling in many named exports (DeploymentTypeEnum, ISocket, …). Spread the
|
|
92
|
+
// real module so the closed overrides below don't have to enumerate them.
|
|
93
|
+
...((await importOriginal()) as Record<string, unknown>),
|
|
87
94
|
buildGithubSuperblocksSyncWorkflow: vi.fn(),
|
|
88
95
|
buildGithubSuperblocksSyncWorkflowFromBaseUrl: vi.fn(),
|
|
89
96
|
ConflictError: class ConflictError extends Error {},
|
|
@@ -104,6 +111,12 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
104
111
|
initialize = vi.fn(async () => undefined);
|
|
105
112
|
removeIntegrationCache = vi.fn(async () => undefined);
|
|
106
113
|
chatSessionStore = { invalidateCache: vi.fn() };
|
|
114
|
+
// `dev.mts` reads this at startup for two paths: `syncHomeNpmrc`
|
|
115
|
+
// (APPS-4231 home `~/.npmrc` writer) and the startup `installPackages`
|
|
116
|
+
// call (APPS-4251 per-org `allow_install_scripts`). Returning undefined
|
|
117
|
+
// keeps the install path on the "no policy resolved" branch and is also
|
|
118
|
+
// valid input for the mocked-out `syncHomeNpmrc`.
|
|
119
|
+
getNpmRegistryClient = vi.fn(() => undefined);
|
|
107
120
|
},
|
|
108
121
|
AiServiceFeatureFlags: class {
|
|
109
122
|
static create = vi.fn(() => ({}));
|
|
@@ -115,6 +128,21 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
115
128
|
stripResolvedFromLockfile: vi.fn(async () => undefined),
|
|
116
129
|
}));
|
|
117
130
|
|
|
131
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", () => ({
|
|
132
|
+
shouldIgnoreInstallScripts: vi.fn(() => false),
|
|
133
|
+
// Used by the dependency-install-classifier reachable from installPackages'
|
|
134
|
+
// catch path (APPS-4450). Previously the only test that hit this codepath
|
|
135
|
+
// (`logs a structured error when the background install fails`) didn't
|
|
136
|
+
// assert classification, so the missing exports surfaced silently as a
|
|
137
|
+
// ReferenceError instead of an InitialInstallFailed. The new
|
|
138
|
+
// restart-vs-degrade test depends on installPackages re-throwing an actual
|
|
139
|
+
// InitialInstallFailed marker, so stub all classifier-required exports.
|
|
140
|
+
scrubSecrets: vi.fn((value: string) => value),
|
|
141
|
+
parseNpmJsonDiagnostic: vi.fn(() => ""),
|
|
142
|
+
parseNpmJsonError: vi.fn(() => null),
|
|
143
|
+
classifyNpmExecStderr: vi.fn(() => null),
|
|
144
|
+
}));
|
|
145
|
+
|
|
118
146
|
vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
119
147
|
createGitService: vi.fn(async () => undefined),
|
|
120
148
|
}));
|
|
@@ -187,6 +215,25 @@ vi.mock("./automatic-upgrades.js", () => ({
|
|
|
187
215
|
checkVersionsAndWritePackageJson: (
|
|
188
216
|
...args: Parameters<typeof mockCheckVersions>
|
|
189
217
|
) => mockCheckVersions(...args),
|
|
218
|
+
buildInstallEnv: (userconfigPath: string) => ({
|
|
219
|
+
...process.env,
|
|
220
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
221
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
222
|
+
}),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
// The home `~/.npmrc` writer (APPS-4231) is a best-effort step that runs
|
|
226
|
+
// before auto-upgrade. Mock it out here so these tests stay focused on the
|
|
227
|
+
// DBFS / S3 restore behaviour. The `AiService` mock above exposes a
|
|
228
|
+
// minimal `getNpmRegistryClient()` stub so the call site doesn't `TypeError`
|
|
229
|
+
// and silently swallow this mock.
|
|
230
|
+
vi.mock("./home-npmrc.mjs", () => ({
|
|
231
|
+
syncHomeNpmrc: vi.fn(async () => ({
|
|
232
|
+
outcome: "skipped-not-configured",
|
|
233
|
+
path: "/tmp/.superblocks/npmrc",
|
|
234
|
+
})),
|
|
235
|
+
superblocksNpmrcPath: vi.fn(() => "/tmp/.superblocks/npmrc"),
|
|
236
|
+
superblocksLogsPath: vi.fn((appDir: string) => `${appDir}/.superblocks/logs`),
|
|
190
237
|
}));
|
|
191
238
|
|
|
192
239
|
vi.mock("./git-repo-setup.mjs", () => ({
|
|
@@ -246,6 +293,8 @@ function buildDevOptions(overrides: Record<string, unknown> = {}) {
|
|
|
246
293
|
tokenManager: {
|
|
247
294
|
getToken: vi.fn(() => "test-token"),
|
|
248
295
|
onTokenRefresh: vi.fn(),
|
|
296
|
+
updateToken: vi.fn(),
|
|
297
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
249
298
|
},
|
|
250
299
|
getCurrentToken: vi.fn(() => "test-token"),
|
|
251
300
|
sdk: buildMockSdk(),
|
|
@@ -718,29 +767,26 @@ describe("dev background package install join semantics", () => {
|
|
|
718
767
|
it("logs a structured error when the background install fails", async () => {
|
|
719
768
|
const deferred = setupDeferredInstallMock({ fail: true });
|
|
720
769
|
// Use upgradePromises so the install runs without forcing the upload
|
|
721
|
-
// path — the failure
|
|
722
|
-
//
|
|
723
|
-
//
|
|
770
|
+
// path — the failure propagates through the pre-Vite join. With the
|
|
771
|
+
// dependency-install-classifier fully wired in tests, the rejection is
|
|
772
|
+
// an InitialInstallFailed marker and `handleStartupError` routes it to
|
|
773
|
+
// "degrade" (Vite still starts up, dev() resolves with the error
|
|
774
|
+
// recorded on devServerStatus). The point of THIS test is the
|
|
775
|
+
// independent `.catch` backstop on packageInstallPromise — it must
|
|
776
|
+
// log a structured error regardless of which downstream join consumes
|
|
777
|
+
// the rejection.
|
|
724
778
|
mockCheckVersions.mockResolvedValue({
|
|
725
779
|
cliUpdated: false,
|
|
726
780
|
upgradePromises: [Promise.resolve()],
|
|
727
781
|
});
|
|
728
782
|
const { dev } = await import("./dev.mjs");
|
|
729
783
|
|
|
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
|
-
});
|
|
784
|
+
const devPromise = dev(buildDevOptions() as any).catch(() => undefined);
|
|
738
785
|
|
|
739
786
|
await deferred.awaitInstallStarted();
|
|
740
787
|
await deferred.finishInstall();
|
|
741
788
|
await devPromise;
|
|
742
789
|
|
|
743
|
-
expect(caughtError).toBeDefined();
|
|
744
790
|
// The .catch backstop fires regardless and tags the log with errorId so
|
|
745
791
|
// alert rules can match without needing typed attributes.
|
|
746
792
|
expect(
|
|
@@ -752,4 +798,154 @@ describe("dev background package install join semantics", () => {
|
|
|
752
798
|
),
|
|
753
799
|
).toBe(true);
|
|
754
800
|
});
|
|
801
|
+
|
|
802
|
+
// APPS-4450 review: an InitialInstallFailed observed in the "before upload"
|
|
803
|
+
// join must NOT preempt a successful CLI upgrade restart. Previously the
|
|
804
|
+
// join's rejection was caught by the outer sync/setup catch (→ degrade) and
|
|
805
|
+
// execution never reached the `hasCliUpdated` branch, so the restart was
|
|
806
|
+
// silently dropped. The fix catches the join locally when `hasCliUpdated`
|
|
807
|
+
// and falls through to the restart block.
|
|
808
|
+
it("prioritises CLI restart over install-failure degrade when both fire in the upload path", async () => {
|
|
809
|
+
const deferred = setupDeferredInstallMock({ fail: true });
|
|
810
|
+
// CLI upgrade succeeds + `forcePackageInstall` triggers the upload path.
|
|
811
|
+
mockCheckVersions.mockResolvedValue({
|
|
812
|
+
cliUpdated: true,
|
|
813
|
+
upgradePromises: [Promise.resolve()],
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Capture `process.exit` synchronously; the production path calls it on
|
|
817
|
+
// the restart with AUTO_UPGRADE_EXIT_CODE (mocked to 99). Track every
|
|
818
|
+
// call and throw a tagged error so the test can assert the exit code
|
|
819
|
+
// without actually terminating the vitest worker. We replace
|
|
820
|
+
// `process.exit` directly instead of `vi.spyOn` — vitest's `process`
|
|
821
|
+
// object is partially shadowed and `vi.spyOn(process, "exit")` swallows
|
|
822
|
+
// calls intermittently here.
|
|
823
|
+
const originalExit = process.exit;
|
|
824
|
+
const exitCalls: Array<number | undefined> = [];
|
|
825
|
+
(process as any).exit = (code?: number) => {
|
|
826
|
+
exitCalls.push(code);
|
|
827
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
const { dev } = await import("./dev.mjs");
|
|
832
|
+
|
|
833
|
+
let caughtError: unknown;
|
|
834
|
+
const devPromise = dev(
|
|
835
|
+
buildDevOptions({ forcePackageInstall: true }) as any,
|
|
836
|
+
).catch((err: unknown) => {
|
|
837
|
+
caughtError = err;
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await deferred.awaitInstallStarted();
|
|
841
|
+
await deferred.finishInstall();
|
|
842
|
+
await devPromise;
|
|
843
|
+
|
|
844
|
+
// First exit call MUST be the CLI-restart with AUTO_UPGRADE_EXIT_CODE
|
|
845
|
+
// (mocked to 99). The throw escapes through the outer catch, which
|
|
846
|
+
// routes the non-InitialInstallFailed thrown wrapper through
|
|
847
|
+
// handleStartupError → exit, so a follow-up `process.exit(1)` is
|
|
848
|
+
// expected and benign. The invariant is the FIRST call.
|
|
849
|
+
expect(exitCalls[0]).toBe(99);
|
|
850
|
+
// devPromise resolved (or rejected) after the chain settled.
|
|
851
|
+
expect(caughtError).toBeDefined();
|
|
852
|
+
// Upload was skipped — install failed before the upload body could run
|
|
853
|
+
// and the local catch routed straight to the restart branch.
|
|
854
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
855
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
856
|
+
// Restart actually got reached (logged + skip-upload reason logged).
|
|
857
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
858
|
+
"CLI was updated, restarting the dev server…",
|
|
859
|
+
);
|
|
860
|
+
expect(
|
|
861
|
+
mockLogger.info.mock.calls.some(
|
|
862
|
+
(call) =>
|
|
863
|
+
typeof call[0] === "string" &&
|
|
864
|
+
call[0].includes(
|
|
865
|
+
"Initial package install failed before startup upload",
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
).toBe(true);
|
|
869
|
+
} finally {
|
|
870
|
+
(process as any).exit = originalExit;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// APPS-4450 review (companion to above): the new local try/catch in the
|
|
875
|
+
// upload path must only swallow `InitialInstallFailed`. A non-marker
|
|
876
|
+
// rejection from `joinUpgradeThenInstall("before upload")` (e.g. a failed
|
|
877
|
+
// package upgrade) must rethrow so the outer sync/setup catch routes it to
|
|
878
|
+
// `process.exit(1)` — even when `hasCliUpdated` is true. Otherwise an
|
|
879
|
+
// upgrade rejection could be silently dropped on the upload path.
|
|
880
|
+
it("rethrows non-InitialInstallFailed join errors so the outer catch exits", async () => {
|
|
881
|
+
// Install succeeds instantly — only the UPGRADE rejects, so the join's
|
|
882
|
+
// first await (`joinPackageUpgrade`) is what surfaces.
|
|
883
|
+
execMock.mockImplementation(
|
|
884
|
+
(
|
|
885
|
+
_cmd: string,
|
|
886
|
+
opts: unknown,
|
|
887
|
+
cb?: (err: Error | null, result?: { stdout: string }) => void,
|
|
888
|
+
) => {
|
|
889
|
+
const callback =
|
|
890
|
+
typeof opts === "function"
|
|
891
|
+
? (opts as (err: Error | null, result?: { stdout: string }) => void)
|
|
892
|
+
: cb;
|
|
893
|
+
callback?.(null, { stdout: "installed" });
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
// hasCliUpdated=true ensures we'd hit the restart branch IF the local
|
|
897
|
+
// catch wrongly swallowed the rejection. A rejecting upgrade promise
|
|
898
|
+
// surfaces a generic Error (not InitialInstallFailed), so the local catch
|
|
899
|
+
// must rethrow.
|
|
900
|
+
mockCheckVersions.mockResolvedValue({
|
|
901
|
+
cliUpdated: true,
|
|
902
|
+
upgradePromises: [Promise.reject(new Error("simulated upgrade failure"))],
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const originalExit = process.exit;
|
|
906
|
+
const exitCalls: Array<number | undefined> = [];
|
|
907
|
+
(process as any).exit = (code?: number) => {
|
|
908
|
+
exitCalls.push(code);
|
|
909
|
+
throw new Error(`__test_process_exit__:${String(code)}`);
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
try {
|
|
913
|
+
const { dev } = await import("./dev.mjs");
|
|
914
|
+
|
|
915
|
+
let caughtError: unknown;
|
|
916
|
+
await dev(buildDevOptions({ forcePackageInstall: true }) as any).catch(
|
|
917
|
+
(err: unknown) => {
|
|
918
|
+
caughtError = err;
|
|
919
|
+
},
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// First exit MUST be the outer-catch exit(1), NOT the
|
|
923
|
+
// AUTO_UPGRADE_EXIT_CODE (99). If the local catch wrongly swallowed
|
|
924
|
+
// the upgrade rejection, execution would fall through to the
|
|
925
|
+
// `if (hasCliUpdated)` restart branch and call exit(99).
|
|
926
|
+
expect(exitCalls[0]).toBe(1);
|
|
927
|
+
expect(exitCalls[0]).not.toBe(99);
|
|
928
|
+
expect(caughtError).toBeDefined();
|
|
929
|
+
// Upload skipped — join rejected before the upload body could run.
|
|
930
|
+
expect(mockSyncService.uploadDirectory).not.toHaveBeenCalled();
|
|
931
|
+
expect(mockSyncService.uploadDirectoryNowIfNeeded).not.toHaveBeenCalled();
|
|
932
|
+
// Restart was NOT reached — outer catch exited before that block.
|
|
933
|
+
expect(mockLogger.info).not.toHaveBeenCalledWith(
|
|
934
|
+
"CLI was updated, restarting the dev server…",
|
|
935
|
+
);
|
|
936
|
+
// The skip-upload-for-restart log must NOT fire either; it's gated on
|
|
937
|
+
// the `InitialInstallFailed` branch of the local catch.
|
|
938
|
+
expect(
|
|
939
|
+
mockLogger.info.mock.calls.some(
|
|
940
|
+
(call) =>
|
|
941
|
+
typeof call[0] === "string" &&
|
|
942
|
+
call[0].includes(
|
|
943
|
+
"Initial package install failed before startup upload",
|
|
944
|
+
),
|
|
945
|
+
),
|
|
946
|
+
).toBe(false);
|
|
947
|
+
} finally {
|
|
948
|
+
(process as any).exit = originalExit;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
755
951
|
});
|
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
} from "vitest";
|
|
15
15
|
|
|
16
16
|
const execMock = vi.fn();
|
|
17
|
+
const execFileMock = vi.fn();
|
|
17
18
|
vi.mock("node:child_process", () => ({
|
|
18
|
-
default: { exec: execMock },
|
|
19
|
+
default: { exec: execMock, execFile: execFileMock },
|
|
19
20
|
exec: execMock,
|
|
21
|
+
execFile: execFileMock,
|
|
20
22
|
}));
|
|
21
23
|
|
|
22
24
|
vi.mock("node:readline", () => ({
|
|
@@ -89,7 +91,12 @@ vi.mock("colorette", () => ({
|
|
|
89
91
|
green: (s: string) => s,
|
|
90
92
|
}));
|
|
91
93
|
|
|
92
|
-
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>),
|
|
93
100
|
buildGithubSuperblocksSyncWorkflow: vi.fn(),
|
|
94
101
|
buildGithubSuperblocksSyncWorkflowFromBaseUrl: vi.fn(),
|
|
95
102
|
ConflictError: class ConflictError extends Error {},
|
|
@@ -110,6 +117,7 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
110
117
|
initialize = vi.fn(async () => undefined);
|
|
111
118
|
removeIntegrationCache = vi.fn(async () => undefined);
|
|
112
119
|
chatSessionStore = { invalidateCache: vi.fn() };
|
|
120
|
+
getNpmRegistryClient = vi.fn(() => undefined);
|
|
113
121
|
},
|
|
114
122
|
AiServiceFeatureFlags: class {
|
|
115
123
|
static create = vi.fn(() => ({}));
|
|
@@ -121,6 +129,10 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
|
121
129
|
stripResolvedFromLockfile: vi.fn(async () => undefined),
|
|
122
130
|
}));
|
|
123
131
|
|
|
132
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", () => ({
|
|
133
|
+
shouldIgnoreInstallScripts: vi.fn(() => false),
|
|
134
|
+
}));
|
|
135
|
+
|
|
124
136
|
const { startupOpLog, buildMockGitService } = vi.hoisted(() => {
|
|
125
137
|
const startupOpLog: string[] = [];
|
|
126
138
|
function buildMockGitService(cwd: string) {
|
|
@@ -254,6 +266,25 @@ vi.mock("./automatic-upgrades.js", () => ({
|
|
|
254
266
|
checkVersionsAndWritePackageJson: (
|
|
255
267
|
...args: Parameters<typeof mockCheckVersions>
|
|
256
268
|
) => mockCheckVersions(...args),
|
|
269
|
+
buildInstallEnv: (userconfigPath: string) => ({
|
|
270
|
+
...process.env,
|
|
271
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
272
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
273
|
+
}),
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
// The home `~/.npmrc` writer (APPS-4231) is a best-effort step that runs
|
|
277
|
+
// before auto-upgrade. Mock it out here so this test stays focused on the
|
|
278
|
+
// git-before-DBFS ordering. The `AiService` mock above exposes a minimal
|
|
279
|
+
// `getNpmRegistryClient()` stub so the call site doesn't `TypeError`
|
|
280
|
+
// and silently swallow this mock.
|
|
281
|
+
vi.mock("./home-npmrc.mjs", () => ({
|
|
282
|
+
syncHomeNpmrc: vi.fn(async () => ({
|
|
283
|
+
outcome: "skipped-not-configured",
|
|
284
|
+
path: "/tmp/.superblocks/npmrc",
|
|
285
|
+
})),
|
|
286
|
+
superblocksNpmrcPath: vi.fn(() => "/tmp/.superblocks/npmrc"),
|
|
287
|
+
superblocksLogsPath: vi.fn((appDir: string) => `${appDir}/.superblocks/logs`),
|
|
257
288
|
}));
|
|
258
289
|
|
|
259
290
|
vi.mock("./git-repo-setup.mjs", () => ({
|
|
@@ -330,6 +361,8 @@ function buildDevOptions(cwd: string, overrides: Record<string, unknown> = {}) {
|
|
|
330
361
|
tokenManager: {
|
|
331
362
|
getToken: vi.fn(() => "test-token"),
|
|
332
363
|
onTokenRefresh: vi.fn(),
|
|
364
|
+
updateToken: vi.fn(),
|
|
365
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
333
366
|
},
|
|
334
367
|
getCurrentToken: vi.fn(() => "test-token"),
|
|
335
368
|
sdk: buildMockSdk(),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the cold-start `TokenManager` priming step (APPS-4231 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Background
|
|
5
|
+
* ----------
|
|
6
|
+
* `NpmRegistryClient.getJwt()` (`packages/vite-plugin-file-sync/src/ai-service/index.ts`)
|
|
7
|
+
* has two bearer-credential sources:
|
|
8
|
+
* 1. `tokenManagerJwt`, seeded synchronously at `AiService` construction
|
|
9
|
+
* from `tokenManager.getCurrentToken()` and kept current via the
|
|
10
|
+
* `tokenUpdated` listener for future refreshes.
|
|
11
|
+
* 2. `clark?.context?.jwt`, primed lazily by the first socket RPC's
|
|
12
|
+
* `peerAuthorization` header (`socket-manager.ts`).
|
|
13
|
+
*
|
|
14
|
+
* `TokenManager.updateToken()` has exactly one production writer outside
|
|
15
|
+
* `dev()` itself: `AuthHotReloadServer` (`auth-hot-reload.mts`). That socket
|
|
16
|
+
* is explicitly disabled on live-edit pods (SABS sets
|
|
17
|
+
* `SUPERBLOCKS_AUTH_HOT_RELOAD=false`). So on cold start neither source is
|
|
18
|
+
* primed until the UI socket lands — which is AFTER `syncHomeNpmrc` and
|
|
19
|
+
* AFTER the AiService's `TemplateRenderer` prefetch fires.
|
|
20
|
+
*
|
|
21
|
+
* The CLI does have a usable token in scope at `dev()` entry
|
|
22
|
+
* (`tokenConfig.token`, sourced from auth.json or `/_sb_activate`).
|
|
23
|
+
* `primeTokenManagerWithInitialToken` seeds it. Because `TokenManager`
|
|
24
|
+
* retains the current token as state and consumers read it synchronously
|
|
25
|
+
* during construction (see `getCurrentToken()` on `ITokenManager`), the
|
|
26
|
+
* call-site ordering between the prime and consumer construction is NOT
|
|
27
|
+
* load-bearing: the seed reaches AiService whether the prime fires before
|
|
28
|
+
* or after `new AiService(...)`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, expect, it, vi } from "vitest";
|
|
32
|
+
|
|
33
|
+
import { TokenManager } from "../dev-utils/token-manager.js";
|
|
34
|
+
import { primeTokenManagerWithInitialToken } from "./dev.mjs";
|
|
35
|
+
|
|
36
|
+
describe("primeTokenManagerWithInitialToken", () => {
|
|
37
|
+
it("emits a `tokenUpdated` event carrying the supplied token to listeners subscribed before the call", () => {
|
|
38
|
+
const tokenManager = new TokenManager();
|
|
39
|
+
const events: string[] = [];
|
|
40
|
+
tokenManager.on("tokenUpdated", (event) => {
|
|
41
|
+
events.push(event.token);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
primeTokenManagerWithInitialToken(tokenManager, "jwt-from-cold-start");
|
|
45
|
+
|
|
46
|
+
expect(events).toEqual(["jwt-from-cold-start"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("no-ops when the token is empty (no spurious `tokenUpdated` event)", () => {
|
|
50
|
+
const tokenManager = new TokenManager();
|
|
51
|
+
const events: string[] = [];
|
|
52
|
+
tokenManager.on("tokenUpdated", (event) => {
|
|
53
|
+
events.push(event.token);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
primeTokenManagerWithInitialToken(tokenManager, "");
|
|
57
|
+
|
|
58
|
+
expect(events).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("TokenManager state retention (cold-start contract)", () => {
|
|
63
|
+
it("exposes the primed token via getCurrentToken() to consumers constructed AFTER the prime call", () => {
|
|
64
|
+
// This is what makes the dev.mts call site order-independent. AiService
|
|
65
|
+
// reads tokenManager.getCurrentToken() synchronously in its constructor
|
|
66
|
+
// to seed `tokenManagerJwt` — so even if `dev()` primes BEFORE
|
|
67
|
+
// `new AiService(...)`, the seed survives.
|
|
68
|
+
const tokenManager = new TokenManager();
|
|
69
|
+
primeTokenManagerWithInitialToken(tokenManager, "seed-jwt");
|
|
70
|
+
|
|
71
|
+
expect(tokenManager.getCurrentToken()).toBe("seed-jwt");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns undefined from getCurrentToken() when no token has been set", () => {
|
|
75
|
+
const tokenManager = new TokenManager();
|
|
76
|
+
|
|
77
|
+
expect(tokenManager.getCurrentToken()).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does NOT auto-replay prior emissions to late-attached listeners", () => {
|
|
81
|
+
// Deliberate: auto-replaying would cause refresh-handling subscribers
|
|
82
|
+
// (e.g. AutoConnectingRpcClient, which closes and reconnects the socket
|
|
83
|
+
// on every `tokenUpdated`) to fire on what is conceptually a seed read.
|
|
84
|
+
// Late-attached subscribers MUST opt in via `getCurrentToken()` if they
|
|
85
|
+
// need the current value at attach time; the event channel is for
|
|
86
|
+
// changes only.
|
|
87
|
+
const tokenManager = new TokenManager();
|
|
88
|
+
primeTokenManagerWithInitialToken(tokenManager, "abc");
|
|
89
|
+
|
|
90
|
+
const lateListener = vi.fn();
|
|
91
|
+
tokenManager.on("tokenUpdated", lateListener);
|
|
92
|
+
|
|
93
|
+
expect(lateListener).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("overwrites the stored token on each updateToken call so refreshes win", () => {
|
|
97
|
+
const tokenManager = new TokenManager();
|
|
98
|
+
primeTokenManagerWithInitialToken(tokenManager, "first");
|
|
99
|
+
tokenManager.updateToken("second");
|
|
100
|
+
|
|
101
|
+
expect(tokenManager.getCurrentToken()).toBe("second");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { InitialInstallFailed } from "./dependency-install-classifier.mjs";
|
|
4
|
+
import { handleStartupError } from "./dev.mjs";
|
|
5
|
+
|
|
6
|
+
// Mock the metrics module so we can assert recordInitialInstallFailure calls.
|
|
7
|
+
// vi.mock is hoisted, so we use vi.hoisted to declare the spy before the factory.
|
|
8
|
+
const { recordInitialInstallFailureMock } = vi.hoisted(() => ({
|
|
9
|
+
recordInitialInstallFailureMock: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../dev-utils/dev-server-metrics.mjs", () => ({
|
|
12
|
+
devServerMetrics: {
|
|
13
|
+
recordInitialInstallFailure: recordInitialInstallFailureMock,
|
|
14
|
+
flush: vi.fn(),
|
|
15
|
+
recordEndpoint: vi.fn(),
|
|
16
|
+
recordSocketUpgrade: vi.fn(),
|
|
17
|
+
recordViteEagerInit: vi.fn(),
|
|
18
|
+
recordWarmPrewarm: vi.fn(),
|
|
19
|
+
recordWarmActivation: vi.fn(),
|
|
20
|
+
recordWarmHandoff: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const makeLogger = () =>
|
|
25
|
+
({ error: () => {}, info: () => {}, warn: () => {} }) as any;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
recordInitialInstallFailureMock.mockClear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("handleStartupError", () => {
|
|
32
|
+
it("records + does NOT exit for InitialInstallFailed", () => {
|
|
33
|
+
const status = { serverErrors: [] as any[] };
|
|
34
|
+
const marker = new InitialInstallFailed({
|
|
35
|
+
type: "dev-server/dependency-install",
|
|
36
|
+
timestamp: "t",
|
|
37
|
+
category: "dependency_conflict",
|
|
38
|
+
rawError: "x",
|
|
39
|
+
});
|
|
40
|
+
const decision = handleStartupError(marker, status, makeLogger());
|
|
41
|
+
expect(decision).toBe("degrade");
|
|
42
|
+
expect(status.serverErrors).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("emits the install-failure metric with category on the degrade path", () => {
|
|
46
|
+
const status = { serverErrors: [] as any[] };
|
|
47
|
+
const marker = new InitialInstallFailed({
|
|
48
|
+
type: "dev-server/dependency-install",
|
|
49
|
+
timestamp: "t",
|
|
50
|
+
category: "registry_auth_failed",
|
|
51
|
+
npmErrorCode: "E401",
|
|
52
|
+
hasAnyRegistryConfigured: true,
|
|
53
|
+
rawError: "x",
|
|
54
|
+
});
|
|
55
|
+
handleStartupError(marker, status, makeLogger());
|
|
56
|
+
expect(recordInitialInstallFailureMock).toHaveBeenCalledOnce();
|
|
57
|
+
expect(recordInitialInstallFailureMock).toHaveBeenCalledWith({
|
|
58
|
+
category: "registry_auth_failed",
|
|
59
|
+
npmErrorCode: "E401",
|
|
60
|
+
hasAnyRegistryConfigured: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does NOT emit the install-failure metric on the exit path", () => {
|
|
65
|
+
const status = { serverErrors: [] as any[] };
|
|
66
|
+
handleStartupError(new Error("lock failed"), status, makeLogger());
|
|
67
|
+
expect(recordInitialInstallFailureMock).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("exits for a non-marker error (upgrade / lock / sync)", () => {
|
|
71
|
+
const status = { serverErrors: [] as any[] };
|
|
72
|
+
const decision = handleStartupError(
|
|
73
|
+
new Error("lock failed"),
|
|
74
|
+
status,
|
|
75
|
+
makeLogger(),
|
|
76
|
+
);
|
|
77
|
+
expect(decision).toBe("exit");
|
|
78
|
+
expect(status.serverErrors).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|