osdy-pi 0.1.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -161
- package/bin/osdy-pi.mjs +127 -0
- package/extensions/osdy-pi/account-profiles.test.ts +517 -0
- package/extensions/osdy-pi/account-profiles.ts +581 -0
- package/extensions/osdy-pi/animation.ts +47 -5
- package/extensions/osdy-pi/constants.ts +282 -103
- package/extensions/osdy-pi/diff-panel.ts +6 -14
- package/extensions/osdy-pi/editor-settings.test.ts +163 -0
- package/extensions/osdy-pi/editor-settings.ts +112 -0
- package/extensions/osdy-pi/metrics.ts +8 -135
- package/extensions/osdy-pi/plugin-events.ts +26 -0
- package/extensions/osdy-pi/profile-label.ts +29 -0
- package/extensions/osdy-pi/runtime-helpers.test.ts +26 -0
- package/extensions/osdy-pi/runtime-helpers.ts +87 -44
- package/extensions/osdy-pi/runtime.test.ts +62 -0
- package/extensions/osdy-pi/runtime.ts +256 -64
- package/extensions/osdy-pi/types.ts +33 -10
- package/extensions/osdy-pi/ui.test.ts +51 -0
- package/extensions/osdy-pi/ui.ts +298 -114
- package/extensions/osdy-pi/utils.ts +43 -1
- package/extensions/osdy-pi/working-tree.ts +25 -6
- package/package.json +22 -5
- package/scripts/osdy-pi-account-profiles.mjs +501 -0
- package/themes/osdy-pi-catppuccin-frappe.json +94 -0
- package/themes/osdy-pi-catppuccin-latte.json +94 -0
- package/themes/osdy-pi-catppuccin-macchiato.json +94 -0
- package/themes/osdy-pi-catppuccin-mocha.json +94 -0
- package/themes/osdy-pi-dark.json +0 -6
- package/themes/osdy-pi-dracula.json +83 -0
- package/themes/osdy-pi-kanagawa-dragon.json +91 -0
- package/themes/osdy-pi-kanagawa-lotus.json +92 -0
- package/themes/osdy-pi-kanagawa-wave.json +91 -0
- package/themes/osdy-pi-lucent-orange.json +80 -0
- package/themes/osdy-pi-matrix.json +83 -0
- package/themes/osdy-pi-new.json +91 -0
- package/themes/osdy-pi-sexy.json +84 -0
- package/themes/osdy-pi-tokyo-night.json +83 -0
- package/themes/osdy-pi-light.json +0 -84
- package/themes/osdy-pi-simple.json +0 -83
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import type { ChildProcess } from "node:child_process";
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6
|
+
// @ts-ignore Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
7
|
+
import { handoffToAccount, manageAccountProfile, manageAccountProfiles, parseDefaultAccountResult, waitForLauncherReady } from "./account-profiles.ts";
|
|
8
|
+
|
|
9
|
+
class FakeLauncher extends EventEmitter {
|
|
10
|
+
disconnected = false;
|
|
11
|
+
unreferenced = false;
|
|
12
|
+
|
|
13
|
+
disconnect(): void {
|
|
14
|
+
this.disconnected = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
unref(): void {
|
|
18
|
+
this.unreferenced = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
asChildProcess(): ChildProcess {
|
|
22
|
+
return this as unknown as ChildProcess;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void test("parses default account command output without treating status text as a profile", () => {
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
parseDefaultAccountResult({ code: 0, stdout: "work\n", stderr: "" }),
|
|
29
|
+
{ status: "valid", profile: "work" },
|
|
30
|
+
);
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
parseDefaultAccountResult({
|
|
33
|
+
code: 0,
|
|
34
|
+
stdout: "No default account.\n",
|
|
35
|
+
stderr: "",
|
|
36
|
+
}),
|
|
37
|
+
{ status: "unset" },
|
|
38
|
+
);
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
parseDefaultAccountResult({
|
|
41
|
+
code: 0,
|
|
42
|
+
stdout: "Default account metadata is invalid; no account selected.\n",
|
|
43
|
+
stderr: "",
|
|
44
|
+
}),
|
|
45
|
+
{ status: "invalid" },
|
|
46
|
+
);
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
parseDefaultAccountResult({
|
|
49
|
+
code: 0,
|
|
50
|
+
stdout: "unexpected output",
|
|
51
|
+
stderr: "",
|
|
52
|
+
}),
|
|
53
|
+
{
|
|
54
|
+
status: "error",
|
|
55
|
+
message: "unexpected default account output: unexpected output",
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
parseDefaultAccountResult({
|
|
60
|
+
code: 1,
|
|
61
|
+
stdout: "",
|
|
62
|
+
stderr: "command failed\n",
|
|
63
|
+
}),
|
|
64
|
+
{ status: "error", message: "command failed" },
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
void test("shows parsed default status and account info accurately", async (t) => {
|
|
69
|
+
await t.test(
|
|
70
|
+
"Show current distinguishes valid, unset, invalid, and unavailable defaults",
|
|
71
|
+
async () => {
|
|
72
|
+
for (const [result, expected] of [
|
|
73
|
+
[{ code: 0, stdout: "work\n", stderr: "" }, "Default account: work"],
|
|
74
|
+
[
|
|
75
|
+
{ code: 0, stdout: "No default account.\n", stderr: "" },
|
|
76
|
+
"No default account.",
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
{
|
|
80
|
+
code: 0,
|
|
81
|
+
stdout: "Default account metadata is invalid; no account selected.\n",
|
|
82
|
+
stderr: "",
|
|
83
|
+
},
|
|
84
|
+
"Default account metadata is invalid; no account selected.",
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
{ code: 0, stdout: "bad status", stderr: "" },
|
|
88
|
+
"Default account is unavailable; clear or fix the default account before continuing.",
|
|
89
|
+
],
|
|
90
|
+
] as const) {
|
|
91
|
+
const notices: string[] = [];
|
|
92
|
+
const answers = ["Default", "Show current", "Cancel"];
|
|
93
|
+
await manageAccountProfiles(
|
|
94
|
+
{
|
|
95
|
+
isIdle: () => true,
|
|
96
|
+
waitForIdle: () => Promise.resolve(),
|
|
97
|
+
shutdown: () => undefined,
|
|
98
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
99
|
+
ui: {
|
|
100
|
+
notify: (message: string) => notices.push(message),
|
|
101
|
+
select: () => Promise.resolve(answers.shift()),
|
|
102
|
+
input: () => Promise.resolve(undefined),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
profiles: () => Promise.resolve(["work"]),
|
|
107
|
+
run: () => Promise.resolve(result),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(notices, [expected]);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
await t.test(
|
|
115
|
+
"Show current warns when the default command cannot start",
|
|
116
|
+
async () => {
|
|
117
|
+
const notices: string[] = [];
|
|
118
|
+
const answers = ["Default", "Show current", "Cancel"];
|
|
119
|
+
await manageAccountProfiles(
|
|
120
|
+
{
|
|
121
|
+
isIdle: () => true,
|
|
122
|
+
waitForIdle: () => Promise.resolve(),
|
|
123
|
+
shutdown: () => undefined,
|
|
124
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
125
|
+
ui: {
|
|
126
|
+
notify: (message: string) => notices.push(message),
|
|
127
|
+
select: () => Promise.resolve(answers.shift()),
|
|
128
|
+
input: () => Promise.resolve(undefined),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
profiles: () => Promise.resolve(["work"]),
|
|
133
|
+
run: () => Promise.reject(new Error("spawn failed")),
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
assert.deepEqual(notices, [
|
|
137
|
+
"Default account is unavailable; clear or fix the default account before continuing.",
|
|
138
|
+
]);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
await t.test(
|
|
142
|
+
"Account info marks only valid defaults and explains invalid metadata",
|
|
143
|
+
async () => {
|
|
144
|
+
const notices: string[] = [];
|
|
145
|
+
const answers = ["Account info", "Cancel"];
|
|
146
|
+
await manageAccountProfiles(
|
|
147
|
+
{
|
|
148
|
+
isIdle: () => true,
|
|
149
|
+
waitForIdle: () => Promise.resolve(),
|
|
150
|
+
shutdown: () => undefined,
|
|
151
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
152
|
+
ui: {
|
|
153
|
+
notify: (message: string) => notices.push(message),
|
|
154
|
+
select: () => Promise.resolve(answers.shift()),
|
|
155
|
+
input: () => Promise.resolve(undefined),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
profiles: () => Promise.resolve(["work"]),
|
|
160
|
+
run: () =>
|
|
161
|
+
Promise.resolve({
|
|
162
|
+
code: 0,
|
|
163
|
+
stdout: "Default account metadata is invalid; no account selected.\n",
|
|
164
|
+
stderr: "",
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
assert.deepEqual(notices, [
|
|
169
|
+
"Accounts:\nwork\nDefault account metadata is invalid; clear or fix the default account.",
|
|
170
|
+
]);
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
await t.test(
|
|
174
|
+
"Account info notes an unavailable default without marking a profile",
|
|
175
|
+
async () => {
|
|
176
|
+
const notices: string[] = [];
|
|
177
|
+
const answers = ["Account info", "Cancel"];
|
|
178
|
+
await manageAccountProfiles(
|
|
179
|
+
{
|
|
180
|
+
isIdle: () => true,
|
|
181
|
+
waitForIdle: () => Promise.resolve(),
|
|
182
|
+
shutdown: () => undefined,
|
|
183
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
184
|
+
ui: {
|
|
185
|
+
notify: (message: string) => notices.push(message),
|
|
186
|
+
select: () => Promise.resolve(answers.shift()),
|
|
187
|
+
input: () => Promise.resolve(undefined),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
profiles: () => Promise.resolve(["work"]),
|
|
192
|
+
run: () => Promise.resolve({ code: 1, stdout: "", stderr: "failed" }),
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
assert.deepEqual(notices, ["Accounts:\nwork\nDefault account unavailable."]);
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
void test("protects removal when the default metadata is invalid or unavailable", async () => {
|
|
201
|
+
for (const result of [
|
|
202
|
+
{
|
|
203
|
+
code: 0,
|
|
204
|
+
stdout: "Default account metadata is invalid; no account selected.\n",
|
|
205
|
+
stderr: "",
|
|
206
|
+
},
|
|
207
|
+
{ code: 0, stdout: "bad status", stderr: "" },
|
|
208
|
+
]) {
|
|
209
|
+
const calls: string[][] = [];
|
|
210
|
+
const notices: string[] = [];
|
|
211
|
+
await manageAccountProfile(
|
|
212
|
+
{
|
|
213
|
+
isIdle: () => true,
|
|
214
|
+
waitForIdle: () => Promise.resolve(),
|
|
215
|
+
shutdown: () => undefined,
|
|
216
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
217
|
+
ui: {
|
|
218
|
+
notify: (message: string) => notices.push(message),
|
|
219
|
+
select: () => Promise.resolve("work"),
|
|
220
|
+
input: () => Promise.resolve("work"),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
"remove",
|
|
224
|
+
{
|
|
225
|
+
profiles: () => Promise.resolve(["work", "other"]),
|
|
226
|
+
run: (args) => {
|
|
227
|
+
calls.push(args);
|
|
228
|
+
return Promise.resolve(result);
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
assert.deepEqual(calls, [["account", "default"]]);
|
|
233
|
+
assert.match(notices[0] ?? "", /clear or fix the default account/);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
void test("opens a looped account manager and exits cleanly on Cancel", async () => {
|
|
238
|
+
const calls: string[][] = [];
|
|
239
|
+
const notices: string[] = [];
|
|
240
|
+
const answers = ["Account info", "Cancel"];
|
|
241
|
+
await manageAccountProfiles(
|
|
242
|
+
{
|
|
243
|
+
isIdle: () => true,
|
|
244
|
+
waitForIdle: () => Promise.resolve(),
|
|
245
|
+
shutdown: () => assert.fail("must not close"),
|
|
246
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
247
|
+
ui: {
|
|
248
|
+
notify: (message: string) => notices.push(message),
|
|
249
|
+
select: () => Promise.resolve(answers.shift()),
|
|
250
|
+
input: () => Promise.resolve(undefined),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
profiles: () => Promise.resolve(["work"]),
|
|
255
|
+
run: (args) => {
|
|
256
|
+
calls.push(args);
|
|
257
|
+
return Promise.resolve({ code: 0, stdout: "work\n", stderr: "" });
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
assert.deepEqual(calls, [
|
|
262
|
+
["account", "default"],
|
|
263
|
+
["account", "default"],
|
|
264
|
+
]);
|
|
265
|
+
assert.deepEqual(notices, ["Accounts:\nwork (default)"]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
void test("manages profile rename and permanent removal prompts through authoritative subprocess commands", async (t) => {
|
|
269
|
+
await t.test("rename sequences target and new name", async () => {
|
|
270
|
+
const calls: string[][] = [];
|
|
271
|
+
const notices: string[] = [];
|
|
272
|
+
const answers = ["work", "renamed"];
|
|
273
|
+
await manageAccountProfile(
|
|
274
|
+
{
|
|
275
|
+
isIdle: () => true,
|
|
276
|
+
waitForIdle: () => Promise.resolve(),
|
|
277
|
+
shutdown: () => undefined,
|
|
278
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
279
|
+
ui: {
|
|
280
|
+
notify: (message: string) => notices.push(message),
|
|
281
|
+
select: () => Promise.resolve(answers.shift()),
|
|
282
|
+
input: () => Promise.resolve(answers.shift()),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
"rename",
|
|
286
|
+
{
|
|
287
|
+
profiles: () => Promise.resolve(["active", "work"]),
|
|
288
|
+
activeProfile: "active",
|
|
289
|
+
run: (args) => {
|
|
290
|
+
calls.push(args);
|
|
291
|
+
return Promise.resolve({ code: 0, stdout: "", stderr: "" });
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
assert.deepEqual(calls, [["account", "rename", "work", "renamed"]]);
|
|
296
|
+
assert.deepEqual(notices, ["Account profile renamed to renamed."]);
|
|
297
|
+
});
|
|
298
|
+
await t.test(
|
|
299
|
+
"default removal selects replacement and cancellation is a no-op",
|
|
300
|
+
async () => {
|
|
301
|
+
const calls: string[][] = [];
|
|
302
|
+
const answers = ["work", "other", "work"];
|
|
303
|
+
await manageAccountProfile(
|
|
304
|
+
{
|
|
305
|
+
isIdle: () => true,
|
|
306
|
+
waitForIdle: () => Promise.resolve(),
|
|
307
|
+
shutdown: () => undefined,
|
|
308
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
309
|
+
ui: {
|
|
310
|
+
notify: () => undefined,
|
|
311
|
+
select: () => Promise.resolve(answers.shift()),
|
|
312
|
+
input: () => Promise.resolve(answers.shift()),
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
"remove",
|
|
316
|
+
{
|
|
317
|
+
profiles: () => Promise.resolve(["active", "work", "other"]),
|
|
318
|
+
activeProfile: "active",
|
|
319
|
+
run: (args) => {
|
|
320
|
+
calls.push(args);
|
|
321
|
+
return Promise.resolve(
|
|
322
|
+
args[1] === "default"
|
|
323
|
+
? { code: 0, stdout: "work\n", stderr: "" }
|
|
324
|
+
: { code: 0, stdout: "", stderr: "" },
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
assert.deepEqual(calls, [
|
|
330
|
+
["account", "default"],
|
|
331
|
+
[
|
|
332
|
+
"account",
|
|
333
|
+
"remove",
|
|
334
|
+
"work",
|
|
335
|
+
"--confirm",
|
|
336
|
+
"work",
|
|
337
|
+
"--replacement",
|
|
338
|
+
"other",
|
|
339
|
+
],
|
|
340
|
+
]);
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
await t.test(
|
|
344
|
+
"cancellation is a no-op and failures notify without closing Pi",
|
|
345
|
+
async () => {
|
|
346
|
+
const notices: string[] = [];
|
|
347
|
+
await manageAccountProfile(
|
|
348
|
+
{
|
|
349
|
+
isIdle: () => true,
|
|
350
|
+
waitForIdle: () => Promise.resolve(),
|
|
351
|
+
shutdown: () => assert.fail("must not close"),
|
|
352
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
353
|
+
ui: {
|
|
354
|
+
notify: (message: string) => notices.push(message),
|
|
355
|
+
select: () => Promise.resolve(undefined),
|
|
356
|
+
input: () => Promise.resolve(undefined),
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
"remove",
|
|
360
|
+
{
|
|
361
|
+
profiles: () => Promise.resolve(["work"]),
|
|
362
|
+
run: () => Promise.resolve({ code: 1, stdout: "", stderr: "unused" }),
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
assert.equal(notices.length, 0);
|
|
366
|
+
const answers: string[] = ["work", "renamed"];
|
|
367
|
+
await manageAccountProfile(
|
|
368
|
+
{
|
|
369
|
+
isIdle: () => true,
|
|
370
|
+
waitForIdle: () => Promise.resolve(),
|
|
371
|
+
shutdown: () => assert.fail("must not close"),
|
|
372
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
373
|
+
ui: {
|
|
374
|
+
notify: (message: string) => notices.push(message),
|
|
375
|
+
select: () => Promise.resolve(answers.shift()),
|
|
376
|
+
input: () => Promise.resolve(answers.shift()),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
"rename",
|
|
380
|
+
{
|
|
381
|
+
profiles: () => Promise.resolve(["work"]),
|
|
382
|
+
run: () => Promise.resolve({ code: 1, stdout: "", stderr: "failed" }),
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
assert.deepEqual(notices, ["Rename failed: failed"]);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
void test("waits for the Pi-ready IPC message before disconnecting the launcher", async () => {
|
|
391
|
+
const launcher = new FakeLauncher();
|
|
392
|
+
const ready = waitForLauncherReady(launcher.asChildProcess());
|
|
393
|
+
assert.equal(launcher.disconnected, false);
|
|
394
|
+
launcher.emit("spawn");
|
|
395
|
+
assert.equal(launcher.disconnected, false);
|
|
396
|
+
launcher.emit("message", { type: "osdy-pi-ready" });
|
|
397
|
+
await ready;
|
|
398
|
+
assert.equal(launcher.disconnected, true);
|
|
399
|
+
assert.equal(launcher.unreferenced, true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
void test("rejects launcher error, early exit, and timeout before readiness", async (t) => {
|
|
403
|
+
await t.test("error message", async () => {
|
|
404
|
+
const launcher = new FakeLauncher();
|
|
405
|
+
const ready = waitForLauncherReady(launcher.asChildProcess());
|
|
406
|
+
launcher.emit("message", {
|
|
407
|
+
type: "osdy-pi-error",
|
|
408
|
+
message: "Pi spawn failed",
|
|
409
|
+
});
|
|
410
|
+
await assert.rejects(ready, /Pi spawn failed/);
|
|
411
|
+
});
|
|
412
|
+
await t.test("early exit", async () => {
|
|
413
|
+
const launcher = new FakeLauncher();
|
|
414
|
+
const ready = waitForLauncherReady(launcher.asChildProcess());
|
|
415
|
+
launcher.emit("exit", 1, null);
|
|
416
|
+
await assert.rejects(ready, /exited before confirming readiness/);
|
|
417
|
+
});
|
|
418
|
+
await t.test("timeout", async () => {
|
|
419
|
+
const launcher = new FakeLauncher();
|
|
420
|
+
let expire: (() => void) | undefined;
|
|
421
|
+
const ready = waitForLauncherReady(
|
|
422
|
+
launcher.asChildProcess(),
|
|
423
|
+
10,
|
|
424
|
+
(callback) => {
|
|
425
|
+
expire = callback;
|
|
426
|
+
return setTimeout(() => undefined, 60_000);
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
expire?.();
|
|
430
|
+
await assert.rejects(ready, /timed out/);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
void test("account handoff waits for idle, starts the launcher with the current session, then shuts down", async () => {
|
|
435
|
+
const events: string[] = [];
|
|
436
|
+
await handoffToAccount(
|
|
437
|
+
{
|
|
438
|
+
isIdle: () => false,
|
|
439
|
+
waitForIdle: () =>
|
|
440
|
+
Promise.resolve().then(() => {
|
|
441
|
+
events.push("idle");
|
|
442
|
+
}),
|
|
443
|
+
shutdown: () => events.push("shutdown"),
|
|
444
|
+
sessionManager: { getSessionFile: () => "/tmp/current.jsonl" },
|
|
445
|
+
ui: {
|
|
446
|
+
notify: (message: string) => events.push(message),
|
|
447
|
+
select: () => Promise.resolve(undefined),
|
|
448
|
+
input: () => Promise.resolve(undefined),
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
"work",
|
|
452
|
+
{
|
|
453
|
+
spawn: (command, args) =>
|
|
454
|
+
Promise.resolve().then(() => {
|
|
455
|
+
events.push(`${command} ${args.join(" ")}`);
|
|
456
|
+
events.push("spawned");
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
assert.deepEqual(events, [
|
|
461
|
+
"idle",
|
|
462
|
+
"osdy-pi account use work -- --session /tmp/current.jsonl",
|
|
463
|
+
"spawned",
|
|
464
|
+
"shutdown",
|
|
465
|
+
]);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
void test("account handoff keeps the current Pi session running when its session cannot be safely resumed", async () => {
|
|
469
|
+
const notices: string[] = [];
|
|
470
|
+
await handoffToAccount(
|
|
471
|
+
{
|
|
472
|
+
isIdle: () => true,
|
|
473
|
+
waitForIdle: () => Promise.resolve(),
|
|
474
|
+
shutdown: () => assert.fail("must not shut down"),
|
|
475
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
476
|
+
ui: {
|
|
477
|
+
notify: (message: string) => notices.push(message),
|
|
478
|
+
select: () => Promise.resolve(undefined),
|
|
479
|
+
input: () => Promise.resolve(undefined),
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
"work",
|
|
483
|
+
{ spawn: () => Promise.resolve().then(() => assert.fail("must not launch")) },
|
|
484
|
+
);
|
|
485
|
+
assert.deepEqual(notices, [
|
|
486
|
+
"Cannot switch accounts: the current session has not been saved yet.",
|
|
487
|
+
]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
void test("account handoff keeps the current Pi session running when the launcher fails asynchronously", async () => {
|
|
491
|
+
const events: string[] = [];
|
|
492
|
+
await handoffToAccount(
|
|
493
|
+
{
|
|
494
|
+
isIdle: () => true,
|
|
495
|
+
waitForIdle: () => Promise.resolve(),
|
|
496
|
+
shutdown: () => events.push("shutdown"),
|
|
497
|
+
sessionManager: { getSessionFile: () => "/tmp/current.jsonl" },
|
|
498
|
+
ui: {
|
|
499
|
+
notify: (message: string) => events.push(message),
|
|
500
|
+
select: () => Promise.resolve(undefined),
|
|
501
|
+
input: () => Promise.resolve(undefined),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
"work",
|
|
505
|
+
{
|
|
506
|
+
spawn: () =>
|
|
507
|
+
Promise.resolve().then(() => {
|
|
508
|
+
events.push("launch attempted");
|
|
509
|
+
throw new Error("not found");
|
|
510
|
+
}),
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
assert.deepEqual(events, [
|
|
514
|
+
"launch attempted",
|
|
515
|
+
"Cannot switch accounts: unable to start the replacement Pi process.",
|
|
516
|
+
]);
|
|
517
|
+
});
|