@superblocksteam/sdk 2.0.123-next.0 → 2.0.124-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts +37 -1
  3. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  4. package/dist/cli-replacement/automatic-upgrades.js +162 -10
  5. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  6. package/dist/cli-replacement/automatic-upgrades.test.js +377 -8
  7. package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
  8. package/dist/cli-replacement/dependency-install-classifier.d.mts +21 -0
  9. package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -0
  10. package/dist/cli-replacement/dependency-install-classifier.mjs +83 -0
  11. package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -0
  12. package/dist/cli-replacement/dependency-install-classifier.test.d.mts +2 -0
  13. package/dist/cli-replacement/dependency-install-classifier.test.d.mts.map +1 -0
  14. package/dist/cli-replacement/dependency-install-classifier.test.mjs +51 -0
  15. package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -0
  16. package/dist/cli-replacement/dev-s3-restore.test.mjs +170 -14
  17. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  18. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +33 -2
  19. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  20. package/dist/cli-replacement/dev-token-priming.test.d.mts +31 -0
  21. package/dist/cli-replacement/dev-token-priming.test.d.mts.map +1 -0
  22. package/dist/cli-replacement/dev-token-priming.test.mjs +87 -0
  23. package/dist/cli-replacement/dev-token-priming.test.mjs.map +1 -0
  24. package/dist/cli-replacement/dev.d.mts +36 -0
  25. package/dist/cli-replacement/dev.d.mts.map +1 -1
  26. package/dist/cli-replacement/dev.interception.test.d.mts +2 -0
  27. package/dist/cli-replacement/dev.interception.test.d.mts.map +1 -0
  28. package/dist/cli-replacement/dev.interception.test.mjs +68 -0
  29. package/dist/cli-replacement/dev.interception.test.mjs.map +1 -0
  30. package/dist/cli-replacement/dev.mjs +396 -62
  31. package/dist/cli-replacement/dev.mjs.map +1 -1
  32. package/dist/cli-replacement/home-npmrc.d.mts +180 -0
  33. package/dist/cli-replacement/home-npmrc.d.mts.map +1 -0
  34. package/dist/cli-replacement/home-npmrc.mjs +283 -0
  35. package/dist/cli-replacement/home-npmrc.mjs.map +1 -0
  36. package/dist/cli-replacement/home-npmrc.test.d.mts +10 -0
  37. package/dist/cli-replacement/home-npmrc.test.d.mts.map +1 -0
  38. package/dist/cli-replacement/home-npmrc.test.mjs +582 -0
  39. package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -0
  40. package/dist/cli-replacement/install-packages.classify.test.d.mts +2 -0
  41. package/dist/cli-replacement/install-packages.classify.test.d.mts.map +1 -0
  42. package/dist/cli-replacement/install-packages.classify.test.mjs +125 -0
  43. package/dist/cli-replacement/install-packages.classify.test.mjs.map +1 -0
  44. package/dist/cli-replacement/install-packages.npm-registry.test.d.mts +2 -0
  45. package/dist/cli-replacement/install-packages.npm-registry.test.d.mts.map +1 -0
  46. package/dist/cli-replacement/install-packages.npm-registry.test.mjs +260 -0
  47. package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -0
  48. package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts +58 -0
  49. package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts.map +1 -0
  50. package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs +224 -0
  51. package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs.map +1 -0
  52. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts +11 -0
  53. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts.map +1 -0
  54. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs +317 -0
  55. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs.map +1 -0
  56. package/dist/cli-replacement/userconfig-env.integration.test.d.mts +26 -0
  57. package/dist/cli-replacement/userconfig-env.integration.test.d.mts.map +1 -0
  58. package/dist/cli-replacement/userconfig-env.integration.test.mjs +148 -0
  59. package/dist/cli-replacement/userconfig-env.integration.test.mjs.map +1 -0
  60. package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
  61. package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
  62. package/dist/dev-utils/dev-server-metrics.mjs +84 -0
  63. package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
  64. package/dist/dev-utils/dev-server-metrics.test.d.mts +2 -0
  65. package/dist/dev-utils/dev-server-metrics.test.d.mts.map +1 -0
  66. package/dist/dev-utils/dev-server-metrics.test.mjs +26 -0
  67. package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -0
  68. package/dist/dev-utils/dev-server.d.mts +23 -1
  69. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  70. package/dist/dev-utils/dev-server.mjs +21 -9
  71. package/dist/dev-utils/dev-server.mjs.map +1 -1
  72. package/dist/dev-utils/dev-server.status.test.d.mts +2 -0
  73. package/dist/dev-utils/dev-server.status.test.d.mts.map +1 -0
  74. package/dist/dev-utils/dev-server.status.test.mjs +41 -0
  75. package/dist/dev-utils/dev-server.status.test.mjs.map +1 -0
  76. package/dist/dev-utils/token-manager.d.ts +31 -0
  77. package/dist/dev-utils/token-manager.d.ts.map +1 -1
  78. package/dist/dev-utils/token-manager.js +34 -0
  79. package/dist/dev-utils/token-manager.js.map +1 -1
  80. package/dist/telemetry/local-obs.js +1 -1
  81. package/dist/telemetry/local-obs.js.map +1 -1
  82. package/dist/telemetry/util.js +1 -1
  83. package/dist/types/scoped-jwt-token-payload.d.ts +1 -0
  84. package/dist/types/scoped-jwt-token-payload.d.ts.map +1 -1
  85. package/dist/version-control.d.mts.map +1 -1
  86. package/dist/version-control.mjs +6 -7
  87. package/dist/version-control.mjs.map +1 -1
  88. package/package.json +12 -12
  89. package/src/cli-replacement/automatic-upgrades.test.ts +530 -8
  90. package/src/cli-replacement/automatic-upgrades.ts +179 -7
  91. package/src/cli-replacement/dependency-install-classifier.mts +118 -0
  92. package/src/cli-replacement/dependency-install-classifier.test.mts +72 -0
  93. package/src/cli-replacement/dev-s3-restore.test.mts +210 -14
  94. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +35 -2
  95. package/src/cli-replacement/dev-token-priming.test.mts +103 -0
  96. package/src/cli-replacement/dev.interception.test.mts +80 -0
  97. package/src/cli-replacement/dev.mts +495 -92
  98. package/src/cli-replacement/home-npmrc.mts +409 -0
  99. package/src/cli-replacement/home-npmrc.test.mts +757 -0
  100. package/src/cli-replacement/install-packages.classify.test.mts +168 -0
  101. package/src/cli-replacement/install-packages.npm-registry.test.mts +345 -0
  102. package/src/cli-replacement/post-upgrade-lockfile-strip.mts +296 -0
  103. package/src/cli-replacement/post-upgrade-lockfile-strip.test.mts +482 -0
  104. package/src/cli-replacement/userconfig-env.integration.test.mts +189 -0
  105. package/src/dev-utils/dev-server-metrics.mts +96 -0
  106. package/src/dev-utils/dev-server-metrics.test.mts +38 -0
  107. package/src/dev-utils/dev-server.mts +48 -8
  108. package/src/dev-utils/dev-server.status.test.mts +58 -0
  109. package/src/dev-utils/token-manager.ts +36 -0
  110. package/src/telemetry/local-obs.ts +1 -1
  111. package/src/telemetry/util.ts +1 -1
  112. package/src/types/scoped-jwt-token-payload.ts +1 -0
  113. package/src/version-control.mts +8 -6
  114. package/tsconfig.tsbuildinfo +1 -1
  115. package/.turbo/turbo-publish-package.log +0 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Failure-classification coverage for the dev-server startup `installPackages`
3
+ * (APPS-4450). When the verification install fails, `installPackages` no longer
4
+ * rethrows the raw `exec` rejection — it classifies the npm `--json` envelope
5
+ * into a structured `DependencyInstallError` and throws the
6
+ * `InitialInstallFailed` marker so the `dev()` catch can degrade by origin.
7
+ *
8
+ * Mirrors the mock style of the sibling `install-packages.npm-registry.test.mts`
9
+ * (stub `node:child_process`, `package-manager-detector`, `node:fs/promises`,
10
+ * and the logging module; hand a fake `NpmRegistryClient`). The one new wrinkle
11
+ * is the failure path: the promisified `exec` rejects with an error object that
12
+ * carries the ERESOLVE `--json` payload on `.stdout` and the npm error-code
13
+ * banner on `.stderr`, exactly as `util.promisify(child_process.exec)` does in
14
+ * production (it preserves stdout/stderr on the rejected error).
15
+ */
16
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
17
+
18
+ import type { installPackages as InstallPackagesType } from "./dev.mjs";
19
+
20
+ const execMock = vi.fn();
21
+ const execFileMock = vi.fn();
22
+ vi.mock("node:child_process", () => ({
23
+ default: { exec: execMock, execFile: execFileMock },
24
+ exec: execMock,
25
+ execFile: execFileMock,
26
+ }));
27
+
28
+ const detectMock = vi.fn();
29
+ vi.mock("package-manager-detector/detect", () => ({
30
+ detect: (...args: unknown[]) => detectMock(...args),
31
+ }));
32
+
33
+ const resolveCommandMock = vi.fn();
34
+ vi.mock("package-manager-detector", () => ({
35
+ resolveCommand: (...args: unknown[]) => resolveCommandMock(...args),
36
+ }));
37
+
38
+ vi.mock("node:fs/promises", () => ({
39
+ readFile: vi.fn(async () => "{}"),
40
+ writeFile: vi.fn(async () => undefined),
41
+ // `installPackages` ensures `<app>/.superblocks/logs` exists (npm's
42
+ // `logs-dir` target) before spawning the install; without this stub the
43
+ // real `mkdir` is undefined and throws before the exec mock, misclassifying
44
+ // the failure as `unknown`. Mirrors the sibling npm-registry test.
45
+ mkdir: vi.fn(async () => undefined),
46
+ }));
47
+
48
+ const mockLogger = {
49
+ info: vi.fn(),
50
+ warn: vi.fn(),
51
+ error: vi.fn(),
52
+ debug: vi.fn(),
53
+ };
54
+
55
+ vi.mock("../telemetry/logging.js", () => ({
56
+ getLogger: () => mockLogger,
57
+ getErrorMeta: (err: unknown) => ({
58
+ error: { kind: "Error", message: err instanceof Error ? err.message : "" },
59
+ }),
60
+ }));
61
+
62
+ type NpmRegistryFetchResult = {
63
+ source: "configured" | "not-configured" | "stale" | "unreachable";
64
+ config: { configured: boolean; allowInstallScripts?: boolean };
65
+ };
66
+
67
+ function fakeRegistryClient(
68
+ result: NpmRegistryFetchResult | (() => Promise<NpmRegistryFetchResult>),
69
+ ) {
70
+ return {
71
+ getConfig: vi.fn(async () =>
72
+ typeof result === "function" ? result() : result,
73
+ ),
74
+ } as unknown as Parameters<typeof InstallPackagesType>[2];
75
+ }
76
+
77
+ function notConfigured(): NpmRegistryFetchResult {
78
+ return { source: "not-configured", config: { configured: false } };
79
+ }
80
+
81
+ /**
82
+ * The ERESOLVE `--json` envelope npm writes to stdout when a peer-dependency
83
+ * conflict aborts the install. `util.promisify(exec)` rejects with an Error
84
+ * whose `.stdout` carries this body and whose `.stderr` carries the
85
+ * `npm error code ERESOLVE` banner — both preserved separately on the
86
+ * rejected error (the contract the classifier relies on).
87
+ */
88
+ const ERESOLVE_JSON_STDOUT = JSON.stringify({
89
+ error: {
90
+ code: "ERESOLVE",
91
+ summary: "unable to resolve dependency tree",
92
+ detail:
93
+ "Found: react@17.0.2\nCould not resolve dependency:\npeer react@^18.0.0 from @some/pkg@1.0.0",
94
+ },
95
+ });
96
+
97
+ const ERESOLVE_STDERR = [
98
+ "npm error code ERESOLVE",
99
+ "npm error ERESOLVE unable to resolve dependency tree",
100
+ ].join("\n");
101
+
102
+ beforeAll(async () => {
103
+ await import("./dev.mjs");
104
+ });
105
+
106
+ beforeEach(() => {
107
+ vi.clearAllMocks();
108
+ detectMock.mockResolvedValue({
109
+ name: "npm",
110
+ agent: "npm",
111
+ version: "10.0.0",
112
+ });
113
+ resolveCommandMock.mockImplementation(
114
+ (_agent: string, _action: string, args: string[]) => ({
115
+ command: "npm",
116
+ args: ["install", ...args],
117
+ }),
118
+ );
119
+ });
120
+
121
+ describe("installPackages classifies install failures (APPS-4450)", () => {
122
+ it("throws InitialInstallFailed(dependency_conflict) on an ERESOLVE install", async () => {
123
+ const { installPackages } = await import("./dev.mjs");
124
+ const client = fakeRegistryClient(notConfigured());
125
+
126
+ // Reject exactly the way `util.promisify(child_process.exec)` does: the
127
+ // rejected value is an Error with `.stdout` and `.stderr` preserved
128
+ // separately. The ERESOLVE `--json` body lives on `.stdout`.
129
+ execMock.mockImplementation(
130
+ (
131
+ _cmd: string,
132
+ _opts: unknown,
133
+ cb?: (err: unknown, result?: { stdout: string }) => void,
134
+ ) => {
135
+ const callback = (typeof _opts === "function" ? _opts : cb) as (
136
+ err: unknown,
137
+ result?: { stdout: string },
138
+ ) => void;
139
+ const err = Object.assign(
140
+ new Error("Command failed: npm install --fund=false --audit=false"),
141
+ {
142
+ stdout: ERESOLVE_JSON_STDOUT,
143
+ stderr: ERESOLVE_STDERR,
144
+ code: 1,
145
+ },
146
+ );
147
+ callback(err);
148
+ },
149
+ );
150
+
151
+ await expect(
152
+ installPackages("/tmp/app", mockLogger as never, client),
153
+ ).rejects.toMatchObject({
154
+ name: "InitialInstallFailed",
155
+ serverError: {
156
+ category: "dependency_conflict",
157
+ npmErrorCode: "ERESOLVE",
158
+ },
159
+ });
160
+
161
+ // Guard against the mock being silently bypassed: if `dev.mjs` ever binds
162
+ // `exec` to the REAL `child_process.exec` (a module-scope capture before
163
+ // this file's mock is live), real npm runs against the missing `/tmp/app`
164
+ // and the classification quietly degrades to unknown/undefined. Asserting
165
+ // the stub fired turns that into an explicit failure instead. (APPS-4450)
166
+ expect(execMock).toHaveBeenCalledTimes(1);
167
+ });
168
+ });
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Argv-matrix coverage for the dev-server startup `installPackages` honoring
3
+ * the per-org `allow_install_scripts` policy (APPS-4251).
4
+ *
5
+ * Mirrors the structure of
6
+ * `packages/vite-plugin-file-sync/src/ai-service/app-interface/shell.npm-registry.test.ts`
7
+ * (the AppShell-side coverage): we stub `package-manager-detector`, capture
8
+ * the args handed to `resolveCommand`, drive a fake `NpmRegistryClient`, and
9
+ * assert whether `--ignore-scripts` made it onto the install argv.
10
+ *
11
+ * Why exercise this here rather than via the full `dev()` test surface:
12
+ * `installPackages` is the unit that picked up the new policy plumbing.
13
+ * Testing through `dev()` would need ~250 lines of mock setup unrelated to
14
+ * the policy decision, and the test would still be asserting the same
15
+ * `resolveCommand`-args invariant. The function is intentionally exported
16
+ * from `dev.mts` for this purpose.
17
+ */
18
+ import {
19
+ beforeAll,
20
+ beforeEach,
21
+ describe,
22
+ expect,
23
+ it,
24
+ vi,
25
+ type Mock,
26
+ } from "vitest";
27
+
28
+ import type { installPackages as InstallPackagesType } from "./dev.mjs";
29
+
30
+ const execMock = vi.fn();
31
+ const execFileMock = vi.fn();
32
+ vi.mock("node:child_process", () => ({
33
+ default: { exec: execMock, execFile: execFileMock },
34
+ exec: execMock,
35
+ execFile: execFileMock,
36
+ }));
37
+
38
+ const detectMock = vi.fn();
39
+ vi.mock("package-manager-detector/detect", () => ({
40
+ detect: (...args: unknown[]) => detectMock(...args),
41
+ }));
42
+
43
+ const resolveCommandMock = vi.fn();
44
+ vi.mock("package-manager-detector", () => ({
45
+ resolveCommand: (...args: unknown[]) => resolveCommandMock(...args),
46
+ }));
47
+
48
+ vi.mock("node:fs/promises", () => ({
49
+ readFile: vi.fn(async () => "{}"),
50
+ writeFile: vi.fn(async () => undefined),
51
+ // `installPackages` ensures the `<app>/.superblocks/logs` dir exists
52
+ // before spawning the install (npm's `logs-dir` target).
53
+ mkdir: vi.fn(async () => undefined),
54
+ }));
55
+
56
+ const mockLogger = {
57
+ info: vi.fn(),
58
+ warn: vi.fn(),
59
+ error: vi.fn(),
60
+ debug: vi.fn(),
61
+ };
62
+
63
+ vi.mock("../telemetry/logging.js", () => ({
64
+ getLogger: () => mockLogger,
65
+ getErrorMeta: (err: unknown) => ({
66
+ error: { kind: "Error", message: err instanceof Error ? err.message : "" },
67
+ }),
68
+ }));
69
+
70
+ type NpmRegistryFetchResult = {
71
+ source: "configured" | "not-configured" | "stale";
72
+ config: {
73
+ configured: boolean;
74
+ allowInstallScripts?: boolean;
75
+ };
76
+ };
77
+
78
+ function fakeRegistryClient(
79
+ result: NpmRegistryFetchResult | (() => Promise<NpmRegistryFetchResult>),
80
+ ) {
81
+ return {
82
+ getConfig: vi.fn(async () =>
83
+ typeof result === "function" ? result() : result,
84
+ ),
85
+ // The dev-server install path only calls `getConfig()`; the rest of the
86
+ // `NpmRegistryClient` surface (JWT refresh, cache TTL) is exercised by
87
+ // the vite-plugin-file-sync side. Casting through `unknown` lets us
88
+ // hand this minimal stub to a parameter typed `NpmRegistryClient`
89
+ // without dragging the full class into the test.
90
+ } as unknown as Parameters<typeof InstallPackagesType>[2];
91
+ }
92
+
93
+ function notConfigured(allowInstallScripts?: boolean): NpmRegistryFetchResult {
94
+ return {
95
+ source: "not-configured",
96
+ config: {
97
+ configured: false,
98
+ ...(allowInstallScripts !== undefined ? { allowInstallScripts } : {}),
99
+ },
100
+ };
101
+ }
102
+
103
+ // Pre-import `./dev.mjs` once so the heavy module-load cost (it transitively
104
+ // pulls in `vite-plugin-file-sync/git-service`, `AiService`, etc.) is paid
105
+ // outside any test's per-test timeout. Under turbo parallelism (CI runs the
106
+ // `cli` unit-test job alongside ai-service-templates, cli, mcp-server, and
107
+ // sdk in the same worker pool), the first dynamic `await import("./dev.mjs")`
108
+ // inside a test was being CPU-starved past the default 5s testTimeout — the
109
+ // test that owned the cold import then timed out, leaked its in-flight
110
+ // `resolveCommand` call past the `beforeEach` `clearAllMocks`, and the *next*
111
+ // test observed two recorded calls instead of one. Hoisting to `beforeAll`
112
+ // keeps the per-test surface fast and order-independent. The repeated
113
+ // `await import("./dev.mjs")` lines below remain as cheap cached lookups —
114
+ // no behavioural change.
115
+ beforeAll(async () => {
116
+ await import("./dev.mjs");
117
+ });
118
+
119
+ beforeEach(() => {
120
+ vi.clearAllMocks();
121
+ // Default: npm package manager, install resolves to a real command so we
122
+ // can read back the args the call site passed in.
123
+ detectMock.mockResolvedValue({
124
+ name: "npm",
125
+ agent: "npm",
126
+ version: "10.0.0",
127
+ });
128
+ resolveCommandMock.mockImplementation(
129
+ (_agent: string, _action: string, args: string[]) => ({
130
+ command: "npm",
131
+ args: ["install", ...args],
132
+ }),
133
+ );
134
+ execMock.mockImplementation(
135
+ (
136
+ _cmd: string,
137
+ _opts: unknown,
138
+ cb?: (err: null, result: { stdout: string }) => void,
139
+ ) => {
140
+ if (typeof _opts === "function") {
141
+ (_opts as (err: null, result: { stdout: string }) => void)(null, {
142
+ stdout: "ok",
143
+ });
144
+ } else if (cb) {
145
+ cb(null, { stdout: "ok" });
146
+ }
147
+ },
148
+ );
149
+ });
150
+
151
+ function getResolveCommandArgs(): string[] {
152
+ expect(resolveCommandMock).toHaveBeenCalledTimes(1);
153
+ const [, , args] = (resolveCommandMock as Mock).mock.calls[0] as [
154
+ string,
155
+ string,
156
+ string[],
157
+ ];
158
+ return args;
159
+ }
160
+
161
+ /** Pull the exec options (incl. `env`) out of the spawned subprocess
162
+ * call. We assert against the env-overlay because that is the mechanism
163
+ * userconfig pinning uses for npm AND pnpm (pnpm rejects `--userconfig=`
164
+ * as a CLI flag — pnpm/pnpm#6036). */
165
+ function getExecOptions(): { cwd?: string; env?: NodeJS.ProcessEnv } {
166
+ const lastCall = (execMock as Mock).mock.calls.at(-1) as [
167
+ string,
168
+ { cwd?: string; env?: NodeJS.ProcessEnv },
169
+ ...unknown[],
170
+ ];
171
+ return lastCall[1];
172
+ }
173
+
174
+ describe("installPackages honors allow_install_scripts policy", () => {
175
+ it("appends --ignore-scripts when allow_install_scripts === false", async () => {
176
+ const { installPackages } = await import("./dev.mjs");
177
+ const client = fakeRegistryClient(notConfigured(false));
178
+
179
+ await installPackages("/tmp/app", mockLogger as never, client);
180
+
181
+ const args = getResolveCommandArgs();
182
+ expect(args).toContain("--ignore-scripts");
183
+ // Existing baseline npm flags must survive.
184
+ expect(args).toContain("--fund=false");
185
+ expect(args).toContain("--audit=false");
186
+ });
187
+
188
+ it("does NOT append --ignore-scripts when allow_install_scripts === true", async () => {
189
+ const { installPackages } = await import("./dev.mjs");
190
+ const client = fakeRegistryClient(notConfigured(true));
191
+
192
+ await installPackages("/tmp/app", mockLogger as never, client);
193
+
194
+ expect(getResolveCommandArgs()).not.toContain("--ignore-scripts");
195
+ });
196
+
197
+ it("does NOT append --ignore-scripts when allow_install_scripts is unset", async () => {
198
+ const { installPackages } = await import("./dev.mjs");
199
+ const client = fakeRegistryClient(notConfigured(undefined));
200
+
201
+ await installPackages("/tmp/app", mockLogger as never, client);
202
+
203
+ expect(getResolveCommandArgs()).not.toContain("--ignore-scripts");
204
+ });
205
+
206
+ it("does NOT append --ignore-scripts when no client is wired in (test/opt-out path)", async () => {
207
+ const { installPackages } = await import("./dev.mjs");
208
+
209
+ await installPackages("/tmp/app", mockLogger as never);
210
+
211
+ expect(getResolveCommandArgs()).not.toContain("--ignore-scripts");
212
+ });
213
+
214
+ it("swallows policy-resolution errors and proceeds without --ignore-scripts", async () => {
215
+ // A transient registry-endpoint outage with no cache must not abort the
216
+ // user's startup install. The client surfaces the throw; we log and
217
+ // fall back to "scripts allowed" (today's behaviour). The 404 /
218
+ // last-known-good preservation that prevents this in practice is
219
+ // covered by `npm-registry.test.ts` on the vite-plugin-file-sync side.
220
+ const { installPackages } = await import("./dev.mjs");
221
+ const client = fakeRegistryClient(async () => {
222
+ throw new Error("registry endpoint unreachable");
223
+ });
224
+
225
+ await installPackages("/tmp/app", mockLogger as never, client);
226
+
227
+ expect(getResolveCommandArgs()).not.toContain("--ignore-scripts");
228
+ expect(mockLogger.warn).toHaveBeenCalledWith(
229
+ expect.stringContaining("Could not resolve npm install-scripts policy"),
230
+ expect.objectContaining({ error: expect.any(Object) }),
231
+ );
232
+ });
233
+
234
+ it("appends --ignore-scripts under pnpm too (no npm-only baseline flags)", async () => {
235
+ detectMock.mockResolvedValue({
236
+ name: "pnpm",
237
+ agent: "pnpm",
238
+ version: "9.0.0",
239
+ });
240
+ resolveCommandMock.mockImplementation(
241
+ (_agent: string, _action: string, args: string[]) => ({
242
+ command: "pnpm",
243
+ args: ["install", ...args],
244
+ }),
245
+ );
246
+ const { installPackages } = await import("./dev.mjs");
247
+ const client = fakeRegistryClient(notConfigured(false));
248
+
249
+ await installPackages("/tmp/app", mockLogger as never, client);
250
+
251
+ const args = getResolveCommandArgs();
252
+ expect(args).toContain("--ignore-scripts");
253
+ expect(args).not.toContain("--fund=false");
254
+ expect(args).not.toContain("--audit=false");
255
+ });
256
+ });
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Userconfig plumbing for the dev-server startup install.
260
+ //
261
+ // `installPackages` points npm/pnpm at the Superblocks-owned userconfig
262
+ // (`~/.superblocks/npmrc`) so the user-app install resolves
263
+ // `@superblocksteam/library`, `@superblocksteam/sdk-api`, and any private
264
+ // scope through the same registry config the CLI auto-upgrade uses.
265
+ //
266
+ // The mechanism is the spawned subprocess env overlay (both
267
+ // `NPM_CONFIG_USERCONFIG` and `PNPM_CONFIG_USERCONFIG`), not a CLI flag.
268
+ // pnpm rejects `--userconfig=` (pnpm/pnpm#6036), and pnpm 11+ stopped
269
+ // honouring the `npm_config_*` env vars — setting both forms covers
270
+ // npm, pnpm <= 10, and pnpm 11+ uniformly.
271
+ // `userconfig-env.integration.test.mts` in this directory exercises the
272
+ // real npm + pnpm binaries to pin that contract.
273
+ // ---------------------------------------------------------------------------
274
+
275
+ const USERCONFIG_PATTERN = /[/\\]\.superblocks[/\\]npmrc$/;
276
+
277
+ describe("installPackages userconfig env plumbing", () => {
278
+ it("sets both NPM_CONFIG_USERCONFIG and PNPM_CONFIG_USERCONFIG under npm", async () => {
279
+ const { installPackages } = await import("./dev.mjs");
280
+ const client = fakeRegistryClient(notConfigured(undefined));
281
+
282
+ await installPackages("/tmp/app", mockLogger as never, client);
283
+
284
+ const opts = getExecOptions();
285
+ expect(opts.env?.NPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
286
+ expect(opts.env?.PNPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
287
+ // npm's debug log is routed into `<app>/.superblocks/logs` so a failed
288
+ // install in the live-edit pod leaves a collectable log.
289
+ expect(opts.env?.NPM_CONFIG_LOGS_DIR).toBe("/tmp/app/.superblocks/logs");
290
+ // Argv carries the baseline npm flags only — no --userconfig (pnpm
291
+ // would reject it; env is the mechanism instead).
292
+ const args = getResolveCommandArgs();
293
+ expect(args.some((a) => a.startsWith("--userconfig="))).toBe(false);
294
+ expect(args).toContain("--fund=false");
295
+ expect(args).toContain("--audit=false");
296
+ });
297
+
298
+ it("sets both NPM_CONFIG_USERCONFIG and PNPM_CONFIG_USERCONFIG under pnpm", async () => {
299
+ detectMock.mockResolvedValue({
300
+ name: "pnpm",
301
+ agent: "pnpm",
302
+ version: "11.0.0",
303
+ });
304
+ resolveCommandMock.mockImplementation(
305
+ (_agent: string, _action: string, args: string[]) => ({
306
+ command: "pnpm",
307
+ args: ["install", ...args],
308
+ }),
309
+ );
310
+ const { installPackages } = await import("./dev.mjs");
311
+ const client = fakeRegistryClient(notConfigured(undefined));
312
+
313
+ await installPackages("/tmp/app", mockLogger as never, client);
314
+
315
+ const opts = getExecOptions();
316
+ expect(opts.env?.NPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
317
+ expect(opts.env?.PNPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
318
+ const args = getResolveCommandArgs();
319
+ expect(args.some((a) => a.startsWith("--userconfig="))).toBe(false);
320
+ expect(args).not.toContain("--audit=false");
321
+ });
322
+
323
+ it("sets the userconfig env alongside --ignore-scripts when policy is restrictive", async () => {
324
+ // The install-scripts policy gate (argv) and the userconfig pin
325
+ // (env) are independent knobs that must co-exist.
326
+ const { installPackages } = await import("./dev.mjs");
327
+ const client = fakeRegistryClient(notConfigured(false));
328
+
329
+ await installPackages("/tmp/app", mockLogger as never, client);
330
+
331
+ const args = getResolveCommandArgs();
332
+ expect(args).toContain("--ignore-scripts");
333
+ const opts = getExecOptions();
334
+ expect(opts.env?.NPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
335
+ expect(opts.env?.PNPM_CONFIG_USERCONFIG).toMatch(USERCONFIG_PATTERN);
336
+ });
337
+
338
+ // PATH-preservation coverage lives in two places that don't trip the
339
+ // turbo `no-undeclared-env-vars` lint:
340
+ // - `buildInstallEnv`'s "accepts a custom base env" test verifies
341
+ // the merge semantics directly without referencing `process.env`.
342
+ // - The real-subprocess integration test
343
+ // (`userconfig-env.integration.test.mts`) would fail to resolve
344
+ // `npm`/`pnpm` on PATH if the overlay clobbered it.
345
+ });