@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +8 -6
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/dev-s3-restore.test.mjs +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +1 -0
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +148 -1
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +259 -4
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +429 -28
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/client.billing-usage.test.js +25 -0
- package/dist/client.billing-usage.test.js.map +1 -1
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/collect-sdk-apis.d.mts +61 -3
- package/dist/collect-sdk-apis.d.mts.map +1 -1
- package/dist/collect-sdk-apis.mjs +97 -6
- package/dist/collect-sdk-apis.mjs.map +1 -1
- package/dist/collect-sdk-apis.test.mjs +277 -1
- package/dist/collect-sdk-apis.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +17 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs +108 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.mjs +16 -2
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +9 -0
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +44 -27
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.mjs +31 -1
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
- package/dist/dev-utils/fatal-exit.d.mts +17 -4
- package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
- package/dist/dev-utils/fatal-exit.mjs +4 -3
- package/dist/dev-utils/fatal-exit.mjs.map +1 -1
- package/dist/extract-api-integrations.d.mts +1 -1
- package/dist/extract-api-integrations.d.mts.map +1 -1
- package/dist/extract-api-integrations.mjs +1 -1
- package/dist/extract-api-integrations.mjs.map +1 -1
- package/dist/flag.d.ts +5 -0
- package/dist/flag.d.ts.map +1 -1
- package/dist/flag.js +7 -0
- package/dist/flag.js.map +1 -1
- package/dist/flag.test.js +18 -0
- package/dist/flag.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/common.d.ts +1 -0
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/common.js.map +1 -1
- package/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
- package/dist/vite-plugin-generate-api-build-manifest.mjs +46 -0
- package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
- package/package.json +6 -6
- package/src/cli-replacement/automatic-upgrades.ts +8 -6
- package/src/cli-replacement/dev-s3-restore.test.mts +1 -0
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +1 -0
- package/src/cli-replacement/dev.interception.test.mts +334 -10
- package/src/cli-replacement/dev.mts +499 -33
- package/src/client.billing-usage.test.ts +26 -0
- package/src/client.ts +21 -0
- package/src/collect-sdk-apis.mts +141 -5
- package/src/collect-sdk-apis.test.mts +380 -1
- package/src/dev-utils/dev-server-metrics.dependency-failure.test.mts +149 -0
- package/src/dev-utils/dev-server-metrics.mts +25 -1
- package/src/dev-utils/dev-server.mts +52 -29
- package/src/dev-utils/dev-server.status.test.mts +42 -0
- package/src/dev-utils/fatal-exit.mts +18 -4
- package/src/extract-api-integrations.mts +3 -0
- package/src/flag.test.ts +21 -0
- package/src/flag.ts +8 -0
- package/src/index.ts +1 -0
- package/src/types/common.ts +1 -0
- package/src/vite-plugin-generate-api-build-manifest.mts +74 -4
- 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":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,MAAM,WAAW,CAAC;AAC3B,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,4CAA4C,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EACL,WAAW,EACX,kBAAkB,GACnB,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
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.
|
|
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.
|
|
61
|
-
"@superblocksteam/shared": "0.
|
|
62
|
-
"@superblocksteam/telemetry": "2.0.
|
|
63
|
-
"@superblocksteam/util": "2.0.
|
|
64
|
-
"@superblocksteam/vite-plugin-file-sync": "2.0.
|
|
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
|
|
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
|
|
187
|
-
//
|
|
188
|
-
// the
|
|
189
|
-
//
|
|
190
|
-
//
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
32
|
-
|
|
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: []
|
|
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: []
|
|
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: []
|
|
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: []
|
|
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: []
|
|
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
|
+
});
|