@cursorpool-dev/cli 0.5.6
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/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import {
|
|
8
|
+
CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR,
|
|
9
|
+
CURSOR_POOL_PATCH_MARKER,
|
|
10
|
+
} from '../../patcher/src/marker';
|
|
11
|
+
import { writeRuntimeInfo } from '../../service/src/runtime';
|
|
12
|
+
import type { CompatibilityManifestEntry } from '../../shared/src/manifest';
|
|
13
|
+
import { buildCompatManifestSignature } from '../src/compat';
|
|
14
|
+
import { getExtensionState } from '../src/extensionBundle';
|
|
15
|
+
import { getLinkedExtensionState, linkedExtensionPathForDir } from '../src/extensionLink';
|
|
16
|
+
import { readInstallRecord } from '../src/installRecord';
|
|
17
|
+
import { install } from '../src/install';
|
|
18
|
+
import { readTrialRecord } from '../src/trial';
|
|
19
|
+
import { readClientConfig } from '../../shared/src/clientConfig';
|
|
20
|
+
|
|
21
|
+
const targetRelativePath =
|
|
22
|
+
'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
23
|
+
const alwaysLocalRelativePath =
|
|
24
|
+
'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
|
|
25
|
+
const workbenchRelativePath = 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js';
|
|
26
|
+
const composerAuthGateAnchor =
|
|
27
|
+
'get when(){return p()&&!fzC},get fallback(){return he(DGC,{})}';
|
|
28
|
+
const composerSubmitAuthGateAnchor =
|
|
29
|
+
'if(!p()){e.cursorAuthenticationService.login(),e.commandService.executeCommand(wV,"general");return}';
|
|
30
|
+
const agentLoopRunAnchor = 'await this.agentClientService.run(te,H,$e,Ne,Ce,T,ze,me,we,[],ct)';
|
|
31
|
+
const buildFlagsLocalModeAnchor = 'localMode:!1';
|
|
32
|
+
const localProviderConfigAnchor =
|
|
33
|
+
'async getLocalAgentProviderConfig(e){const t="[AgentClientService][getLocalAgentProviderConfig]",i=L0.localMode?await this.shellEnvironmentService.getShellEnv():{},r=e?.credentials,s=r?.case==="apiKeyCredentials"?r.value:void 0,o=this.reactiveStorageService.applicationUserPersistentStorage,a=o.useOpenAIKey===!0?this.cursorAuthenticationService.openAIKey()??void 0:void 0,u=xgS({apiKeyCandidates:[{value:s?.apiKey,source:"modelDetails.apiKeyCredentials.apiKey"},{value:a,source:"storage.openAIKey"}],baseUrlCandidates:[{value:s?.baseUrl,source:"modelDetails.apiKeyCredentials.baseUrl"},{value:o.openAIBaseUrl,source:"storage.openAIBaseUrl"}]});return{baseUrl:u.baseUrl,apiKey:u.apiKey}}createDefaultLocalModel(e){return "default"}';
|
|
34
|
+
const agentClientRunLocalModeAnchor =
|
|
35
|
+
'if(L0.localMode){try{g.onNetworkPhaseStart?.()}catch(b){this.logService.warn("[AgentClientService] onNetworkPhaseStart callback failed in local mode",b)}return this.runLocalAgentInExtensionHost(e,t,i,r,s,a,d,h,g)}return this.client.run(e,t,i,r,s,o,a,u,d,h,g)';
|
|
36
|
+
|
|
37
|
+
function workbenchFixture() {
|
|
38
|
+
return [
|
|
39
|
+
`function composer(){return he(Mt,{${composerAuthGateAnchor},get children(){return "controls"}})}`,
|
|
40
|
+
`async function submit(){${composerSubmitAuthGateAnchor};return "submitted";}`,
|
|
41
|
+
`const flags={${buildFlagsLocalModeAnchor}}`,
|
|
42
|
+
localProviderConfigAnchor,
|
|
43
|
+
`async function runAgentLoop(){${agentLoopRunAnchor}}`,
|
|
44
|
+
`async function agentClientRun(){const g={};${agentClientRunLocalModeAnchor}}`,
|
|
45
|
+
].join(';');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function createFixtureApp() {
|
|
49
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-install-'));
|
|
50
|
+
const appPath = join(tempDir, 'Cursor.app');
|
|
51
|
+
const targetPath = join(appPath, targetRelativePath);
|
|
52
|
+
const targetContent = `function main() { return "agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
|
|
53
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
|
|
54
|
+
recursive: true,
|
|
55
|
+
});
|
|
56
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-always-local/dist'), {
|
|
57
|
+
recursive: true,
|
|
58
|
+
});
|
|
59
|
+
await mkdir(join(appPath, 'Contents/Resources/app/out/vs/workbench'), {
|
|
60
|
+
recursive: true,
|
|
61
|
+
});
|
|
62
|
+
await writeFile(
|
|
63
|
+
join(appPath, 'Contents/Resources/app/product.json'),
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
version: '3.5.38',
|
|
66
|
+
commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
67
|
+
}),
|
|
68
|
+
'utf8',
|
|
69
|
+
);
|
|
70
|
+
await writeFile(targetPath, targetContent, 'utf8');
|
|
71
|
+
await writeFile(join(appPath, alwaysLocalRelativePath), 'function alwaysLocal(){}\n', 'utf8');
|
|
72
|
+
await writeFile(join(appPath, workbenchRelativePath), workbenchFixture(), 'utf8');
|
|
73
|
+
|
|
74
|
+
const expectedSha256 = createHash('sha256').update(targetContent).digest('hex');
|
|
75
|
+
const compatEntry: CompatibilityManifestEntry = {
|
|
76
|
+
platform: process.platform,
|
|
77
|
+
arch: process.arch,
|
|
78
|
+
cursorVersion: '3.5.38',
|
|
79
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
80
|
+
supportStatus: 'supported',
|
|
81
|
+
targetRelativePath,
|
|
82
|
+
expectedSha256,
|
|
83
|
+
structureSignature: 'fixture',
|
|
84
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
85
|
+
verifyMarker: 'cursor-pool',
|
|
86
|
+
restoreStrategy: 'external-backup',
|
|
87
|
+
minCliVersion: '0.0.0',
|
|
88
|
+
minExtensionVersion: '0.0.0',
|
|
89
|
+
minServiceVersion: '0.0.0',
|
|
90
|
+
requiresWritableAppBundle: true,
|
|
91
|
+
requiresAdHocResign: false,
|
|
92
|
+
userMessage: 'fixture supported',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
appPath,
|
|
97
|
+
tempDir,
|
|
98
|
+
targetPath,
|
|
99
|
+
runtimeFile: join(tempDir, 'runtime.json'),
|
|
100
|
+
backupDir: join(tempDir, 'backups'),
|
|
101
|
+
compatEntry,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function createLinuxFixtureApp() {
|
|
106
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-install-linux-'));
|
|
107
|
+
const appPath = join(tempDir, 'squashfs-root');
|
|
108
|
+
const targetRelativePath = 'usr/share/cursor/resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
109
|
+
const targetPath = join(appPath, targetRelativePath);
|
|
110
|
+
const targetContent = `function main() { return "linux-agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
|
|
111
|
+
await mkdir(join(appPath, 'usr/share/cursor/resources/app/extensions/cursor-agent-exec/dist'), {
|
|
112
|
+
recursive: true,
|
|
113
|
+
});
|
|
114
|
+
await mkdir(join(appPath, 'usr/share/cursor/resources/app/extensions/cursor-always-local/dist'), {
|
|
115
|
+
recursive: true,
|
|
116
|
+
});
|
|
117
|
+
await mkdir(join(appPath, 'usr/share/cursor/resources/app/out/vs/workbench'), {
|
|
118
|
+
recursive: true,
|
|
119
|
+
});
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(appPath, 'usr/share/cursor/resources/app/product.json'),
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
version: '3.6.31',
|
|
124
|
+
commit: 'linux-commit',
|
|
125
|
+
}),
|
|
126
|
+
'utf8',
|
|
127
|
+
);
|
|
128
|
+
await writeFile(targetPath, targetContent, 'utf8');
|
|
129
|
+
await writeFile(
|
|
130
|
+
join(appPath, 'usr/share/cursor/resources/app/extensions/cursor-always-local/dist/main.js'),
|
|
131
|
+
'function alwaysLocal(){}\n',
|
|
132
|
+
'utf8',
|
|
133
|
+
);
|
|
134
|
+
await writeFile(
|
|
135
|
+
join(appPath, 'usr/share/cursor/resources/app/out/vs/workbench/workbench.desktop.main.js'),
|
|
136
|
+
workbenchFixture(),
|
|
137
|
+
'utf8',
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const expectedSha256 = createHash('sha256').update(targetContent).digest('hex');
|
|
141
|
+
const compatEntry: CompatibilityManifestEntry = {
|
|
142
|
+
platform: 'linux',
|
|
143
|
+
arch: process.arch,
|
|
144
|
+
cursorVersion: '3.6.31',
|
|
145
|
+
cursorCommit: 'linux-commit',
|
|
146
|
+
supportStatus: 'supported',
|
|
147
|
+
targetRelativePath,
|
|
148
|
+
expectedSha256,
|
|
149
|
+
structureSignature: 'linux-fixture',
|
|
150
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
151
|
+
verifyMarker: 'cursor-pool',
|
|
152
|
+
restoreStrategy: 'external-backup',
|
|
153
|
+
minCliVersion: '0.0.0',
|
|
154
|
+
minExtensionVersion: '0.0.0',
|
|
155
|
+
minServiceVersion: '0.0.0',
|
|
156
|
+
requiresWritableAppBundle: true,
|
|
157
|
+
requiresAdHocResign: false,
|
|
158
|
+
userMessage: 'linux fixture supported',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
appPath,
|
|
163
|
+
tempDir,
|
|
164
|
+
targetPath,
|
|
165
|
+
runtimeFile: join(tempDir, 'runtime.json'),
|
|
166
|
+
backupDir: join(tempDir, 'backups'),
|
|
167
|
+
compatEntry,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resignCompatEntry(entry: CompatibilityManifestEntry): CompatibilityManifestEntry {
|
|
172
|
+
return {
|
|
173
|
+
...entry,
|
|
174
|
+
requiresAdHocResign: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function createRemoteOnlyFixtureApp() {
|
|
179
|
+
const fixture = await createFixtureApp();
|
|
180
|
+
const productPath = join(fixture.appPath, 'Contents/Resources/app/product.json');
|
|
181
|
+
await writeFile(
|
|
182
|
+
productPath,
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
version: '3.7.0',
|
|
185
|
+
commit: 'remote-commit',
|
|
186
|
+
}),
|
|
187
|
+
'utf8',
|
|
188
|
+
);
|
|
189
|
+
const compatEntry = {
|
|
190
|
+
...fixture.compatEntry,
|
|
191
|
+
cursorVersion: '3.7.0',
|
|
192
|
+
cursorCommit: 'remote-commit',
|
|
193
|
+
userMessage: 'remote fixture supported',
|
|
194
|
+
};
|
|
195
|
+
return { ...fixture, compatEntry };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test('install reports Cursor version, simulated extension, service, patch, and health status', async () => {
|
|
199
|
+
const fixture = await createFixtureApp();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const output = await install({
|
|
203
|
+
appPath: fixture.appPath,
|
|
204
|
+
runtimeFile: fixture.runtimeFile,
|
|
205
|
+
backupDir: fixture.backupDir,
|
|
206
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
207
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
208
|
+
compatEntries: [fixture.compatEntry],
|
|
209
|
+
stopServiceAfterInstall: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.match(output, /Cursor 3\.5\.38/);
|
|
213
|
+
assert.match(output, /mode: disposable/);
|
|
214
|
+
assert.match(output, /app: .*Cursor\.app/);
|
|
215
|
+
assert.match(output, /extension: bundled/);
|
|
216
|
+
assert.match(output, /trial: recorded/);
|
|
217
|
+
assert.match(output, /service: running/);
|
|
218
|
+
assert.match(output, /patch: applied/);
|
|
219
|
+
assert.match(output, /health: ok/);
|
|
220
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
221
|
+
} finally {
|
|
222
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('install detects Linux Cursor AppImage layout under usr/share/cursor', async () => {
|
|
227
|
+
const fixture = await createLinuxFixtureApp();
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const output = await install({
|
|
231
|
+
appPath: fixture.appPath,
|
|
232
|
+
platform: 'linux',
|
|
233
|
+
arch: process.arch,
|
|
234
|
+
runtimeFile: fixture.runtimeFile,
|
|
235
|
+
backupDir: fixture.backupDir,
|
|
236
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
237
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
238
|
+
compatEntries: [fixture.compatEntry],
|
|
239
|
+
stopServiceAfterInstall: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.match(output, /Cursor 3\.6\.31/);
|
|
243
|
+
assert.match(output, /patch: applied/);
|
|
244
|
+
assert.match(output, /health: ok/);
|
|
245
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
246
|
+
} finally {
|
|
247
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('install can use a signed remote compatibility manifest from api base url', async () => {
|
|
252
|
+
const fixture = await createRemoteOnlyFixtureApp();
|
|
253
|
+
const envelope = {
|
|
254
|
+
version: 9,
|
|
255
|
+
signatureAlgorithm: 'hmac-sha256-dev',
|
|
256
|
+
signatureKeyId: 'dev-compatibility-key',
|
|
257
|
+
signature: buildCompatManifestSignature(9, [fixture.compatEntry]),
|
|
258
|
+
rules: [fixture.compatEntry],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const output = await install({
|
|
263
|
+
appPath: fixture.appPath,
|
|
264
|
+
runtimeFile: fixture.runtimeFile,
|
|
265
|
+
backupDir: fixture.backupDir,
|
|
266
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
267
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
268
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
269
|
+
fetchCompatManifest: async (url) => {
|
|
270
|
+
assert.equal(url, 'https://platform.example.test/api/client/compatibility/manifest');
|
|
271
|
+
return { ok: true, json: async () => envelope };
|
|
272
|
+
},
|
|
273
|
+
stopServiceAfterInstall: true,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
assert.match(output, /Cursor 3\.7\.0/);
|
|
277
|
+
assert.match(output, /patch: applied/);
|
|
278
|
+
} finally {
|
|
279
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('install passes API base URL to detached service startup', async () => {
|
|
284
|
+
const fixture = await createFixtureApp();
|
|
285
|
+
const serviceCalls: Record<string, unknown>[] = [];
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await install({
|
|
289
|
+
appPath: fixture.appPath,
|
|
290
|
+
runtimeFile: fixture.runtimeFile,
|
|
291
|
+
backupDir: fixture.backupDir,
|
|
292
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
293
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
294
|
+
compatEntries: [fixture.compatEntry],
|
|
295
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
296
|
+
startDetachedService: async (options) => {
|
|
297
|
+
serviceCalls.push(options);
|
|
298
|
+
await writeRuntimeInfo(
|
|
299
|
+
{ host: '127.0.0.1', port: 56393, runtimeId: 'runtime-1' },
|
|
300
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
301
|
+
);
|
|
302
|
+
return { host: '127.0.0.1', port: 56393, runtimeId: 'runtime-1' };
|
|
303
|
+
},
|
|
304
|
+
fetchHealth: async () => ({ ok: true, healthy: true }),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
assert.equal(serviceCalls[0]?.apiBaseUrl, 'https://platform.example.test');
|
|
308
|
+
} finally {
|
|
309
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('install persists API base URL and installs user autostart for real client runtime', async () => {
|
|
314
|
+
const fixture = await createFixtureApp();
|
|
315
|
+
const configFile = join(fixture.tempDir, 'client-config.json');
|
|
316
|
+
const autostartCalls: Record<string, unknown>[] = [];
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const output = await install({
|
|
320
|
+
appPath: fixture.appPath,
|
|
321
|
+
runtimeFile: fixture.runtimeFile,
|
|
322
|
+
backupDir: fixture.backupDir,
|
|
323
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
324
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
325
|
+
compatEntries: [fixture.compatEntry],
|
|
326
|
+
apiBaseUrl: 'https://platform.example.test/',
|
|
327
|
+
clientConfigFile: configFile,
|
|
328
|
+
installUserAutostart: async (options) => {
|
|
329
|
+
autostartCalls.push(options);
|
|
330
|
+
return { state: 'installed' as const };
|
|
331
|
+
},
|
|
332
|
+
startDetachedService: async () => {
|
|
333
|
+
await writeRuntimeInfo(
|
|
334
|
+
{ host: '127.0.0.1', port: 56393, runtimeId: 'runtime-1' },
|
|
335
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
336
|
+
);
|
|
337
|
+
return { host: '127.0.0.1', port: 56393, runtimeId: 'runtime-1' };
|
|
338
|
+
},
|
|
339
|
+
fetchHealth: async () => ({ ok: true, healthy: true }),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
assert.deepEqual(await readClientConfig({ configFile }), {
|
|
343
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
344
|
+
});
|
|
345
|
+
assert.equal(autostartCalls[0]?.configFile, configFile);
|
|
346
|
+
assert.equal(autostartCalls[0]?.runtimeFile, fixture.runtimeFile);
|
|
347
|
+
assert.match(output, /autostart: installed/);
|
|
348
|
+
} finally {
|
|
349
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('real-mode install writes install record and reports real mode', async () => {
|
|
354
|
+
const fixture = await createFixtureApp();
|
|
355
|
+
const installRecordFile = join(fixture.tempDir, 'install.json');
|
|
356
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
357
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'real-extensions');
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const output = await install({
|
|
361
|
+
realAppPath: fixture.appPath,
|
|
362
|
+
yes: true,
|
|
363
|
+
runtimeFile: fixture.runtimeFile,
|
|
364
|
+
backupDir: fixture.backupDir,
|
|
365
|
+
installRecordFile,
|
|
366
|
+
extensionInstallPath,
|
|
367
|
+
cursorExtensionsDir,
|
|
368
|
+
compatEntries: [fixture.compatEntry],
|
|
369
|
+
stopServiceAfterInstall: true,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
assert.match(output, /mode: real/);
|
|
373
|
+
assert.match(output, /patch: applied/);
|
|
374
|
+
assert.match(output, /install-record: recorded/);
|
|
375
|
+
assert.doesNotMatch(output, /trial: recorded/);
|
|
376
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
377
|
+
|
|
378
|
+
const record = await readInstallRecord({ installRecordFile });
|
|
379
|
+
assert.ok(record);
|
|
380
|
+
assert.equal(record.mode, 'real');
|
|
381
|
+
assert.equal(record.appPath, fixture.appPath);
|
|
382
|
+
assert.equal(record.cursorVersion, '3.5.38');
|
|
383
|
+
assert.equal(record.extensionInstallPath, extensionInstallPath);
|
|
384
|
+
assert.equal(record.extensionLinkedPath, linkedExtensionPathForDir(cursorExtensionsDir));
|
|
385
|
+
assert.equal(record.lastOperation, 'install');
|
|
386
|
+
assert.equal(record.lastOperationStatus, 'ok');
|
|
387
|
+
} finally {
|
|
388
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('real-mode install rolls back install record after health failure', async () => {
|
|
393
|
+
const fixture = await createFixtureApp();
|
|
394
|
+
const installRecordFile = join(fixture.tempDir, 'install.json');
|
|
395
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
await assert.rejects(
|
|
399
|
+
install({
|
|
400
|
+
realAppPath: fixture.appPath,
|
|
401
|
+
yes: true,
|
|
402
|
+
runtimeFile: fixture.runtimeFile,
|
|
403
|
+
backupDir: fixture.backupDir,
|
|
404
|
+
installRecordFile,
|
|
405
|
+
extensionInstallPath,
|
|
406
|
+
compatEntries: [fixture.compatEntry],
|
|
407
|
+
stopServiceAfterInstall: true,
|
|
408
|
+
fetchHealth: async () => ({ ok: false, healthy: false }),
|
|
409
|
+
}),
|
|
410
|
+
/Service health check failed/,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
assert.equal(await readInstallRecord({ installRecordFile }), null);
|
|
414
|
+
} finally {
|
|
415
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('real-mode install requires confirmation when yes is missing', async () => {
|
|
420
|
+
const fixture = await createFixtureApp();
|
|
421
|
+
const originalTargetContent = await readFile(fixture.targetPath, 'utf8');
|
|
422
|
+
const installRecordFile = join(fixture.tempDir, 'install.json');
|
|
423
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
await assert.rejects(
|
|
427
|
+
install({
|
|
428
|
+
realAppPath: fixture.appPath,
|
|
429
|
+
isInteractive: false,
|
|
430
|
+
runtimeFile: fixture.runtimeFile,
|
|
431
|
+
backupDir: fixture.backupDir,
|
|
432
|
+
installRecordFile,
|
|
433
|
+
extensionInstallPath,
|
|
434
|
+
compatEntries: [fixture.compatEntry],
|
|
435
|
+
stopServiceAfterInstall: true,
|
|
436
|
+
}),
|
|
437
|
+
/Pass --yes to confirm install for real Cursor/,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
assert.equal(await readFile(fixture.targetPath, 'utf8'), originalTargetContent);
|
|
441
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'missing');
|
|
442
|
+
assert.equal(await readInstallRecord({ installRecordFile }), null);
|
|
443
|
+
} finally {
|
|
444
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('install links extension into an explicit Cursor extensions directory', async () => {
|
|
449
|
+
const fixture = await createFixtureApp();
|
|
450
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
451
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
452
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'Cursor-Pool-Trial-Extensions');
|
|
453
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const output = await install({
|
|
457
|
+
appPath: fixture.appPath,
|
|
458
|
+
runtimeFile: fixture.runtimeFile,
|
|
459
|
+
backupDir: fixture.backupDir,
|
|
460
|
+
trialRecordDir,
|
|
461
|
+
extensionInstallPath,
|
|
462
|
+
cursorExtensionsDir,
|
|
463
|
+
compatEntries: [fixture.compatEntry],
|
|
464
|
+
stopServiceAfterInstall: true,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
assert.match(output, /extension: linked/);
|
|
468
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'bundled');
|
|
469
|
+
assert.equal(await getLinkedExtensionState(linkedPath), 'linked');
|
|
470
|
+
|
|
471
|
+
const trialRecord = await readTrialRecord(fixture.appPath, { trialRecordDir });
|
|
472
|
+
assert.equal(trialRecord?.extensionState, 'linked');
|
|
473
|
+
assert.equal(trialRecord?.extensionInstallPath, extensionInstallPath);
|
|
474
|
+
assert.equal(trialRecord?.extensionLinkedPath, linkedPath);
|
|
475
|
+
} finally {
|
|
476
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('install refreshes explicit Cursor extensions index for the user client', async () => {
|
|
481
|
+
const fixture = await createFixtureApp();
|
|
482
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
483
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'real-extensions');
|
|
484
|
+
await mkdir(cursorExtensionsDir, { recursive: true });
|
|
485
|
+
await writeFile(
|
|
486
|
+
join(cursorExtensionsDir, 'extensions.json'),
|
|
487
|
+
JSON.stringify([
|
|
488
|
+
{
|
|
489
|
+
identifier: { id: 'cursor-pool.@cursor-pool/extension' },
|
|
490
|
+
relativeLocation: 'cursor-pool.extension-0.0.0',
|
|
491
|
+
version: '0.0.0',
|
|
492
|
+
},
|
|
493
|
+
]),
|
|
494
|
+
'utf8',
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
await install({
|
|
499
|
+
realAppPath: fixture.appPath,
|
|
500
|
+
yes: true,
|
|
501
|
+
runtimeFile: fixture.runtimeFile,
|
|
502
|
+
backupDir: fixture.backupDir,
|
|
503
|
+
installRecordFile: join(fixture.tempDir, 'install.json'),
|
|
504
|
+
extensionInstallPath,
|
|
505
|
+
cursorExtensionsDir,
|
|
506
|
+
compatEntries: [fixture.compatEntry],
|
|
507
|
+
stopServiceAfterInstall: true,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const index = JSON.parse(await readFile(join(cursorExtensionsDir, 'extensions.json'), 'utf8'));
|
|
511
|
+
assert.deepEqual(index, [
|
|
512
|
+
{
|
|
513
|
+
identifier: { id: 'cursor-pool.cursorpool' },
|
|
514
|
+
location: {
|
|
515
|
+
$mid: 1,
|
|
516
|
+
path: linkedExtensionPathForDir(cursorExtensionsDir),
|
|
517
|
+
scheme: 'file',
|
|
518
|
+
},
|
|
519
|
+
relativeLocation: 'cursor-pool.extension-0.0.0',
|
|
520
|
+
version: '0.0.0',
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
} finally {
|
|
524
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('install ad-hoc resigns macOS app when compatibility requires it', async () => {
|
|
529
|
+
const fixture = await createFixtureApp();
|
|
530
|
+
const resignCalls: string[] = [];
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const output = await install({
|
|
534
|
+
appPath: fixture.appPath,
|
|
535
|
+
runtimeFile: fixture.runtimeFile,
|
|
536
|
+
backupDir: fixture.backupDir,
|
|
537
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
538
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
539
|
+
compatEntries: [resignCompatEntry(fixture.compatEntry)],
|
|
540
|
+
stopServiceAfterInstall: true,
|
|
541
|
+
adHocResignApp: async (appPath) => {
|
|
542
|
+
resignCalls.push(appPath);
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
assert.deepEqual(resignCalls, process.platform === 'darwin' ? [fixture.appPath] : []);
|
|
547
|
+
assert.match(output, process.platform === 'darwin' ? /resign: ad-hoc/ : /resign: skipped/);
|
|
548
|
+
} finally {
|
|
549
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('install rolls back patch when required ad-hoc resign fails', async () => {
|
|
554
|
+
const fixture = await createFixtureApp();
|
|
555
|
+
const originalTargetContent = await readFile(fixture.targetPath, 'utf8');
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
await assert.rejects(
|
|
559
|
+
install({
|
|
560
|
+
appPath: fixture.appPath,
|
|
561
|
+
runtimeFile: fixture.runtimeFile,
|
|
562
|
+
backupDir: fixture.backupDir,
|
|
563
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
564
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
565
|
+
compatEntries: [resignCompatEntry(fixture.compatEntry)],
|
|
566
|
+
stopServiceAfterInstall: true,
|
|
567
|
+
adHocResignApp: async () => {
|
|
568
|
+
throw new Error('synthetic codesign failure');
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
571
|
+
/synthetic codesign failure/,
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
assert.equal(await readFile(fixture.targetPath, 'utf8'), originalTargetContent);
|
|
575
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir: join(fixture.tempDir, 'trials') }), null);
|
|
576
|
+
} finally {
|
|
577
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('install rollback stops service and removes runtime file when patch fails after service start', async () => {
|
|
582
|
+
const fixture = await createFixtureApp();
|
|
583
|
+
let startedPort: number | undefined;
|
|
584
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
585
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
await assert.rejects(
|
|
589
|
+
install({
|
|
590
|
+
appPath: fixture.appPath,
|
|
591
|
+
runtimeFile: fixture.runtimeFile,
|
|
592
|
+
backupDir: fixture.backupDir,
|
|
593
|
+
trialRecordDir,
|
|
594
|
+
extensionInstallPath,
|
|
595
|
+
compatEntries: [fixture.compatEntry],
|
|
596
|
+
stopServiceAfterInstall: true,
|
|
597
|
+
patchCursorAgentExec: async () => {
|
|
598
|
+
const runtime = JSON.parse(await readFile(fixture.runtimeFile, 'utf8')) as {
|
|
599
|
+
port: number;
|
|
600
|
+
};
|
|
601
|
+
startedPort = runtime.port;
|
|
602
|
+
throw new Error('synthetic patch failure');
|
|
603
|
+
},
|
|
604
|
+
}),
|
|
605
|
+
/synthetic patch failure/,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
await assert.rejects(readFile(fixture.runtimeFile, 'utf8'), /ENOENT/);
|
|
609
|
+
assert.equal(typeof startedPort, 'number');
|
|
610
|
+
await assert.rejects(fetch(`http://127.0.0.1:${startedPort}/health`), /fetch failed/);
|
|
611
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'missing');
|
|
612
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir }), null);
|
|
613
|
+
} finally {
|
|
614
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('install rollback stops service and removes runtime file when restore fails after health failure', async () => {
|
|
619
|
+
const fixture = await createFixtureApp();
|
|
620
|
+
let startedPort: number | undefined;
|
|
621
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
622
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
await assert.rejects(
|
|
626
|
+
install({
|
|
627
|
+
appPath: fixture.appPath,
|
|
628
|
+
runtimeFile: fixture.runtimeFile,
|
|
629
|
+
backupDir: fixture.backupDir,
|
|
630
|
+
trialRecordDir,
|
|
631
|
+
extensionInstallPath,
|
|
632
|
+
compatEntries: [fixture.compatEntry],
|
|
633
|
+
stopServiceAfterInstall: true,
|
|
634
|
+
fetchHealth: async () => {
|
|
635
|
+
const runtime = JSON.parse(await readFile(fixture.runtimeFile, 'utf8')) as {
|
|
636
|
+
port: number;
|
|
637
|
+
};
|
|
638
|
+
startedPort = runtime.port;
|
|
639
|
+
return { ok: false, healthy: false };
|
|
640
|
+
},
|
|
641
|
+
restoreCursorAgentExec: async () => {
|
|
642
|
+
throw new Error('synthetic restore failure');
|
|
643
|
+
},
|
|
644
|
+
}),
|
|
645
|
+
/Service health check failed/,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
await assert.rejects(readFile(fixture.runtimeFile, 'utf8'), /ENOENT/);
|
|
649
|
+
assert.equal(typeof startedPort, 'number');
|
|
650
|
+
await assert.rejects(fetch(`http://127.0.0.1:${startedPort}/health`), /fetch failed/);
|
|
651
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'missing');
|
|
652
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir }), null);
|
|
653
|
+
} finally {
|
|
654
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('failed reinstall preserves existing patch, trial record, and extension bundle', async () => {
|
|
659
|
+
const fixture = await createFixtureApp();
|
|
660
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
661
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
await install({
|
|
665
|
+
appPath: fixture.appPath,
|
|
666
|
+
runtimeFile: fixture.runtimeFile,
|
|
667
|
+
backupDir: fixture.backupDir,
|
|
668
|
+
trialRecordDir,
|
|
669
|
+
extensionInstallPath,
|
|
670
|
+
compatEntries: [fixture.compatEntry],
|
|
671
|
+
stopServiceAfterInstall: true,
|
|
672
|
+
});
|
|
673
|
+
const trialRecord = await readTrialRecord(fixture.appPath, { trialRecordDir });
|
|
674
|
+
assert.ok(trialRecord);
|
|
675
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'bundled');
|
|
676
|
+
const sentinelExtensionContent = 'module.exports = { sentinel: true };\n';
|
|
677
|
+
await writeFile(join(extensionInstallPath, 'dist/extension.js'), sentinelExtensionContent, 'utf8');
|
|
678
|
+
await writeRuntimeInfo(
|
|
679
|
+
{ host: '127.0.0.1', port: 45123, runtimeId: 'pre-existing-runtime' },
|
|
680
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
681
|
+
);
|
|
682
|
+
const existingRuntimeJson = await readFile(fixture.runtimeFile, 'utf8');
|
|
683
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
684
|
+
|
|
685
|
+
await assert.rejects(
|
|
686
|
+
install({
|
|
687
|
+
appPath: fixture.appPath,
|
|
688
|
+
runtimeFile: fixture.runtimeFile,
|
|
689
|
+
backupDir: fixture.backupDir,
|
|
690
|
+
trialRecordDir,
|
|
691
|
+
extensionInstallPath,
|
|
692
|
+
compatEntries: [fixture.compatEntry],
|
|
693
|
+
stopServiceAfterInstall: true,
|
|
694
|
+
fetchHealth: async () => ({ ok: false, healthy: false }),
|
|
695
|
+
}),
|
|
696
|
+
/Service health check failed/,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
assert.match(await readFile(fixture.targetPath, 'utf8'), new RegExp(CURSOR_POOL_PATCH_MARKER));
|
|
700
|
+
assert.equal(await getExtensionState(extensionInstallPath), 'bundled');
|
|
701
|
+
assert.equal(
|
|
702
|
+
await readFile(join(extensionInstallPath, 'dist/extension.js'), 'utf8'),
|
|
703
|
+
sentinelExtensionContent,
|
|
704
|
+
);
|
|
705
|
+
assert.equal(await readFile(fixture.runtimeFile, 'utf8'), existingRuntimeJson);
|
|
706
|
+
assert.deepEqual(await readTrialRecord(fixture.appPath, { trialRecordDir }), trialRecord);
|
|
707
|
+
} finally {
|
|
708
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('failed linked reinstall restores pre-existing linked extension bytes', async () => {
|
|
713
|
+
const fixture = await createFixtureApp();
|
|
714
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
715
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
716
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'Extensions');
|
|
717
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
718
|
+
const sentinelExtensionContent = 'module.exports = { sentinel: "linked" };\n';
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
await install({
|
|
722
|
+
appPath: fixture.appPath,
|
|
723
|
+
runtimeFile: fixture.runtimeFile,
|
|
724
|
+
backupDir: fixture.backupDir,
|
|
725
|
+
trialRecordDir,
|
|
726
|
+
extensionInstallPath,
|
|
727
|
+
cursorExtensionsDir,
|
|
728
|
+
compatEntries: [fixture.compatEntry],
|
|
729
|
+
stopServiceAfterInstall: true,
|
|
730
|
+
});
|
|
731
|
+
await writeFile(join(linkedPath, 'dist/extension.js'), sentinelExtensionContent, 'utf8');
|
|
732
|
+
const trialRecord = await readTrialRecord(fixture.appPath, { trialRecordDir });
|
|
733
|
+
assert.ok(trialRecord);
|
|
734
|
+
|
|
735
|
+
await assert.rejects(
|
|
736
|
+
install({
|
|
737
|
+
appPath: fixture.appPath,
|
|
738
|
+
runtimeFile: fixture.runtimeFile,
|
|
739
|
+
backupDir: fixture.backupDir,
|
|
740
|
+
trialRecordDir,
|
|
741
|
+
extensionInstallPath,
|
|
742
|
+
cursorExtensionsDir,
|
|
743
|
+
compatEntries: [fixture.compatEntry],
|
|
744
|
+
stopServiceAfterInstall: true,
|
|
745
|
+
fetchHealth: async () => ({ ok: false, healthy: false }),
|
|
746
|
+
}),
|
|
747
|
+
/Service health check failed/,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
assert.equal(await getLinkedExtensionState(linkedPath), 'linked');
|
|
751
|
+
assert.equal(await readFile(join(linkedPath, 'dist/extension.js'), 'utf8'), sentinelExtensionContent);
|
|
752
|
+
assert.deepEqual(await readTrialRecord(fixture.appPath, { trialRecordDir }), trialRecord);
|
|
753
|
+
} finally {
|
|
754
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test('failed linked install restores pre-existing incomplete linked bytes', async () => {
|
|
759
|
+
const fixture = await createFixtureApp();
|
|
760
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
761
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
762
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'Extensions');
|
|
763
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
764
|
+
const sentinelPath = join(linkedPath, 'sentinel.txt');
|
|
765
|
+
|
|
766
|
+
await mkdir(linkedPath, { recursive: true });
|
|
767
|
+
await writeFile(sentinelPath, 'keep incomplete bytes\n', 'utf8');
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
await assert.rejects(
|
|
771
|
+
install({
|
|
772
|
+
appPath: fixture.appPath,
|
|
773
|
+
runtimeFile: fixture.runtimeFile,
|
|
774
|
+
backupDir: fixture.backupDir,
|
|
775
|
+
trialRecordDir,
|
|
776
|
+
extensionInstallPath,
|
|
777
|
+
cursorExtensionsDir,
|
|
778
|
+
compatEntries: [fixture.compatEntry],
|
|
779
|
+
stopServiceAfterInstall: true,
|
|
780
|
+
fetchHealth: async () => ({ ok: false, healthy: false }),
|
|
781
|
+
}),
|
|
782
|
+
/Service health check failed/,
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
assert.equal(await readFile(sentinelPath, 'utf8'), 'keep incomplete bytes\n');
|
|
786
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir }), null);
|
|
787
|
+
} finally {
|
|
788
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('failed link operation removes partially-created linked extension bytes', async () => {
|
|
793
|
+
const fixture = await createFixtureApp();
|
|
794
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
795
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
796
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'Extensions');
|
|
797
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
await assert.rejects(
|
|
801
|
+
install({
|
|
802
|
+
appPath: fixture.appPath,
|
|
803
|
+
runtimeFile: fixture.runtimeFile,
|
|
804
|
+
backupDir: fixture.backupDir,
|
|
805
|
+
trialRecordDir,
|
|
806
|
+
extensionInstallPath,
|
|
807
|
+
cursorExtensionsDir,
|
|
808
|
+
compatEntries: [fixture.compatEntry],
|
|
809
|
+
stopServiceAfterInstall: true,
|
|
810
|
+
linkExtensionBundle: async () => {
|
|
811
|
+
await mkdir(join(linkedPath, 'dist'), { recursive: true });
|
|
812
|
+
await writeFile(join(linkedPath, 'package.json'), JSON.stringify({ name: 'partial' }), 'utf8');
|
|
813
|
+
await writeFile(join(linkedPath, 'dist/extension.js'), 'partial\n', 'utf8');
|
|
814
|
+
throw new Error('synthetic link failure');
|
|
815
|
+
},
|
|
816
|
+
}),
|
|
817
|
+
/synthetic link failure/,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
assert.equal(await getLinkedExtensionState(linkedPath), 'missing');
|
|
821
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir }), null);
|
|
822
|
+
} finally {
|
|
823
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test('failed bundle operation restores pre-existing extension bundle bytes', async () => {
|
|
828
|
+
const fixture = await createFixtureApp();
|
|
829
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
830
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
831
|
+
const sentinelExtensionContent = 'module.exports = { sentinel: "bundle-failure" };\n';
|
|
832
|
+
await mkdir(join(extensionInstallPath, 'dist'), { recursive: true });
|
|
833
|
+
await writeFile(join(extensionInstallPath, 'package.json'), JSON.stringify({ name: 'sentinel' }), 'utf8');
|
|
834
|
+
await writeFile(join(extensionInstallPath, 'dist/extension.js'), sentinelExtensionContent, 'utf8');
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
await assert.rejects(
|
|
838
|
+
install({
|
|
839
|
+
appPath: fixture.appPath,
|
|
840
|
+
runtimeFile: fixture.runtimeFile,
|
|
841
|
+
backupDir: fixture.backupDir,
|
|
842
|
+
trialRecordDir,
|
|
843
|
+
extensionInstallPath,
|
|
844
|
+
compatEntries: [fixture.compatEntry],
|
|
845
|
+
stopServiceAfterInstall: true,
|
|
846
|
+
bundleExtension: async () => {
|
|
847
|
+
await rm(extensionInstallPath, { recursive: true, force: true });
|
|
848
|
+
throw new Error('synthetic bundle failure');
|
|
849
|
+
},
|
|
850
|
+
}),
|
|
851
|
+
/synthetic bundle failure/,
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
assert.equal(
|
|
855
|
+
await readFile(join(extensionInstallPath, 'dist/extension.js'), 'utf8'),
|
|
856
|
+
sentinelExtensionContent,
|
|
857
|
+
);
|
|
858
|
+
assert.equal(await readTrialRecord(fixture.appPath, { trialRecordDir }), null);
|
|
859
|
+
} finally {
|
|
860
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
861
|
+
}
|
|
862
|
+
});
|