@superblocksteam/sdk 2.0.110 → 2.0.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Tests for checkVersionsAndWritePackageJson, specifically around the
3
+ * skipCliUpgrade parameter introduced for warm pool activation.
4
+ *
5
+ * Warm pods ship with the latest CLI, so an auto-upgrade would pointlessly
6
+ * restart the process and defeat the warm pool. But we still need the
7
+ * package.json version sync when resuming an existing app so the user's
8
+ * @superblocksteam/library pin matches the target version.
9
+ */
10
+
11
+ import fs from "node:fs/promises";
12
+
13
+ import { describe, it, expect, beforeEach, vi } from "vitest";
14
+
15
+ import { checkVersionsAndWritePackageJson } from "./automatic-upgrades.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Mocks
19
+ // ---------------------------------------------------------------------------
20
+
21
+ vi.mock("node:fs/promises", () => ({
22
+ default: {
23
+ readFile: vi.fn(),
24
+ writeFile: vi.fn(),
25
+ },
26
+ }));
27
+
28
+ vi.mock("node:child_process", () => ({
29
+ exec: vi.fn(),
30
+ }));
31
+
32
+ const upgradeWithPackageManagerCalls: unknown[] = [];
33
+ vi.mock("package-manager-detector/detect", () => ({
34
+ detect: vi.fn(async () => ({
35
+ name: "pnpm",
36
+ agent: "pnpm",
37
+ version: "10.0.0",
38
+ })),
39
+ }));
40
+
41
+ vi.mock("package-manager-detector", () => ({
42
+ resolveCommand: vi.fn(() => ({ command: "pnpm", args: ["install"] })),
43
+ }));
44
+
45
+ vi.mock("./version-detection.js", () => ({
46
+ getCurrentCliVersion: vi.fn(async () => ({
47
+ version: "2.0.0",
48
+ alias: undefined,
49
+ })),
50
+ clearCliVersionCache: vi.fn(),
51
+ }));
52
+
53
+ vi.mock("../telemetry/logging.js", () => ({
54
+ getLogger: () => ({
55
+ info: vi.fn(),
56
+ warn: vi.fn(),
57
+ error: vi.fn(),
58
+ debug: vi.fn(),
59
+ }),
60
+ getErrorMeta: vi.fn(() => ({})),
61
+ }));
62
+
63
+ vi.mock("../telemetry/index.js", () => ({
64
+ getTracer: () => ({
65
+ startActiveSpan: vi.fn((_name, fn) => fn({ end: vi.fn() })),
66
+ }),
67
+ }));
68
+
69
+ vi.mock("@superblocksteam/shared", async () => {
70
+ return {
71
+ NON_SB_ORG_UPDATE_ERROR: "non_sb_org_update_error",
72
+ // traceFunction in tests is a passthrough — we just want the fn to run.
73
+ traceFunction: async ({ fn }: { fn: () => Promise<unknown> }) => {
74
+ return await fn();
75
+ },
76
+ };
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Test fixtures
81
+ // ---------------------------------------------------------------------------
82
+
83
+ const TARGET_CLI_VERSION = "2.1.0"; // newer than the mocked current 2.0.0
84
+ const TARGET_LIBRARY_VERSION = "1.9.0";
85
+
86
+ const mockLockService = {
87
+ shutdown: vi.fn(async () => undefined),
88
+ } as unknown as Parameters<typeof checkVersionsAndWritePackageJson>[0];
89
+
90
+ const mockConfig: Parameters<typeof checkVersionsAndWritePackageJson>[1] = {
91
+ id: "app-123",
92
+ branchName: "main",
93
+ superblocksBaseUrl: "https://example.test",
94
+ token: "test-token",
95
+ userId: "user-1",
96
+ userEmail: "test@example.com",
97
+ organizationId: "org-1",
98
+ featureFlags: {},
99
+ } as unknown as Parameters<typeof checkVersionsAndWritePackageJson>[1];
100
+
101
+ beforeEach(() => {
102
+ vi.clearAllMocks();
103
+ upgradeWithPackageManagerCalls.length = 0;
104
+
105
+ // Mock global fetch used by getRemoteVersions
106
+ global.fetch = vi.fn(async () => ({
107
+ ok: true,
108
+ json: async () => ({
109
+ data: {
110
+ cli: TARGET_CLI_VERSION,
111
+ library: TARGET_LIBRARY_VERSION,
112
+ },
113
+ }),
114
+ })) as unknown as typeof fetch;
115
+
116
+ // Default package.json content
117
+ vi.mocked(fs.readFile).mockResolvedValue(
118
+ JSON.stringify({
119
+ name: "test-app",
120
+ dependencies: {
121
+ "@superblocksteam/library": "1.0.0",
122
+ },
123
+ }),
124
+ );
125
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Tests
130
+ // ---------------------------------------------------------------------------
131
+
132
+ describe("checkVersionsAndWritePackageJson", () => {
133
+ describe("skipCliUpgrade=true (warm pool activation)", () => {
134
+ it("still writes package.json with the target library version", async () => {
135
+ await checkVersionsAndWritePackageJson(
136
+ mockLockService,
137
+ mockConfig,
138
+ false, // forceUpgrade
139
+ true, // skipCliUpgrade — the warm pool case
140
+ );
141
+
142
+ expect(fs.writeFile).toHaveBeenCalledTimes(1);
143
+ const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0];
144
+ const parsed = JSON.parse(writtenContent as string);
145
+ expect(parsed.dependencies["@superblocksteam/library"]).toBe(
146
+ TARGET_LIBRARY_VERSION,
147
+ );
148
+ });
149
+
150
+ it("does NOT call the oclif CLI upgrader even when CLI is stale", async () => {
151
+ const { exec } = await import("node:child_process");
152
+ // If an upgrade were attempted, upgradeCliWithOclif would exec
153
+ // `which superblocks`. We assert that does not happen.
154
+
155
+ await checkVersionsAndWritePackageJson(
156
+ mockLockService,
157
+ mockConfig,
158
+ false,
159
+ true,
160
+ );
161
+
162
+ expect(exec).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it("returns cliUpdated=false regardless of CLI version gap", async () => {
166
+ const result = await checkVersionsAndWritePackageJson(
167
+ mockLockService,
168
+ mockConfig,
169
+ false,
170
+ true,
171
+ );
172
+
173
+ expect(result.cliUpdated).toBe(false);
174
+ });
175
+
176
+ it("skipCliUpgrade overrides forceUpgrade", async () => {
177
+ const { exec } = await import("node:child_process");
178
+
179
+ await checkVersionsAndWritePackageJson(
180
+ mockLockService,
181
+ mockConfig,
182
+ true, // forceUpgrade — should be overridden by skipCliUpgrade
183
+ true, // skipCliUpgrade wins
184
+ );
185
+
186
+ expect(exec).not.toHaveBeenCalled();
187
+ // But package.json still gets written
188
+ expect(fs.writeFile).toHaveBeenCalledTimes(1);
189
+ });
190
+ });
191
+
192
+ describe("skipCliUpgrade=false (default)", () => {
193
+ it("writes package.json with the target library version", async () => {
194
+ await checkVersionsAndWritePackageJson(
195
+ mockLockService,
196
+ mockConfig,
197
+ false,
198
+ false,
199
+ );
200
+
201
+ expect(fs.writeFile).toHaveBeenCalledTimes(1);
202
+ const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0];
203
+ const parsed = JSON.parse(writtenContent as string);
204
+ expect(parsed.dependencies["@superblocksteam/library"]).toBe(
205
+ TARGET_LIBRARY_VERSION,
206
+ );
207
+ });
208
+
209
+ it("defaults to false when omitted (backward compat)", async () => {
210
+ // 3-arg call (pre-warm-pool signature) should behave as skipCliUpgrade=false
211
+ const result = await checkVersionsAndWritePackageJson(
212
+ mockLockService,
213
+ mockConfig,
214
+ false,
215
+ );
216
+
217
+ // Still writes package.json
218
+ expect(fs.writeFile).toHaveBeenCalledTimes(1);
219
+ expect(result).toBeDefined();
220
+ });
221
+ });
222
+
223
+ describe("SNAPSHOT library versions", () => {
224
+ it("pins to ephemeral alias when library target is a SNAPSHOT", async () => {
225
+ const snapshotVersion = "1.9.0-SNAPSHOT.abc123";
226
+ global.fetch = vi.fn(async () => ({
227
+ ok: true,
228
+ json: async () => ({
229
+ data: {
230
+ cli: TARGET_CLI_VERSION,
231
+ library: snapshotVersion,
232
+ },
233
+ }),
234
+ })) as unknown as typeof fetch;
235
+
236
+ await checkVersionsAndWritePackageJson(
237
+ mockLockService,
238
+ mockConfig,
239
+ false,
240
+ true, // warm pool
241
+ );
242
+
243
+ expect(fs.writeFile).toHaveBeenCalledTimes(1);
244
+ const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0];
245
+ const parsed = JSON.parse(writtenContent as string);
246
+ expect(parsed.dependencies["@superblocksteam/library"]).toBe(
247
+ `npm:@superblocksteam/library-ephemeral@${snapshotVersion}`,
248
+ );
249
+ });
250
+ });
251
+ });
@@ -181,6 +181,7 @@ export async function checkVersionsAndWritePackageJson(
181
181
  lockService: LockService,
182
182
  config: ApplicationConfigWithTokenConfigAndUserInfo,
183
183
  forceUpgrade: boolean = false,
184
+ skipCliUpgrade: boolean = false,
184
185
  ): Promise<{
185
186
  cliUpdated: boolean;
186
187
  upgradePromises: Promise<void>[];
@@ -269,7 +270,11 @@ export async function checkVersionsAndWritePackageJson(
269
270
  }
270
271
 
271
272
  try {
272
- if ((cliNeedsUpgrade || forceUpgrade) && currentCliInfo) {
273
+ if (
274
+ !skipCliUpgrade &&
275
+ (cliNeedsUpgrade || forceUpgrade) &&
276
+ currentCliInfo
277
+ ) {
273
278
  const cliUpgradePromise = traceFunction({
274
279
  name: "upgradeCli",
275
280
  tracer,
@@ -1,5 +1,6 @@
1
1
  import * as child_process from "node:child_process";
2
2
  import * as nodeFs from "node:fs/promises";
3
+ import type { Server as HttpServer } from "node:http";
3
4
  import path from "node:path";
4
5
  import * as readline from "node:readline";
5
6
  import { promisify } from "node:util";
@@ -248,6 +249,14 @@ async function installPackages(cwd: string, logger: Logger) {
248
249
  export enum DevServerAutoUpgradeMode {
249
250
  SKIP = "skip-upgrade",
250
251
  FORCE = "force-upgrade",
252
+ /**
253
+ * Skip CLI auto-upgrade but still run the package.json version sync.
254
+ * Used by warm pool activation — the warm pod ships with the latest CLI
255
+ * so an auto-upgrade would pointlessly restart the process, but the user's
256
+ * application package.json still needs to be pinned to the target
257
+ * @superblocksteam/library version.
258
+ */
259
+ SKIP_CLI_ONLY = "skip-cli-only",
251
260
  }
252
261
 
253
262
  export async function dev(options: {
@@ -284,6 +293,9 @@ export async function dev(options: {
284
293
 
285
294
  /** Pre-fetched application data from the CLI validation step (avoids a duplicate network call). */
286
295
  prefetchedApplication?: MultiPageApplicationWrapper;
296
+
297
+ /** Pre-existing HTTP server from warm standby mode (avoids port gap on transition). */
298
+ existingServer?: HttpServer;
287
299
  }) {
288
300
  const {
289
301
  cwd,
@@ -311,6 +323,9 @@ export async function dev(options: {
311
323
  const tracer = getTracer();
312
324
  const logger = getLogger(options.logger);
313
325
  const skipAutoUpgrade = autoUpgradeMode === DevServerAutoUpgradeMode.SKIP;
326
+ const skipCliUpgrade =
327
+ skipAutoUpgrade ||
328
+ autoUpgradeMode === DevServerAutoUpgradeMode.SKIP_CLI_ONLY;
314
329
 
315
330
  await tracer.startActiveSpan("devServerStartup", async (startupSpan) => {
316
331
  try {
@@ -663,8 +678,13 @@ export async function dev(options: {
663
678
  let upgradePromises: Promise<void>[] = [];
664
679
  const forceUpgrade =
665
680
  options.autoUpgradeMode === DevServerAutoUpgradeMode.FORCE;
666
- // Determine whether we must try to auto-upgrade the CLI
667
- if (!skipAutoUpgrade || forceUpgrade) {
681
+ // Run version check when we haven't fully disabled auto-upgrades,
682
+ // when force is requested, or when only the CLI upgrade is skipped
683
+ // (warm pool still needs the package.json sync).
684
+ const skipCliOnly =
685
+ options.autoUpgradeMode ===
686
+ DevServerAutoUpgradeMode.SKIP_CLI_ONLY;
687
+ if (!skipAutoUpgrade || forceUpgrade || skipCliOnly) {
668
688
  await tracer.startActiveSpan(
669
689
  "versionCheckAndUpgrade",
670
690
  async (span) => {
@@ -684,6 +704,7 @@ export async function dev(options: {
684
704
  lockService,
685
705
  applicationConfigWithTokenConfigAndUserInfo,
686
706
  forceUpgrade,
707
+ skipCliUpgrade,
687
708
  );
688
709
  hasCliUpdated = result.cliUpdated;
689
710
  upgradePromises = result.upgradePromises;
@@ -886,6 +907,10 @@ export async function dev(options: {
886
907
  logger: options.logger,
887
908
  sdk,
888
909
  superblocksBaseUrl: tokenConfig.superblocksBaseUrl,
910
+ existingServer: options.existingServer,
911
+ // TODO: Remove this cast — build the options object to match
912
+ // CreateDevServerOptions directly so new required fields cause a
913
+ // compile error instead of silently passing undefined.
889
914
  } as unknown as Parameters<typeof createDevServer>[0];
890
915
  const result = await createDevServer(createDevServerOptions);
891
916
  span.end();