@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,2428 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
3
|
+
import { chmod, mkdtemp, readFile, rm, stat, 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
|
+
loginWithPassword,
|
|
9
|
+
loginWithCode,
|
|
10
|
+
logoutPlatform,
|
|
11
|
+
platformCatalog,
|
|
12
|
+
platformStatus,
|
|
13
|
+
selectPlatformProduct,
|
|
14
|
+
sendHeartbeat,
|
|
15
|
+
startPlatformMode,
|
|
16
|
+
stopPlatformMode,
|
|
17
|
+
} from '../src/platformSession';
|
|
18
|
+
|
|
19
|
+
type RouteResult = Record<string, unknown> | { statusCode: number; body: Record<string, unknown> };
|
|
20
|
+
type RouteHandler = (
|
|
21
|
+
body: Record<string, unknown>,
|
|
22
|
+
request: IncomingMessage,
|
|
23
|
+
) => RouteResult | Promise<RouteResult>;
|
|
24
|
+
|
|
25
|
+
function normalizeRouteResult(result: RouteResult) {
|
|
26
|
+
if ('statusCode' in result && 'body' in result) {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { statusCode: 200, body: result };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function jsonResponse(response: ServerResponse, statusCode: number, body: Record<string, unknown>) {
|
|
34
|
+
const payload = JSON.stringify(body);
|
|
35
|
+
response.writeHead(statusCode, {
|
|
36
|
+
'content-type': 'application/json',
|
|
37
|
+
'content-length': Buffer.byteLength(payload),
|
|
38
|
+
});
|
|
39
|
+
response.end(payload);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readBody(request: IncomingMessage) {
|
|
43
|
+
const chunks: Buffer[] = [];
|
|
44
|
+
for await (const chunk of request) {
|
|
45
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
46
|
+
}
|
|
47
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
48
|
+
return body ? (JSON.parse(body) as Record<string, unknown>) : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function createApiServer(routes: Record<string, RouteHandler>) {
|
|
52
|
+
const server = createServer(async (request, response) => {
|
|
53
|
+
const key = `${request.method} ${request.url}`;
|
|
54
|
+
const route = routes[key];
|
|
55
|
+
if (!route) {
|
|
56
|
+
jsonResponse(response, 404, { ok: false });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = normalizeRouteResult(await route(await readBody(request), request));
|
|
61
|
+
jsonResponse(response, result.statusCode, result.body);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
65
|
+
const address = server.address();
|
|
66
|
+
assert.equal(typeof address, 'object');
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
apiBaseUrl: `http://127.0.0.1:${address?.port}`,
|
|
70
|
+
close: () => new Promise<void>((resolve, reject) => {
|
|
71
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('loginWithCode exchanges code and writes a minimal session file', async () => {
|
|
77
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
78
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
79
|
+
const api = await createApiServer({
|
|
80
|
+
'POST /auth/device-token': (body) => {
|
|
81
|
+
assert.equal(body.code, 'CODE-123');
|
|
82
|
+
assert.deepEqual(body.device, { name: 'Lin Mac', os: 'darwin', arch: 'arm64' });
|
|
83
|
+
return {
|
|
84
|
+
deviceToken: 'cp_dev_secret',
|
|
85
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
86
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const status = await loginWithCode({
|
|
93
|
+
code: 'CODE-123',
|
|
94
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
95
|
+
sessionFile,
|
|
96
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.equal(status.state, 'logged-in');
|
|
100
|
+
assert.deepEqual(status.user, { id: 'usr_1', email: 'dev@example.com' });
|
|
101
|
+
assert.deepEqual(status.device, {
|
|
102
|
+
id: 'dev_1',
|
|
103
|
+
status: 'active',
|
|
104
|
+
lastHeartbeatAt: '2026-05-30T00:00:00Z',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
108
|
+
const savedStat = await stat(sessionFile);
|
|
109
|
+
assert.equal(saved.apiBaseUrl, api.apiBaseUrl);
|
|
110
|
+
assert.equal(saved.deviceToken, 'cp_dev_secret');
|
|
111
|
+
assert.equal(savedStat.mode & 0o777, 0o600);
|
|
112
|
+
assert.equal(typeof saved.createdAt, 'string');
|
|
113
|
+
assert.match(saved.createdAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
114
|
+
assert.deepEqual(saved.user, status.user);
|
|
115
|
+
assert.deepEqual(saved.device, status.device);
|
|
116
|
+
} finally {
|
|
117
|
+
await api.close();
|
|
118
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('loginWithPassword exchanges credentials and writes a minimal session file', async () => {
|
|
123
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-password-'));
|
|
124
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
125
|
+
const api = await createApiServer({
|
|
126
|
+
'POST /auth/password-device-token': (body) => {
|
|
127
|
+
assert.equal(body.email, 'dev@example.com');
|
|
128
|
+
assert.equal(body.password, 'correct-password');
|
|
129
|
+
assert.deepEqual(body.device, { name: 'Lin Mac', os: 'darwin', arch: 'arm64' });
|
|
130
|
+
return {
|
|
131
|
+
deviceToken: 'cp_dev_secret',
|
|
132
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
133
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const status = await loginWithPassword({
|
|
140
|
+
email: 'dev@example.com',
|
|
141
|
+
password: 'correct-password',
|
|
142
|
+
apiBaseUrl: `${api.apiBaseUrl}/`,
|
|
143
|
+
sessionFile,
|
|
144
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
assert.equal(status.state, 'logged-in');
|
|
148
|
+
assert.deepEqual(status.user, { id: 'usr_1', email: 'dev@example.com' });
|
|
149
|
+
|
|
150
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
151
|
+
const savedStat = await stat(sessionFile);
|
|
152
|
+
assert.equal(saved.apiBaseUrl, api.apiBaseUrl);
|
|
153
|
+
assert.equal(saved.deviceToken, 'cp_dev_secret');
|
|
154
|
+
assert.equal(savedStat.mode & 0o777, 0o600);
|
|
155
|
+
assert.deepEqual(saved.user, status.user);
|
|
156
|
+
} finally {
|
|
157
|
+
await api.close();
|
|
158
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('loginWithPassword preserves platform password login error code', async () => {
|
|
163
|
+
const api = await createApiServer({
|
|
164
|
+
'POST /auth/password-device-token': () => ({
|
|
165
|
+
statusCode: 401,
|
|
166
|
+
body: { detail: { code: 'PASSWORD_LOGIN_INVALID' } },
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await assert.rejects(
|
|
172
|
+
loginWithPassword({
|
|
173
|
+
email: 'dev@example.com',
|
|
174
|
+
password: 'wrong-password',
|
|
175
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
176
|
+
}),
|
|
177
|
+
(error: unknown) => {
|
|
178
|
+
assert.equal((error as { platformCode?: unknown }).platformCode, 'PASSWORD_LOGIN_INVALID');
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
} finally {
|
|
183
|
+
await api.close();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('platformStatus propagates local session write failures after /me succeeds', async () => {
|
|
188
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
189
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
190
|
+
const api = await createApiServer({
|
|
191
|
+
'POST /auth/device-token': () => ({
|
|
192
|
+
deviceToken: 'cp_dev_secret',
|
|
193
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
194
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
195
|
+
}),
|
|
196
|
+
'GET /me': () => ({
|
|
197
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
198
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:02:00Z' },
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await loginWithCode({
|
|
204
|
+
code: 'CODE-123',
|
|
205
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
206
|
+
sessionFile,
|
|
207
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
208
|
+
});
|
|
209
|
+
await chmod(sessionFile, 0o400);
|
|
210
|
+
|
|
211
|
+
await assert.rejects(
|
|
212
|
+
platformStatus({ sessionFile }),
|
|
213
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === 'EACCES',
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
await chmod(sessionFile, 0o600).catch(() => {});
|
|
217
|
+
await api.close();
|
|
218
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('platformStatus distinguishes logged-out and offline states', async () => {
|
|
223
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
224
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
225
|
+
const api = await createApiServer({
|
|
226
|
+
'POST /auth/device-token': () => ({
|
|
227
|
+
deviceToken: 'cp_dev_secret',
|
|
228
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
229
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
assert.deepEqual(await platformStatus({ sessionFile }), { state: 'logged-out' });
|
|
235
|
+
|
|
236
|
+
await loginWithCode({
|
|
237
|
+
code: 'CODE-123',
|
|
238
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
239
|
+
sessionFile,
|
|
240
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
241
|
+
});
|
|
242
|
+
} finally {
|
|
243
|
+
await api.close();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const status = await platformStatus({ sessionFile });
|
|
248
|
+
assert.equal(status.state, 'offline');
|
|
249
|
+
assert.deepEqual(status.user, { id: 'usr_1', email: 'dev@example.com' });
|
|
250
|
+
assert.equal(status.device?.id, 'dev_1');
|
|
251
|
+
} finally {
|
|
252
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('platformStatus returns invalid-token when /me responds with HTTP 401', async () => {
|
|
257
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
258
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
259
|
+
const api = await createApiServer({
|
|
260
|
+
'POST /auth/device-token': () => ({
|
|
261
|
+
deviceToken: 'cp_dev_secret',
|
|
262
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
263
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
264
|
+
}),
|
|
265
|
+
'GET /me': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_INVALID' } } }),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await loginWithCode({
|
|
270
|
+
code: 'CODE-123',
|
|
271
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
272
|
+
sessionFile,
|
|
273
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const status = await platformStatus({ sessionFile });
|
|
277
|
+
|
|
278
|
+
assert.equal(status.state, 'invalid-token');
|
|
279
|
+
assert.deepEqual(status.user, { id: 'usr_1', email: 'dev@example.com' });
|
|
280
|
+
assert.equal(status.device?.id, 'dev_1');
|
|
281
|
+
} finally {
|
|
282
|
+
await api.close();
|
|
283
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('platformStatus releases active mode when token becomes invalid', async () => {
|
|
288
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
289
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
290
|
+
const api = await createApiServer({
|
|
291
|
+
'POST /auth/device-token': () => ({
|
|
292
|
+
deviceToken: 'cp_dev_secret',
|
|
293
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
294
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
295
|
+
}),
|
|
296
|
+
'GET /me': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await loginWithCode({
|
|
301
|
+
code: 'CODE-123',
|
|
302
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
303
|
+
sessionFile,
|
|
304
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
305
|
+
});
|
|
306
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
307
|
+
await writeFile(
|
|
308
|
+
sessionFile,
|
|
309
|
+
`${JSON.stringify({
|
|
310
|
+
...saved,
|
|
311
|
+
platformMode: 'active',
|
|
312
|
+
activeProductId: 'prod_basic',
|
|
313
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
314
|
+
}, null, 2)}\n`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const status = await platformStatus({ sessionFile });
|
|
318
|
+
|
|
319
|
+
assert.equal(status.state, 'invalid-token');
|
|
320
|
+
assert.equal(status.mode?.state, 'inactive');
|
|
321
|
+
assert.equal(status.mode?.releaseReason, 'invalid-token');
|
|
322
|
+
assert.match(status.mode?.releasedAt ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
323
|
+
|
|
324
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
325
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
326
|
+
assert.equal(updated.lastModeReleaseReason, 'invalid-token');
|
|
327
|
+
} finally {
|
|
328
|
+
await api.close();
|
|
329
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const poolSession = {
|
|
334
|
+
id: 'pool_sess_1',
|
|
335
|
+
productId: 'prod_basic',
|
|
336
|
+
providerType: 'mock-provider',
|
|
337
|
+
status: 'active' as const,
|
|
338
|
+
startedAt: '2026-05-31T00:00:00.000Z',
|
|
339
|
+
expiresAt: '2026-05-31T01:00:00.000Z',
|
|
340
|
+
routeTokenExpiresAt: '2026-05-31T00:10:00.000Z',
|
|
341
|
+
routeStrategy: 'platform-gateway' as const,
|
|
342
|
+
bannedModels: [],
|
|
343
|
+
capabilities: {
|
|
344
|
+
streaming: true,
|
|
345
|
+
usageEstimate: false,
|
|
346
|
+
hardSpendLimit: false,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
test('platform session accepts pool session and route token fields', async () => {
|
|
351
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
352
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
353
|
+
const api = await createApiServer({
|
|
354
|
+
'POST /auth/device-token': () => ({
|
|
355
|
+
deviceToken: 'cp_dev_secret',
|
|
356
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
357
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
358
|
+
}),
|
|
359
|
+
'GET /me': () => ({
|
|
360
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
361
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:05:00Z' },
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await loginWithCode({
|
|
367
|
+
code: 'CODE-123',
|
|
368
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
369
|
+
sessionFile,
|
|
370
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
371
|
+
});
|
|
372
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
373
|
+
await writeFile(
|
|
374
|
+
sessionFile,
|
|
375
|
+
`${JSON.stringify({
|
|
376
|
+
...saved,
|
|
377
|
+
selectedProductId: 'prod_basic',
|
|
378
|
+
platformMode: 'active',
|
|
379
|
+
activeProductId: 'prod_basic',
|
|
380
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
381
|
+
poolSession,
|
|
382
|
+
routeToken: {
|
|
383
|
+
token: 'rt_secret_value',
|
|
384
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
385
|
+
},
|
|
386
|
+
}, null, 2)}\n`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const status = await platformStatus({ sessionFile });
|
|
390
|
+
|
|
391
|
+
assert.equal(status.state, 'logged-in');
|
|
392
|
+
assert.equal(status.mode.state, 'active');
|
|
393
|
+
assert.equal(status.mode.productId, 'prod_basic');
|
|
394
|
+
} finally {
|
|
395
|
+
await api.close();
|
|
396
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('soft release clears pool session and route token', async () => {
|
|
401
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
402
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
403
|
+
const api = await createApiServer({
|
|
404
|
+
'POST /auth/device-token': () => ({
|
|
405
|
+
deviceToken: 'cp_dev_secret',
|
|
406
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
407
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
408
|
+
}),
|
|
409
|
+
'GET /me': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
await loginWithCode({
|
|
414
|
+
code: 'CODE-123',
|
|
415
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
416
|
+
sessionFile,
|
|
417
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
418
|
+
});
|
|
419
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
420
|
+
await writeFile(
|
|
421
|
+
sessionFile,
|
|
422
|
+
`${JSON.stringify({
|
|
423
|
+
...saved,
|
|
424
|
+
platformMode: 'active',
|
|
425
|
+
activeProductId: 'prod_basic',
|
|
426
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
427
|
+
poolSession,
|
|
428
|
+
routeToken: {
|
|
429
|
+
token: 'rt_secret_value',
|
|
430
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
431
|
+
},
|
|
432
|
+
}, null, 2)}\n`,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const status = await platformStatus({ sessionFile });
|
|
436
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
437
|
+
|
|
438
|
+
assert.equal(status.state, 'invalid-token');
|
|
439
|
+
assert.equal(status.mode?.state, 'inactive');
|
|
440
|
+
assert.equal(Object.hasOwn(updated, 'poolSession'), false);
|
|
441
|
+
assert.equal(Object.hasOwn(updated, 'routeToken'), false);
|
|
442
|
+
} finally {
|
|
443
|
+
await api.close();
|
|
444
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('platformStatus releases active mode when device is no longer active', async () => {
|
|
449
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
450
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
451
|
+
const api = await createApiServer({
|
|
452
|
+
'POST /auth/device-token': () => ({
|
|
453
|
+
deviceToken: 'cp_dev_secret',
|
|
454
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
455
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
456
|
+
}),
|
|
457
|
+
'GET /me': () => ({
|
|
458
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
459
|
+
device: { id: 'dev_1', status: 'removed', lastHeartbeatAt: '2026-05-30T00:05:00Z' },
|
|
460
|
+
}),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
await loginWithCode({
|
|
465
|
+
code: 'CODE-123',
|
|
466
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
467
|
+
sessionFile,
|
|
468
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
469
|
+
});
|
|
470
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
471
|
+
await writeFile(
|
|
472
|
+
sessionFile,
|
|
473
|
+
`${JSON.stringify({
|
|
474
|
+
...saved,
|
|
475
|
+
platformMode: 'active',
|
|
476
|
+
activeProductId: 'prod_basic',
|
|
477
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
478
|
+
}, null, 2)}\n`,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const status = await platformStatus({ sessionFile });
|
|
482
|
+
|
|
483
|
+
assert.equal(status.state, 'logged-in');
|
|
484
|
+
assert.equal(status.device.status, 'removed');
|
|
485
|
+
assert.equal(status.mode.state, 'inactive');
|
|
486
|
+
assert.equal(status.mode.releaseReason, 'device-inactive');
|
|
487
|
+
} finally {
|
|
488
|
+
await api.close();
|
|
489
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('platformStatus does not resurrect mode stopped while status request is in flight', async () => {
|
|
494
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
495
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
496
|
+
let releaseMe: ((value: void) => void) | undefined;
|
|
497
|
+
const meCanReturn = new Promise<void>((resolve) => {
|
|
498
|
+
releaseMe = resolve;
|
|
499
|
+
});
|
|
500
|
+
const api = await createApiServer({
|
|
501
|
+
'POST /auth/device-token': () => ({
|
|
502
|
+
deviceToken: 'cp_dev_secret',
|
|
503
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
504
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
505
|
+
}),
|
|
506
|
+
'GET /me': async () => {
|
|
507
|
+
await meCanReturn;
|
|
508
|
+
return {
|
|
509
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
510
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:05:00Z' },
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
await loginWithCode({
|
|
517
|
+
code: 'CODE-123',
|
|
518
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
519
|
+
sessionFile,
|
|
520
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
521
|
+
});
|
|
522
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
523
|
+
await writeFile(
|
|
524
|
+
sessionFile,
|
|
525
|
+
`${JSON.stringify({
|
|
526
|
+
...saved,
|
|
527
|
+
platformMode: 'active',
|
|
528
|
+
activeProductId: 'prod_basic',
|
|
529
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
530
|
+
}, null, 2)}\n`,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const statusPromise = platformStatus({ sessionFile });
|
|
534
|
+
await stopPlatformMode({ sessionFile });
|
|
535
|
+
releaseMe?.();
|
|
536
|
+
const status = await statusPromise;
|
|
537
|
+
|
|
538
|
+
assert.equal(status.state, 'logged-in');
|
|
539
|
+
assert.equal(status.mode.state, 'inactive');
|
|
540
|
+
|
|
541
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
542
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
543
|
+
assert.equal(Object.hasOwn(updated, 'activeProductId'), false);
|
|
544
|
+
assert.equal(Object.hasOwn(updated, 'platformModeStartedAt'), false);
|
|
545
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleaseReason'), false);
|
|
546
|
+
} finally {
|
|
547
|
+
releaseMe?.();
|
|
548
|
+
await api.close();
|
|
549
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('sendHeartbeat posts payload and updates session lastHeartbeatAt', async () => {
|
|
554
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
555
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
556
|
+
const api = await createApiServer({
|
|
557
|
+
'POST /auth/device-token': () => ({
|
|
558
|
+
deviceToken: 'cp_dev_secret',
|
|
559
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
560
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
561
|
+
}),
|
|
562
|
+
'POST /devices/heartbeat': (body, request) => {
|
|
563
|
+
assert.equal(request.headers.authorization, 'Bearer cp_dev_secret');
|
|
564
|
+
assert.deepEqual(body, { serviceStatus: 'running' });
|
|
565
|
+
return {
|
|
566
|
+
ok: true,
|
|
567
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:01:00Z' },
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
await loginWithCode({
|
|
574
|
+
code: 'CODE-123',
|
|
575
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
576
|
+
sessionFile,
|
|
577
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const status = await sendHeartbeat({ sessionFile, payload: { serviceStatus: 'running' } });
|
|
581
|
+
|
|
582
|
+
assert.equal(status.state, 'logged-in');
|
|
583
|
+
assert.equal(status.device?.lastHeartbeatAt, '2026-05-30T00:01:00Z');
|
|
584
|
+
|
|
585
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
586
|
+
assert.deepEqual(saved.device, status.device);
|
|
587
|
+
} finally {
|
|
588
|
+
await api.close();
|
|
589
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('sendHeartbeat returns invalid-token when heartbeat responds with HTTP 401', async () => {
|
|
594
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
595
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
596
|
+
const api = await createApiServer({
|
|
597
|
+
'POST /auth/device-token': () => ({
|
|
598
|
+
deviceToken: 'cp_dev_secret',
|
|
599
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
600
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
601
|
+
}),
|
|
602
|
+
'POST /devices/heartbeat': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
await loginWithCode({
|
|
607
|
+
code: 'CODE-123',
|
|
608
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
609
|
+
sessionFile,
|
|
610
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const status = await sendHeartbeat({ sessionFile, payload: { serviceStatus: 'running' } });
|
|
614
|
+
|
|
615
|
+
assert.equal(status.state, 'invalid-token');
|
|
616
|
+
assert.deepEqual(status.user, { id: 'usr_1', email: 'dev@example.com' });
|
|
617
|
+
assert.equal(status.device?.id, 'dev_1');
|
|
618
|
+
} finally {
|
|
619
|
+
await api.close();
|
|
620
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('sendHeartbeat releases active mode on invalid token and inactive device', async () => {
|
|
625
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
626
|
+
const invalidTokenSessionFile = join(tempDir, 'invalid-token-session.json');
|
|
627
|
+
const inactiveDeviceSessionFile = join(tempDir, 'inactive-device-session.json');
|
|
628
|
+
const invalidApi = await createApiServer({
|
|
629
|
+
'POST /auth/device-token': () => ({
|
|
630
|
+
deviceToken: 'cp_dev_secret',
|
|
631
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
632
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
633
|
+
}),
|
|
634
|
+
'POST /devices/heartbeat': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
635
|
+
});
|
|
636
|
+
const inactiveApi = await createApiServer({
|
|
637
|
+
'POST /auth/device-token': () => ({
|
|
638
|
+
deviceToken: 'cp_dev_secret',
|
|
639
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
640
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
641
|
+
}),
|
|
642
|
+
'POST /devices/heartbeat': () => ({
|
|
643
|
+
ok: true,
|
|
644
|
+
device: { id: 'dev_1', status: 'disabled', lastHeartbeatAt: '2026-05-30T00:05:00Z' },
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await loginWithCode({
|
|
650
|
+
code: 'CODE-123',
|
|
651
|
+
apiBaseUrl: invalidApi.apiBaseUrl,
|
|
652
|
+
sessionFile: invalidTokenSessionFile,
|
|
653
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
654
|
+
});
|
|
655
|
+
await loginWithCode({
|
|
656
|
+
code: 'CODE-123',
|
|
657
|
+
apiBaseUrl: inactiveApi.apiBaseUrl,
|
|
658
|
+
sessionFile: inactiveDeviceSessionFile,
|
|
659
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
660
|
+
});
|
|
661
|
+
for (const file of [invalidTokenSessionFile, inactiveDeviceSessionFile]) {
|
|
662
|
+
const saved = JSON.parse(await readFile(file, 'utf8')) as Record<string, unknown>;
|
|
663
|
+
await writeFile(
|
|
664
|
+
file,
|
|
665
|
+
`${JSON.stringify({
|
|
666
|
+
...saved,
|
|
667
|
+
platformMode: 'active',
|
|
668
|
+
activeProductId: 'prod_basic',
|
|
669
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
670
|
+
}, null, 2)}\n`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const invalidStatus = await sendHeartbeat({ sessionFile: invalidTokenSessionFile });
|
|
675
|
+
assert.equal(invalidStatus.state, 'invalid-token');
|
|
676
|
+
assert.equal(invalidStatus.mode?.state, 'inactive');
|
|
677
|
+
assert.equal(invalidStatus.mode?.releaseReason, 'invalid-token');
|
|
678
|
+
|
|
679
|
+
const inactiveStatus = await sendHeartbeat({ sessionFile: inactiveDeviceSessionFile });
|
|
680
|
+
assert.equal(inactiveStatus.state, 'logged-in');
|
|
681
|
+
assert.equal(inactiveStatus.mode.state, 'inactive');
|
|
682
|
+
assert.equal(inactiveStatus.mode.releaseReason, 'device-inactive');
|
|
683
|
+
} finally {
|
|
684
|
+
await invalidApi.close();
|
|
685
|
+
await inactiveApi.close();
|
|
686
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('sendHeartbeat does not resurrect mode stopped while heartbeat is in flight', async () => {
|
|
691
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
692
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
693
|
+
const api = await createApiServer({
|
|
694
|
+
'POST /auth/device-token': () => ({
|
|
695
|
+
deviceToken: 'cp_dev_secret',
|
|
696
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
697
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
698
|
+
}),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
await loginWithCode({
|
|
703
|
+
code: 'CODE-123',
|
|
704
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
705
|
+
sessionFile,
|
|
706
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
707
|
+
});
|
|
708
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
709
|
+
await writeFile(
|
|
710
|
+
sessionFile,
|
|
711
|
+
`${JSON.stringify({
|
|
712
|
+
...saved,
|
|
713
|
+
platformMode: 'active',
|
|
714
|
+
activeProductId: 'prod_basic',
|
|
715
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
716
|
+
}, null, 2)}\n`,
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
const heartbeat = await sendHeartbeat({
|
|
720
|
+
sessionFile,
|
|
721
|
+
postHeartbeat: async () => {
|
|
722
|
+
await stopPlatformMode({ sessionFile });
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:05:00Z' },
|
|
726
|
+
};
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
assert.equal(heartbeat.state, 'logged-in');
|
|
731
|
+
assert.equal(heartbeat.mode.state, 'inactive');
|
|
732
|
+
|
|
733
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
734
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
735
|
+
assert.equal(Object.hasOwn(updated, 'activeProductId'), false);
|
|
736
|
+
assert.equal(Object.hasOwn(updated, 'platformModeStartedAt'), false);
|
|
737
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleaseReason'), false);
|
|
738
|
+
} finally {
|
|
739
|
+
await api.close();
|
|
740
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('sendHeartbeat propagates local session write failures after heartbeat succeeds', async () => {
|
|
745
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
746
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
747
|
+
const api = await createApiServer({
|
|
748
|
+
'POST /auth/device-token': () => ({
|
|
749
|
+
deviceToken: 'cp_dev_secret',
|
|
750
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
751
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
752
|
+
}),
|
|
753
|
+
'POST /devices/heartbeat': () => ({
|
|
754
|
+
ok: true,
|
|
755
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:03:00Z' },
|
|
756
|
+
}),
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
await loginWithCode({
|
|
761
|
+
code: 'CODE-123',
|
|
762
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
763
|
+
sessionFile,
|
|
764
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
765
|
+
});
|
|
766
|
+
await chmod(sessionFile, 0o400);
|
|
767
|
+
|
|
768
|
+
await assert.rejects(
|
|
769
|
+
sendHeartbeat({ sessionFile, payload: { serviceStatus: 'running' } }),
|
|
770
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === 'EACCES',
|
|
771
|
+
);
|
|
772
|
+
} finally {
|
|
773
|
+
await chmod(sessionFile, 0o600).catch(() => {});
|
|
774
|
+
await api.close();
|
|
775
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('logoutPlatform deletes the local session even when API is offline', async () => {
|
|
780
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
781
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
782
|
+
const api = await createApiServer({
|
|
783
|
+
'POST /auth/device-token': () => ({
|
|
784
|
+
deviceToken: 'cp_dev_secret',
|
|
785
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
786
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
787
|
+
}),
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
await loginWithCode({
|
|
792
|
+
code: 'CODE-123',
|
|
793
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
794
|
+
sessionFile,
|
|
795
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
796
|
+
});
|
|
797
|
+
} finally {
|
|
798
|
+
await api.close();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
assert.equal((await logoutPlatform({ sessionFile })).state, 'logged-out');
|
|
803
|
+
assert.deepEqual(await platformStatus({ sessionFile }), { state: 'logged-out' });
|
|
804
|
+
} finally {
|
|
805
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('platformCatalog returns logged-out without a local session and does not call platform API', async () => {
|
|
810
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
811
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
812
|
+
let requestCount = 0;
|
|
813
|
+
const api = await createApiServer({
|
|
814
|
+
'GET /account/summary': () => {
|
|
815
|
+
requestCount += 1;
|
|
816
|
+
return { credits: 1000 };
|
|
817
|
+
},
|
|
818
|
+
'GET /products': () => {
|
|
819
|
+
requestCount += 1;
|
|
820
|
+
return { products: [] };
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
826
|
+
state: 'logged-out',
|
|
827
|
+
products: [],
|
|
828
|
+
});
|
|
829
|
+
assert.equal(requestCount, 0);
|
|
830
|
+
} finally {
|
|
831
|
+
await api.close();
|
|
832
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test('platformCatalog fetches account summary and products with device token', async () => {
|
|
837
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
838
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
839
|
+
const api = await createApiServer({
|
|
840
|
+
'POST /auth/device-token': () => ({
|
|
841
|
+
deviceToken: 'cp_dev_secret',
|
|
842
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
843
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
844
|
+
}),
|
|
845
|
+
'GET /account/summary': (_body, request) => {
|
|
846
|
+
assert.equal(request.headers.authorization, 'Bearer cp_dev_secret');
|
|
847
|
+
return { credits: 1000 };
|
|
848
|
+
},
|
|
849
|
+
'GET /products': (_body, request) => {
|
|
850
|
+
assert.equal(request.headers.authorization, 'Bearer cp_dev_secret');
|
|
851
|
+
return {
|
|
852
|
+
products: [
|
|
853
|
+
{
|
|
854
|
+
id: 'prod_basic',
|
|
855
|
+
name: '基础组',
|
|
856
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
857
|
+
status: 'available',
|
|
858
|
+
minCredits: 100,
|
|
859
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
860
|
+
billingRate: 1,
|
|
861
|
+
billingUnitCredits: 1,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
id: 'prod_missing_status',
|
|
865
|
+
name: '缺少状态',
|
|
866
|
+
description: '缺少状态字段,应该被过滤。',
|
|
867
|
+
minCredits: 1,
|
|
868
|
+
usageLabel: '不展示',
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
id: 'prod_negative_credits',
|
|
872
|
+
name: '负数积分',
|
|
873
|
+
description: 'minCredits 为负数,应该被过滤。',
|
|
874
|
+
status: 'available',
|
|
875
|
+
minCredits: -1,
|
|
876
|
+
usageLabel: '不展示',
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: 'prod_decimal_credits',
|
|
880
|
+
name: '小数积分',
|
|
881
|
+
description: 'minCredits 为小数,应该被过滤。',
|
|
882
|
+
status: 'available',
|
|
883
|
+
minCredits: 1.5,
|
|
884
|
+
usageLabel: '不展示',
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: 'prod_zero_credits',
|
|
888
|
+
name: '零积分组',
|
|
889
|
+
description: 'minCredits 为非负整数,应该保留。',
|
|
890
|
+
status: 'available',
|
|
891
|
+
minCredits: 0,
|
|
892
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
await loginWithCode({
|
|
901
|
+
code: 'CODE-123',
|
|
902
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
903
|
+
sessionFile,
|
|
904
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
908
|
+
state: 'logged-in',
|
|
909
|
+
account: { credits: 1000 },
|
|
910
|
+
mode: { state: 'inactive' },
|
|
911
|
+
products: [
|
|
912
|
+
{
|
|
913
|
+
id: 'prod_basic',
|
|
914
|
+
name: '基础组',
|
|
915
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
916
|
+
status: 'available',
|
|
917
|
+
minCredits: 100,
|
|
918
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: 'prod_zero_credits',
|
|
922
|
+
name: '零积分组',
|
|
923
|
+
description: 'minCredits 为非负整数,应该保留。',
|
|
924
|
+
status: 'available',
|
|
925
|
+
minCredits: 0,
|
|
926
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
});
|
|
930
|
+
} finally {
|
|
931
|
+
await api.close();
|
|
932
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test('platformCatalog returns invalid-token when account summary responds with HTTP 401', async () => {
|
|
937
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
938
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
939
|
+
const api = await createApiServer({
|
|
940
|
+
'POST /auth/device-token': () => ({
|
|
941
|
+
deviceToken: 'cp_dev_secret',
|
|
942
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
943
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
944
|
+
}),
|
|
945
|
+
'GET /account/summary': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
946
|
+
'GET /products': () => ({ products: [] }),
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
await loginWithCode({
|
|
951
|
+
code: 'CODE-123',
|
|
952
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
953
|
+
sessionFile,
|
|
954
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
958
|
+
state: 'invalid-token',
|
|
959
|
+
mode: { state: 'inactive' },
|
|
960
|
+
products: [],
|
|
961
|
+
});
|
|
962
|
+
} finally {
|
|
963
|
+
await api.close();
|
|
964
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('platformCatalog releases active mode when account summary responds with HTTP 401', async () => {
|
|
969
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
970
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
971
|
+
const api = await createApiServer({
|
|
972
|
+
'POST /auth/device-token': () => ({
|
|
973
|
+
deviceToken: 'cp_dev_secret',
|
|
974
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
975
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
976
|
+
}),
|
|
977
|
+
'GET /account/summary': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
978
|
+
'GET /products': () => ({ products: [] }),
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
await loginWithCode({
|
|
983
|
+
code: 'CODE-123',
|
|
984
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
985
|
+
sessionFile,
|
|
986
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
987
|
+
});
|
|
988
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
989
|
+
await writeFile(
|
|
990
|
+
sessionFile,
|
|
991
|
+
`${JSON.stringify({
|
|
992
|
+
...saved,
|
|
993
|
+
platformMode: 'active',
|
|
994
|
+
activeProductId: 'prod_basic',
|
|
995
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
996
|
+
}, null, 2)}\n`,
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
const catalog = await platformCatalog({ sessionFile });
|
|
1000
|
+
|
|
1001
|
+
assert.equal(catalog.state, 'invalid-token');
|
|
1002
|
+
assert.equal(catalog.mode?.state, 'inactive');
|
|
1003
|
+
assert.equal(catalog.mode?.releaseReason, 'invalid-token');
|
|
1004
|
+
assert.match(catalog.mode?.releasedAt ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
1005
|
+
|
|
1006
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1007
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
1008
|
+
assert.equal(updated.lastModeReleaseReason, 'invalid-token');
|
|
1009
|
+
} finally {
|
|
1010
|
+
await api.close();
|
|
1011
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test('platformCatalog returns invalid-token when products responds with HTTP 401', async () => {
|
|
1016
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1017
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1018
|
+
const api = await createApiServer({
|
|
1019
|
+
'POST /auth/device-token': () => ({
|
|
1020
|
+
deviceToken: 'cp_dev_secret',
|
|
1021
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1022
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1023
|
+
}),
|
|
1024
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1025
|
+
'GET /products': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } }),
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
await loginWithCode({
|
|
1030
|
+
code: 'CODE-123',
|
|
1031
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1032
|
+
sessionFile,
|
|
1033
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1037
|
+
state: 'invalid-token',
|
|
1038
|
+
mode: { state: 'inactive' },
|
|
1039
|
+
products: [],
|
|
1040
|
+
});
|
|
1041
|
+
} finally {
|
|
1042
|
+
await api.close();
|
|
1043
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test('platformCatalog prioritizes delayed product 401 over earlier account summary 500', async () => {
|
|
1048
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1049
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1050
|
+
const api = await createApiServer({
|
|
1051
|
+
'POST /auth/device-token': () => ({
|
|
1052
|
+
deviceToken: 'cp_dev_secret',
|
|
1053
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1054
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1055
|
+
}),
|
|
1056
|
+
'GET /account/summary': () => ({ statusCode: 500, body: { ok: false } }),
|
|
1057
|
+
'GET /products': async () => {
|
|
1058
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1059
|
+
return { statusCode: 401, body: { detail: { code: 'TOKEN_REVOKED' } } };
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
await loginWithCode({
|
|
1065
|
+
code: 'CODE-123',
|
|
1066
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1067
|
+
sessionFile,
|
|
1068
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1072
|
+
state: 'invalid-token',
|
|
1073
|
+
mode: { state: 'inactive' },
|
|
1074
|
+
products: [],
|
|
1075
|
+
});
|
|
1076
|
+
} finally {
|
|
1077
|
+
await api.close();
|
|
1078
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
test('platformCatalog returns offline when platform API is unreachable', async () => {
|
|
1083
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1084
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1085
|
+
const api = await createApiServer({
|
|
1086
|
+
'POST /auth/device-token': () => ({
|
|
1087
|
+
deviceToken: 'cp_dev_secret',
|
|
1088
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1089
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1090
|
+
}),
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
await loginWithCode({
|
|
1095
|
+
code: 'CODE-123',
|
|
1096
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1097
|
+
sessionFile,
|
|
1098
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1099
|
+
});
|
|
1100
|
+
} finally {
|
|
1101
|
+
await api.close();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1106
|
+
state: 'offline',
|
|
1107
|
+
products: [],
|
|
1108
|
+
});
|
|
1109
|
+
} finally {
|
|
1110
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test('platformCatalog returns offline when platform API responds with non-401 error', async () => {
|
|
1115
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1116
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1117
|
+
const api = await createApiServer({
|
|
1118
|
+
'POST /auth/device-token': () => ({
|
|
1119
|
+
deviceToken: 'cp_dev_secret',
|
|
1120
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1121
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1122
|
+
}),
|
|
1123
|
+
'GET /account/summary': () => ({ statusCode: 500, body: { ok: false } }),
|
|
1124
|
+
'GET /products': () => ({ products: [] }),
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
await loginWithCode({
|
|
1129
|
+
code: 'CODE-123',
|
|
1130
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1131
|
+
sessionFile,
|
|
1132
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1136
|
+
state: 'offline',
|
|
1137
|
+
products: [],
|
|
1138
|
+
});
|
|
1139
|
+
} finally {
|
|
1140
|
+
await api.close();
|
|
1141
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
test('selectPlatformProduct stores an available product in the local session', async () => {
|
|
1146
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1147
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1148
|
+
const api = await createApiServer({
|
|
1149
|
+
'POST /auth/device-token': () => ({
|
|
1150
|
+
deviceToken: 'cp_dev_secret',
|
|
1151
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1152
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1153
|
+
}),
|
|
1154
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1155
|
+
'GET /products': () => ({
|
|
1156
|
+
products: [
|
|
1157
|
+
{
|
|
1158
|
+
id: 'prod_basic',
|
|
1159
|
+
name: '基础组',
|
|
1160
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1161
|
+
status: 'available',
|
|
1162
|
+
minCredits: 100,
|
|
1163
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1164
|
+
},
|
|
1165
|
+
],
|
|
1166
|
+
}),
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
await loginWithCode({
|
|
1171
|
+
code: 'CODE-123',
|
|
1172
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1173
|
+
sessionFile,
|
|
1174
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile, productId: 'prod_basic' }), {
|
|
1178
|
+
state: 'selected',
|
|
1179
|
+
selectedProductId: 'prod_basic',
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1183
|
+
assert.equal(saved.selectedProductId, 'prod_basic');
|
|
1184
|
+
assert.equal(Object.hasOwn(saved, 'products'), false);
|
|
1185
|
+
assert.equal(Object.hasOwn(saved, 'account'), false);
|
|
1186
|
+
|
|
1187
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1188
|
+
state: 'logged-in',
|
|
1189
|
+
account: { credits: 1000 },
|
|
1190
|
+
mode: { state: 'inactive' },
|
|
1191
|
+
selectedProductId: 'prod_basic',
|
|
1192
|
+
products: [
|
|
1193
|
+
{
|
|
1194
|
+
id: 'prod_basic',
|
|
1195
|
+
name: '基础组',
|
|
1196
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1197
|
+
status: 'available',
|
|
1198
|
+
minCredits: 100,
|
|
1199
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
});
|
|
1203
|
+
} finally {
|
|
1204
|
+
await api.close();
|
|
1205
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test('selectPlatformProduct reports logged-out without a local session', async () => {
|
|
1210
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1211
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1212
|
+
|
|
1213
|
+
try {
|
|
1214
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile, productId: 'prod_basic' }), {
|
|
1215
|
+
state: 'logged-out',
|
|
1216
|
+
});
|
|
1217
|
+
} finally {
|
|
1218
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test('selectPlatformProduct rejects missing and unavailable products without writing selection', async () => {
|
|
1223
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1224
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1225
|
+
const api = await createApiServer({
|
|
1226
|
+
'POST /auth/device-token': () => ({
|
|
1227
|
+
deviceToken: 'cp_dev_secret',
|
|
1228
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1229
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1230
|
+
}),
|
|
1231
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1232
|
+
'GET /products': () => ({
|
|
1233
|
+
products: [
|
|
1234
|
+
{
|
|
1235
|
+
id: 'prod_pro',
|
|
1236
|
+
name: '高级组',
|
|
1237
|
+
description: '开发态高级商品,用于验证多商品展示。',
|
|
1238
|
+
status: 'unavailable',
|
|
1239
|
+
minCredits: 500,
|
|
1240
|
+
usageLabel: '按请求消耗,倍率 2x',
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
}),
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
try {
|
|
1247
|
+
await loginWithCode({
|
|
1248
|
+
code: 'CODE-123',
|
|
1249
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1250
|
+
sessionFile,
|
|
1251
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile, productId: 'prod_missing' }), {
|
|
1255
|
+
state: 'product-not-found',
|
|
1256
|
+
productId: 'prod_missing',
|
|
1257
|
+
});
|
|
1258
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile, productId: 'prod_pro' }), {
|
|
1259
|
+
state: 'product-unavailable',
|
|
1260
|
+
productId: 'prod_pro',
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1264
|
+
assert.equal(Object.hasOwn(saved, 'selectedProductId'), false);
|
|
1265
|
+
} finally {
|
|
1266
|
+
await api.close();
|
|
1267
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
test('selectPlatformProduct returns catalog failure states without writing selection', async () => {
|
|
1272
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1273
|
+
const invalidSessionFile = join(tempDir, 'invalid-session.json');
|
|
1274
|
+
const offlineSessionFile = join(tempDir, 'offline-session.json');
|
|
1275
|
+
const invalidApi = await createApiServer({
|
|
1276
|
+
'POST /auth/device-token': () => ({
|
|
1277
|
+
deviceToken: 'cp_dev_secret',
|
|
1278
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1279
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1280
|
+
}),
|
|
1281
|
+
'GET /account/summary': () => ({ statusCode: 401, body: { detail: { code: 'TOKEN_INVALID' } } }),
|
|
1282
|
+
'GET /products': () => ({ products: [] }),
|
|
1283
|
+
});
|
|
1284
|
+
const offlineApi = await createApiServer({
|
|
1285
|
+
'POST /auth/device-token': () => ({
|
|
1286
|
+
deviceToken: 'cp_dev_secret',
|
|
1287
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1288
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1289
|
+
}),
|
|
1290
|
+
'GET /account/summary': () => ({ statusCode: 500, body: { ok: false } }),
|
|
1291
|
+
'GET /products': () => ({ products: [] }),
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
await loginWithCode({
|
|
1296
|
+
code: 'CODE-123',
|
|
1297
|
+
apiBaseUrl: invalidApi.apiBaseUrl,
|
|
1298
|
+
sessionFile: invalidSessionFile,
|
|
1299
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1300
|
+
});
|
|
1301
|
+
await loginWithCode({
|
|
1302
|
+
code: 'CODE-123',
|
|
1303
|
+
apiBaseUrl: offlineApi.apiBaseUrl,
|
|
1304
|
+
sessionFile: offlineSessionFile,
|
|
1305
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile: invalidSessionFile, productId: 'prod_basic' }), {
|
|
1309
|
+
state: 'invalid-token',
|
|
1310
|
+
});
|
|
1311
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile: offlineSessionFile, productId: 'prod_basic' }), {
|
|
1312
|
+
state: 'offline',
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
const invalidSaved = JSON.parse(await readFile(invalidSessionFile, 'utf8')) as Record<string, unknown>;
|
|
1316
|
+
const offlineSaved = JSON.parse(await readFile(offlineSessionFile, 'utf8')) as Record<string, unknown>;
|
|
1317
|
+
assert.equal(Object.hasOwn(invalidSaved, 'selectedProductId'), false);
|
|
1318
|
+
assert.equal(Object.hasOwn(offlineSaved, 'selectedProductId'), false);
|
|
1319
|
+
} finally {
|
|
1320
|
+
await invalidApi.close();
|
|
1321
|
+
await offlineApi.close();
|
|
1322
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test('selectPlatformProduct does not resurrect active mode cleaned during catalog refresh', async () => {
|
|
1327
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1328
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1329
|
+
const api = await createApiServer({
|
|
1330
|
+
'POST /auth/device-token': () => ({
|
|
1331
|
+
deviceToken: 'cp_dev_secret',
|
|
1332
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1333
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1334
|
+
}),
|
|
1335
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1336
|
+
'GET /products': () => ({
|
|
1337
|
+
products: [
|
|
1338
|
+
{
|
|
1339
|
+
id: 'prod_basic',
|
|
1340
|
+
name: '基础组',
|
|
1341
|
+
description: '可选择商品。',
|
|
1342
|
+
status: 'available',
|
|
1343
|
+
minCredits: 100,
|
|
1344
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1345
|
+
},
|
|
1346
|
+
],
|
|
1347
|
+
}),
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
try {
|
|
1351
|
+
await loginWithCode({
|
|
1352
|
+
code: 'CODE-123',
|
|
1353
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1354
|
+
sessionFile,
|
|
1355
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1356
|
+
});
|
|
1357
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1358
|
+
await writeFile(
|
|
1359
|
+
sessionFile,
|
|
1360
|
+
`${JSON.stringify({
|
|
1361
|
+
...saved,
|
|
1362
|
+
selectedProductId: 'prod_removed',
|
|
1363
|
+
platformMode: 'active',
|
|
1364
|
+
activeProductId: 'prod_removed',
|
|
1365
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
1366
|
+
}, null, 2)}\n`,
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
assert.deepEqual(await selectPlatformProduct({ sessionFile, productId: 'prod_basic' }), {
|
|
1370
|
+
state: 'selected',
|
|
1371
|
+
selectedProductId: 'prod_basic',
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1375
|
+
assert.equal(updated.selectedProductId, 'prod_basic');
|
|
1376
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
1377
|
+
assert.equal(Object.hasOwn(updated, 'activeProductId'), false);
|
|
1378
|
+
assert.equal(Object.hasOwn(updated, 'platformModeStartedAt'), false);
|
|
1379
|
+
assert.equal(updated.lastModeReleaseReason, 'product-missing');
|
|
1380
|
+
} finally {
|
|
1381
|
+
await api.close();
|
|
1382
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
test('platformCatalog clears stale selectedProductId when product disappears', async () => {
|
|
1387
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1388
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1389
|
+
const api = await createApiServer({
|
|
1390
|
+
'POST /auth/device-token': () => ({
|
|
1391
|
+
deviceToken: 'cp_dev_secret',
|
|
1392
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1393
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1394
|
+
}),
|
|
1395
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1396
|
+
'GET /products': () => ({ products: [] }),
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
try {
|
|
1400
|
+
await loginWithCode({
|
|
1401
|
+
code: 'CODE-123',
|
|
1402
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1403
|
+
sessionFile,
|
|
1404
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1405
|
+
});
|
|
1406
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1407
|
+
await writeFile(sessionFile, `${JSON.stringify({ ...saved, selectedProductId: 'prod_missing' }, null, 2)}\n`);
|
|
1408
|
+
|
|
1409
|
+
assert.deepEqual(await platformCatalog({ sessionFile }), {
|
|
1410
|
+
state: 'logged-in',
|
|
1411
|
+
account: { credits: 1000 },
|
|
1412
|
+
mode: { state: 'inactive' },
|
|
1413
|
+
products: [],
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1417
|
+
assert.equal(Object.hasOwn(updated, 'selectedProductId'), false);
|
|
1418
|
+
} finally {
|
|
1419
|
+
await api.close();
|
|
1420
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test('platformCatalog releases active mode when active product disappears but keeps selection when still selected', async () => {
|
|
1425
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1426
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1427
|
+
const api = await createApiServer({
|
|
1428
|
+
'POST /auth/device-token': () => ({
|
|
1429
|
+
deviceToken: 'cp_dev_secret',
|
|
1430
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1431
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1432
|
+
}),
|
|
1433
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1434
|
+
'GET /products': () => ({
|
|
1435
|
+
products: [
|
|
1436
|
+
{
|
|
1437
|
+
id: 'prod_basic',
|
|
1438
|
+
name: '基础组',
|
|
1439
|
+
description: '仍然被选择,但 active 商品已消失。',
|
|
1440
|
+
status: 'available',
|
|
1441
|
+
minCredits: 100,
|
|
1442
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1445
|
+
}),
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
try {
|
|
1449
|
+
await loginWithCode({
|
|
1450
|
+
code: 'CODE-123',
|
|
1451
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1452
|
+
sessionFile,
|
|
1453
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1454
|
+
});
|
|
1455
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1456
|
+
await writeFile(
|
|
1457
|
+
sessionFile,
|
|
1458
|
+
`${JSON.stringify({
|
|
1459
|
+
...saved,
|
|
1460
|
+
selectedProductId: 'prod_basic',
|
|
1461
|
+
platformMode: 'active',
|
|
1462
|
+
activeProductId: 'prod_removed',
|
|
1463
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
1464
|
+
}, null, 2)}\n`,
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
const catalog = await platformCatalog({ sessionFile });
|
|
1468
|
+
|
|
1469
|
+
assert.equal(catalog.state, 'logged-in');
|
|
1470
|
+
assert.equal(catalog.selectedProductId, 'prod_basic');
|
|
1471
|
+
assert.equal(catalog.mode.state, 'inactive');
|
|
1472
|
+
assert.equal(catalog.mode.releaseReason, 'product-missing');
|
|
1473
|
+
assert.match(catalog.mode.releasedAt ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
1474
|
+
|
|
1475
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1476
|
+
assert.equal(updated.selectedProductId, 'prod_basic');
|
|
1477
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
1478
|
+
assert.equal(updated.lastModeReleaseReason, 'product-missing');
|
|
1479
|
+
assert.equal(typeof updated.lastModeReleasedAt, 'string');
|
|
1480
|
+
} finally {
|
|
1481
|
+
await api.close();
|
|
1482
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
test('platformCatalog releases active mode when active product becomes unavailable', async () => {
|
|
1487
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1488
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1489
|
+
const api = await createApiServer({
|
|
1490
|
+
'POST /auth/device-token': () => ({
|
|
1491
|
+
deviceToken: 'cp_dev_secret',
|
|
1492
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1493
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1494
|
+
}),
|
|
1495
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1496
|
+
'GET /products': () => ({
|
|
1497
|
+
products: [
|
|
1498
|
+
{
|
|
1499
|
+
id: 'prod_basic',
|
|
1500
|
+
name: '基础组',
|
|
1501
|
+
description: '商品已经下架。',
|
|
1502
|
+
status: 'unavailable',
|
|
1503
|
+
minCredits: 100,
|
|
1504
|
+
usageLabel: '不可用',
|
|
1505
|
+
},
|
|
1506
|
+
],
|
|
1507
|
+
}),
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
await loginWithCode({
|
|
1512
|
+
code: 'CODE-123',
|
|
1513
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1514
|
+
sessionFile,
|
|
1515
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1516
|
+
});
|
|
1517
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1518
|
+
await writeFile(
|
|
1519
|
+
sessionFile,
|
|
1520
|
+
`${JSON.stringify({
|
|
1521
|
+
...saved,
|
|
1522
|
+
selectedProductId: 'prod_basic',
|
|
1523
|
+
platformMode: 'active',
|
|
1524
|
+
activeProductId: 'prod_basic',
|
|
1525
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
1526
|
+
}, null, 2)}\n`,
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
const catalog = await platformCatalog({ sessionFile });
|
|
1530
|
+
|
|
1531
|
+
assert.equal(catalog.state, 'logged-in');
|
|
1532
|
+
assert.equal(catalog.mode.state, 'inactive');
|
|
1533
|
+
assert.equal(catalog.mode.releaseReason, 'product-unavailable');
|
|
1534
|
+
} finally {
|
|
1535
|
+
await api.close();
|
|
1536
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
test('platformCatalog releases active mode when credits fall below active product minimum', async () => {
|
|
1541
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1542
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1543
|
+
const api = await createApiServer({
|
|
1544
|
+
'POST /auth/device-token': () => ({
|
|
1545
|
+
deviceToken: 'cp_dev_secret',
|
|
1546
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1547
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1548
|
+
}),
|
|
1549
|
+
'GET /account/summary': () => ({ credits: 80 }),
|
|
1550
|
+
'GET /products': () => ({
|
|
1551
|
+
products: [
|
|
1552
|
+
{
|
|
1553
|
+
id: 'prod_basic',
|
|
1554
|
+
name: '基础组',
|
|
1555
|
+
description: '余额不足。',
|
|
1556
|
+
status: 'available',
|
|
1557
|
+
minCredits: 100,
|
|
1558
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1559
|
+
},
|
|
1560
|
+
],
|
|
1561
|
+
}),
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
try {
|
|
1565
|
+
await loginWithCode({
|
|
1566
|
+
code: 'CODE-123',
|
|
1567
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1568
|
+
sessionFile,
|
|
1569
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1570
|
+
});
|
|
1571
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1572
|
+
await writeFile(
|
|
1573
|
+
sessionFile,
|
|
1574
|
+
`${JSON.stringify({
|
|
1575
|
+
...saved,
|
|
1576
|
+
selectedProductId: 'prod_basic',
|
|
1577
|
+
platformMode: 'active',
|
|
1578
|
+
activeProductId: 'prod_basic',
|
|
1579
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
1580
|
+
}, null, 2)}\n`,
|
|
1581
|
+
);
|
|
1582
|
+
|
|
1583
|
+
const catalog = await platformCatalog({ sessionFile });
|
|
1584
|
+
|
|
1585
|
+
assert.equal(catalog.state, 'logged-in');
|
|
1586
|
+
assert.equal(catalog.mode.state, 'inactive');
|
|
1587
|
+
assert.equal(catalog.mode.releaseReason, 'insufficient-credits');
|
|
1588
|
+
} finally {
|
|
1589
|
+
await api.close();
|
|
1590
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
test('platform mode starts after selected available product and stops while preserving selection', async () => {
|
|
1595
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1596
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1597
|
+
const api = await createApiServer({
|
|
1598
|
+
'POST /auth/device-token': () => ({
|
|
1599
|
+
deviceToken: 'cp_dev_secret',
|
|
1600
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1601
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1602
|
+
}),
|
|
1603
|
+
'GET /me': () => ({
|
|
1604
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1605
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:30Z' },
|
|
1606
|
+
}),
|
|
1607
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1608
|
+
'GET /products': () => ({
|
|
1609
|
+
products: [
|
|
1610
|
+
{
|
|
1611
|
+
id: 'prod_basic',
|
|
1612
|
+
name: '基础组',
|
|
1613
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1614
|
+
status: 'available',
|
|
1615
|
+
minCredits: 100,
|
|
1616
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1617
|
+
},
|
|
1618
|
+
],
|
|
1619
|
+
}),
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
try {
|
|
1623
|
+
await loginWithCode({
|
|
1624
|
+
code: 'CODE-123',
|
|
1625
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1626
|
+
sessionFile,
|
|
1627
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
assert.deepEqual(await platformStatus({ sessionFile }), {
|
|
1631
|
+
state: 'logged-in',
|
|
1632
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1633
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:30Z' },
|
|
1634
|
+
mode: { state: 'inactive' },
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
await selectPlatformProduct({ sessionFile, productId: 'prod_basic' });
|
|
1638
|
+
const started = await startPlatformMode({
|
|
1639
|
+
sessionFile,
|
|
1640
|
+
startPoolSession: async () => ({
|
|
1641
|
+
state: 'started',
|
|
1642
|
+
session: poolSession,
|
|
1643
|
+
routeToken: 'rt_secret_value',
|
|
1644
|
+
}),
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
assert.equal(started.state, 'active');
|
|
1648
|
+
assert.equal(started.productId, 'prod_basic');
|
|
1649
|
+
assert.match(started.startedAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
1650
|
+
|
|
1651
|
+
const activeCatalog = await platformCatalog({ sessionFile });
|
|
1652
|
+
assert.equal(activeCatalog.state, 'logged-in');
|
|
1653
|
+
assert.deepEqual(activeCatalog.mode, {
|
|
1654
|
+
state: 'active',
|
|
1655
|
+
productId: 'prod_basic',
|
|
1656
|
+
startedAt: started.startedAt,
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
const stopped = await stopPlatformMode({ sessionFile });
|
|
1660
|
+
assert.deepEqual(stopped, { state: 'inactive' });
|
|
1661
|
+
|
|
1662
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1663
|
+
assert.equal(saved.selectedProductId, 'prod_basic');
|
|
1664
|
+
assert.equal(Object.hasOwn(saved, 'platformMode'), false);
|
|
1665
|
+
assert.equal(Object.hasOwn(saved, 'activeProductId'), false);
|
|
1666
|
+
assert.equal(Object.hasOwn(saved, 'platformModeStartedAt'), false);
|
|
1667
|
+
} finally {
|
|
1668
|
+
await api.close();
|
|
1669
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
test('startPlatformMode creates a pool session and stores route token', async () => {
|
|
1674
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1675
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1676
|
+
const api = await createApiServer({
|
|
1677
|
+
'POST /auth/device-token': () => ({
|
|
1678
|
+
deviceToken: 'cp_dev_secret',
|
|
1679
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1680
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1681
|
+
}),
|
|
1682
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1683
|
+
'GET /products': () => ({
|
|
1684
|
+
products: [{
|
|
1685
|
+
id: 'prod_basic',
|
|
1686
|
+
name: '基础组',
|
|
1687
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1688
|
+
status: 'available',
|
|
1689
|
+
minCredits: 100,
|
|
1690
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1691
|
+
}],
|
|
1692
|
+
}),
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
await loginWithCode({
|
|
1697
|
+
code: 'CODE-123',
|
|
1698
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1699
|
+
sessionFile,
|
|
1700
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1701
|
+
});
|
|
1702
|
+
await selectPlatformProduct({ sessionFile, productId: 'prod_basic' });
|
|
1703
|
+
|
|
1704
|
+
const mode = await startPlatformMode({
|
|
1705
|
+
sessionFile,
|
|
1706
|
+
startPoolSession: async ({ session, productId }) => {
|
|
1707
|
+
assert.equal(session.deviceToken, 'cp_dev_secret');
|
|
1708
|
+
assert.equal(productId, 'prod_basic');
|
|
1709
|
+
return {
|
|
1710
|
+
state: 'started',
|
|
1711
|
+
session: poolSession,
|
|
1712
|
+
routeToken: 'rt_secret_value',
|
|
1713
|
+
};
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
assert.equal(mode.state, 'active');
|
|
1718
|
+
assert.equal(mode.productId, 'prod_basic');
|
|
1719
|
+
|
|
1720
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1721
|
+
assert.deepEqual(saved.poolSession, poolSession);
|
|
1722
|
+
assert.deepEqual(saved.routeToken, {
|
|
1723
|
+
token: 'rt_secret_value',
|
|
1724
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
1725
|
+
});
|
|
1726
|
+
} finally {
|
|
1727
|
+
await api.close();
|
|
1728
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
test('startPlatformMode calls default pool session endpoint without body device token', async () => {
|
|
1733
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1734
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1735
|
+
const api = await createApiServer({
|
|
1736
|
+
'POST /auth/device-token': () => ({
|
|
1737
|
+
deviceToken: 'cp_dev_secret',
|
|
1738
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1739
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1740
|
+
}),
|
|
1741
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1742
|
+
'GET /products': () => ({
|
|
1743
|
+
products: [{
|
|
1744
|
+
id: 'prod_basic',
|
|
1745
|
+
name: '基础组',
|
|
1746
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1747
|
+
status: 'available',
|
|
1748
|
+
minCredits: 100,
|
|
1749
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1750
|
+
}],
|
|
1751
|
+
}),
|
|
1752
|
+
'POST /pool-sessions/start': (body, request) => {
|
|
1753
|
+
assert.equal(request.headers.authorization, 'Bearer cp_dev_secret');
|
|
1754
|
+
assert.deepEqual(body, { productId: 'prod_basic' });
|
|
1755
|
+
return {
|
|
1756
|
+
state: 'started',
|
|
1757
|
+
session: poolSession,
|
|
1758
|
+
routeToken: 'rt_secret_value',
|
|
1759
|
+
};
|
|
1760
|
+
},
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
try {
|
|
1764
|
+
await loginWithCode({
|
|
1765
|
+
code: 'CODE-123',
|
|
1766
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1767
|
+
sessionFile,
|
|
1768
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1769
|
+
});
|
|
1770
|
+
await selectPlatformProduct({ sessionFile, productId: 'prod_basic' });
|
|
1771
|
+
|
|
1772
|
+
const mode = await startPlatformMode({ sessionFile });
|
|
1773
|
+
|
|
1774
|
+
assert.equal(mode.state, 'active');
|
|
1775
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1776
|
+
assert.deepEqual(saved.poolSession, poolSession);
|
|
1777
|
+
assert.deepEqual(saved.routeToken, {
|
|
1778
|
+
token: 'rt_secret_value',
|
|
1779
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
1780
|
+
});
|
|
1781
|
+
} finally {
|
|
1782
|
+
await api.close();
|
|
1783
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
test('startPlatformMode does not activate when pool session start fails', async () => {
|
|
1788
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1789
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1790
|
+
const api = await createApiServer({
|
|
1791
|
+
'POST /auth/device-token': () => ({
|
|
1792
|
+
deviceToken: 'cp_dev_secret',
|
|
1793
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1794
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1795
|
+
}),
|
|
1796
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1797
|
+
'GET /products': () => ({
|
|
1798
|
+
products: [{
|
|
1799
|
+
id: 'prod_basic',
|
|
1800
|
+
name: '基础组',
|
|
1801
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1802
|
+
status: 'available',
|
|
1803
|
+
minCredits: 100,
|
|
1804
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1805
|
+
}],
|
|
1806
|
+
}),
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
try {
|
|
1810
|
+
await loginWithCode({
|
|
1811
|
+
code: 'CODE-123',
|
|
1812
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1813
|
+
sessionFile,
|
|
1814
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1815
|
+
});
|
|
1816
|
+
await selectPlatformProduct({ sessionFile, productId: 'prod_basic' });
|
|
1817
|
+
|
|
1818
|
+
const mode = await startPlatformMode({
|
|
1819
|
+
sessionFile,
|
|
1820
|
+
startPoolSession: async () => ({ state: 'failed', reason: 'PROVIDER_UNAVAILABLE' }),
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
assert.deepEqual(mode, { state: 'provider-unavailable' });
|
|
1824
|
+
|
|
1825
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1826
|
+
assert.equal(Object.hasOwn(saved, 'platformMode'), false);
|
|
1827
|
+
assert.equal(Object.hasOwn(saved, 'poolSession'), false);
|
|
1828
|
+
assert.equal(Object.hasOwn(saved, 'routeToken'), false);
|
|
1829
|
+
} finally {
|
|
1830
|
+
await api.close();
|
|
1831
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
test('startPlatformMode sanitizes pool session response before saving', async () => {
|
|
1836
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1837
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1838
|
+
const api = await createApiServer({
|
|
1839
|
+
'POST /auth/device-token': () => ({
|
|
1840
|
+
deviceToken: 'cp_dev_secret',
|
|
1841
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1842
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1843
|
+
}),
|
|
1844
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1845
|
+
'GET /products': () => ({
|
|
1846
|
+
products: [{
|
|
1847
|
+
id: 'prod_basic',
|
|
1848
|
+
name: '基础组',
|
|
1849
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
1850
|
+
status: 'available',
|
|
1851
|
+
minCredits: 100,
|
|
1852
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1853
|
+
}],
|
|
1854
|
+
}),
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
try {
|
|
1858
|
+
await loginWithCode({
|
|
1859
|
+
code: 'CODE-123',
|
|
1860
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1861
|
+
sessionFile,
|
|
1862
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1863
|
+
});
|
|
1864
|
+
await selectPlatformProduct({ sessionFile, productId: 'prod_basic' });
|
|
1865
|
+
|
|
1866
|
+
const mode = await startPlatformMode({
|
|
1867
|
+
sessionFile,
|
|
1868
|
+
startPoolSession: async () => ({
|
|
1869
|
+
state: 'started',
|
|
1870
|
+
session: {
|
|
1871
|
+
...poolSession,
|
|
1872
|
+
providerSecret: 'discard-me',
|
|
1873
|
+
authorization: 'Bearer secret',
|
|
1874
|
+
routeToken: 'rt_nested_secret',
|
|
1875
|
+
bannedModels: ['bad-model', 'sk-secret-token'],
|
|
1876
|
+
availableModels: ['gpt-test', 'apiKey=secret'],
|
|
1877
|
+
} as typeof poolSession,
|
|
1878
|
+
routeToken: 'rt_secret_value',
|
|
1879
|
+
}),
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
assert.equal(mode.state, 'active');
|
|
1883
|
+
|
|
1884
|
+
const savedText = await readFile(sessionFile, 'utf8');
|
|
1885
|
+
const saved = JSON.parse(savedText) as Record<string, unknown>;
|
|
1886
|
+
|
|
1887
|
+
assert.deepEqual(saved.poolSession, {
|
|
1888
|
+
...poolSession,
|
|
1889
|
+
bannedModels: ['bad-model'],
|
|
1890
|
+
availableModels: ['gpt-test'],
|
|
1891
|
+
});
|
|
1892
|
+
assert.deepEqual(saved.routeToken, {
|
|
1893
|
+
token: 'rt_secret_value',
|
|
1894
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
1895
|
+
});
|
|
1896
|
+
assert.doesNotMatch(savedText, /discard-me|Bearer secret|rt_nested_secret|sk-secret-token|apiKey=secret/);
|
|
1897
|
+
} finally {
|
|
1898
|
+
await api.close();
|
|
1899
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
test('startPlatformMode clears previous release reason on successful start', async () => {
|
|
1904
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1905
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1906
|
+
const api = await createApiServer({
|
|
1907
|
+
'POST /auth/device-token': () => ({
|
|
1908
|
+
deviceToken: 'cp_dev_secret',
|
|
1909
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1910
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1911
|
+
}),
|
|
1912
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
1913
|
+
'GET /products': () => ({
|
|
1914
|
+
products: [
|
|
1915
|
+
{
|
|
1916
|
+
id: 'prod_basic',
|
|
1917
|
+
name: '基础组',
|
|
1918
|
+
description: '可重新启动。',
|
|
1919
|
+
status: 'available',
|
|
1920
|
+
minCredits: 100,
|
|
1921
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
1922
|
+
},
|
|
1923
|
+
],
|
|
1924
|
+
}),
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
try {
|
|
1928
|
+
await loginWithCode({
|
|
1929
|
+
code: 'CODE-123',
|
|
1930
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1931
|
+
sessionFile,
|
|
1932
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1933
|
+
});
|
|
1934
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1935
|
+
await writeFile(
|
|
1936
|
+
sessionFile,
|
|
1937
|
+
`${JSON.stringify({
|
|
1938
|
+
...saved,
|
|
1939
|
+
selectedProductId: 'prod_basic',
|
|
1940
|
+
lastModeReleaseReason: 'insufficient-credits',
|
|
1941
|
+
lastModeReleasedAt: '2026-05-31T00:05:00.000Z',
|
|
1942
|
+
}, null, 2)}\n`,
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
const started = await startPlatformMode({
|
|
1946
|
+
sessionFile,
|
|
1947
|
+
startPoolSession: async () => ({
|
|
1948
|
+
state: 'started',
|
|
1949
|
+
session: poolSession,
|
|
1950
|
+
routeToken: 'rt_secret_value',
|
|
1951
|
+
}),
|
|
1952
|
+
});
|
|
1953
|
+
assert.equal(started.state, 'active');
|
|
1954
|
+
|
|
1955
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1956
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleaseReason'), false);
|
|
1957
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleasedAt'), false);
|
|
1958
|
+
} finally {
|
|
1959
|
+
await api.close();
|
|
1960
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
test('stopPlatformMode does not record a release reason', async () => {
|
|
1965
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
1966
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
1967
|
+
const api = await createApiServer({
|
|
1968
|
+
'POST /auth/device-token': () => ({
|
|
1969
|
+
deviceToken: 'cp_dev_secret',
|
|
1970
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
1971
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
1972
|
+
}),
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
try {
|
|
1976
|
+
await loginWithCode({
|
|
1977
|
+
code: 'CODE-123',
|
|
1978
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
1979
|
+
sessionFile,
|
|
1980
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
1981
|
+
});
|
|
1982
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1983
|
+
await writeFile(
|
|
1984
|
+
sessionFile,
|
|
1985
|
+
`${JSON.stringify({
|
|
1986
|
+
...saved,
|
|
1987
|
+
platformMode: 'active',
|
|
1988
|
+
activeProductId: 'prod_basic',
|
|
1989
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
1990
|
+
}, null, 2)}\n`,
|
|
1991
|
+
);
|
|
1992
|
+
|
|
1993
|
+
assert.deepEqual(await stopPlatformMode({ sessionFile }), { state: 'inactive' });
|
|
1994
|
+
|
|
1995
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
1996
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleaseReason'), false);
|
|
1997
|
+
assert.equal(Object.hasOwn(updated, 'lastModeReleasedAt'), false);
|
|
1998
|
+
} finally {
|
|
1999
|
+
await api.close();
|
|
2000
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
test('stopPlatformMode stops pool session and clears route capability', async () => {
|
|
2005
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2006
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2007
|
+
const api = await createApiServer({
|
|
2008
|
+
'POST /auth/device-token': () => ({
|
|
2009
|
+
deviceToken: 'cp_dev_secret',
|
|
2010
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2011
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2012
|
+
}),
|
|
2013
|
+
});
|
|
2014
|
+
const stopCalls: Array<Record<string, unknown>> = [];
|
|
2015
|
+
|
|
2016
|
+
try {
|
|
2017
|
+
await loginWithCode({
|
|
2018
|
+
code: 'CODE-123',
|
|
2019
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2020
|
+
sessionFile,
|
|
2021
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2022
|
+
});
|
|
2023
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2024
|
+
await writeFile(
|
|
2025
|
+
sessionFile,
|
|
2026
|
+
`${JSON.stringify({
|
|
2027
|
+
...saved,
|
|
2028
|
+
selectedProductId: 'prod_basic',
|
|
2029
|
+
platformMode: 'active',
|
|
2030
|
+
activeProductId: 'prod_basic',
|
|
2031
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2032
|
+
poolSession,
|
|
2033
|
+
routeToken: {
|
|
2034
|
+
token: 'rt_secret_value',
|
|
2035
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
2036
|
+
},
|
|
2037
|
+
}, null, 2)}\n`,
|
|
2038
|
+
);
|
|
2039
|
+
|
|
2040
|
+
const mode = await stopPlatformMode({
|
|
2041
|
+
sessionFile,
|
|
2042
|
+
stopPoolSession: async (request) => {
|
|
2043
|
+
stopCalls.push(request);
|
|
2044
|
+
return { state: 'stopped', sessionId: request.poolSessionId };
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
assert.deepEqual(mode, { state: 'inactive' });
|
|
2049
|
+
assert.deepEqual(stopCalls, [{
|
|
2050
|
+
session: {
|
|
2051
|
+
...saved,
|
|
2052
|
+
selectedProductId: 'prod_basic',
|
|
2053
|
+
platformMode: 'active',
|
|
2054
|
+
activeProductId: 'prod_basic',
|
|
2055
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2056
|
+
poolSession,
|
|
2057
|
+
routeToken: {
|
|
2058
|
+
token: 'rt_secret_value',
|
|
2059
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
2060
|
+
},
|
|
2061
|
+
},
|
|
2062
|
+
poolSessionId: 'pool_sess_1',
|
|
2063
|
+
reason: 'user-stop',
|
|
2064
|
+
}]);
|
|
2065
|
+
|
|
2066
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2067
|
+
assert.equal(updated.selectedProductId, 'prod_basic');
|
|
2068
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
2069
|
+
assert.equal(Object.hasOwn(updated, 'poolSession'), false);
|
|
2070
|
+
assert.equal(Object.hasOwn(updated, 'routeToken'), false);
|
|
2071
|
+
} finally {
|
|
2072
|
+
await api.close();
|
|
2073
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
test('stopPlatformMode calls default pool stop endpoint without body device token', async () => {
|
|
2078
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2079
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2080
|
+
const api = await createApiServer({
|
|
2081
|
+
'POST /auth/device-token': () => ({
|
|
2082
|
+
deviceToken: 'cp_dev_secret',
|
|
2083
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2084
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2085
|
+
}),
|
|
2086
|
+
'POST /pool-sessions/pool_sess_1/stop': (body, request) => {
|
|
2087
|
+
assert.equal(request.headers.authorization, 'Bearer cp_dev_secret');
|
|
2088
|
+
assert.deepEqual(body, { reason: 'user-stop' });
|
|
2089
|
+
return { state: 'stopped', sessionId: 'pool_sess_1' };
|
|
2090
|
+
},
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
try {
|
|
2094
|
+
await loginWithCode({
|
|
2095
|
+
code: 'CODE-123',
|
|
2096
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2097
|
+
sessionFile,
|
|
2098
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2099
|
+
});
|
|
2100
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2101
|
+
await writeFile(
|
|
2102
|
+
sessionFile,
|
|
2103
|
+
`${JSON.stringify({
|
|
2104
|
+
...saved,
|
|
2105
|
+
selectedProductId: 'prod_basic',
|
|
2106
|
+
platformMode: 'active',
|
|
2107
|
+
activeProductId: 'prod_basic',
|
|
2108
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2109
|
+
poolSession,
|
|
2110
|
+
routeToken: {
|
|
2111
|
+
token: 'rt_secret_value',
|
|
2112
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
2113
|
+
},
|
|
2114
|
+
}, null, 2)}\n`,
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
assert.deepEqual(await stopPlatformMode({ sessionFile }), { state: 'inactive' });
|
|
2118
|
+
|
|
2119
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2120
|
+
assert.equal(Object.hasOwn(updated, 'poolSession'), false);
|
|
2121
|
+
assert.equal(Object.hasOwn(updated, 'routeToken'), false);
|
|
2122
|
+
} finally {
|
|
2123
|
+
await api.close();
|
|
2124
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
test('stopPlatformMode clears route capability when pool stop fails', async () => {
|
|
2129
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2130
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2131
|
+
const api = await createApiServer({
|
|
2132
|
+
'POST /auth/device-token': () => ({
|
|
2133
|
+
deviceToken: 'cp_dev_secret',
|
|
2134
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2135
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2136
|
+
}),
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
try {
|
|
2140
|
+
await loginWithCode({
|
|
2141
|
+
code: 'CODE-123',
|
|
2142
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2143
|
+
sessionFile,
|
|
2144
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2145
|
+
});
|
|
2146
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2147
|
+
await writeFile(
|
|
2148
|
+
sessionFile,
|
|
2149
|
+
`${JSON.stringify({
|
|
2150
|
+
...saved,
|
|
2151
|
+
platformMode: 'active',
|
|
2152
|
+
activeProductId: 'prod_basic',
|
|
2153
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2154
|
+
poolSession,
|
|
2155
|
+
routeToken: {
|
|
2156
|
+
token: 'rt_secret_value',
|
|
2157
|
+
expiresAt: '2026-05-31T00:10:00.000Z',
|
|
2158
|
+
},
|
|
2159
|
+
}, null, 2)}\n`,
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
const mode = await stopPlatformMode({
|
|
2163
|
+
sessionFile,
|
|
2164
|
+
stopPoolSession: async () => {
|
|
2165
|
+
throw new Error('offline');
|
|
2166
|
+
},
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
assert.deepEqual(mode, { state: 'inactive' });
|
|
2170
|
+
|
|
2171
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2172
|
+
assert.equal(Object.hasOwn(updated, 'poolSession'), false);
|
|
2173
|
+
assert.equal(Object.hasOwn(updated, 'routeToken'), false);
|
|
2174
|
+
} finally {
|
|
2175
|
+
await api.close();
|
|
2176
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
test('startPlatformMode returns explicit failure states without writing active mode', async () => {
|
|
2181
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2182
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2183
|
+
const api = await createApiServer({
|
|
2184
|
+
'POST /auth/device-token': () => ({
|
|
2185
|
+
deviceToken: 'cp_dev_secret',
|
|
2186
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2187
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2188
|
+
}),
|
|
2189
|
+
'GET /account/summary': () => ({ credits: 50 }),
|
|
2190
|
+
'GET /products': () => ({
|
|
2191
|
+
products: [
|
|
2192
|
+
{
|
|
2193
|
+
id: 'prod_basic',
|
|
2194
|
+
name: '基础组',
|
|
2195
|
+
description: '开发态基础商品,用于验证扩展展示。',
|
|
2196
|
+
status: 'available',
|
|
2197
|
+
minCredits: 100,
|
|
2198
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
id: 'prod_disabled',
|
|
2202
|
+
name: 'Disabled',
|
|
2203
|
+
description: 'Disabled product',
|
|
2204
|
+
status: 'unavailable',
|
|
2205
|
+
minCredits: 10,
|
|
2206
|
+
usageLabel: '不可用',
|
|
2207
|
+
},
|
|
2208
|
+
],
|
|
2209
|
+
}),
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
try {
|
|
2213
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), { state: 'logged-out' });
|
|
2214
|
+
|
|
2215
|
+
await loginWithCode({
|
|
2216
|
+
code: 'CODE-123',
|
|
2217
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2218
|
+
sessionFile,
|
|
2219
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), { state: 'product-not-selected' });
|
|
2223
|
+
|
|
2224
|
+
await writeFile(
|
|
2225
|
+
sessionFile,
|
|
2226
|
+
`${JSON.stringify({
|
|
2227
|
+
...(JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>),
|
|
2228
|
+
selectedProductId: 'prod_missing',
|
|
2229
|
+
}, null, 2)}\n`,
|
|
2230
|
+
);
|
|
2231
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), { state: 'product-not-selected' });
|
|
2232
|
+
|
|
2233
|
+
await writeFile(
|
|
2234
|
+
sessionFile,
|
|
2235
|
+
`${JSON.stringify({
|
|
2236
|
+
...(JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>),
|
|
2237
|
+
selectedProductId: 'prod_disabled',
|
|
2238
|
+
}, null, 2)}\n`,
|
|
2239
|
+
);
|
|
2240
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), {
|
|
2241
|
+
state: 'product-unavailable',
|
|
2242
|
+
productId: 'prod_disabled',
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
await writeFile(
|
|
2246
|
+
sessionFile,
|
|
2247
|
+
`${JSON.stringify({
|
|
2248
|
+
...(JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>),
|
|
2249
|
+
selectedProductId: 'prod_basic',
|
|
2250
|
+
}, null, 2)}\n`,
|
|
2251
|
+
);
|
|
2252
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), {
|
|
2253
|
+
state: 'insufficient-credits',
|
|
2254
|
+
productId: 'prod_basic',
|
|
2255
|
+
requiredCredits: 100,
|
|
2256
|
+
currentCredits: 50,
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2260
|
+
assert.equal(Object.hasOwn(saved, 'platformMode'), false);
|
|
2261
|
+
} finally {
|
|
2262
|
+
await api.close();
|
|
2263
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
test('startPlatformMode uses session cleaned during catalog refresh', async () => {
|
|
2268
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2269
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2270
|
+
const api = await createApiServer({
|
|
2271
|
+
'POST /auth/device-token': () => ({
|
|
2272
|
+
deviceToken: 'cp_dev_secret',
|
|
2273
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2274
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2275
|
+
}),
|
|
2276
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
2277
|
+
'GET /products': () => ({
|
|
2278
|
+
products: [
|
|
2279
|
+
{
|
|
2280
|
+
id: 'prod_basic',
|
|
2281
|
+
name: '基础组',
|
|
2282
|
+
description: 'catalog 中存在但本地旧选择不存在。',
|
|
2283
|
+
status: 'available',
|
|
2284
|
+
minCredits: 100,
|
|
2285
|
+
usageLabel: '按请求消耗,倍率 1x',
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
2288
|
+
}),
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
try {
|
|
2292
|
+
await loginWithCode({
|
|
2293
|
+
code: 'CODE-123',
|
|
2294
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2295
|
+
sessionFile,
|
|
2296
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2297
|
+
});
|
|
2298
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2299
|
+
await writeFile(
|
|
2300
|
+
sessionFile,
|
|
2301
|
+
`${JSON.stringify({
|
|
2302
|
+
...saved,
|
|
2303
|
+
selectedProductId: 'prod_removed',
|
|
2304
|
+
platformMode: 'active',
|
|
2305
|
+
activeProductId: 'prod_removed',
|
|
2306
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2307
|
+
}, null, 2)}\n`,
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
assert.deepEqual(await startPlatformMode({ sessionFile }), { state: 'product-not-selected' });
|
|
2311
|
+
|
|
2312
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2313
|
+
assert.equal(Object.hasOwn(updated, 'selectedProductId'), false);
|
|
2314
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
2315
|
+
assert.equal(updated.lastModeReleaseReason, 'product-missing');
|
|
2316
|
+
} finally {
|
|
2317
|
+
await api.close();
|
|
2318
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
test('platformCatalog clears active mode when stale selected product is removed', async () => {
|
|
2323
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2324
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2325
|
+
const api = await createApiServer({
|
|
2326
|
+
'POST /auth/device-token': () => ({
|
|
2327
|
+
deviceToken: 'cp_dev_secret',
|
|
2328
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2329
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2330
|
+
}),
|
|
2331
|
+
'GET /account/summary': () => ({ credits: 1000 }),
|
|
2332
|
+
'GET /products': () => ({ products: [] }),
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
try {
|
|
2336
|
+
await loginWithCode({
|
|
2337
|
+
code: 'CODE-123',
|
|
2338
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2339
|
+
sessionFile,
|
|
2340
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2341
|
+
});
|
|
2342
|
+
const saved = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2343
|
+
await writeFile(
|
|
2344
|
+
sessionFile,
|
|
2345
|
+
`${JSON.stringify({
|
|
2346
|
+
...saved,
|
|
2347
|
+
selectedProductId: 'prod_missing',
|
|
2348
|
+
platformMode: 'active',
|
|
2349
|
+
activeProductId: 'prod_missing',
|
|
2350
|
+
platformModeStartedAt: '2026-05-31T00:00:00.000Z',
|
|
2351
|
+
}, null, 2)}\n`,
|
|
2352
|
+
);
|
|
2353
|
+
|
|
2354
|
+
const catalog = await platformCatalog({ sessionFile });
|
|
2355
|
+
|
|
2356
|
+
assert.equal(catalog.state, 'logged-in');
|
|
2357
|
+
assert.deepEqual(catalog.account, { credits: 1000 });
|
|
2358
|
+
assert.equal(catalog.mode.state, 'inactive');
|
|
2359
|
+
assert.equal(catalog.mode.releaseReason, 'product-missing');
|
|
2360
|
+
assert.match(catalog.mode.releasedAt ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
2361
|
+
assert.deepEqual(catalog.products, []);
|
|
2362
|
+
|
|
2363
|
+
const updated = JSON.parse(await readFile(sessionFile, 'utf8')) as Record<string, unknown>;
|
|
2364
|
+
assert.equal(Object.hasOwn(updated, 'selectedProductId'), false);
|
|
2365
|
+
assert.equal(Object.hasOwn(updated, 'platformMode'), false);
|
|
2366
|
+
assert.equal(updated.lastModeReleaseReason, 'product-missing');
|
|
2367
|
+
assert.equal(typeof updated.lastModeReleasedAt, 'string');
|
|
2368
|
+
} finally {
|
|
2369
|
+
await api.close();
|
|
2370
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
test('platformStatus treats corrupted and incomplete session files as logged-out', async () => {
|
|
2375
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-platform-'));
|
|
2376
|
+
const sessionFile = join(tempDir, 'session.json');
|
|
2377
|
+
|
|
2378
|
+
try {
|
|
2379
|
+
await writeFile(sessionFile, '{not-json', 'utf8');
|
|
2380
|
+
assert.deepEqual(await platformStatus({ sessionFile }), { state: 'logged-out' });
|
|
2381
|
+
|
|
2382
|
+
await writeFile(sessionFile, JSON.stringify({
|
|
2383
|
+
apiBaseUrl: 'http://127.0.0.1:9',
|
|
2384
|
+
deviceToken: 'cp_dev_secret',
|
|
2385
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2386
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2387
|
+
}), 'utf8');
|
|
2388
|
+
assert.deepEqual(await platformStatus({ sessionFile }), { state: 'logged-out' });
|
|
2389
|
+
} finally {
|
|
2390
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
test('loginWithCode expands ~/ session files under HOME', async () => {
|
|
2395
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-home-'));
|
|
2396
|
+
const previousHome = process.env.HOME;
|
|
2397
|
+
process.env.HOME = tempDir;
|
|
2398
|
+
const api = await createApiServer({
|
|
2399
|
+
'POST /auth/device-token': () => ({
|
|
2400
|
+
deviceToken: 'cp_dev_secret',
|
|
2401
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
2402
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
2403
|
+
}),
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
try {
|
|
2407
|
+
await loginWithCode({
|
|
2408
|
+
code: 'CODE-123',
|
|
2409
|
+
apiBaseUrl: api.apiBaseUrl,
|
|
2410
|
+
sessionFile: '~/.cursor-pool/session.json',
|
|
2411
|
+
device: { name: 'Lin Mac', os: 'darwin', arch: 'arm64' },
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
const saved = JSON.parse(
|
|
2415
|
+
await readFile(join(tempDir, '.cursor-pool', 'session.json'), 'utf8'),
|
|
2416
|
+
) as Record<string, unknown>;
|
|
2417
|
+
assert.equal(saved.deviceToken, 'cp_dev_secret');
|
|
2418
|
+
assert.equal(typeof saved.createdAt, 'string');
|
|
2419
|
+
} finally {
|
|
2420
|
+
if (previousHome === undefined) {
|
|
2421
|
+
delete process.env.HOME;
|
|
2422
|
+
} else {
|
|
2423
|
+
process.env.HOME = previousHome;
|
|
2424
|
+
}
|
|
2425
|
+
await api.close();
|
|
2426
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2427
|
+
}
|
|
2428
|
+
});
|