@superblocksteam/sdk 2.0.129 → 2.0.130-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 +235 -42
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +406 -3
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +8 -9
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts +6 -0
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -1
- package/dist/cli-replacement/home-npmrc.mjs +22 -0
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -1
- package/dist/client.billing-usage.test.d.ts +2 -0
- package/dist/client.billing-usage.test.d.ts.map +1 -0
- package/dist/client.billing-usage.test.js +66 -0
- package/dist/client.billing-usage.test.js.map +1 -0
- package/dist/client.d.ts +41 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +15 -1
- package/dist/client.js.map +1 -1
- package/dist/dev-utils/dev-server-metrics.boot.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.mjs +55 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.d.mts +32 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +60 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs +18 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +28 -2
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +68 -7
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.mjs +28 -1
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
- package/dist/flag.d.ts +14 -0
- package/dist/flag.d.ts.map +1 -1
- package/dist/flag.js +17 -0
- package/dist/flag.js.map +1 -1
- package/dist/flag.test.d.ts +2 -0
- package/dist/flag.test.d.ts.map +1 -0
- package/dist/flag.test.js +54 -0
- package/dist/flag.test.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +3 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +11 -1
- package/dist/sdk.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/package.json +8 -8
- package/src/cli-replacement/automatic-upgrades.test.ts +556 -3
- package/src/cli-replacement/automatic-upgrades.ts +251 -54
- package/src/cli-replacement/dev.mts +9 -10
- package/src/cli-replacement/home-npmrc.mts +45 -0
- package/src/client.billing-usage.test.ts +73 -0
- package/src/client.ts +66 -1
- package/src/dev-utils/dev-server-metrics.boot.test.mts +83 -0
- package/src/dev-utils/dev-server-metrics.mts +65 -0
- package/src/dev-utils/dev-server-metrics.test.mts +24 -1
- package/src/dev-utils/dev-server.mts +96 -7
- package/src/dev-utils/dev-server.status.test.mts +35 -1
- package/src/flag.test.ts +63 -0
- package/src/flag.ts +24 -0
- package/src/index.ts +9 -0
- package/src/sdk.ts +20 -0
- package/src/types/common.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/turbo.json +2 -1
|
@@ -15,6 +15,11 @@ import * as path from "node:path";
|
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
17
17
|
|
|
18
|
+
import type {
|
|
19
|
+
NpmInstallMetricInput,
|
|
20
|
+
NpmInstallSpanAttributes,
|
|
21
|
+
} from "@superblocksteam/telemetry";
|
|
22
|
+
|
|
18
23
|
import {
|
|
19
24
|
buildGlobalInstallArgs,
|
|
20
25
|
buildInstallEnv,
|
|
@@ -65,7 +70,12 @@ vi.mock("./version-detection.js", () => ({
|
|
|
65
70
|
// either declare its mocks inline OR use `vi.hoisted()` to hoist the mock
|
|
66
71
|
// fns alongside the mock registration. The latter lets us reuse the mocks
|
|
67
72
|
// inside test bodies (e.g. `.mockReturnValue(...)`).
|
|
68
|
-
const {
|
|
73
|
+
const {
|
|
74
|
+
stripUpgradedCliLockfilesMock,
|
|
75
|
+
loggerMock,
|
|
76
|
+
recordNpmInstallMock,
|
|
77
|
+
spanSetAttributeMock,
|
|
78
|
+
} = vi.hoisted(() => {
|
|
69
79
|
return {
|
|
70
80
|
stripUpgradedCliLockfilesMock: vi.fn(async () => ({})),
|
|
71
81
|
loggerMock: {
|
|
@@ -74,6 +84,29 @@ const { stripUpgradedCliLockfilesMock, loggerMock } = vi.hoisted(() => {
|
|
|
74
84
|
error: vi.fn(),
|
|
75
85
|
debug: vi.fn(),
|
|
76
86
|
},
|
|
87
|
+
// APPS-4544: spy on the npm-install metric emit. The source calls
|
|
88
|
+
// `recordNpmInstall` once per `upgradeCliWithPackageManager` invocation
|
|
89
|
+
// (in a `finally`), so a test asserting the metric attributes is the
|
|
90
|
+
// contract we want pinned. We mock only this symbol — the rest of
|
|
91
|
+
// `@superblocksteam/telemetry` (sanitize/redact helpers, structured
|
|
92
|
+
// logger shims) is left untouched via `importOriginal` below.
|
|
93
|
+
//
|
|
94
|
+
// Signature is pinned to the real `recordNpmInstall` shape so the
|
|
95
|
+
// type-aware mock.calls[0] tuple resolves to `[NpmInstallMetricInput]`
|
|
96
|
+
// — otherwise `vi.fn(() => …)` infers `[]` and the test assertions
|
|
97
|
+
// can't index `[0]` without a cast.
|
|
98
|
+
recordNpmInstallMock: vi.fn<
|
|
99
|
+
(input: NpmInstallMetricInput) => NpmInstallSpanAttributes
|
|
100
|
+
>(() => ({
|
|
101
|
+
"npm.registry_host": "unknown",
|
|
102
|
+
"npm.outcome": "success",
|
|
103
|
+
})),
|
|
104
|
+
// APPS-4544: capture `span.setAttribute(...)` calls so the
|
|
105
|
+
// `upgradeCli`-span `outcome` attribute assertion can verify the
|
|
106
|
+
// emitted trace fact. The real OTel `Span#setAttribute` returns
|
|
107
|
+
// the span for chaining, but the source under test never chains,
|
|
108
|
+
// so a vi.fn with default `undefined` return is fine.
|
|
109
|
+
spanSetAttributeMock: vi.fn(),
|
|
77
110
|
};
|
|
78
111
|
});
|
|
79
112
|
|
|
@@ -106,12 +139,47 @@ vi.mock("@superblocksteam/shared", async (importOriginal) => {
|
|
|
106
139
|
...actual,
|
|
107
140
|
NON_SB_ORG_UPDATE_ERROR: "non_sb_org_update_error",
|
|
108
141
|
// traceFunction in tests is a passthrough — we just want the fn to run.
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
// APPS-4544: pass a fake span so callbacks that set attributes
|
|
143
|
+
// (e.g. the `upgradeCli` outcome attribute) have somewhere to write,
|
|
144
|
+
// and the test can assert on the recorded calls. Callbacks that
|
|
145
|
+
// ignore the parameter are unaffected.
|
|
146
|
+
//
|
|
147
|
+
// ASSUMPTION (APPS-4544, scout F6): the SAME `spanSetAttributeMock` is
|
|
148
|
+
// handed to every nested `traceFunction` callback (`upgradeCli`,
|
|
149
|
+
// `upgradeCliWithOclif`, `upgradePackageWithPackageManager - cli`,
|
|
150
|
+
// `stripUpgradedCliLockfiles`, …). Today only the outer `upgradeCli`
|
|
151
|
+
// callback calls `span.setAttribute`, so assertions like
|
|
152
|
+
// `toHaveBeenCalledWith("outcome", "success")` are unambiguous. If a
|
|
153
|
+
// future inner fn starts setting span attributes, give it a dedicated
|
|
154
|
+
// span mock (or assert by call index) so its writes don't pollute the
|
|
155
|
+
// outcome assertions here.
|
|
156
|
+
traceFunction: async ({
|
|
157
|
+
fn,
|
|
158
|
+
}: {
|
|
159
|
+
fn: (span: {
|
|
160
|
+
setAttribute: typeof spanSetAttributeMock;
|
|
161
|
+
}) => Promise<unknown>;
|
|
162
|
+
}) => {
|
|
163
|
+
return await fn({ setAttribute: spanSetAttributeMock });
|
|
111
164
|
},
|
|
112
165
|
};
|
|
113
166
|
});
|
|
114
167
|
|
|
168
|
+
// APPS-4544: override only `recordNpmInstall`. The other npm-registry helpers
|
|
169
|
+
// (`bucketNpmRegistryHost`, `normalizeInstallOutcome`,
|
|
170
|
+
// `redactNpmRegistryHostsFromText`) the source still consumes must keep their
|
|
171
|
+
// real implementations so the existing `[npm-install-blocked]` log assertions
|
|
172
|
+
// continue to exercise the real bucket + redaction paths — a mocked
|
|
173
|
+
// `bucketNpmRegistryHost` would silently mask a regression of the original
|
|
174
|
+
// APPS-4381 fix.
|
|
175
|
+
vi.mock("@superblocksteam/telemetry", async (importOriginal) => {
|
|
176
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
177
|
+
return {
|
|
178
|
+
...actual,
|
|
179
|
+
recordNpmInstall: recordNpmInstallMock,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
115
183
|
// ---------------------------------------------------------------------------
|
|
116
184
|
// Test fixtures
|
|
117
185
|
// ---------------------------------------------------------------------------
|
|
@@ -140,6 +208,13 @@ beforeEach(() => {
|
|
|
140
208
|
upgradeWithPackageManagerCalls.length = 0;
|
|
141
209
|
// Restore default mock return values after `clearAllMocks` resets them.
|
|
142
210
|
stripUpgradedCliLockfilesMock.mockImplementation(async () => ({}));
|
|
211
|
+
// APPS-4544: `clearAllMocks` wipes the implementation set in `vi.hoisted`,
|
|
212
|
+
// so re-pin the typed default so callers see a `NpmInstallSpanAttributes`
|
|
213
|
+
// return value instead of `undefined`.
|
|
214
|
+
recordNpmInstallMock.mockImplementation(() => ({
|
|
215
|
+
"npm.registry_host": "unknown",
|
|
216
|
+
"npm.outcome": "success",
|
|
217
|
+
}));
|
|
143
218
|
|
|
144
219
|
// Mock global fetch used by getRemoteVersions
|
|
145
220
|
global.fetch = vi.fn(async () => ({
|
|
@@ -770,6 +845,484 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
770
845
|
);
|
|
771
846
|
});
|
|
772
847
|
|
|
848
|
+
// -----------------------------------------------------------------
|
|
849
|
+
// APPS-4544: superblocks.npm.install.* metric emission for the
|
|
850
|
+
// CLI auto-upgrade runner. The structured `[npm-install-blocked]`
|
|
851
|
+
// log line above only fires on a perimeter block, so operators
|
|
852
|
+
// had no metric to alert on "auto-upgrade just failed everywhere"
|
|
853
|
+
// (vs. an isolated install in the app shell). These tests pin
|
|
854
|
+
// the metric attribute contract so a future refactor of
|
|
855
|
+
// `upgradeCliWithPackageManager` can't silently drop it.
|
|
856
|
+
// -----------------------------------------------------------------
|
|
857
|
+
it("APPS-4544: emits recordNpmInstall on the happy path with runner=auto_upgrade, outcome=success", async () => {
|
|
858
|
+
const { exec } = await import("node:child_process");
|
|
859
|
+
const execMock = vi.mocked(exec);
|
|
860
|
+
// Drive the same flow as `mockNpmFallbackExec` but with a clean
|
|
861
|
+
// (no-error) exit so the metric records outcome="success".
|
|
862
|
+
execMock.mockImplementation(((
|
|
863
|
+
cmd: string,
|
|
864
|
+
...rest: unknown[]
|
|
865
|
+
): unknown => {
|
|
866
|
+
const cb = (
|
|
867
|
+
typeof rest[rest.length - 1] === "function"
|
|
868
|
+
? rest[rest.length - 1]
|
|
869
|
+
: undefined
|
|
870
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
871
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
872
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
873
|
+
return {};
|
|
874
|
+
}
|
|
875
|
+
if (cmd.includes("superblocks update")) {
|
|
876
|
+
// Force fallback to the package-manager path.
|
|
877
|
+
cb?.(null, { stdout: "this version is not updatable\n", stderr: "" });
|
|
878
|
+
return {};
|
|
879
|
+
}
|
|
880
|
+
// Successful install — no error.
|
|
881
|
+
queueMicrotask(() => cb?.(null));
|
|
882
|
+
return { stdout: new EventEmitter(), stderr: new EventEmitter() };
|
|
883
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
884
|
+
|
|
885
|
+
// Post-install version validation must report the target version,
|
|
886
|
+
// otherwise `upgradeCliWithPackageManager` throws and the metric
|
|
887
|
+
// flips to `other` via the catch.
|
|
888
|
+
const versionDetection = await import("./version-detection.js");
|
|
889
|
+
vi.mocked(versionDetection.getCurrentCliVersion)
|
|
890
|
+
.mockResolvedValueOnce({ version: "2.0.0", alias: undefined }) // initial check (stale)
|
|
891
|
+
.mockResolvedValueOnce({
|
|
892
|
+
version: TARGET_CLI_VERSION,
|
|
893
|
+
alias: undefined,
|
|
894
|
+
}); // post-install validation
|
|
895
|
+
|
|
896
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
897
|
+
mockLockService,
|
|
898
|
+
mockConfig,
|
|
899
|
+
false,
|
|
900
|
+
false,
|
|
901
|
+
);
|
|
902
|
+
await Promise.all(result.upgradePromises);
|
|
903
|
+
|
|
904
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
905
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
906
|
+
expect(args).toMatchObject({
|
|
907
|
+
runner: "auto_upgrade",
|
|
908
|
+
outcome: "success",
|
|
909
|
+
// No private registry was configured for this test, so the
|
|
910
|
+
// `configured` boolean falls through to its default (false).
|
|
911
|
+
configured: false,
|
|
912
|
+
});
|
|
913
|
+
expect(typeof args.durationMs).toBe("number");
|
|
914
|
+
expect(args.durationMs).toBeGreaterThanOrEqual(0);
|
|
915
|
+
// On the happy path no classifier ever ran, so the call site
|
|
916
|
+
// leaves `registryHost` undefined; the emitter coerces it to
|
|
917
|
+
// `unknown` via `bucketNpmRegistryHost`.
|
|
918
|
+
expect(args.registryHost).toBeUndefined();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("APPS-4544: emits outcome=registry_unreachable on ECONNREFUSED so the auto_upgrade runner is alertable separately from happy upgrades", async () => {
|
|
922
|
+
await mockNpmFallbackExec(
|
|
923
|
+
[
|
|
924
|
+
"npm error code ECONNREFUSED",
|
|
925
|
+
"npm error errno ECONNREFUSED",
|
|
926
|
+
"npm error network request to https://registry.npmjs.org/@superblocksteam%2fcli failed, reason: connect ECONNREFUSED 127.0.0.1:443",
|
|
927
|
+
].join("\n"),
|
|
928
|
+
new Error("Command failed: pnpm install ..."),
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
932
|
+
/Command failed: pnpm install/,
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
936
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
937
|
+
expect(args.runner).toBe("auto_upgrade");
|
|
938
|
+
// Raw `NpmInstallBlocked.reason` flows through; the emitter
|
|
939
|
+
// aliases it to the short `unreachable` outcome internally
|
|
940
|
+
// (`normalizeInstallOutcome`), which the cardinality test in
|
|
941
|
+
// `emitter.test.ts` already pins.
|
|
942
|
+
expect(args.outcome).toBe("registry_unreachable");
|
|
943
|
+
// The classifier extracted the public-npm host from the stderr
|
|
944
|
+
// line, so it appears here unbucketed (`recordNpmInstall` does
|
|
945
|
+
// the bucketing). This pins that we DO pass it through, since
|
|
946
|
+
// otherwise the public/private split on the metric would be
|
|
947
|
+
// dropped for genuine registry failures.
|
|
948
|
+
expect(args.registryHost).toMatch(/registry\.npmjs\.org/);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it("APPS-4544: emits outcome=registry_auth_failed and the private registry host on E401 (the emitter buckets host at the emit boundary)", async () => {
|
|
952
|
+
const privateHost = "customer.jfrog.io";
|
|
953
|
+
await mockNpmFallbackExec(
|
|
954
|
+
[
|
|
955
|
+
"npm error code E401",
|
|
956
|
+
`npm error 401 Unauthorized - GET https://${privateHost}/@superblocksteam%2fcli`,
|
|
957
|
+
].join("\n"),
|
|
958
|
+
new Error("Command failed: pnpm install ..."),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
962
|
+
/Command failed: pnpm install/,
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
966
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
967
|
+
expect(args.runner).toBe("auto_upgrade");
|
|
968
|
+
expect(args.outcome).toBe("registry_auth_failed");
|
|
969
|
+
// The call-site passes the raw host through; `recordNpmInstall`
|
|
970
|
+
// (the real implementation, mocked here only to capture args)
|
|
971
|
+
// is what coerces it to `private`. The emitter cardinality
|
|
972
|
+
// test in `emitter.test.ts` pins that side; this test pins
|
|
973
|
+
// that the upgrade path actually feeds the host in.
|
|
974
|
+
expect(args.registryHost).toBe(privateHost);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it("APPS-4544: emits outcome=other for an unclassified failure (ERESOLVE) so the metric reflects the failure even without a NpmInstallBlocked match", async () => {
|
|
978
|
+
await mockNpmFallbackExec(
|
|
979
|
+
[
|
|
980
|
+
"npm error code ERESOLVE",
|
|
981
|
+
"npm error ERESOLVE unable to resolve dependency tree",
|
|
982
|
+
].join("\n"),
|
|
983
|
+
new Error("Command failed: pnpm install ..."),
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
987
|
+
/Command failed: pnpm install/,
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
991
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
992
|
+
expect(args.runner).toBe("auto_upgrade");
|
|
993
|
+
// No classifier match → `other`. The previous behaviour (before
|
|
994
|
+
// this test was added) was that the install promise rejected but
|
|
995
|
+
// no metric fired at all on the auto_upgrade runner, so a future
|
|
996
|
+
// refactor that drops the unclassified branch from the call site
|
|
997
|
+
// would silently turn this back into a metric gap.
|
|
998
|
+
expect(args.outcome).toBe("other");
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// APPS-4544 (scout F2): the exec can SUCCEED (no error → outcome stays
|
|
1002
|
+
// `success`) yet the upgrade still be broken — npm/pnpm reported success
|
|
1003
|
+
// but the new binary isn't on PATH, a shadowing binary wins, or the
|
|
1004
|
+
// install was corrupted. `upgradeCliWithPackageManager` validates the
|
|
1005
|
+
// post-install version and, on mismatch/missing, flips `outcome` from
|
|
1006
|
+
// `success` to `other` in its `catch` before re-throwing. That
|
|
1007
|
+
// `success → other` flip is the ONLY transition of its kind and has no
|
|
1008
|
+
// other coverage, so pin it: a future refactor that drops the flip would
|
|
1009
|
+
// otherwise emit `outcome=success` for a CLI that's still stale.
|
|
1010
|
+
it("APPS-4544: emits outcome=other when the exec succeeds but post-install version validation fails", async () => {
|
|
1011
|
+
const { exec } = await import("node:child_process");
|
|
1012
|
+
const execMock = vi.mocked(exec);
|
|
1013
|
+
execMock.mockImplementation(((
|
|
1014
|
+
cmd: string,
|
|
1015
|
+
...rest: unknown[]
|
|
1016
|
+
): unknown => {
|
|
1017
|
+
const cb = (
|
|
1018
|
+
typeof rest[rest.length - 1] === "function"
|
|
1019
|
+
? rest[rest.length - 1]
|
|
1020
|
+
: undefined
|
|
1021
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
1022
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
1023
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
1024
|
+
return {};
|
|
1025
|
+
}
|
|
1026
|
+
if (cmd.includes("superblocks update")) {
|
|
1027
|
+
// Force the package-manager fallback (where post-install
|
|
1028
|
+
// validation runs).
|
|
1029
|
+
cb?.(null, { stdout: "this version is not updatable\n", stderr: "" });
|
|
1030
|
+
return {};
|
|
1031
|
+
}
|
|
1032
|
+
// Install exec SUCCEEDS (no error) — so the classifier never runs
|
|
1033
|
+
// and `outcome` stays `success` until post-install validation.
|
|
1034
|
+
queueMicrotask(() => cb?.(null));
|
|
1035
|
+
return { stdout: new EventEmitter(), stderr: new EventEmitter() };
|
|
1036
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
1037
|
+
|
|
1038
|
+
// Initial check reports stale (triggers upgrade); post-install
|
|
1039
|
+
// validation reports a version that does NOT match the target, so
|
|
1040
|
+
// `upgradeCliWithPackageManager` throws "CLI version mismatch".
|
|
1041
|
+
const versionDetection = await import("./version-detection.js");
|
|
1042
|
+
vi.mocked(versionDetection.getCurrentCliVersion)
|
|
1043
|
+
.mockResolvedValueOnce({ version: "2.0.0", alias: undefined }) // initial (stale)
|
|
1044
|
+
.mockResolvedValueOnce({ version: "1.9.0", alias: undefined }); // post-install (mismatch)
|
|
1045
|
+
|
|
1046
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1047
|
+
mockLockService,
|
|
1048
|
+
mockConfig,
|
|
1049
|
+
false,
|
|
1050
|
+
false,
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
// The validation failure rejects the upgrade promise (so dev.mts
|
|
1054
|
+
// won't restart into a stale CLI)...
|
|
1055
|
+
await expect(Promise.all(result.upgradePromises)).rejects.toThrow(
|
|
1056
|
+
/CLI version mismatch after upgrade/,
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// ...and the metric still fires exactly once, flipped to `other`.
|
|
1060
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
1061
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
1062
|
+
expect(args.runner).toBe("auto_upgrade");
|
|
1063
|
+
expect(args.outcome).toBe("other");
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// APPS-4544: ticket explicitly calls out "emit from both
|
|
1067
|
+
// `upgradeCliWithPackageManager` and the oclif path". Without an
|
|
1068
|
+
// oclif-success emission, dashboards undercount successful upgrades
|
|
1069
|
+
// whenever the binary was installed via the oclif tarball channel —
|
|
1070
|
+
// that path skips the pnpm fallback entirely, so there is no other
|
|
1071
|
+
// call site that could fire the metric.
|
|
1072
|
+
it("APPS-4544: emits recordNpmInstall on oclif success without falling back to the package manager", async () => {
|
|
1073
|
+
const { exec } = await import("node:child_process");
|
|
1074
|
+
const execMock = vi.mocked(exec);
|
|
1075
|
+
execMock.mockImplementation(((
|
|
1076
|
+
cmd: string,
|
|
1077
|
+
...rest: unknown[]
|
|
1078
|
+
): unknown => {
|
|
1079
|
+
const cb = (
|
|
1080
|
+
typeof rest[rest.length - 1] === "function"
|
|
1081
|
+
? rest[rest.length - 1]
|
|
1082
|
+
: undefined
|
|
1083
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
1084
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
1085
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
1086
|
+
return {};
|
|
1087
|
+
}
|
|
1088
|
+
if (cmd.includes("superblocks update")) {
|
|
1089
|
+
// No "not updatable" in the output → oclif considers itself
|
|
1090
|
+
// the canonical upgrader and the package-manager fallback does
|
|
1091
|
+
// NOT run.
|
|
1092
|
+
cb?.(null, { stdout: "Upgraded to 2.1.0\n", stderr: "" });
|
|
1093
|
+
return {};
|
|
1094
|
+
}
|
|
1095
|
+
// Any other exec call is unexpected for the oclif-only path.
|
|
1096
|
+
cb?.(new Error(`unexpected exec: ${cmd}`));
|
|
1097
|
+
return {};
|
|
1098
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
1099
|
+
|
|
1100
|
+
const versionDetection = await import("./version-detection.js");
|
|
1101
|
+
vi.mocked(versionDetection.getCurrentCliVersion).mockResolvedValue({
|
|
1102
|
+
version: "2.0.0",
|
|
1103
|
+
alias: undefined,
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1107
|
+
mockLockService,
|
|
1108
|
+
mockConfig,
|
|
1109
|
+
false,
|
|
1110
|
+
false,
|
|
1111
|
+
);
|
|
1112
|
+
// The oclif `recordNpmInstall` fires inside the `upgradeCli`
|
|
1113
|
+
// promise, which is pushed to `upgradePromises` and NOT awaited by
|
|
1114
|
+
// `checkVersionsAndWritePackageJson`. Await it explicitly (matching
|
|
1115
|
+
// the happy-path test) so the assertion can't race the microtask that
|
|
1116
|
+
// emits the metric if the fixture's I/O ordering ever changes.
|
|
1117
|
+
await Promise.all(result.upgradePromises);
|
|
1118
|
+
|
|
1119
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
1120
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
1121
|
+
expect(args).toMatchObject({
|
|
1122
|
+
runner: "auto_upgrade",
|
|
1123
|
+
outcome: "success",
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// APPS-4554: a successful oclif upgrade replaces the on-disk CLI tarball
|
|
1128
|
+
// but the running process keeps the OLD code, so the dev server must
|
|
1129
|
+
// restart to pick it up. `dev.mts` gates that restart on
|
|
1130
|
+
// `result.cliUpdated` (`hasCliUpdated`). Before this fix `cliUpdated` was
|
|
1131
|
+
// only set in the package-manager fallback branch, so an oclif-only
|
|
1132
|
+
// upgrade left the dev server running stale code until a manual restart.
|
|
1133
|
+
// This test pins that the oclif-success branch flips the flag too, so the
|
|
1134
|
+
// two upgrade paths stay symmetric.
|
|
1135
|
+
it("APPS-4554: sets cliUpdated=true on oclif success so the dev server restarts", async () => {
|
|
1136
|
+
const { exec } = await import("node:child_process");
|
|
1137
|
+
const execMock = vi.mocked(exec);
|
|
1138
|
+
execMock.mockImplementation(((
|
|
1139
|
+
cmd: string,
|
|
1140
|
+
...rest: unknown[]
|
|
1141
|
+
): unknown => {
|
|
1142
|
+
const cb = (
|
|
1143
|
+
typeof rest[rest.length - 1] === "function"
|
|
1144
|
+
? rest[rest.length - 1]
|
|
1145
|
+
: undefined
|
|
1146
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
1147
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
1148
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
1149
|
+
return {};
|
|
1150
|
+
}
|
|
1151
|
+
if (cmd.includes("superblocks update")) {
|
|
1152
|
+
// No "not updatable" in the output → oclif succeeds outright and
|
|
1153
|
+
// the package-manager fallback (which historically set the flag)
|
|
1154
|
+
// never runs.
|
|
1155
|
+
cb?.(null, { stdout: "Upgraded to 2.1.0\n", stderr: "" });
|
|
1156
|
+
return {};
|
|
1157
|
+
}
|
|
1158
|
+
cb?.(new Error(`unexpected exec: ${cmd}`));
|
|
1159
|
+
return {};
|
|
1160
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
1161
|
+
|
|
1162
|
+
const versionDetection = await import("./version-detection.js");
|
|
1163
|
+
vi.mocked(versionDetection.getCurrentCliVersion).mockResolvedValue({
|
|
1164
|
+
version: "2.0.0",
|
|
1165
|
+
alias: undefined,
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1169
|
+
mockLockService,
|
|
1170
|
+
mockConfig,
|
|
1171
|
+
false,
|
|
1172
|
+
false,
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
expect(result.cliUpdated).toBe(true);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// APPS-4544: the ticket calls for an `outcome` attribute on the
|
|
1179
|
+
// existing `upgradeCli` span so traces carry the upgrade result.
|
|
1180
|
+
// This is the trace-side complement to the install metric — a
|
|
1181
|
+
// single attribute lets operators correlate a failed CLI upgrade
|
|
1182
|
+
// span with the pod that crash-looped, without correlating
|
|
1183
|
+
// metric series back to traces by host+timestamp.
|
|
1184
|
+
it("APPS-4544: sets outcome=success on the upgradeCli span when the upgrade completes", async () => {
|
|
1185
|
+
const { exec } = await import("node:child_process");
|
|
1186
|
+
const execMock = vi.mocked(exec);
|
|
1187
|
+
execMock.mockImplementation(((
|
|
1188
|
+
cmd: string,
|
|
1189
|
+
...rest: unknown[]
|
|
1190
|
+
): unknown => {
|
|
1191
|
+
const cb = (
|
|
1192
|
+
typeof rest[rest.length - 1] === "function"
|
|
1193
|
+
? rest[rest.length - 1]
|
|
1194
|
+
: undefined
|
|
1195
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
1196
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
1197
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
1198
|
+
return {};
|
|
1199
|
+
}
|
|
1200
|
+
if (cmd.includes("superblocks update")) {
|
|
1201
|
+
cb?.(null, { stdout: "Upgraded to 2.1.0\n", stderr: "" });
|
|
1202
|
+
return {};
|
|
1203
|
+
}
|
|
1204
|
+
cb?.(null);
|
|
1205
|
+
return { stdout: new EventEmitter(), stderr: new EventEmitter() };
|
|
1206
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
1207
|
+
|
|
1208
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1209
|
+
mockLockService,
|
|
1210
|
+
mockConfig,
|
|
1211
|
+
false,
|
|
1212
|
+
false,
|
|
1213
|
+
);
|
|
1214
|
+
// The `upgradeCli` span callback runs inside one of the
|
|
1215
|
+
// `upgradePromises` (the per-package upgrade futures pushed
|
|
1216
|
+
// during the outer `checkVersionsAndUpgrade` span). The
|
|
1217
|
+
// top-level `checkVersionsAndWritePackageJson` resolves before
|
|
1218
|
+
// those finish — match the product call site that consumes
|
|
1219
|
+
// `result.upgradePromises` directly.
|
|
1220
|
+
await Promise.all(result.upgradePromises);
|
|
1221
|
+
|
|
1222
|
+
expect(spanSetAttributeMock).toHaveBeenCalledWith("outcome", "success");
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it("APPS-4544: sets outcome=failure on the upgradeCli span when the package manager throws", async () => {
|
|
1226
|
+
await mockNpmFallbackExec(
|
|
1227
|
+
["npm error code ECONNREFUSED", "npm error errno ECONNREFUSED"].join(
|
|
1228
|
+
"\n",
|
|
1229
|
+
),
|
|
1230
|
+
new Error("Command failed: pnpm install ..."),
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1234
|
+
mockLockService,
|
|
1235
|
+
mockConfig,
|
|
1236
|
+
false,
|
|
1237
|
+
false,
|
|
1238
|
+
);
|
|
1239
|
+
// The pkg-manager fallback rejects the upgrade promise; capture
|
|
1240
|
+
// it so the test doesn't surface as an unhandled rejection,
|
|
1241
|
+
// then assert on the span attribute the `catch` set.
|
|
1242
|
+
await expect(Promise.all(result.upgradePromises)).rejects.toThrow(
|
|
1243
|
+
/Command failed: pnpm install/,
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
expect(spanSetAttributeMock).toHaveBeenCalledWith("outcome", "failure");
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// APPS-4544 (Bugbot 613adfc7): when the detected package manager can't be
|
|
1250
|
+
// turned into a global install command, `resolveCommand` returns
|
|
1251
|
+
// undefined. The old code logged and returned without throwing, so the
|
|
1252
|
+
// npm fallback became a silent no-op — the `upgradeCli` span still
|
|
1253
|
+
// recorded `outcome=success` and no `recordNpmInstall` fired, producing a
|
|
1254
|
+
// trace that claimed a successful auto-upgrade with no matching install
|
|
1255
|
+
// metric. The fix emits a `runner=auto_upgrade` failure sample and flips
|
|
1256
|
+
// the span to `outcome=failure` so the no-op is visible in metrics and
|
|
1257
|
+
// traces — but it does NOT throw, because a rejected upgrade promise
|
|
1258
|
+
// routes through `dev.mts` to `process.exit(1)` and crash-loops the
|
|
1259
|
+
// live-edit pod. The no-op dev-server restart that still follows, graceful
|
|
1260
|
+
// degrade, and user surfacing of the failed upgrade are tracked by
|
|
1261
|
+
// APPS-4457, so this test asserts the promise RESOLVES.
|
|
1262
|
+
it("APPS-4544: emits outcome=other and fails the span (without throwing) when no global install command can be resolved", async () => {
|
|
1263
|
+
const { exec } = await import("node:child_process");
|
|
1264
|
+
const execMock = vi.mocked(exec);
|
|
1265
|
+
execMock.mockImplementation(((
|
|
1266
|
+
cmd: string,
|
|
1267
|
+
...rest: unknown[]
|
|
1268
|
+
): unknown => {
|
|
1269
|
+
const cb = (
|
|
1270
|
+
typeof rest[rest.length - 1] === "function"
|
|
1271
|
+
? rest[rest.length - 1]
|
|
1272
|
+
: undefined
|
|
1273
|
+
) as ((err: Error | null, result?: unknown) => void) | undefined;
|
|
1274
|
+
if (cmd === "which superblocks" || cmd === "where superblocks") {
|
|
1275
|
+
cb?.(null, { stdout: "/usr/local/bin/superblocks\n", stderr: "" });
|
|
1276
|
+
return {};
|
|
1277
|
+
}
|
|
1278
|
+
if (cmd.includes("superblocks update")) {
|
|
1279
|
+
// "not updatable" → oclif defers to the package-manager fallback,
|
|
1280
|
+
// where `resolveCommand` is consulted.
|
|
1281
|
+
cb?.(null, { stdout: "this version is not updatable\n", stderr: "" });
|
|
1282
|
+
return {};
|
|
1283
|
+
}
|
|
1284
|
+
// The install exec must never run: `resolveCommand` fails first.
|
|
1285
|
+
cb?.(new Error(`unexpected exec: ${cmd}`));
|
|
1286
|
+
return {};
|
|
1287
|
+
}) as unknown as Parameters<typeof execMock.mockImplementation>[0]);
|
|
1288
|
+
|
|
1289
|
+
// Force the single source-side `resolveCommand` call (the global
|
|
1290
|
+
// install command builder) to fail to resolve a command. `*Once`
|
|
1291
|
+
// takes precedence over the default mock implementation for the next
|
|
1292
|
+
// call only, which is exactly the one we want to break.
|
|
1293
|
+
const { resolveCommand } = await import("package-manager-detector");
|
|
1294
|
+
vi.mocked(resolveCommand).mockReturnValueOnce(
|
|
1295
|
+
undefined as unknown as ReturnType<typeof resolveCommand>,
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
1299
|
+
mockLockService,
|
|
1300
|
+
mockConfig,
|
|
1301
|
+
false,
|
|
1302
|
+
false,
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
// The upgrade promise must RESOLVE, not reject: a no-resolvable-command
|
|
1306
|
+
// upgrade is a no-op the live-edit pod degrades through, not a throw that
|
|
1307
|
+
// `dev.mts` would route to `process.exit(1)` (crash-loop). Graceful
|
|
1308
|
+
// degrade + user surfacing of the failed upgrade is tracked by APPS-4457.
|
|
1309
|
+
await expect(Promise.all(result.upgradePromises)).resolves.toHaveLength(
|
|
1310
|
+
1,
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
// A failure sample is still recorded for the auto_upgrade runner so the
|
|
1314
|
+
// metric isn't silently missing for this path.
|
|
1315
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
1316
|
+
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
1317
|
+
expect(args.runner).toBe("auto_upgrade");
|
|
1318
|
+
expect(args.outcome).toBe("other");
|
|
1319
|
+
|
|
1320
|
+
// The span outcome must still reflect the failure, not the default
|
|
1321
|
+
// success, so the no-op upgrade stays visible in traces even though the
|
|
1322
|
+
// promise resolves.
|
|
1323
|
+
expect(spanSetAttributeMock).toHaveBeenCalledWith("outcome", "failure");
|
|
1324
|
+
});
|
|
1325
|
+
|
|
773
1326
|
it("leaves sdk-api alone when the server response omits sdkApi (older deployment)", async () => {
|
|
774
1327
|
global.fetch = vi.fn(async () => ({
|
|
775
1328
|
ok: true,
|