@superblocksteam/sdk 2.0.157-next.4 → 2.0.157-next.5
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 +4 -8
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +2 -1
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dev-s3-restore.test.mjs +6 -2
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +6 -2
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts +2 -0
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs +487 -0
- package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts +6 -6
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +12 -12
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +100 -42
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/dev-utils/dev-server-json-parser.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-json-parser.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-json-parser.test.mjs +60 -0
- package/dist/dev-utils/dev-server-json-parser.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +36 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +80 -73
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts +2 -0
- package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.shutdown-lock.test.mjs +48 -0
- package/dist/dev-utils/dev-server.shutdown-lock.test.mjs.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.d.mts +71 -0
- package/dist/dev-utils/fatal-process-barrier.d.mts.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.mjs +161 -0
- package/dist/dev-utils/fatal-process-barrier.mjs.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts +2 -0
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs +9 -0
- package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.test.d.mts +2 -0
- package/dist/dev-utils/fatal-process-barrier.test.d.mts.map +1 -0
- package/dist/dev-utils/fatal-process-barrier.test.mjs +281 -0
- package/dist/dev-utils/fatal-process-barrier.test.mjs.map +1 -0
- package/dist/telemetry/logging.js +1 -1
- package/dist/telemetry/logging.js.map +1 -1
- package/dist/telemetry/safe-stringify.d.ts.map +1 -1
- package/dist/telemetry/safe-stringify.js +25 -9
- package/dist/telemetry/safe-stringify.js.map +1 -1
- package/package.json +10 -6
- package/src/cli-replacement/automatic-upgrades.test.ts +2 -1
- package/src/cli-replacement/automatic-upgrades.ts +4 -11
- package/src/cli-replacement/dev-s3-restore.test.mts +6 -2
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +6 -2
- package/src/cli-replacement/dev-startup-lock-acquisition.test.mts +573 -0
- package/src/cli-replacement/dev.interception.test.mts +12 -12
- package/src/cli-replacement/dev.mts +112 -56
- package/src/dev-utils/dev-server-json-parser.test.mts +89 -0
- package/src/dev-utils/dev-server.mts +110 -93
- package/src/dev-utils/dev-server.shutdown-lock.test.mts +58 -0
- package/src/dev-utils/fatal-process-barrier.mts +262 -0
- package/src/dev-utils/fatal-process-barrier.spawn-fixture.mts +12 -0
- package/src/dev-utils/fatal-process-barrier.test.mts +346 -0
- package/src/telemetry/logging.ts +1 -1
- package/src/telemetry/safe-stringify.ts +25 -10
- package/test/safe-stringify.test.mts +25 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -543,12 +543,10 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
543
543
|
getErrorMeta(error),
|
|
544
544
|
);
|
|
545
545
|
await traceFunction({
|
|
546
|
-
name: "
|
|
546
|
+
name: "relinquishLock",
|
|
547
547
|
tracer,
|
|
548
548
|
fn: async () => {
|
|
549
|
-
await lockService.
|
|
550
|
-
serverInitiated: false,
|
|
551
|
-
});
|
|
549
|
+
await lockService.relinquishLock();
|
|
552
550
|
},
|
|
553
551
|
});
|
|
554
552
|
hasFailedToUpgrade = true;
|
|
@@ -751,14 +749,9 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
751
749
|
// Restart CLI if anything was updated
|
|
752
750
|
if (hasFailedToUpgrade) {
|
|
753
751
|
try {
|
|
754
|
-
await lockService.
|
|
755
|
-
serverInitiated: false,
|
|
756
|
-
});
|
|
752
|
+
await lockService.relinquishLock();
|
|
757
753
|
} catch (error) {
|
|
758
|
-
logger.error(
|
|
759
|
-
"Error shutting down lock service and releasing lock",
|
|
760
|
-
getErrorMeta(error),
|
|
761
|
-
);
|
|
754
|
+
logger.error("Error handing the lock back", getErrorMeta(error));
|
|
762
755
|
}
|
|
763
756
|
process.exit(1);
|
|
764
757
|
}
|
|
@@ -174,9 +174,11 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
|
174
174
|
|
|
175
175
|
const mockLockService = {
|
|
176
176
|
acquireLock: vi.fn(async () => undefined),
|
|
177
|
-
|
|
177
|
+
relinquishLock: vi.fn(async () => undefined),
|
|
178
|
+
notifyClosedByServer: vi.fn(async () => undefined),
|
|
178
179
|
shutdownAndExit: vi.fn(async () => undefined),
|
|
179
180
|
setIsRestarting: vi.fn(),
|
|
181
|
+
setOnLockConflict: vi.fn(),
|
|
180
182
|
isLocked: false,
|
|
181
183
|
connectedUsers: [],
|
|
182
184
|
wasRecentlyActive: false,
|
|
@@ -186,9 +188,11 @@ const mockLockService = {
|
|
|
186
188
|
vi.mock("@superblocksteam/vite-plugin-file-sync/lock-service", () => ({
|
|
187
189
|
LockService: class {
|
|
188
190
|
acquireLock = mockLockService.acquireLock;
|
|
189
|
-
|
|
191
|
+
relinquishLock = mockLockService.relinquishLock;
|
|
192
|
+
notifyClosedByServer = mockLockService.notifyClosedByServer;
|
|
190
193
|
shutdownAndExit = mockLockService.shutdownAndExit;
|
|
191
194
|
setIsRestarting = mockLockService.setIsRestarting;
|
|
195
|
+
setOnLockConflict = mockLockService.setOnLockConflict;
|
|
192
196
|
isLocked = mockLockService.isLocked;
|
|
193
197
|
connectedUsers = mockLockService.connectedUsers;
|
|
194
198
|
wasRecentlyActive = mockLockService.wasRecentlyActive;
|
|
@@ -224,9 +224,11 @@ vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
|
224
224
|
|
|
225
225
|
const mockLockService = {
|
|
226
226
|
acquireLock: vi.fn(async () => undefined),
|
|
227
|
-
|
|
227
|
+
relinquishLock: vi.fn(async () => undefined),
|
|
228
|
+
notifyClosedByServer: vi.fn(async () => undefined),
|
|
228
229
|
shutdownAndExit: vi.fn(async () => undefined),
|
|
229
230
|
setIsRestarting: vi.fn(),
|
|
231
|
+
setOnLockConflict: vi.fn(),
|
|
230
232
|
isLocked: false,
|
|
231
233
|
connectedUsers: [],
|
|
232
234
|
wasRecentlyActive: false,
|
|
@@ -236,9 +238,11 @@ const mockLockService = {
|
|
|
236
238
|
vi.mock("@superblocksteam/vite-plugin-file-sync/lock-service", () => ({
|
|
237
239
|
LockService: class {
|
|
238
240
|
acquireLock = mockLockService.acquireLock;
|
|
239
|
-
|
|
241
|
+
relinquishLock = mockLockService.relinquishLock;
|
|
242
|
+
notifyClosedByServer = mockLockService.notifyClosedByServer;
|
|
240
243
|
shutdownAndExit = mockLockService.shutdownAndExit;
|
|
241
244
|
setIsRestarting = mockLockService.setIsRestarting;
|
|
245
|
+
setOnLockConflict = mockLockService.setOnLockConflict;
|
|
242
246
|
isLocked = mockLockService.isLocked;
|
|
243
247
|
connectedUsers = mockLockService.connectedUsers;
|
|
244
248
|
wasRecentlyActive = mockLockService.wasRecentlyActive;
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
it,
|
|
12
|
+
vi,
|
|
13
|
+
type Mock,
|
|
14
|
+
} from "vitest";
|
|
15
|
+
|
|
16
|
+
const execMock = vi.fn();
|
|
17
|
+
const execFileMock = vi.fn();
|
|
18
|
+
vi.mock("node:child_process", () => ({
|
|
19
|
+
default: { exec: execMock, execFile: execFileMock },
|
|
20
|
+
exec: execMock,
|
|
21
|
+
execFile: execFileMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("node:readline", () => ({
|
|
25
|
+
default: {
|
|
26
|
+
createInterface: vi.fn(() => ({ question: vi.fn(), close: vi.fn() })),
|
|
27
|
+
},
|
|
28
|
+
createInterface: vi.fn(() => ({ question: vi.fn(), close: vi.fn() })),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("fs-extra", () => ({
|
|
32
|
+
default: {
|
|
33
|
+
existsSync: vi.fn(() => true),
|
|
34
|
+
readFileSync: vi.fn(() => "{}"),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("package-manager-detector/detect", () => ({
|
|
39
|
+
detect: vi.fn(async () => ({
|
|
40
|
+
name: "npm",
|
|
41
|
+
agent: "npm",
|
|
42
|
+
version: "10.0.0",
|
|
43
|
+
})),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock("package-manager-detector", () => ({
|
|
47
|
+
resolveCommand: vi.fn(() => ({
|
|
48
|
+
command: "npm",
|
|
49
|
+
args: ["install", "--fund=false", "--audit=false"],
|
|
50
|
+
})),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
vi.mock("read-pkg", () => ({
|
|
54
|
+
readPackage: vi.fn(async () => ({
|
|
55
|
+
name: "test-app",
|
|
56
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const mockLogger = {
|
|
61
|
+
info: vi.fn(),
|
|
62
|
+
warn: vi.fn(),
|
|
63
|
+
error: vi.fn(),
|
|
64
|
+
debug: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
vi.mock("../telemetry/logging.js", () => ({
|
|
68
|
+
getLogger: () => mockLogger,
|
|
69
|
+
getErrorMeta: vi.fn(() => ({})),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
vi.mock("../telemetry/index.js", () => ({
|
|
73
|
+
getTracer: () => ({
|
|
74
|
+
startActiveSpan: vi.fn(
|
|
75
|
+
async (
|
|
76
|
+
_name: string,
|
|
77
|
+
fn: (span: {
|
|
78
|
+
end: () => void;
|
|
79
|
+
setStatus: () => void;
|
|
80
|
+
}) => Promise<unknown>,
|
|
81
|
+
) => fn({ end: vi.fn(), setStatus: vi.fn() }),
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
isCloudPrem: false,
|
|
85
|
+
shutdownTelemetry: vi.fn(async () => true),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
vi.mock("@opentelemetry/api", () => ({
|
|
89
|
+
SpanStatusCode: { ERROR: 2 },
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
vi.mock("colorette", () => ({
|
|
93
|
+
green: (s: string) => s,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
vi.mock("@superblocksteam/shared", async (importOriginal) => ({
|
|
97
|
+
// dev.mts now transitively loads @superblocksteam/shared (via the npm-error
|
|
98
|
+
// classifier re-exported from vite-plugin-file-sync/npm-registry — APPS-4450),
|
|
99
|
+
// pulling in many named exports (DeploymentTypeEnum, ISocket, …). Spread the
|
|
100
|
+
// real module so the closed overrides below don't have to enumerate them.
|
|
101
|
+
...((await importOriginal()) as Record<string, unknown>),
|
|
102
|
+
buildGithubSuperblocksSyncWorkflow: vi.fn(),
|
|
103
|
+
buildGithubSuperblocksSyncWorkflowFromBaseUrl: vi.fn(),
|
|
104
|
+
ConflictError: class ConflictError extends Error {},
|
|
105
|
+
DEFAULT_NON_GIT_BRANCH: "__non_git__",
|
|
106
|
+
isGitHubRemoteUrl: vi.fn(() => false),
|
|
107
|
+
NotFoundError: class NotFoundError extends Error {},
|
|
108
|
+
SUPERBLOCKS_LIVE_GIT_BRANCH: "__live__",
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
vi.mock("@superblocksteam/util", () => ({
|
|
112
|
+
maskUnixSignals: async function maskUnixSignals(fn: () => Promise<void>) {
|
|
113
|
+
return fn();
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/ai-service", () => ({
|
|
118
|
+
AiService: class {
|
|
119
|
+
initialize = vi.fn(async () => undefined);
|
|
120
|
+
notifyMigrationAppArtifactsReady = vi.fn();
|
|
121
|
+
removeIntegrationCache = vi.fn(async () => undefined);
|
|
122
|
+
remediateDeclaredDependencies = vi.fn(async () => undefined);
|
|
123
|
+
chatSessionStore = { invalidateCache: vi.fn() };
|
|
124
|
+
getNpmRegistryClient = mockGetNpmRegistryClient;
|
|
125
|
+
},
|
|
126
|
+
AiServiceFeatureFlags: class {
|
|
127
|
+
static create = vi.fn(() => ({}));
|
|
128
|
+
},
|
|
129
|
+
SnapshotManager: class {
|
|
130
|
+
executePendingRestore = vi.fn(async () => false);
|
|
131
|
+
},
|
|
132
|
+
hasEditLifecycleCapability: vi.fn(() => false),
|
|
133
|
+
isSdkApiTemplate: vi.fn(() => false),
|
|
134
|
+
stripResolvedFromLockfile: vi.fn(async () => undefined),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/npm-registry", () => ({
|
|
138
|
+
shouldIgnoreInstallScripts: vi.fn(() => false),
|
|
139
|
+
maybeWriteNpmrcForDir: vi.fn(async () => {
|
|
140
|
+
// Delay before the log push so this test fails if syncHomeNpmrc is
|
|
141
|
+
// fire-and-forget (initiation order alone would still look correct).
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
143
|
+
startupOpLog.push("project-npmrc:maybeWriteNpmrcForDir");
|
|
144
|
+
}),
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const { startupOpLog, buildMockGitService, mockGetNpmRegistryClient } =
|
|
148
|
+
vi.hoisted(() => {
|
|
149
|
+
const startupOpLog: string[] = [];
|
|
150
|
+
const mockGetNpmRegistryClient = vi.fn(
|
|
151
|
+
():
|
|
152
|
+
| {
|
|
153
|
+
getConfig: ReturnType<typeof vi.fn>;
|
|
154
|
+
organizationId: string;
|
|
155
|
+
}
|
|
156
|
+
| undefined => ({
|
|
157
|
+
organizationId: "org-1",
|
|
158
|
+
getConfig: vi.fn(async () => ({
|
|
159
|
+
source: "not-configured",
|
|
160
|
+
config: { configured: false },
|
|
161
|
+
})),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
function buildMockGitService(cwd: string) {
|
|
165
|
+
return {
|
|
166
|
+
workDir: cwd,
|
|
167
|
+
configure: vi.fn(async () => {
|
|
168
|
+
startupOpLog.push("git:configure");
|
|
169
|
+
}),
|
|
170
|
+
init: vi.fn(async () => {
|
|
171
|
+
startupOpLog.push("git:init");
|
|
172
|
+
}),
|
|
173
|
+
addRemote: vi.fn(async () => {
|
|
174
|
+
startupOpLog.push("git:addRemote");
|
|
175
|
+
}),
|
|
176
|
+
fetch: vi.fn(async () => {
|
|
177
|
+
startupOpLog.push("git:fetch");
|
|
178
|
+
}),
|
|
179
|
+
raw: vi.fn(async (args: string | string[]) => {
|
|
180
|
+
const argv = Array.isArray(args) ? args : [args];
|
|
181
|
+
startupOpLog.push(`git:raw:${argv.join(" ")}`);
|
|
182
|
+
if (argv[0] === "ls-remote" && argv.includes("__live__")) {
|
|
183
|
+
return "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\trefs/heads/__live__\n";
|
|
184
|
+
}
|
|
185
|
+
if (argv[0] === "branch" && argv.includes("--show-current")) {
|
|
186
|
+
return "__live__\n";
|
|
187
|
+
}
|
|
188
|
+
if (argv[0] === "commit") {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
return "";
|
|
192
|
+
}),
|
|
193
|
+
push: vi.fn(async () => {
|
|
194
|
+
startupOpLog.push("git:push");
|
|
195
|
+
}),
|
|
196
|
+
getDefaultBranch: vi.fn(async () => "main"),
|
|
197
|
+
getRemotes: vi.fn(async () => [
|
|
198
|
+
{ name: "origin", refs: { fetch: "https://example.test/repo.git" } },
|
|
199
|
+
]),
|
|
200
|
+
remote: vi.fn(async () => {
|
|
201
|
+
startupOpLog.push("git:remote");
|
|
202
|
+
}),
|
|
203
|
+
revparse: vi.fn(async (ref: string) => {
|
|
204
|
+
startupOpLog.push(`git:revparse:${ref}`);
|
|
205
|
+
return "abc123\n";
|
|
206
|
+
}),
|
|
207
|
+
checkout: vi.fn(async () => {
|
|
208
|
+
startupOpLog.push("git:checkout");
|
|
209
|
+
}),
|
|
210
|
+
checkoutOrCreate: vi.fn(async () => {
|
|
211
|
+
startupOpLog.push("git:checkoutOrCreate");
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { startupOpLog, buildMockGitService, mockGetNpmRegistryClient };
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/git-service", () => ({
|
|
219
|
+
createGitService: vi.fn((cwd: string) => {
|
|
220
|
+
startupOpLog.push("createGitService");
|
|
221
|
+
return buildMockGitService(cwd);
|
|
222
|
+
}),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const mockLockService = {
|
|
226
|
+
acquireLock: vi.fn(async () => undefined),
|
|
227
|
+
forceTakeover: vi.fn(async () => undefined),
|
|
228
|
+
inspectLock: vi.fn(async () => ({
|
|
229
|
+
ownerName: "someone@example.com",
|
|
230
|
+
canForceTakeover: false,
|
|
231
|
+
lockLifetimeMs: 60_000,
|
|
232
|
+
timeSinceLastActivityMs: 0,
|
|
233
|
+
})),
|
|
234
|
+
setOnLockConflict: vi.fn(),
|
|
235
|
+
relinquishLock: vi.fn(async () => undefined),
|
|
236
|
+
notifyClosedByServer: vi.fn(async () => undefined),
|
|
237
|
+
shutdownAndExit: vi.fn(async () => undefined),
|
|
238
|
+
setIsRestarting: vi.fn(),
|
|
239
|
+
isLocked: false,
|
|
240
|
+
// The git-service mock below simulates a live/target branch mismatch, which
|
|
241
|
+
// is what drives `ensureRuntimeDbfsBranchConsistency`. Both of the methods it
|
|
242
|
+
// calls have to exist here or the first one throws a TypeError that an outer
|
|
243
|
+
// catch swallows — leaving the suite green without ever having run the
|
|
244
|
+
// interaction.
|
|
245
|
+
isLockedWhenSettled: vi.fn(async () => false),
|
|
246
|
+
switchBranch: vi.fn(async () => undefined),
|
|
247
|
+
connectedUsers: [],
|
|
248
|
+
wasRecentlyActive: false,
|
|
249
|
+
timeSinceLastActivity: 0,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/lock-service", () => ({
|
|
253
|
+
LockService: class {
|
|
254
|
+
acquireLock = mockLockService.acquireLock;
|
|
255
|
+
forceTakeover = mockLockService.forceTakeover;
|
|
256
|
+
inspectLock = mockLockService.inspectLock;
|
|
257
|
+
setOnLockConflict = mockLockService.setOnLockConflict;
|
|
258
|
+
relinquishLock = mockLockService.relinquishLock;
|
|
259
|
+
notifyClosedByServer = mockLockService.notifyClosedByServer;
|
|
260
|
+
shutdownAndExit = mockLockService.shutdownAndExit;
|
|
261
|
+
setIsRestarting = mockLockService.setIsRestarting;
|
|
262
|
+
isLocked = mockLockService.isLocked;
|
|
263
|
+
isLockedWhenSettled = mockLockService.isLockedWhenSettled;
|
|
264
|
+
switchBranch = mockLockService.switchBranch;
|
|
265
|
+
connectedUsers = mockLockService.connectedUsers;
|
|
266
|
+
wasRecentlyActive = mockLockService.wasRecentlyActive;
|
|
267
|
+
timeSinceLastActivity = mockLockService.timeSinceLastActivity;
|
|
268
|
+
},
|
|
269
|
+
LockType: { CSB: "CSB", LOCAL: "LOCAL" },
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/operation-queue", () => ({
|
|
273
|
+
OperationQueue: class {
|
|
274
|
+
enqueue = vi.fn(async (fn: () => Promise<unknown>) => fn());
|
|
275
|
+
},
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/server-rpc", () => ({
|
|
279
|
+
AutoConnectingRpcClient: class {},
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
const mockSyncService = {
|
|
283
|
+
downloadDirectory: vi.fn(async () => {
|
|
284
|
+
startupOpLog.push("dbfs:downloadDirectory");
|
|
285
|
+
}),
|
|
286
|
+
uploadDirectory: vi.fn(async () => undefined),
|
|
287
|
+
uploadDirectoryNowIfNeeded: vi.fn(async () => undefined),
|
|
288
|
+
cancelPendingSync: vi.fn(),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/sync-service", () => ({
|
|
292
|
+
SyncService: class {
|
|
293
|
+
downloadDirectory = mockSyncService.downloadDirectory;
|
|
294
|
+
uploadDirectory = mockSyncService.uploadDirectory;
|
|
295
|
+
uploadDirectoryNowIfNeeded = mockSyncService.uploadDirectoryNowIfNeeded;
|
|
296
|
+
cancelPendingSync = mockSyncService.cancelPendingSync;
|
|
297
|
+
},
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
vi.mock("@superblocksteam/vite-plugin-file-sync/draft-interface", () => ({}));
|
|
301
|
+
|
|
302
|
+
vi.mock("./version-detection.js", () => ({
|
|
303
|
+
getCurrentCliVersion: vi.fn(async () => ({
|
|
304
|
+
version: "2.0.0",
|
|
305
|
+
alias: undefined,
|
|
306
|
+
})),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const mockCheckVersions = vi.fn(async () => ({
|
|
310
|
+
cliUpdated: false,
|
|
311
|
+
upgradePromises: [] as Promise<void>[],
|
|
312
|
+
}));
|
|
313
|
+
vi.mock("./automatic-upgrades.js", () => ({
|
|
314
|
+
checkVersionsAndWritePackageJson: (
|
|
315
|
+
...args: Parameters<typeof mockCheckVersions>
|
|
316
|
+
) => mockCheckVersions(...args),
|
|
317
|
+
buildInstallEnv: (userconfigPath: string) => ({
|
|
318
|
+
...process.env,
|
|
319
|
+
NPM_CONFIG_USERCONFIG: userconfigPath,
|
|
320
|
+
PNPM_CONFIG_USERCONFIG: userconfigPath,
|
|
321
|
+
}),
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
// The home `~/.npmrc` writer (APPS-4231) is a best-effort step that runs
|
|
325
|
+
// before auto-upgrade. Mock it out here so this test stays focused on the
|
|
326
|
+
// git-before-DBFS ordering. The `AiService` mock above exposes a minimal
|
|
327
|
+
// `getNpmRegistryClient()` stub so the call site doesn't `TypeError`
|
|
328
|
+
// and silently swallow this mock.
|
|
329
|
+
vi.mock("./home-npmrc.mjs", () => ({
|
|
330
|
+
syncHomeNpmrc: vi.fn(async () => {
|
|
331
|
+
// Longer delay than maybeWriteNpmrcForDir: if both run without await,
|
|
332
|
+
// project pushes first and the order assertion fails.
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
334
|
+
startupOpLog.push("home-npmrc:syncHomeNpmrc");
|
|
335
|
+
return {
|
|
336
|
+
outcome: "skipped-not-configured",
|
|
337
|
+
path: "/tmp/.superblocks/npmrc",
|
|
338
|
+
};
|
|
339
|
+
}),
|
|
340
|
+
superblocksNpmrcPath: vi.fn(() => "/tmp/.superblocks/npmrc"),
|
|
341
|
+
superblocksLogsPath: vi.fn((appDir: string) => `${appDir}/.superblocks/logs`),
|
|
342
|
+
}));
|
|
343
|
+
|
|
344
|
+
vi.mock("./git-repo-setup.mjs", () => ({
|
|
345
|
+
ensureRemoteHasDefaultBranch: vi.fn(async () => undefined),
|
|
346
|
+
getGitErrorFields: vi.fn(() => ({})),
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
const mockCreateDevServer = vi.fn(async () => http.createServer());
|
|
350
|
+
vi.mock("../dev-utils/dev-server.mjs", () => ({
|
|
351
|
+
createDevServer: (...args: Parameters<typeof mockCreateDevServer>) =>
|
|
352
|
+
mockCreateDevServer(...args),
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
vi.mock("../index.js", () => ({
|
|
356
|
+
AUTO_UPGRADE_EXIT_CODE: 99,
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
const SERVER_DBFS_HASH = "dbfs-head-after-clark";
|
|
360
|
+
|
|
361
|
+
function buildMockSdk() {
|
|
362
|
+
const hashLocalDirectory = vi
|
|
363
|
+
.fn()
|
|
364
|
+
.mockImplementationOnce(async () => {
|
|
365
|
+
startupOpLog.push("sdk:hashLocalDirectory:before-dbfs");
|
|
366
|
+
return { hash: "git-tree-without-uncommitted-clark-change" };
|
|
367
|
+
})
|
|
368
|
+
.mockImplementationOnce(async () => {
|
|
369
|
+
startupOpLog.push("sdk:hashLocalDirectory:after-dbfs");
|
|
370
|
+
return { hash: SERVER_DBFS_HASH };
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
hashLocalDirectory,
|
|
375
|
+
dbfsGetApplicationDirectoryHash: vi.fn(async () => SERVER_DBFS_HASH),
|
|
376
|
+
getApplicationGitConfig: vi.fn(async () => ({
|
|
377
|
+
gitRemoteUrl: "https://github.com/example/superblocks-app.git",
|
|
378
|
+
hasCredential: false,
|
|
379
|
+
})),
|
|
380
|
+
fetchCurrentUser: vi.fn(async () => ({
|
|
381
|
+
user: {
|
|
382
|
+
id: "user-1",
|
|
383
|
+
name: "Test User",
|
|
384
|
+
email: "test@example.com",
|
|
385
|
+
currentOrganizationId: "org-1",
|
|
386
|
+
},
|
|
387
|
+
organizations: [{ id: "org-1", pluginExecutionVersions: {} }],
|
|
388
|
+
flagBootstrap: {},
|
|
389
|
+
})),
|
|
390
|
+
getFeatureFlagsForCurrentUser: vi.fn(async () => ({
|
|
391
|
+
devServerCloudInactivityTimeoutMinutes: () => 30,
|
|
392
|
+
devServerLocalInactivityTimeoutMinutes: () => 30,
|
|
393
|
+
enableSessionRecording: () => false,
|
|
394
|
+
removeManualCommitsEnabled: () => false,
|
|
395
|
+
})),
|
|
396
|
+
getFeatureFlagsForUser: vi.fn(() => ({
|
|
397
|
+
devServerCloudInactivityTimeoutMinutes: () => 30,
|
|
398
|
+
devServerLocalInactivityTimeoutMinutes: () => 30,
|
|
399
|
+
})),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildDevOptions(cwd: string, overrides: Record<string, unknown> = {}) {
|
|
404
|
+
return {
|
|
405
|
+
cwd,
|
|
406
|
+
downloadFirst: true,
|
|
407
|
+
autoUpgradeMode: "skip-upgrade",
|
|
408
|
+
applicationConfig: {
|
|
409
|
+
id: "app-test",
|
|
410
|
+
branchName: "main",
|
|
411
|
+
},
|
|
412
|
+
tokenConfig: {
|
|
413
|
+
superblocksBaseUrl: "https://example.test",
|
|
414
|
+
token: "test-token",
|
|
415
|
+
},
|
|
416
|
+
tokenManager: {
|
|
417
|
+
getToken: vi.fn(() => "test-token"),
|
|
418
|
+
onTokenRefresh: vi.fn(),
|
|
419
|
+
updateToken: vi.fn(),
|
|
420
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
421
|
+
},
|
|
422
|
+
getCurrentToken: vi.fn(() => "test-token"),
|
|
423
|
+
sdk: buildMockSdk(),
|
|
424
|
+
...overrides,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function setupExecMock() {
|
|
429
|
+
execMock.mockImplementation(
|
|
430
|
+
(
|
|
431
|
+
_cmd: string,
|
|
432
|
+
_opts: unknown,
|
|
433
|
+
cb?: (err: null, result: { stdout: string }) => void,
|
|
434
|
+
) => {
|
|
435
|
+
startupOpLog.push("installPackages:exec");
|
|
436
|
+
if (typeof _opts === "function") {
|
|
437
|
+
(_opts as (err: null, result: { stdout: string }) => void)(null, {
|
|
438
|
+
stdout: "installed",
|
|
439
|
+
});
|
|
440
|
+
} else if (cb) {
|
|
441
|
+
cb(null, { stdout: "installed" });
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
describe("dev startup: who takes the live-edit lock, and when (ENG-6090)", () => {
|
|
448
|
+
let testCwd: string;
|
|
449
|
+
|
|
450
|
+
beforeEach(async () => {
|
|
451
|
+
vi.clearAllMocks();
|
|
452
|
+
delete process.env.SUPERBLOCKS_IS_CSB;
|
|
453
|
+
mockGetNpmRegistryClient.mockImplementation(() => ({
|
|
454
|
+
organizationId: "org-1",
|
|
455
|
+
getConfig: vi.fn(async () => ({
|
|
456
|
+
source: "not-configured",
|
|
457
|
+
config: { configured: false },
|
|
458
|
+
})),
|
|
459
|
+
}));
|
|
460
|
+
startupOpLog.length = 0;
|
|
461
|
+
setupExecMock();
|
|
462
|
+
mockCheckVersions.mockResolvedValue({
|
|
463
|
+
cliUpdated: false,
|
|
464
|
+
upgradePromises: [],
|
|
465
|
+
});
|
|
466
|
+
testCwd = await fs.mkdtemp(path.join(os.tmpdir(), "sb-dev-lock-acquire-"));
|
|
467
|
+
const { readPackage } = await import("read-pkg");
|
|
468
|
+
(readPackage as Mock).mockResolvedValue({
|
|
469
|
+
name: "test-app",
|
|
470
|
+
dependencies: { "@superblocksteam/library": "1.0.0" },
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
afterEach(async () => {
|
|
475
|
+
await fs.rm(testCwd, { recursive: true, force: true });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it.each([
|
|
479
|
+
["cloud", "true"],
|
|
480
|
+
["local", undefined],
|
|
481
|
+
])("does not take the lock at boot on the %s path", async (_name, isCsb) => {
|
|
482
|
+
if (isCsb) {
|
|
483
|
+
process.env.SUPERBLOCKS_IS_CSB = isCsb;
|
|
484
|
+
}
|
|
485
|
+
const { dev } = await import("./dev.mjs");
|
|
486
|
+
|
|
487
|
+
await dev(buildDevOptions(testCwd) as any);
|
|
488
|
+
|
|
489
|
+
// Nothing startup does is gated on holding the lock — not the DBFS
|
|
490
|
+
// download, not the package install, not the hash upload. Taking it before
|
|
491
|
+
// any client exists is holding it on behalf of nobody, which is the state
|
|
492
|
+
// the startup no-socket watchdog existed to clean up. A local dev server
|
|
493
|
+
// has no more claim to it at boot than a sandbox does: the browser tab
|
|
494
|
+
// that has not opened yet is the client, not the process.
|
|
495
|
+
expect(mockLockService.acquireLock).not.toHaveBeenCalled();
|
|
496
|
+
expect(mockLockService.forceTakeover).not.toHaveBeenCalled();
|
|
497
|
+
// ...and it still boots and serves.
|
|
498
|
+
expect(mockSyncService.downloadDirectory).toHaveBeenCalled();
|
|
499
|
+
expect(mockCreateDevServer).toHaveBeenCalled();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("hands the conflict handler to the lock service instead of racing it at boot", async () => {
|
|
503
|
+
const { dev } = await import("./dev.mjs");
|
|
504
|
+
|
|
505
|
+
await dev(buildDevOptions(testCwd) as any);
|
|
506
|
+
|
|
507
|
+
// The reporting that used to wrap the boot-time acquire still exists; it is
|
|
508
|
+
// wired to where acquisition actually happens now.
|
|
509
|
+
expect(mockLockService.setOnLockConflict).toHaveBeenCalledWith(
|
|
510
|
+
expect.any(Function),
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("reports a conflict without stopping the dev server", async () => {
|
|
515
|
+
const { dev } = await import("./dev.mjs");
|
|
516
|
+
mockLockService.inspectLock.mockResolvedValueOnce({
|
|
517
|
+
ownerName: "someone@example.com",
|
|
518
|
+
canForceTakeover: false,
|
|
519
|
+
lockLifetimeMs: 120_000,
|
|
520
|
+
timeSinceLastActivityMs: 0,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await dev(buildDevOptions(testCwd) as any);
|
|
524
|
+
const onConflict = mockLockService.setOnLockConflict.mock.calls[0]![0];
|
|
525
|
+
await onConflict(new Error("Another user is currently editing this app."));
|
|
526
|
+
|
|
527
|
+
// Someone else holds it, so this client cannot edit — but the sandbox is
|
|
528
|
+
// still serving preview and the lock does not decide how long it lives.
|
|
529
|
+
expect(mockLockService.relinquishLock).not.toHaveBeenCalled();
|
|
530
|
+
expect(mockLockService.shutdownAndExit).not.toHaveBeenCalled();
|
|
531
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
532
|
+
expect.stringContaining("Could not take the live-edit lock"),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("stops on an unresolvable conflict under the VS Code extension", async () => {
|
|
537
|
+
process.env.SUPERBLOCKS_VSCODE = "true";
|
|
538
|
+
const { shutdownTelemetry } = await import("../telemetry/index.js");
|
|
539
|
+
const exit = vi
|
|
540
|
+
.spyOn(process, "exit")
|
|
541
|
+
.mockImplementation(() => undefined as never);
|
|
542
|
+
const { dev } = await import("./dev.mjs");
|
|
543
|
+
|
|
544
|
+
await dev(buildDevOptions(testCwd) as any);
|
|
545
|
+
const onConflict = mockLockService.setOnLockConflict.mock.calls[0]![0];
|
|
546
|
+
await onConflict(new Error("Another user is currently editing this app."));
|
|
547
|
+
|
|
548
|
+
// The extension answers a takeover prompt by restarting this command with
|
|
549
|
+
// --force-takeover, and that hand-off has always ended with this process
|
|
550
|
+
// gone. Staying up would leave the replacement fighting us for the port.
|
|
551
|
+
expect(exit).toHaveBeenCalledWith(1);
|
|
552
|
+
// Through the same teardown as the other two fatal exits, not
|
|
553
|
+
// `shutdownAndExit`: that one exits synchronously from its own `finally`,
|
|
554
|
+
// which made the span end and the telemetry flush below it unreachable —
|
|
555
|
+
// so the trace for the conflict never left the process.
|
|
556
|
+
expect(mockLockService.shutdownAndExit).not.toHaveBeenCalled();
|
|
557
|
+
expect(mockLockService.relinquishLock).toHaveBeenCalled();
|
|
558
|
+
expect(shutdownTelemetry).toHaveBeenCalled();
|
|
559
|
+
exit.mockRestore();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("honours an explicit force takeover at boot", async () => {
|
|
563
|
+
process.env.SUPERBLOCKS_IS_CSB = "true";
|
|
564
|
+
const { dev } = await import("./dev.mjs");
|
|
565
|
+
|
|
566
|
+
// The one thing that still acquires at boot, and it is not lock-type
|
|
567
|
+
// specific: someone asked for it by name.
|
|
568
|
+
await dev(buildDevOptions(testCwd, { forceTakeover: true }) as any);
|
|
569
|
+
|
|
570
|
+
expect(mockLockService.forceTakeover).toHaveBeenCalled();
|
|
571
|
+
expect(mockLockService.acquireLock).not.toHaveBeenCalled();
|
|
572
|
+
});
|
|
573
|
+
});
|