@superblocksteam/sdk 2.0.151 → 2.0.152-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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  3. package/dist/cli-replacement/automatic-upgrades.js +8 -6
  4. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  5. package/dist/cli-replacement/dev-s3-restore.test.mjs +1 -0
  6. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  7. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +1 -0
  8. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  9. package/dist/cli-replacement/dev.d.mts +148 -1
  10. package/dist/cli-replacement/dev.d.mts.map +1 -1
  11. package/dist/cli-replacement/dev.interception.test.mjs +259 -4
  12. package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
  13. package/dist/cli-replacement/dev.mjs +429 -28
  14. package/dist/cli-replacement/dev.mjs.map +1 -1
  15. package/dist/client.billing-usage.test.js +25 -0
  16. package/dist/client.billing-usage.test.js.map +1 -1
  17. package/dist/client.d.ts +19 -0
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/collect-sdk-apis.d.mts +61 -3
  21. package/dist/collect-sdk-apis.d.mts.map +1 -1
  22. package/dist/collect-sdk-apis.mjs +97 -6
  23. package/dist/collect-sdk-apis.mjs.map +1 -1
  24. package/dist/collect-sdk-apis.test.mjs +277 -1
  25. package/dist/collect-sdk-apis.test.mjs.map +1 -1
  26. package/dist/dev-utils/dev-server-metrics.d.mts +17 -0
  27. package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
  28. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts +2 -0
  29. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts.map +1 -0
  30. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs +108 -0
  31. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs.map +1 -0
  32. package/dist/dev-utils/dev-server-metrics.mjs +16 -2
  33. package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
  34. package/dist/dev-utils/dev-server.d.mts +9 -0
  35. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  36. package/dist/dev-utils/dev-server.mjs +44 -27
  37. package/dist/dev-utils/dev-server.mjs.map +1 -1
  38. package/dist/dev-utils/dev-server.status.test.mjs +31 -1
  39. package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
  40. package/dist/dev-utils/fatal-exit.d.mts +17 -4
  41. package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
  42. package/dist/dev-utils/fatal-exit.mjs +4 -3
  43. package/dist/dev-utils/fatal-exit.mjs.map +1 -1
  44. package/dist/extract-api-integrations.d.mts +1 -1
  45. package/dist/extract-api-integrations.d.mts.map +1 -1
  46. package/dist/extract-api-integrations.mjs +1 -1
  47. package/dist/extract-api-integrations.mjs.map +1 -1
  48. package/dist/flag.d.ts +5 -0
  49. package/dist/flag.d.ts.map +1 -1
  50. package/dist/flag.js +7 -0
  51. package/dist/flag.js.map +1 -1
  52. package/dist/flag.test.js +18 -0
  53. package/dist/flag.test.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/types/common.d.ts +1 -0
  58. package/dist/types/common.d.ts.map +1 -1
  59. package/dist/types/common.js.map +1 -1
  60. package/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
  61. package/dist/vite-plugin-generate-api-build-manifest.mjs +46 -0
  62. package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
  63. package/package.json +6 -6
  64. package/src/cli-replacement/automatic-upgrades.ts +8 -6
  65. package/src/cli-replacement/dev-s3-restore.test.mts +1 -0
  66. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +1 -0
  67. package/src/cli-replacement/dev.interception.test.mts +334 -10
  68. package/src/cli-replacement/dev.mts +499 -33
  69. package/src/client.billing-usage.test.ts +26 -0
  70. package/src/client.ts +21 -0
  71. package/src/collect-sdk-apis.mts +141 -5
  72. package/src/collect-sdk-apis.test.mts +380 -1
  73. package/src/dev-utils/dev-server-metrics.dependency-failure.test.mts +149 -0
  74. package/src/dev-utils/dev-server-metrics.mts +25 -1
  75. package/src/dev-utils/dev-server.mts +52 -29
  76. package/src/dev-utils/dev-server.status.test.mts +42 -0
  77. package/src/dev-utils/fatal-exit.mts +18 -4
  78. package/src/extract-api-integrations.mts +3 -0
  79. package/src/flag.test.ts +21 -0
  80. package/src/flag.ts +8 -0
  81. package/src/index.ts +1 -0
  82. package/src/types/common.ts +1 -0
  83. package/src/vite-plugin-generate-api-build-manifest.mts +74 -4
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -17,6 +17,28 @@ const BUILD_MANIFEST_HASHED_RE = /[\\/]build-manifest(?:-[A-Za-z0-9]+)?\.js$/;
17
17
  function isBuildManifestModule(id) {
18
18
  return (id.endsWith(BUILD_MANIFEST_SUFFIX) || BUILD_MANIFEST_HASHED_RE.test(id));
19
19
  }
20
+ /**
21
+ * Emits a structured WARN when the logger supports it, falling back to a
22
+ * plain `warn` with just the human-readable message otherwise.
23
+ * `warnStructured` is optional on the `Logger` type (future/alternate
24
+ * implementations may omit it), so calling it with `?.` alone would let an
25
+ * auth-relevant warning disappear silently on such a logger. Both call
26
+ * sites in this plugin route through here instead of duplicating the
27
+ * fallback branch.
28
+ *
29
+ * `message` must be the plain, uncolored text — it becomes the OTel log
30
+ * body when `warnStructured` is available, and ANSI escapes have no place
31
+ * in a structured body. Coloring is applied only in the console-facing
32
+ * fallback branch below.
33
+ */
34
+ function warnStructured(logger, message, attributes) {
35
+ if (logger.warnStructured) {
36
+ logger.warnStructured(message, attributes);
37
+ }
38
+ else {
39
+ logger.warn(yellow(message));
40
+ }
41
+ }
20
42
  /**
21
43
  * Vite plugin that generates a build manifest for the application.
22
44
  *
@@ -74,6 +96,30 @@ export function generateApiBuildManifestPlugin(root) {
74
96
  ? await collectSdkApisFromRegistry(root, {
75
97
  pathExists: (p) => fs.pathExists(p),
76
98
  readFile: (p, enc) => fs.readFile(p, enc),
99
+ realpath: (p) => fs.realpath(p),
100
+ }, {
101
+ onUnresolvedIntegrations: (apiName, entryPoint, unresolvedKeys) => {
102
+ warnStructured(logger, `[superblocks] Could not resolve integration(s) ${unresolvedKeys
103
+ .map((k) => `"${k}"`)
104
+ .join(", ")} declared in ${entryPoint} (API "${apiName}") — check-auth may be skipped for this API in deployed/preview mode. Declare the integration or its ID in the same file.`, {
105
+ // Bounded/numeric only — api name, entry point, and the
106
+ // unresolved keys themselves are free-form text and stay
107
+ // in the message body (searchable, not faceted) to keep
108
+ // OTel log attribute cardinality low. See the
109
+ // `LogAttributes` doc comment in ./telemetry/logging.ts.
110
+ "sdk.integrations.unresolved_count": unresolvedKeys.length,
111
+ });
112
+ },
113
+ onExtractionError: (apiName, entryPoint, error) => {
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ warnStructured(logger, `[superblocks] Failed to extract integrations from ${entryPoint} (API "${apiName}"): ${errorMessage} — check-auth may be skipped for this API in deployed/preview mode.`, {
116
+ // Bounded constant only — api name, entry point, and the
117
+ // raw error message are free-form text and stay in the
118
+ // message body. See the `LogAttributes` doc comment in
119
+ // ./telemetry/logging.ts.
120
+ "sdk.extraction.error_category": "read_or_extract_failed",
121
+ });
122
+ },
77
123
  })
78
124
  : {};
79
125
  manifestCode = `export default ${JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-generate-api-build-manifest.mjs","sourceRoot":"","sources":["../src/vite-plugin-generate-api-build-manifest.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vite-plugin-generate-api-build-manifest.mjs","sourceRoot":"","sources":["../src/vite-plugin-generate-api-build-manifest.mts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superblocksteam/sdk",
3
- "version": "2.0.151",
3
+ "version": "2.0.152-next.1",
4
4
  "description": "Superblocks JS SDK",
5
5
  "homepage": "https://www.superblocks.com",
6
6
  "license": "Superblocks Community Software License",
@@ -57,11 +57,11 @@
57
57
  "vite-tsconfig-paths": "^6.0.4",
58
58
  "winston": "^3.17.0",
59
59
  "yaml": "^2.7.1",
60
- "@superblocksteam/library-shared": "2.0.151",
61
- "@superblocksteam/shared": "0.9604.3",
62
- "@superblocksteam/telemetry": "2.0.151",
63
- "@superblocksteam/util": "2.0.151",
64
- "@superblocksteam/vite-plugin-file-sync": "2.0.151"
60
+ "@superblocksteam/library-shared": "2.0.152-next.1",
61
+ "@superblocksteam/shared": "0.9605.1",
62
+ "@superblocksteam/telemetry": "2.0.152-next.1",
63
+ "@superblocksteam/util": "2.0.152-next.1",
64
+ "@superblocksteam/vite-plugin-file-sync": "2.0.152-next.1"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@eslint/js": "^9.39.2",
@@ -181,13 +181,15 @@ async function upgradeCliWithOclif(targetVersion: string): Promise<boolean> {
181
181
  }
182
182
 
183
183
  // Resolves when an install command ran and post-install validation passed.
184
- // Every failure throws `UpgradeFailed` carrying a classified, non-blocking
184
+ // Every failure throws `UpgradeFailed` carrying a classified
185
185
  // `DependencyUpgradeError` — the no-command branch, an exec failure, and a
186
- // post-install version mismatch all degrade the same way. The dev.mts join
187
- // routes that marker to the degrade path, so the live-edit pod keeps serving
188
- // the currently installed version instead of crash-looping. The auto-upgrade
189
- // is best-effort: the running version still works, so a failed upgrade is
190
- // never fatal.
186
+ // post-install version mismatch all arrive the same way. What the dev.mts join
187
+ // does with that marker depends on who owns the process: on a developer machine
188
+ // it degrades, so the dev server keeps serving the currently installed version.
189
+ // In a disposable sandbox (`SUPERBLOCKS_IS_CSB=true`) a failure that could have
190
+ // mutated `node_modules` is fatal — the process exits so the sandbox is
191
+ // replaced rather than left serving a possibly-tainted tree (ENG-5297). See
192
+ // `handleStartupError` and `upgradeCategoryCouldTaintTree` in dev.mts.
191
193
  async function upgradeCliWithPackageManager(
192
194
  pm: DetectResult,
193
195
  targetVersion: string,
@@ -294,6 +294,7 @@ function buildMockSdk() {
294
294
  devServerCloudInactivityTimeoutMinutes: () => 30,
295
295
  devServerLocalInactivityTimeoutMinutes: () => 30,
296
296
  enableSessionRecording: () => false,
297
+ removeManualCommitsEnabled: () => false,
297
298
  })),
298
299
  getFeatureFlagsForUser: vi.fn(() => ({
299
300
  devServerCloudInactivityTimeoutMinutes: () => 30,
@@ -342,6 +342,7 @@ function buildMockSdk() {
342
342
  devServerCloudInactivityTimeoutMinutes: () => 30,
343
343
  devServerLocalInactivityTimeoutMinutes: () => 30,
344
344
  enableSessionRecording: () => false,
345
+ removeManualCommitsEnabled: () => false,
345
346
  })),
346
347
  getFeatureFlagsForUser: vi.fn(() => ({
347
348
  devServerCloudInactivityTimeoutMinutes: () => 30,
@@ -1,24 +1,35 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
+ import type { DependencyUpgradeError } from "@superblocksteam/library-shared/types";
4
+
5
+ import type * as TelemetryModule from "../telemetry/index.js";
6
+ import type { Logger } from "../telemetry/logging.js";
3
7
  import {
4
8
  InitialInstallFailed,
5
9
  UpgradeFailed,
6
10
  } from "./dependency-install-classifier.mjs";
7
- import { handleStartupError } from "./dev.mjs";
11
+ import {
12
+ flushTelemetryWithTimeout,
13
+ handleStartupError,
14
+ runFatalExitShutdown,
15
+ upgradeCategoryCouldTaintTree,
16
+ type DevServerStatus,
17
+ } from "./dev.mjs";
8
18
 
9
19
  // Mock the metrics module so we can assert recordInitialInstallFailure /
10
20
  // recordUpgradeFailure calls. vi.mock is hoisted, so we use vi.hoisted to
11
21
  // declare the spies before the factory.
12
- const { recordInitialInstallFailureMock, recordUpgradeFailureMock } =
22
+ const { recordInitialInstallFailureMock, recordUpgradeFailureMock, flushMock } =
13
23
  vi.hoisted(() => ({
14
24
  recordInitialInstallFailureMock: vi.fn(),
15
25
  recordUpgradeFailureMock: vi.fn(),
26
+ flushMock: vi.fn(),
16
27
  }));
17
28
  vi.mock("../dev-utils/dev-server-metrics.mjs", () => ({
18
29
  devServerMetrics: {
19
30
  recordInitialInstallFailure: recordInitialInstallFailureMock,
20
31
  recordUpgradeFailure: recordUpgradeFailureMock,
21
- flush: vi.fn(),
32
+ flush: flushMock,
22
33
  recordEndpoint: vi.fn(),
23
34
  recordSocketUpgrade: vi.fn(),
24
35
  recordViteEagerInit: vi.fn(),
@@ -28,17 +39,37 @@ vi.mock("../dev-utils/dev-server-metrics.mjs", () => ({
28
39
  },
29
40
  }));
30
41
 
31
- const makeLogger = () =>
32
- ({ error: () => {}, info: () => {}, warn: () => {} }) as any;
42
+ // The raced telemetry flush is the one piece of the fatal-exit path that talks
43
+ // to the OTel SDK, so it is stubbed to make both race outcomes reachable.
44
+ const { shutdownTelemetryMock } = vi.hoisted(() => ({
45
+ shutdownTelemetryMock: vi.fn<() => Promise<boolean>>(async () => true),
46
+ }));
47
+ vi.mock("../telemetry/index.js", async (importOriginal) => ({
48
+ ...(await importOriginal<typeof TelemetryModule>()),
49
+ shutdownTelemetry: shutdownTelemetryMock,
50
+ }));
51
+
52
+ // Annotated rather than cast so a new required `Logger` member breaks this file
53
+ // instead of being silently absent. `vi.fn()` over `() => {}` keeps every call
54
+ // assertable if a test ever needs to check what was logged.
55
+ const makeLogger = (): Logger => ({
56
+ debug: vi.fn(),
57
+ info: vi.fn(),
58
+ warn: vi.fn(),
59
+ error: vi.fn(),
60
+ });
33
61
 
34
62
  beforeEach(() => {
35
63
  recordInitialInstallFailureMock.mockClear();
36
64
  recordUpgradeFailureMock.mockClear();
65
+ flushMock.mockClear();
66
+ shutdownTelemetryMock.mockReset();
67
+ shutdownTelemetryMock.mockImplementation(async () => true);
37
68
  });
38
69
 
39
70
  describe("handleStartupError", () => {
40
71
  it("records + does NOT exit for InitialInstallFailed", () => {
41
- const status = { serverErrors: [] as any[] };
72
+ const status: DevServerStatus = { serverErrors: [] };
42
73
  const marker = new InitialInstallFailed({
43
74
  type: "dev-server/dependency-install",
44
75
  timestamp: "t",
@@ -51,7 +82,7 @@ describe("handleStartupError", () => {
51
82
  });
52
83
 
53
84
  it("emits the install-failure metric with category on the degrade path", () => {
54
- const status = { serverErrors: [] as any[] };
85
+ const status: DevServerStatus = { serverErrors: [] };
55
86
  const marker = new InitialInstallFailed({
56
87
  type: "dev-server/dependency-install",
57
88
  timestamp: "t",
@@ -70,13 +101,13 @@ describe("handleStartupError", () => {
70
101
  });
71
102
 
72
103
  it("does NOT emit the install-failure metric on the exit path", () => {
73
- const status = { serverErrors: [] as any[] };
104
+ const status: DevServerStatus = { serverErrors: [] };
74
105
  handleStartupError(new Error("lock failed"), status, makeLogger());
75
106
  expect(recordInitialInstallFailureMock).not.toHaveBeenCalled();
76
107
  });
77
108
 
78
109
  it("exits for a non-marker error (lock / sync / genuinely-unusable upgrade env)", () => {
79
- const status = { serverErrors: [] as any[] };
110
+ const status: DevServerStatus = { serverErrors: [] };
80
111
  const decision = handleStartupError(
81
112
  new Error("lock failed"),
82
113
  status,
@@ -86,8 +117,164 @@ describe("handleStartupError", () => {
86
117
  expect(status.serverErrors).toHaveLength(0);
87
118
  });
88
119
 
120
+ // ENG-5297: in a disposable cloud sandbox the upgrade is an in-place
121
+ // `npm install` into the *live* node_modules, and the install manifest
122
+ // includes esbuild. A failed upgrade therefore kills the running esbuild
123
+ // service ("The service was stopped"), rejecting vitePromise — so
124
+ // "keep the current version" preserves a process that can no longer serve
125
+ // anything. Exiting hands the pod to SABS, which already reaps terminal pods
126
+ // (RestartPolicy: Never), instead of leaving a zombie answering 500s.
127
+ it("exits for UpgradeFailed in a disposable sandbox (live tree may be tainted)", () => {
128
+ const status: DevServerStatus = { serverErrors: [] };
129
+ const marker = new UpgradeFailed({
130
+ type: "dev-server/dependency-upgrade",
131
+ timestamp: "t",
132
+ category: "unknown",
133
+ currentVersion: "2.0.148",
134
+ targetVersion: "2.0.149",
135
+ rawError: "dependency upgrade failed (unknown)",
136
+ });
137
+ const decision = handleStartupError(marker, status, makeLogger(), {
138
+ disposableSandbox: true,
139
+ });
140
+ expect(decision).toBe("exit");
141
+ // The failure is still recorded so the exit is observable, not silent.
142
+ expect(status.serverErrors).toHaveLength(1);
143
+ // `outcome` distinguishes an exiting pod from one that merely degraded —
144
+ // without it a crash loop is indistinguishable from a quietly-degraded
145
+ // fleet.
146
+ expect(recordUpgradeFailureMock).toHaveBeenCalledWith(
147
+ expect.objectContaining({ outcome: "exit" }),
148
+ );
149
+ // `record()` and `flush()` gate on the same `isTelemetryInitialized()`
150
+ // check with nothing async in between, so a `flush()` here can never drain
151
+ // the sample recorded a line earlier. The caller's `shutdownTelemetry()`
152
+ // is what actually forces the export.
153
+ expect(flushMock).not.toHaveBeenCalled();
154
+ });
155
+
156
+ // ENG-5336: exiting is the right call only when the failure could have
157
+ // mutated the on-disk tree. npm resolves the whole graph before it extracts
158
+ // anything, so a registry that refused/never answered the metadata fetch
159
+ // provably left `node_modules` — and the running esbuild service — untouched.
160
+ // These are also exactly the failures a registry outage produces, so exiting
161
+ // on them would take out every live-edit pod in the fleet at once, replacing
162
+ // them with pods that hit the same outage.
163
+ const RESOLUTION_PHASE_CATEGORIES = [
164
+ "not_in_registry",
165
+ "registry_auth_failed",
166
+ "registry_unreachable",
167
+ "tls_failed",
168
+ ] as const;
169
+
170
+ // Every category the union names: the resolution-phase four plus the
171
+ // conservative default. Annotated rather than `as const` because a spread into
172
+ // a fresh literal widens `"unknown"` to `string` otherwise, which would take
173
+ // the type checking off `upgradeMarker` at the `it.each` below.
174
+ const ALL_UPGRADE_CATEGORIES: readonly DependencyUpgradeError["category"][] =
175
+ [...RESOLUTION_PHASE_CATEGORIES, "unknown"];
176
+
177
+ // Typing the parameter (rather than casting inside) is what keeps the
178
+ // `it.each` tables below checked end to end: a category added to either array
179
+ // that the marker cannot carry is a compile error, not a silent `any`.
180
+ const upgradeMarker = (category: DependencyUpgradeError["category"]) =>
181
+ new UpgradeFailed({
182
+ type: "dev-server/dependency-upgrade",
183
+ timestamp: "t",
184
+ category,
185
+ currentVersion: "2.0.148",
186
+ targetVersion: "2.0.149",
187
+ rawError: `dependency upgrade failed (${category})`,
188
+ });
189
+
190
+ it.each(RESOLUTION_PHASE_CATEGORIES)(
191
+ "degrades in a disposable sandbox for the resolution-phase category '%s'",
192
+ (category) => {
193
+ const status: DevServerStatus = { serverErrors: [] };
194
+ const decision = handleStartupError(
195
+ upgradeMarker(category),
196
+ status,
197
+ makeLogger(),
198
+ { disposableSandbox: true },
199
+ );
200
+ expect(decision).toBe("degrade");
201
+ expect(status.serverErrors).toHaveLength(1);
202
+ expect(recordUpgradeFailureMock).toHaveBeenCalledWith(
203
+ expect.objectContaining({ category, outcome: "degrade" }),
204
+ );
205
+ },
206
+ );
207
+
208
+ it("still exits in a disposable sandbox for an unclassified failure", () => {
209
+ // ENG-5297 arrived as `category: "unknown"` — the conservative default
210
+ // must keep exiting, or this PR's whole reason for existing is undone.
211
+ const status: DevServerStatus = { serverErrors: [] };
212
+ expect(
213
+ handleStartupError(upgradeMarker("unknown"), status, makeLogger(), {
214
+ disposableSandbox: true,
215
+ }),
216
+ ).toBe("exit");
217
+ });
218
+
219
+ it.each(ALL_UPGRADE_CATEGORIES)(
220
+ "degrades on a developer machine regardless of category ('%s')",
221
+ (category) => {
222
+ const status: DevServerStatus = { serverErrors: [] };
223
+ expect(
224
+ handleStartupError(upgradeMarker(category), status, makeLogger(), {
225
+ disposableSandbox: false,
226
+ }),
227
+ ).toBe("degrade");
228
+ expect(recordUpgradeFailureMock).toHaveBeenCalledWith(
229
+ expect.objectContaining({ outcome: "degrade" }),
230
+ );
231
+ },
232
+ );
233
+
234
+ describe("upgradeCategoryCouldTaintTree", () => {
235
+ it.each(RESOLUTION_PHASE_CATEGORIES)(
236
+ "reports '%s' as unable to have touched the tree",
237
+ (category) => {
238
+ expect(upgradeCategoryCouldTaintTree(category)).toBe(false);
239
+ },
240
+ );
241
+
242
+ it("defaults to 'could have tainted' for an unclassified failure", () => {
243
+ expect(upgradeCategoryCouldTaintTree("unknown")).toBe(true);
244
+ });
245
+
246
+ it("defaults to 'could have tainted' for a category it has never seen", () => {
247
+ // Conservative default: a future category must fail closed (exit), not
248
+ // silently inherit the degrade path. `string` is the real runtime contract
249
+ // here, since the payload crosses the socket — a category outside today's
250
+ // union cannot be put on an `UpgradeFailed` marker without an `as` cast,
251
+ // which is why this function takes the category rather than the marker.
252
+ expect(upgradeCategoryCouldTaintTree("brand_new")).toBe(true);
253
+ });
254
+ });
255
+
256
+ it("labels the degrade outcome and does not force a flush", () => {
257
+ const status: DevServerStatus = { serverErrors: [] };
258
+ const marker = new UpgradeFailed({
259
+ type: "dev-server/dependency-upgrade",
260
+ timestamp: "t",
261
+ category: "unknown",
262
+ currentVersion: "2.0.148",
263
+ targetVersion: "2.0.149",
264
+ rawError: "x",
265
+ });
266
+ handleStartupError(marker, status, makeLogger(), {
267
+ disposableSandbox: false,
268
+ });
269
+ expect(recordUpgradeFailureMock).toHaveBeenCalledWith(
270
+ expect.objectContaining({ outcome: "degrade" }),
271
+ );
272
+ // A surviving process reaches the next export tick on its own.
273
+ expect(flushMock).not.toHaveBeenCalled();
274
+ });
275
+
89
276
  it("records + does NOT exit for UpgradeFailed (best-effort upgrade degrades)", () => {
90
- const status = { serverErrors: [] as any[] };
277
+ const status: DevServerStatus = { serverErrors: [] };
91
278
  const marker = new UpgradeFailed({
92
279
  type: "dev-server/dependency-upgrade",
93
280
  timestamp: "t",
@@ -105,8 +292,145 @@ describe("handleStartupError", () => {
105
292
  category: "not_in_registry",
106
293
  npmErrorCode: "ETARGET",
107
294
  hasAnyRegistryConfigured: true,
295
+ outcome: "degrade",
108
296
  });
109
297
  // The upgrade failure must not be miscounted as an initial-install failure.
110
298
  expect(recordInitialInstallFailureMock).not.toHaveBeenCalled();
111
299
  });
112
300
  });
301
+
302
+ // ENG-5336: the two fatal-exit blocks in `dev()` depend on teardown steps that
303
+ // each talk to something that can fail or stop answering. Losing the lock
304
+ // release means the replacement sandbox's `acquireLock()` gets a ConflictError
305
+ // until the server-side TTL expires — and the replacement is the entire reason
306
+ // the exit path exists. `runFatalExitShutdown` is the shared, exported sequence
307
+ // those blocks call, so these are the guarantees the exit story rests on.
308
+ describe("runFatalExitShutdown", () => {
309
+ const stepTimeoutMs = 5000;
310
+
311
+ it("runs the steps in order: cache removal, then lock release", async () => {
312
+ const calls: string[] = [];
313
+ await runFatalExitShutdown({
314
+ logger: makeLogger(),
315
+ removeIntegrationCache: async () => {
316
+ calls.push("cache");
317
+ },
318
+ shutdownLockService: async () => {
319
+ calls.push("lock");
320
+ },
321
+ });
322
+ expect(calls).toEqual(["cache", "lock"]);
323
+ });
324
+
325
+ it("still releases the lock when cache removal rejects", async () => {
326
+ const shutdownLockService = vi.fn(async () => {});
327
+ await expect(
328
+ runFatalExitShutdown({
329
+ logger: makeLogger(),
330
+ removeIntegrationCache: async () => {
331
+ throw new Error("ai-service IPC is gone");
332
+ },
333
+ shutdownLockService,
334
+ }),
335
+ ).resolves.toBeUndefined();
336
+ expect(shutdownLockService).toHaveBeenCalledOnce();
337
+ });
338
+
339
+ it("still releases the lock when cache removal throws synchronously", async () => {
340
+ const shutdownLockService = vi.fn(async () => {});
341
+ await runFatalExitShutdown({
342
+ logger: makeLogger(),
343
+ removeIntegrationCache: () => {
344
+ throw new Error("sync boom");
345
+ },
346
+ shutdownLockService,
347
+ });
348
+ expect(shutdownLockService).toHaveBeenCalledOnce();
349
+ });
350
+
351
+ it("never rejects when the lock release itself rejects", async () => {
352
+ await expect(
353
+ runFatalExitShutdown({
354
+ logger: makeLogger(),
355
+ shutdownLockService: async () => {
356
+ throw new Error("lock server unreachable");
357
+ },
358
+ }),
359
+ ).resolves.toBeUndefined();
360
+ });
361
+
362
+ it("tolerates absent services (no ai-service / no lock service)", async () => {
363
+ await expect(
364
+ runFatalExitShutdown({ logger: makeLogger() }),
365
+ ).resolves.toBeUndefined();
366
+ });
367
+
368
+ // The hang is the ENG-5297 symptom itself: an unsettled await above the
369
+ // `finally { process.exit(1) }` leaves the pod Ready but zombied forever.
370
+ it("abandons a step that never settles and still runs the later steps", async () => {
371
+ vi.useFakeTimers();
372
+ try {
373
+ const shutdownLockService = vi.fn(async () => {});
374
+ const settled = vi.fn();
375
+ const done = runFatalExitShutdown({
376
+ logger: makeLogger(),
377
+ removeIntegrationCache: () => new Promise<void>(() => {}),
378
+ shutdownLockService,
379
+ }).then(settled);
380
+
381
+ await vi.advanceTimersByTimeAsync(stepTimeoutMs - 1);
382
+ expect(shutdownLockService).not.toHaveBeenCalled();
383
+
384
+ await vi.advanceTimersByTimeAsync(2);
385
+ await done;
386
+ expect(shutdownLockService).toHaveBeenCalledOnce();
387
+ expect(settled).toHaveBeenCalledOnce();
388
+ } finally {
389
+ vi.useRealTimers();
390
+ }
391
+ });
392
+ });
393
+
394
+ // Shared by the fatal-exit sequence above and the auto-upgrade CLI restart in
395
+ // `dev()`. It used to be duplicated at the restart site, and the copy never
396
+ // cleared its timeout timer — an armed 3s timer holds the event loop open for
397
+ // any caller that does not immediately `process.exit`.
398
+ describe("flushTelemetryWithTimeout", () => {
399
+ const flushTimeoutMs = 3000;
400
+
401
+ it("reports a completed flush and leaves no armed timer behind", async () => {
402
+ vi.useFakeTimers();
403
+ try {
404
+ await expect(flushTelemetryWithTimeout()).resolves.toBe(true);
405
+ expect(vi.getTimerCount()).toBe(0);
406
+ } finally {
407
+ vi.useRealTimers();
408
+ }
409
+ });
410
+
411
+ it("reports a flush that did not run (telemetry never initialized)", async () => {
412
+ shutdownTelemetryMock.mockImplementation(async () => false);
413
+ await expect(flushTelemetryWithTimeout()).resolves.toBe(false);
414
+ });
415
+
416
+ // A stuck exporter must not wedge the exit: the timeout wins the race, the
417
+ // caller is told no flush happened, and the timer is still cleaned up.
418
+ it("gives up on a stuck exporter after the timeout and clears the timer", async () => {
419
+ vi.useFakeTimers();
420
+ try {
421
+ shutdownTelemetryMock.mockImplementation(
422
+ () => new Promise<boolean>(() => {}),
423
+ );
424
+ const pending = flushTelemetryWithTimeout();
425
+
426
+ await vi.advanceTimersByTimeAsync(flushTimeoutMs - 1);
427
+ expect(vi.getTimerCount()).toBe(1);
428
+
429
+ await vi.advanceTimersByTimeAsync(2);
430
+ await expect(pending).resolves.toBe(false);
431
+ expect(vi.getTimerCount()).toBe(0);
432
+ } finally {
433
+ vi.useRealTimers();
434
+ }
435
+ });
436
+ });