windows-development-sandbox 1.27.1 → 1.28.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/dist/app/context.d.ts +5 -0
- package/dist/app/context.js +13 -0
- package/dist/app/features/accounts/accounts.def.d.ts +16 -0
- package/dist/app/features/accounts/accounts.run.d.ts +4 -0
- package/dist/app/features/accounts/services.def.d.ts +15 -1
- package/dist/app/features/accounts/services.def.js +67 -31
- package/dist/app/features/accounts/services.run.d.ts +7 -2
- package/dist/app/features/accounts/services.run.js +25 -6
- package/dist/app/features/defs.d.ts +17 -0
- package/dist/app/registry.d.ts +1 -1
- package/dist/app/registry.js +15 -2
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-RSIX7C6D.js → chunk-CB44HTTN.js} +444 -175
- package/dist/bin/{chunk-4TLSWPG7.js → chunk-UZ5S3EMB.js} +1 -1
- package/dist/bin/{chunk-QUHNXHVG.js → chunk-ZMZI3625.js} +46 -41
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/menu/menu.js +6 -1
- package/dist/cli/menu/pages/accounts-page.js +9 -5
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/guest/apps/app-installer.js +7 -5
- package/dist/guest/apps/comet-unpack.js +2 -2
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/runtime/pools.d.ts +4 -4
- package/dist/guest/runtime/pools.js +4 -15
- package/dist/guest/runtime/process.d.ts +5 -2
- package/dist/guest/runtime/process.js +23 -3
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +6 -0
- package/dist/guest/toolchains/engines/epic-launcher.js +19 -1
- package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
- package/dist/guest/toolchains/installers.d.ts +3 -3
- package/dist/guest/toolchains/installers.js +5 -5
- package/dist/guest/toolchains/native/unreal.js +22 -3
- package/dist/guest/toolchains/native/vcruntime.js +2 -2
- package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/guest/toolchains/signature.d.ts +2 -0
- package/dist/guest/toolchains/signature.js +9 -2
- package/dist/guest/toolchains/step-scheduler.js +1 -1
- package/dist/host/accounts/deliver.js +15 -7
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/pc-installs/discover.js +5 -2
- package/dist/host/pc-installs/unreal.d.ts +3 -1
- package/dist/host/pc-installs/unreal.js +27 -1
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +39 -23
- package/dist/runtime/daemon.js +279 -121
- package/dist/runtime/db.js +37 -17
- package/dist/runtime/main.js +340 -109
- package/dist/runtime/netproxy.js +31 -13
- package/dist/runtime/tool.js +37 -16
- package/dist/runtime/view.js +150 -38
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/app.test.js +20 -2
- package/dist/selftest/boot-runner.test.js +15 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/engines.test.js +22 -1
- package/dist/selftest/heavy-load.test.js +116 -4
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/menu-screens.js +52 -1
- package/dist/selftest/menu.test.js +29 -3
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/service-accounts.test.js +77 -2
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/ui-text.test.js +4 -0
- package/dist/shared/accounts/kinds.js +9 -9
- package/dist/shared/apps/catalogue-gamedev.js +1 -0
- package/dist/shared/apps/catalogue.d.ts +2 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/concurrency/pools.js +1 -1
- package/dist/shared/download/archive.js +4 -2
- package/dist/shared/download/index.d.ts +1 -1
- package/dist/shared/download/index.js +1 -1
- package/dist/shared/download/spawn.d.ts +7 -1
- package/dist/shared/download/spawn.js +16 -1
- package/dist/shared/memory/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/memory/unreal-build.d.ts +2 -0
- package/dist/shared/memory/unreal-build.js +21 -0
- package/dist/shared/paths/pc-installs.d.ts +8 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/unreal/launcher-files.d.ts +7 -0
- package/dist/shared/unreal/launcher-files.js +32 -1
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-VPMYTSUL.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +10 -10
- package/schemas/sandbox-config.schema.json +9 -9
|
@@ -8,7 +8,7 @@ import { DEFAULTS } from '../core/config/index.js';
|
|
|
8
8
|
import { isWin } from '../core/util/index.js';
|
|
9
9
|
import { SYSTEM } from '../shared/system.js';
|
|
10
10
|
import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
|
|
11
|
-
import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, } from '../shared/memory/index.js';
|
|
11
|
+
import { BURST_GATE, compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, ubtBuildConfiguration, } from '../shared/memory/index.js';
|
|
12
12
|
import { HeartbeatSchema } from '../shared/channel/index.js';
|
|
13
13
|
import { heartbeatLine, lastEndOf, vanishedCause, stopWarning } from '../host/life/index.js';
|
|
14
14
|
import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
|
|
@@ -16,10 +16,10 @@ import { vgpuWarning, GENTLE_NOTE } from '../host/launch/risk-notes.js';
|
|
|
16
16
|
import { judgeSandbox } from '../host/health/index.js';
|
|
17
17
|
import { cleanUpLeftovers, cleanupQuestion, killArgs } from '../host/sandbox/index.js';
|
|
18
18
|
import { parseTasklist } from '../host/sharing/index.js';
|
|
19
|
-
import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
|
|
19
|
+
import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, memoryWasFineLine, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
|
|
20
20
|
// The pure governor files only: governor.ts loads the guest runtime (session.json), which exists only in a sandbox.
|
|
21
|
-
import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
|
|
22
|
-
import { GovernorCore } from '../guest/governor/core.js';
|
|
21
|
+
import { canLower, canPause, decide, GATE_NAMES, INITIAL_STATE, isLinker, isNeverTouched, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
|
|
22
|
+
import { GateEventSchema, GovernorCore, memgovArgs } from '../guest/governor/core.js';
|
|
23
23
|
const GB = 1024;
|
|
24
24
|
const MB_SANDBOX = DEFAULTS.sandbox;
|
|
25
25
|
const proc = (pid, name, mb, prio = 'normal') => ({
|
|
@@ -96,6 +96,10 @@ export const heavyLoadSuite = {
|
|
|
96
96
|
t.check(cargoJobs(16, 32, 3) === 2 && cargoJobs(16, 32, 8) === 1, 'fewer when several sessions build');
|
|
97
97
|
t.check(cargoJobs(32, 32, 1) === 8, '32 processors, 32 GB: 8 jobs for one session');
|
|
98
98
|
t.check(buildJobs(32, 32) === 8 && buildJobs(32, 64) === 19 && buildJobs(4, 64) === 4 && buildJobs(16, 4) === 2, 'the build budget: 3 GB per job after 6 GB, never more than the processors, at least 2');
|
|
99
|
+
const ubt = ubtBuildConfiguration(buildJobs(20, 36));
|
|
100
|
+
t.check(ubt.includes('<MaxParallelActions>10</MaxParallelActions>') &&
|
|
101
|
+
ubt.includes('xmlns="https://www.unrealengine.com/BuildConfiguration"') &&
|
|
102
|
+
ubtBuildConfiguration(0).includes('<MaxParallelActions>1</MaxParallelActions>'), "UnrealBuildTool's compiles at once follow the build budget (at least 1)", ubt);
|
|
99
103
|
t.check(linkSlotsFor(32) === 8 && linkSlotsFor(16) === 3 && linkSlotsFor(8) === 2 && linkSlotsFor(128) === 8, 'links at once ("auto"): one per 3 GB after 6 GB, 2 to 8');
|
|
100
104
|
t.check(usableCpus(32, 80) === 25 && usableCpus(32, null) === 32 && usableCpus(4, 10) === 2 && usableCpus(8, 100) === 8, 'the processors programs see follow the CPU cap (at least 2)');
|
|
101
105
|
t.section('the governor: which programs');
|
|
@@ -374,6 +378,105 @@ export const heavyLoadSuite = {
|
|
|
374
378
|
t.check(decide({ ...INITIAL_STATE, hostUntil: clock + 60000, hostStep: 4 }, sample(60, 40, procs), clock, 'normal').state
|
|
375
379
|
.step === 4, 'with plenty of memory in the sandbox, a severe PC still turns the dial to 4');
|
|
376
380
|
t.check(core.current.hostUntil === clock + 60000, 'a host request is remembered for its seconds');
|
|
381
|
+
t.section('the memory gate (rate of growth)');
|
|
382
|
+
t.check(['cl', 'link', 'rustc', 'clang-cl', 'lld-link', 'shadercompileworker'].every((n) => GATE_NAMES.includes(n)), 'the gate may freeze compilers, linkers and Unreal shader workers', GATE_NAMES.join(','));
|
|
383
|
+
t.check([
|
|
384
|
+
'node',
|
|
385
|
+
'claude',
|
|
386
|
+
'codex',
|
|
387
|
+
'opencode',
|
|
388
|
+
'cmd',
|
|
389
|
+
'pwsh',
|
|
390
|
+
'powershell',
|
|
391
|
+
'msbuild',
|
|
392
|
+
'dotnet',
|
|
393
|
+
'cargo',
|
|
394
|
+
'ninja',
|
|
395
|
+
'unrealbuildtool',
|
|
396
|
+
'ubahost',
|
|
397
|
+
'unrealeditor',
|
|
398
|
+
'unrealeditor-cmd',
|
|
399
|
+
'mspdbsrv',
|
|
400
|
+
'ptyhost',
|
|
401
|
+
'memgov',
|
|
402
|
+
].every((n) => !GATE_NAMES.includes(n)) &&
|
|
403
|
+
GATE_NAMES.every((n) => (canPause(n) || n === 'shadercompileworker') && !isNeverTouched(n)), 'agents, shells, build coordinators, the editor and the tools themselves are never frozen');
|
|
404
|
+
t.check(JSON.stringify(memgovArgs('normal')) ===
|
|
405
|
+
JSON.stringify([
|
|
406
|
+
'serve',
|
|
407
|
+
'--interval',
|
|
408
|
+
'2',
|
|
409
|
+
'--gate',
|
|
410
|
+
'4096:3000:1500:30000',
|
|
411
|
+
'--gate-names',
|
|
412
|
+
GATE_NAMES.join(','),
|
|
413
|
+
]) && memgovArgs('gentle').includes('3072:3000:2000:30000'), 'memgov starts with the gate for the profile', memgovArgs('gentle').join(' '));
|
|
414
|
+
t.check(['normal', 'gentle'].every((p) => {
|
|
415
|
+
const g = BURST_GATE[p];
|
|
416
|
+
return g.growthMb >= 1024 && g.maxHoldMs <= 30000 && g.releaseMs < g.maxHoldMs && g.windowMs >= 1000;
|
|
417
|
+
}) && BURST_GATE.gentle.growthMb <= BURST_GATE.normal.growthMb, 'the gate is sane: a real burst size, every hold ends within 30 s, vGPU is not looser');
|
|
418
|
+
const gateExamples = [
|
|
419
|
+
{ event: 'gate', at: 1, kind: 'ready', mode: 'notify', growthMb: 4096, windowMs: 3000 },
|
|
420
|
+
{ event: 'gate', at: 1, kind: 'off', reason: 'bad --gate' },
|
|
421
|
+
{ event: 'gate', at: 1, kind: 'trip', grewMb: 4210, ms: 1800, jobMb: 23456, held: [{ pid: 101, name: 'cl' }] },
|
|
422
|
+
{ event: 'gate', at: 1, kind: 'hold', pid: 101, name: 'cl' },
|
|
423
|
+
{ event: 'gate', at: 1, kind: 'release', pid: 101, name: 'cl', heldMs: 1500, forced: false },
|
|
424
|
+
];
|
|
425
|
+
const heldOf = (n) => Array.from({ length: n }, (_, i) => ({ pid: i, name: 'cl' }));
|
|
426
|
+
t.check(gateExamples.every((e) => GateEventSchema.safeParse(e).success) &&
|
|
427
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'hold', pid: 1, name: 'x'.repeat(129) }).success &&
|
|
428
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'trip', grewMb: 1, ms: 1, jobMb: 1, held: heldOf(65) })
|
|
429
|
+
.success &&
|
|
430
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'boom' }).success &&
|
|
431
|
+
!GateEventSchema.safeParse({ at: 1, kind: 'hold', pid: 1, name: 'cl' }).success, 'gate events from memgov are checked: one of each kind passes; a long name, 65 held, a bad kind or no event fail');
|
|
432
|
+
{
|
|
433
|
+
const gateLogs = [];
|
|
434
|
+
const gateCore = new GovernorCore({
|
|
435
|
+
port,
|
|
436
|
+
profile: 'normal',
|
|
437
|
+
now: () => 0,
|
|
438
|
+
logLine: (l) => gateLogs.push(l),
|
|
439
|
+
saveStatus: () => undefined,
|
|
440
|
+
warn: (line) => gateLogs.push(line),
|
|
441
|
+
});
|
|
442
|
+
gateCore.onGate({
|
|
443
|
+
event: 'gate',
|
|
444
|
+
at: 1,
|
|
445
|
+
kind: 'trip',
|
|
446
|
+
grewMb: 4210,
|
|
447
|
+
ms: 1800,
|
|
448
|
+
jobMb: 23456,
|
|
449
|
+
held: [{ pid: 101, name: 'cl' }],
|
|
450
|
+
});
|
|
451
|
+
gateCore.onGate({
|
|
452
|
+
event: 'gate',
|
|
453
|
+
at: 2,
|
|
454
|
+
kind: 'release',
|
|
455
|
+
pid: 101,
|
|
456
|
+
name: 'cl',
|
|
457
|
+
heldMs: 30000,
|
|
458
|
+
forced: true,
|
|
459
|
+
});
|
|
460
|
+
t.check(gateLogs.some((l) => l.includes("governor: gate: the agents' programs grew 4210 MB in 1.8 s (now 23456 MB); holding 1: cl (101)")) &&
|
|
461
|
+
gateLogs.some((l) => l.endsWith('(the longest it may wait)')) &&
|
|
462
|
+
gateCore.current.step === 0, 'gate events go to the memory log and leave the dial alone', gateLogs.join('\n'));
|
|
463
|
+
const heldStatuses = [];
|
|
464
|
+
const heldCore = new GovernorCore({
|
|
465
|
+
port,
|
|
466
|
+
profile: 'normal',
|
|
467
|
+
now: () => 0,
|
|
468
|
+
logLine: () => undefined,
|
|
469
|
+
saveStatus: (s) => heldStatuses.push(s),
|
|
470
|
+
warn: () => undefined,
|
|
471
|
+
});
|
|
472
|
+
heldCore.onSample({ ...sample(60, 40, procs), gateHeld: 3 });
|
|
473
|
+
const heldStatus = heldStatuses[heldStatuses.length - 1];
|
|
474
|
+
t.check(PressureStatusSchema.safeParse(heldStatus).success &&
|
|
475
|
+
heldStatus.held === 3 &&
|
|
476
|
+
heldCore.snapshot?.held === 3, 'the number of held builds reaches the status file and the snapshot', JSON.stringify(heldStatus));
|
|
477
|
+
t.check(HeartbeatSchema.safeParse({ ...beat, held: 3 }).success &&
|
|
478
|
+
heartbeatLine({ ...beat, held: 3 }).includes('3 builds held by the memory gate'), 'the heartbeat carries the held builds into the crash report line');
|
|
479
|
+
}
|
|
377
480
|
t.section("the PC's watchdog");
|
|
378
481
|
const input = (memory, nowMs, cpu = 10, profile = 'normal') => ({
|
|
379
482
|
memory,
|
|
@@ -541,6 +644,13 @@ export const heavyLoadSuite = {
|
|
|
541
644
|
meaning.includes('froze, lost power') &&
|
|
542
645
|
meaning.includes('hardware error') &&
|
|
543
646
|
meaning.includes('vmwp.exe'), 'what each finding usually means, in plain words', meaning);
|
|
647
|
+
t.check(findingLines([ev(1000, 'Application Error', '2026-10-15T19:41:12Z', 'Faulting application name: vmwp.exe')])
|
|
648
|
+
.join('\n')
|
|
649
|
+
.includes('not a program inside the sandbox running out of memory'), 'a crash of vmwp.exe is named as a fault in Windows or a driver, not memory');
|
|
650
|
+
t.check(memoryWasFineLine('x\n2026-11-08T15:30:05.411Z | RAM free/total MB 42325/63170 | commit free/limit MB 52051/75458 | page file auto\n')?.includes('still had 41 GB of RAM and 50 GB') === true &&
|
|
651
|
+
memoryWasFineLine('2026-10-15T19:40:50Z | RAM free/total MB 900/32768 | commit free/limit MB 9000/40000') ===
|
|
652
|
+
null &&
|
|
653
|
+
memoryWasFineLine('') === null, 'the report says when this PC still had plenty of memory, and says nothing when it was low');
|
|
544
654
|
const dumps = path.join(t.at('crash-dumps'), 'Minidump');
|
|
545
655
|
fs.mkdirSync(dumps, { recursive: true });
|
|
546
656
|
fs.writeFileSync(path.join(dumps, '101526-1.dmp'), 'x');
|
|
@@ -576,6 +686,7 @@ export const heavyLoadSuite = {
|
|
|
576
686
|
};
|
|
577
687
|
const sandboxCs = (n) => path.join(t.packageRoot, 'sandbox', 'csharp', n);
|
|
578
688
|
build('ptyhost', [sandboxCs('PtyHost.cs')]);
|
|
689
|
+
build('clipboardwatch', [sandboxCs('ClipboardWatch.cs')], 'library');
|
|
579
690
|
const memgov = build('memgov', [sandboxCs('MemGov.cs')]);
|
|
580
691
|
const hostDir = path.join(t.packageRoot, 'native', 'csharp', 'HostTools');
|
|
581
692
|
const tools = build('HostTools', fs
|
|
@@ -607,6 +718,7 @@ export const heavyLoadSuite = {
|
|
|
607
718
|
// Not JSON: the check below fails.
|
|
608
719
|
}
|
|
609
720
|
t.check(sampleOk && served.status === 0, 'memgov serve prints a well-formed sample and ends when its input closes', firstLine);
|
|
721
|
+
t.check(!(served.stdout ?? '').split(/\r?\n/).some((l) => l.includes('"event":"gate"')), 'a memgov started without --gate never takes the job port (no gate events)');
|
|
610
722
|
for (const script of ['memory-status.ps1', 'stop-events.ps1']) {
|
|
611
723
|
const text = fs.readFileSync(path.join(t.packageRoot, 'native', 'scripts', script), 'utf8');
|
|
612
724
|
t.check(text.startsWith('#Requires -Version 5.1'), script + ' says its PowerShell version');
|
|
@@ -11,6 +11,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { encodeFrame, FrameType } from '../shared/pty/index.js';
|
|
13
13
|
import { SYSTEM } from '../shared/system.js';
|
|
14
|
+
import { GateEventSchema } from '../guest/governor/core.js';
|
|
14
15
|
import { CSC, openViewer, tail, WAIT } from './support/live.js';
|
|
15
16
|
/**
|
|
16
17
|
* The stand-in agent: VT input and SGR mouse tracking on, echoes what it reads ("got <1b>[<0;10;5M"). L prints 500
|
|
@@ -268,6 +269,108 @@ export const liveTerminalSuite = {
|
|
|
268
269
|
Number(set[1]) === Math.max(Math.floor(commit / 2), commit - Math.max(1536, Math.floor((commit * 3) / 100))), 'the ptyhost that creates the job sets the backstop just below the commit limit (auto: 3 %, at least 1.5 GB)', read('job-first.log'));
|
|
269
270
|
t.check(read('job-second.log').includes('joined the existing one'), 'a ptyhost that joins the job never changes its backstop', read('job-second.log'));
|
|
270
271
|
}
|
|
272
|
+
t.section('the memory gate: a burst is held at once and let go in turn');
|
|
273
|
+
{
|
|
274
|
+
// A job of the test's own (never the agents' real one) for both ptyhosts and memgov.
|
|
275
|
+
const jobName = 'Local\\AgentSandboxAgentsJobTest' + process.pid + '-gate-' + Date.now();
|
|
276
|
+
const memgovExe = t.at('memgov-gate.exe');
|
|
277
|
+
const burstExe = t.at('MemoryBurst.exe');
|
|
278
|
+
const gateErrors = compile(path.join(t.packageRoot, 'sandbox', 'csharp', 'MemGov.cs'), memgovExe) ||
|
|
279
|
+
compile(path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'memory-burst', 'MemoryBurst.cs'), burstExe);
|
|
280
|
+
t.check(!gateErrors, 'memgov and the memory burst program compile', gateErrors);
|
|
281
|
+
if (!gateErrors) {
|
|
282
|
+
const ping = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE');
|
|
283
|
+
const holder = '"' + SYSTEM.cmd() + '" /c "' + ping + '" -n 60 127.0.0.1 >nul';
|
|
284
|
+
const progress = t.at('burst-progress.txt');
|
|
285
|
+
const startHost = async (command, log) => spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at(log), command], {
|
|
286
|
+
env: { ...process.env, PTYHOST_BIND: 'loopback', PTYHOST_BACKSTOP_MB: 'none', PTYHOST_JOB_NAME: jobName },
|
|
287
|
+
stdio: 'ignore',
|
|
288
|
+
windowsHide: true,
|
|
289
|
+
});
|
|
290
|
+
const read = (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
|
|
291
|
+
const hosts = [];
|
|
292
|
+
let memgov = null;
|
|
293
|
+
try {
|
|
294
|
+
hosts.push(await startHost(holder, 'gate-a.log'));
|
|
295
|
+
await t.waitFor(() => read(t.at('gate-a.log')).includes('agent job:'), WAIT.screen);
|
|
296
|
+
memgov = spawn(memgovExe, ['serve', '--interval', '1', '--gate', '512:3000:800:4000', '--gate-names', 'memoryburst'], {
|
|
297
|
+
env: { ...process.env, MEMGOV_JOB_NAME: jobName },
|
|
298
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
let text = '';
|
|
302
|
+
memgov.stdout.on('data', (chunk) => (text += chunk.toString('utf8')));
|
|
303
|
+
const events = () => text
|
|
304
|
+
.split(/\r?\n/)
|
|
305
|
+
.filter((l) => l.includes('"event":"gate"'))
|
|
306
|
+
.flatMap((l) => {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = GateEventSchema.safeParse(JSON.parse(l));
|
|
309
|
+
return parsed.success ? [parsed.data] : [];
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const ready = await t.waitFor(() => events().some((e) => e.kind === 'ready'), WAIT.screen);
|
|
316
|
+
const readyEvent = events().find((e) => e.kind === 'ready');
|
|
317
|
+
t.check(Boolean(ready) && readyEvent?.kind === 'ready' && readyEvent.mode === 'notify', "the gate listens to the job's notifications (no polling)", text.slice(-400));
|
|
318
|
+
hosts.push(await startHost('"' + burstExe + '" 1536 "' + progress + '"', 'gate-b.log'));
|
|
319
|
+
const finished = await t.waitFor(() => read(progress).includes('done'), 15000);
|
|
320
|
+
const marks = read(progress)
|
|
321
|
+
.split(/\r?\n/)
|
|
322
|
+
.flatMap((l) => {
|
|
323
|
+
const m = /^(\d+) \d+$/.exec(l.trim());
|
|
324
|
+
return m ? [Number(m[1])] : [];
|
|
325
|
+
});
|
|
326
|
+
const all = events();
|
|
327
|
+
const trips = all.flatMap((e) => (e.kind === 'trip' ? [e] : []));
|
|
328
|
+
const releases = all.flatMap((e) => (e.kind === 'release' ? [e] : []));
|
|
329
|
+
const evidence = text.slice(-1200) + '\nprogress: ' + marks.length + ' lines';
|
|
330
|
+
const firstTrip = trips[0];
|
|
331
|
+
t.check(trips.some((e) => e.held.some((h) => h.name === 'memoryburst')) &&
|
|
332
|
+
firstTrip !== undefined &&
|
|
333
|
+
firstTrip.grewMb >= 512 &&
|
|
334
|
+
firstTrip.grewMb <= 1024, 'a burst past the gate is held at once', evidence);
|
|
335
|
+
const stalls = [];
|
|
336
|
+
for (const trip of trips) {
|
|
337
|
+
if (!trip.held.some((h) => h.name === 'memoryburst'))
|
|
338
|
+
continue;
|
|
339
|
+
const release = releases.find((r) => r.at >= trip.at && r.name === 'memoryburst');
|
|
340
|
+
const until = release ? release.at : Infinity;
|
|
341
|
+
if (marks.some((m) => m > trip.at + 200 && m < until))
|
|
342
|
+
stalls.push('trip at ' + trip.at);
|
|
343
|
+
}
|
|
344
|
+
t.check(stalls.length === 0, 'the held program made no progress more than 200 ms after the trip until it was let go', stalls.join(', ') + '\n' + evidence);
|
|
345
|
+
t.check(releases.length > 0 && releases.every((r) => r.heldMs <= 4500) && releases.some((r) => !r.forced), 'held programs go on in turn, never longer than the most allowed', evidence);
|
|
346
|
+
t.check(Boolean(finished) && trips.length >= 2, 'the burst program reached its end after at least 2 trips', evidence);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (memgov) {
|
|
350
|
+
memgov.stdin.end();
|
|
351
|
+
const ended = memgov;
|
|
352
|
+
await t.waitFor(() => ended.exitCode !== null, 5000);
|
|
353
|
+
t.check(ended.exitCode === 0, 'memgov with the gate ends cleanly when its input closes');
|
|
354
|
+
try {
|
|
355
|
+
if (ended.exitCode === null)
|
|
356
|
+
ended.kill();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const c of hosts) {
|
|
363
|
+
try {
|
|
364
|
+
if (c.exitCode === null)
|
|
365
|
+
c.kill();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
271
374
|
const client = t.at('client.mjs');
|
|
272
375
|
fs.writeFileSync(client, [
|
|
273
376
|
'import { connect, runTerminalClient } from ' +
|
|
@@ -19,6 +19,7 @@ import { SECTION_LABELS } from '../cli/menu/shell.js';
|
|
|
19
19
|
import { Frame, ListScreen, OverlayScreen, Text } from '../cli/tui/index.js';
|
|
20
20
|
export const SIZES = [
|
|
21
21
|
{ width: 200, height: 60 },
|
|
22
|
+
{ width: 120, height: 30 },
|
|
22
23
|
{ width: 100, height: 30 },
|
|
23
24
|
{ width: 80, height: 24 },
|
|
24
25
|
{ width: 60, height: 20 },
|
|
@@ -38,6 +39,48 @@ const DATA_FOLDER_ROW = [
|
|
|
38
39
|
const ACCOUNTS = {
|
|
39
40
|
agent: 'claude',
|
|
40
41
|
services: [
|
|
42
|
+
{
|
|
43
|
+
kind: 'epic',
|
|
44
|
+
label: SERVICE_SPECS.epic.label,
|
|
45
|
+
share: true,
|
|
46
|
+
state: 'none',
|
|
47
|
+
source: null,
|
|
48
|
+
savedAt: null,
|
|
49
|
+
summary: null,
|
|
50
|
+
heldBy: null,
|
|
51
|
+
usedBy: ['unreal'],
|
|
52
|
+
usedFor: 'unreal',
|
|
53
|
+
warnings: [...SERVICE_SPECS.epic.warnings],
|
|
54
|
+
canImport: true,
|
|
55
|
+
canLogin: false,
|
|
56
|
+
needsFile: false,
|
|
57
|
+
fromPc: true,
|
|
58
|
+
hasFileSource: false,
|
|
59
|
+
onPc: true,
|
|
60
|
+
sandboxOnly: false,
|
|
61
|
+
ready: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
kind: 'keystore',
|
|
65
|
+
label: SERVICE_SPECS.keystore.label,
|
|
66
|
+
share: false,
|
|
67
|
+
state: 'none',
|
|
68
|
+
source: null,
|
|
69
|
+
savedAt: null,
|
|
70
|
+
summary: null,
|
|
71
|
+
heldBy: null,
|
|
72
|
+
usedBy: ['flutter'],
|
|
73
|
+
usedFor: 'flutter',
|
|
74
|
+
warnings: [...SERVICE_SPECS.keystore.warnings],
|
|
75
|
+
canImport: true,
|
|
76
|
+
canLogin: false,
|
|
77
|
+
needsFile: true,
|
|
78
|
+
fromPc: false,
|
|
79
|
+
hasFileSource: true,
|
|
80
|
+
onPc: null,
|
|
81
|
+
sandboxOnly: false,
|
|
82
|
+
ready: false,
|
|
83
|
+
},
|
|
41
84
|
{
|
|
42
85
|
kind: 'perplexity',
|
|
43
86
|
label: SERVICE_SPECS.perplexity.label,
|
|
@@ -53,6 +96,10 @@ const ACCOUNTS = {
|
|
|
53
96
|
canImport: true,
|
|
54
97
|
canLogin: true,
|
|
55
98
|
needsFile: false,
|
|
99
|
+
fromPc: true,
|
|
100
|
+
hasFileSource: true,
|
|
101
|
+
onPc: true,
|
|
102
|
+
sandboxOnly: false,
|
|
56
103
|
ready: true,
|
|
57
104
|
},
|
|
58
105
|
{
|
|
@@ -70,6 +117,10 @@ const ACCOUNTS = {
|
|
|
70
117
|
canImport: false,
|
|
71
118
|
canLogin: false,
|
|
72
119
|
needsFile: false,
|
|
120
|
+
fromPc: false,
|
|
121
|
+
hasFileSource: false,
|
|
122
|
+
onPc: null,
|
|
123
|
+
sandboxOnly: true,
|
|
73
124
|
ready: true,
|
|
74
125
|
},
|
|
75
126
|
],
|
|
@@ -423,7 +474,7 @@ export function renderScreens(dir) {
|
|
|
423
474
|
],
|
|
424
475
|
escape: 'cancel',
|
|
425
476
|
});
|
|
426
|
-
out.push(block('5 Settings: Save / Discard / Cancel', Frame.render({ ...dialog.render(bodySize), nav }, size.width, size.height).map((l) => Text.strip(l).replace(/\s+$/, '')), size), block('6 Accounts', draw(5, accountsOptions(ACCOUNTS), size, status), size), block('6 Accounts · codex', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'codex' }, size, status), size), block('6 Accounts · opencode (login not kept)', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'opencode' }, size, status), size));
|
|
477
|
+
out.push(block('5 Settings: Save / Discard / Cancel', Frame.render({ ...dialog.render(bodySize), nav }, size.width, size.height).map((l) => Text.strip(l).replace(/\s+$/, '')), size), block('6 Accounts', draw(5, accountsOptions(ACCOUNTS), size, status), size), ...['epic', 'perplexity', 'comet'].map((kind) => block('6 Accounts · ' + kind, draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'service:' + kind }, size, status), size)), block('6 Accounts · codex', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'codex' }, size, status), size), block('6 Accounts · opencode (login not kept)', draw(5, { ...accountsOptions(ACCOUNTS), startCategory: 'opencode' }, size, status), size));
|
|
427
478
|
for (const category of ['updates', 'data', 'health', 'logs', 'more'])
|
|
428
479
|
out.push(block('7 Maintenance · ' + category, draw(6, { ...maintenanceOptions(maintenance), startCategory: category }, size, status), size));
|
|
429
480
|
}
|
|
@@ -7,7 +7,7 @@ import { Draft } from '../cli/menu/settings/draft.js';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { addedAgents, agentNameProblem, commandFromPackage, newAgent } from '../app/features/agents/index.js';
|
|
9
9
|
import { APP_GROUPS, appRows, REQUEST_APP, requestAppUrl } from '../app/features/apps/index.js';
|
|
10
|
-
import { appsCategories, appsItems } from '../cli/menu/pages/apps-page.js';
|
|
10
|
+
import { appsCategories, appsItems, stageTicks } from '../cli/menu/pages/apps-page.js';
|
|
11
11
|
import { readMenuView, saveMenuView } from '../cli/menu/memory.js';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { argPathKind, optionLabel, optionPathKind, plainDescription } from '../cli/menu/command-fields.js';
|
|
@@ -633,6 +633,24 @@ export const menuSuite = {
|
|
|
633
633
|
appItems.every((i) => groupIds.includes(i.category ?? '')) &&
|
|
634
634
|
appItems.length === appRows(user2, t.dir('apps-groups')).length &&
|
|
635
635
|
APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 7 groups (with your own programs); every catalogue category belongs to one and every app is listed', groupIds.join());
|
|
636
|
+
t.section('a Store app ticked on the Apps page is saved and shown on');
|
|
637
|
+
const appsFile = ConfigFile.user(t.at('apps-store', 'config.json'));
|
|
638
|
+
const appsDraft = new Draft();
|
|
639
|
+
const perplexityRow = (d) => appsItems(appsFile, t.dir('apps-store-data'), d).find((i) => i.value === 'perplexity-desktop')?.checked;
|
|
640
|
+
t.check(perplexityRow(appsDraft) === false, 'the Perplexity desktop app starts off');
|
|
641
|
+
const alreadyOn = appRows(appsFile, t.dir('apps-store-data'))
|
|
642
|
+
.filter((r) => r.on)
|
|
643
|
+
.map((r) => r.id);
|
|
644
|
+
stageTicks(appsFile, appsDraft, [...alreadyOn, 'perplexity-desktop']);
|
|
645
|
+
t.check(perplexityRow(appsDraft) === true && appsDraft.count() === 1, 'ticking it shows it on and stages one change');
|
|
646
|
+
const applied = appsDraft.apply();
|
|
647
|
+
t.check(applied.saved === 1 &&
|
|
648
|
+
applied.problems.length === 0 &&
|
|
649
|
+
appsFile.valueInFile(['apps', 'install']).includes('perplexity-desktop') &&
|
|
650
|
+
perplexityRow(new Draft()) === true, 'Ctrl+S writes it into apps.install and the page still shows it on');
|
|
651
|
+
const toggleCtx = { loaded: { configPath: appsFile.file }, options: {}, dataPath: t.dir('apps-store-data') };
|
|
652
|
+
await runDefinition('apps.toggle', toggleCtx, { id: 'perplexity-desktop', on: false }, quiet());
|
|
653
|
+
t.check(!(ConfigFile.user(appsFile.file).valueInFile(['apps', 'install']) ?? []).includes('perplexity-desktop'), 'and the web toggle (apps.toggle) turns it off in the same list');
|
|
636
654
|
t.section('dialog box');
|
|
637
655
|
const box = dialogBox({ title: 'Unsaved changes', lines: ['You have 2 unsaved changes.'], buttons: 'Save' }, 61);
|
|
638
656
|
const base = Array.from({ length: 12 }, () => 'x'.repeat(60));
|
|
@@ -742,11 +760,19 @@ export const menuSuite = {
|
|
|
742
760
|
const size = /\((\d+)x(\d+)\)/.exec(head);
|
|
743
761
|
return !size || rows.length !== Number(size[2]) || rows.some((l) => Text.width(l) > Number(size[1]) - 1);
|
|
744
762
|
});
|
|
745
|
-
t.equal(overflow.map((b) => b.slice(0, b.indexOf('\n'))), [], 'every screen is exactly as tall as the terminal and never wider, at 200x60, 100x30, 80x24 and 60x20');
|
|
746
|
-
t.check(SIZES.length ===
|
|
763
|
+
t.equal(overflow.map((b) => b.slice(0, b.indexOf('\n'))), [], 'every screen is exactly as tall as the terminal and never wider, at 200x60, 120x30, 100x30, 80x24 and 60x20');
|
|
764
|
+
t.check(SIZES.length === 5 &&
|
|
747
765
|
screens.includes('=== 7 Maintenance · health (80x24) ===') &&
|
|
748
766
|
screens.includes('unsaved: 2') &&
|
|
749
767
|
screens.includes('Save'), 'all screens render at both sizes; the settings screen shows its unsaved count and the leave dialog offers Save');
|
|
768
|
+
const serviceScreens = blocks.filter((b) => /^6 Accounts · (epic|perplexity|comet) \((80x24|120x30)\)/.test(b));
|
|
769
|
+
const serviceRows = serviceScreens.flatMap((b) => b.split('\n').slice(1));
|
|
770
|
+
t.check(serviceScreens.length === 6 &&
|
|
771
|
+
serviceRows.every((l) => !/\bsandbox (account|launch|config)\b/.test(l)) &&
|
|
772
|
+
serviceRows.every((l) => !l.includes('(encrypted)') && !/[A-Za-z]…/.test(l.replace(/(file|sign-in|Forget)…/g, ''))) &&
|
|
773
|
+
serviceRows.some((l) => l.includes('On this PC') && l.includes('found')) &&
|
|
774
|
+
serviceRows.some((l) => l.includes('Perplexity API')) &&
|
|
775
|
+
!serviceRows.some((l) => l.includes('Perplexity API key')), 'the service pages read short at 80 and 120 columns: short names, a few facts, no cut words, no command hints');
|
|
750
776
|
const out = process.env.MENU_SCREENS_OUT;
|
|
751
777
|
if (out)
|
|
752
778
|
fs.writeFileSync(out, screens, 'utf8');
|
|
@@ -47,7 +47,8 @@ export const personalizationSuite = {
|
|
|
47
47
|
const untouched = fs.readFileSync(settings, 'utf8');
|
|
48
48
|
t.check(untouched.startsWith('// edited') &&
|
|
49
49
|
/"guid": "\{x\}",\s*\}/.test(untouched) &&
|
|
50
|
-
/"reloadEnvironmentVariables":\s*true/.test(untouched)
|
|
50
|
+
/"reloadEnvironmentVariables":\s*true/.test(untouched) &&
|
|
51
|
+
/"copyOnSelect":\s*true/.test(untouched), 'until pwsh is installed only the default switches are added (comments and trailing commas survive)');
|
|
51
52
|
t.write(path.join(data, 'tools', 'pwsh', 'pwsh.exe'), 'MZ');
|
|
52
53
|
syncPwshProfile(data, true);
|
|
53
54
|
const added = read();
|
|
@@ -18,9 +18,10 @@ import { clearSecrets, SecretStore } from '../host/secrets/index.js';
|
|
|
18
18
|
import { applyServiceAccounts, readDelivered } from '../guest/accounts/apply.js';
|
|
19
19
|
import { AccountWatch } from '../guest/accounts/watch.js';
|
|
20
20
|
import { epicAdapter } from '../guest/accounts/kinds/epic.js';
|
|
21
|
-
import { epicSettingsFile, epicSettingsFiles, fingerprint, readEpicLogin, SERVICE_KINDS,
|
|
21
|
+
import { epicSettingsFile, epicSettingsFiles, fingerprint, readEpicLogin, SERVICE_KINDS, serviceWanted, withEpicLogin, } from '../shared/accounts/index.js';
|
|
22
22
|
import { RunDir } from '../shared/channel/index.js';
|
|
23
23
|
import { decodeText, encodeText, readBoundedFile } from '../shared/io/index.js';
|
|
24
|
+
import { isKindReady } from '../shared/readiness.js';
|
|
24
25
|
const SIGN_IN = 'PlantedEpicSignIn' + 'AbCdEf0123456789+/'.repeat(6) + '==';
|
|
25
26
|
const RENEWED = 'PlantedRenewed' + 'ZyXw9876543210'.repeat(6);
|
|
26
27
|
const CFX = 'cfxk_PlantedServerKey_0123456789';
|
|
@@ -150,6 +151,11 @@ export const serviceAccountsSuite = {
|
|
|
150
151
|
signIns.remove('epic');
|
|
151
152
|
const fromPc = deliver({ pc: thisPc });
|
|
152
153
|
t.check(fromPc.payloads.epic?.data === SIGN_IN && signIns.info('epic')?.source === 'pc', "shared with nothing saved: this PC's own sign-in is taken and saved, as import would");
|
|
154
|
+
for (const file of epicSettingsFiles(pcLocal))
|
|
155
|
+
fs.writeFileSync(file, INI.replace(SIGN_IN, RENEWED));
|
|
156
|
+
t.check(deliver({ pc: thisPc }).payloads.epic?.data === RENEWED && signIns.read('epic')?.data === RENEWED, "one taken from this PC follows it: the PC's renewed sign-in replaces the stale copy");
|
|
157
|
+
signIns.save('epic', { data: SIGN_IN }, 'sandbox');
|
|
158
|
+
t.check(deliver({ pc: thisPc }).payloads.epic?.data === SIGN_IN && signIns.info('epic')?.source === 'sandbox', 'one a sandbox handed back is newer than the PC’s and is kept');
|
|
153
159
|
signIns.remove('epic');
|
|
154
160
|
t.check(!deliver().payloads.epic, 'without this PC (pc left out) nothing is taken');
|
|
155
161
|
const sharedRun = await f.run({}, config);
|
|
@@ -325,8 +331,77 @@ export const serviceAccountsSuite = {
|
|
|
325
331
|
t.section('the accounts page');
|
|
326
332
|
const row = serviceRow('epic', { ...config, accounts: { ...config.accounts, epic: { share: true } } }, signIns, launchLeases);
|
|
327
333
|
const lines = pageLines(servicePage(row)).join('\n');
|
|
328
|
-
t.check(lines.includes('Epic Games') &&
|
|
334
|
+
t.check(lines.includes('Epic Games') &&
|
|
335
|
+
servicePage(row).sections[0].facts?.some((f) => f.description === 'The agent can use it.') === true &&
|
|
336
|
+
!lines.includes(SIGN_IN), 'a service shows its state and one short warning, never the sign-in');
|
|
329
337
|
t.check(Object.values(accountsView.queries).some((q) => q.id === 'accounts.services') &&
|
|
330
338
|
['accounts.service.import', 'accounts.service.login', 'accounts.service.logout', 'settings.set'].every((id) => accountsView.operations.some((o) => o.id === id)), 'the accounts view offers the services and their actions on every surface');
|
|
339
|
+
t.section('every action a service page shows works from the menu');
|
|
340
|
+
const shareOn = { ...config, accounts: { ...config.accounts, perplexity: { share: true } } };
|
|
341
|
+
const pcFound = { localAppData: t.dir('svc-pc-found'), appData: '', programData: '', env: {} };
|
|
342
|
+
t.write(epicSettingsFile(pcFound.localAppData), INI);
|
|
343
|
+
const pcNone = { localAppData: t.dir('svc-pc-none'), appData: '', programData: '', env: {} };
|
|
344
|
+
const pageStore = new ServiceAccountStore(t.dir('svc-page'), SecretStore.memory());
|
|
345
|
+
const pageLeases = new AccountLeases(t.dir('svc-page-leases'));
|
|
346
|
+
const rowOn = (kind, pc, c = config) => serviceRow(kind, c, pageStore, pageLeases, pc);
|
|
347
|
+
const labelsOf = (row) => servicePage(row).sections[0].actions.map((a) => a.label);
|
|
348
|
+
const descriptionsOf = (r) => (servicePage(r).sections[0].facts ?? []).map((f) => ' ' + (f.description ?? '')).join('');
|
|
349
|
+
const epicFound = rowOn('epic', pcFound);
|
|
350
|
+
const epicText = pageLines(servicePage(epicFound)).join('\n') + descriptionsOf(epicFound);
|
|
351
|
+
t.check(epicFound.onPc === true &&
|
|
352
|
+
/On this PC\s+found/.test(epicText) &&
|
|
353
|
+
epicText.includes("takes this PC's") &&
|
|
354
|
+
!epicText.includes(SIGN_IN), 'a kind that comes from this PC says it is found there (never the sign-in), and what sharing does');
|
|
355
|
+
t.check(rowOn('epic', pcNone).onPc === false &&
|
|
356
|
+
/On this PC\s+not found/.test(pageLines(servicePage(rowOn('epic', pcNone))).join('\n')), 'and says not found when this PC has none');
|
|
357
|
+
t.check(labelsOf(epicFound).includes("Take this PC's sign-in") &&
|
|
358
|
+
!labelsOf(epicFound).some((l) => l.startsWith('Import…')), 'taking it from this PC has a plain name');
|
|
359
|
+
const perplexityLabels = labelsOf(rowOn('perplexity', pcFound, shareOn));
|
|
360
|
+
t.check(perplexityLabels.includes("Take this PC's sign-in") &&
|
|
361
|
+
perplexityLabels.includes('Import from a file…') &&
|
|
362
|
+
perplexityLabels.includes('Type the sign-in…'), 'the API key can be taken from this PC, read from a file or typed');
|
|
363
|
+
const cometRow = rowOn('comet', pcFound);
|
|
364
|
+
const cometText = pageLines(servicePage(cometRow)).join('\n') + descriptionsOf(cometRow);
|
|
365
|
+
t.check(cometRow.onPc === null &&
|
|
366
|
+
cometRow.sandboxOnly &&
|
|
367
|
+
labelsOf(cometRow).every((l) => l === 'Share with sandboxes') &&
|
|
368
|
+
cometText.includes('Sign in once inside a sandbox; it is kept for the next ones.'), 'Comet offers only sharing and says plainly to sign in once inside a sandbox');
|
|
369
|
+
// Every action a page shows is one its kind can do (import: a PC or file way; sign in: a typed way).
|
|
370
|
+
for (const kind of SERVICE_KINDS.filter(isKindReady)) {
|
|
371
|
+
const r = rowOn(kind, pcFound);
|
|
372
|
+
for (const a of servicePage(r).sections[0].actions ?? []) {
|
|
373
|
+
if (a.kind !== 'operation')
|
|
374
|
+
continue;
|
|
375
|
+
const src = HOST_SOURCES[kind];
|
|
376
|
+
const ok = a.op === 'accounts.service.import'
|
|
377
|
+
? Boolean(src.fromPc || src.fromFile)
|
|
378
|
+
: a.op === 'accounts.service.login'
|
|
379
|
+
? Boolean(src.fromAnswers)
|
|
380
|
+
: ['accounts.service.logout', 'settings.set'].includes(a.op);
|
|
381
|
+
t.check(ok, kind + ': "' + a.label + '" is something this kind can do');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// The paths themselves, through the menu's questions (fakeIO): a file, typed, forgotten.
|
|
385
|
+
const envFile = t.write(t.at('svc-page', 'keys.env'), 'PERPLEXITY_API_KEY=pplx-PlantedKey0123456789\n');
|
|
386
|
+
const fileIO = fakeIO([envFile, true]);
|
|
387
|
+
const fromFile = await importServiceFlow(fileIO.io, 'perplexity', undefined, { store: pageStore, pc: pcNone, share: true }, true);
|
|
388
|
+
t.check(pageStore.info('perplexity')?.source === 'file' &&
|
|
389
|
+
fileIO.seen.questions[0].kind === 'path' &&
|
|
390
|
+
!fromFile.message.includes('PlantedKey'), 'Import from a file asks for the file even when this PC could give one, and saves it');
|
|
391
|
+
await logoutServiceFlow(fakeIO([true]).io, 'perplexity', t.dir('svc-page-root'), pageStore, pageLeases);
|
|
392
|
+
const pcWithKey = { ...pcNone, env: { PERPLEXITY_API_KEY: 'pplx-FromThisPc0123456789' } };
|
|
393
|
+
const keyRow = rowOn('perplexity', pcWithKey);
|
|
394
|
+
t.check(keyRow.onPc === true, 'the API key in this terminal is found on this PC');
|
|
395
|
+
await importServiceFlow(fakeIO([true]).io, 'perplexity', undefined, {
|
|
396
|
+
store: pageStore,
|
|
397
|
+
pc: pcWithKey,
|
|
398
|
+
share: true,
|
|
399
|
+
});
|
|
400
|
+
t.check(pageStore.info('perplexity')?.source === 'pc', "Take this PC's sign-in saves the PC's key");
|
|
401
|
+
await loginServiceFlow(fakeIO(['pplx-Typed0123456789']).io, 'perplexity', { store: pageStore, share: true });
|
|
402
|
+
t.check(pageStore.info('perplexity')?.source === 'typed', 'Type the sign-in saves what was typed');
|
|
403
|
+
const savedRow = rowOn('perplexity', pcWithKey);
|
|
404
|
+
t.check(labelsOf(savedRow).includes('Forget…') && savedRow.state === 'saved', 'a saved sign-in offers Forget');
|
|
405
|
+
await t.throws(() => importServiceFlow(fakeIO([true]).io, 'comet', undefined, { store: pageStore, pc: pcWithKey, share: true }), 'inside a sandbox', 'Comet has nothing to import: the error says to sign in inside a sandbox');
|
|
331
406
|
},
|
|
332
407
|
};
|
|
@@ -62,6 +62,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
62
62
|
'sandbox/csharp/AppActivation.cs',
|
|
63
63
|
'sandbox/csharp/BindMount.cs',
|
|
64
64
|
'sandbox/csharp/CargoShim.cs',
|
|
65
|
+
'sandbox/csharp/ClipboardWatch.cs',
|
|
65
66
|
'sandbox/csharp/Desktop.cs',
|
|
66
67
|
'sandbox/csharp/EnvBroadcast.cs',
|
|
67
68
|
'sandbox/csharp/GitShim.cs',
|
|
@@ -95,6 +96,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
95
96
|
'sandbox/scripts/set-machine-environment.ps1',
|
|
96
97
|
'sandbox/scripts/set-registry-values.ps1',
|
|
97
98
|
'sandbox/scripts/start-store-app.ps1',
|
|
99
|
+
'sandbox/scripts/watch-clipboard.ps1',
|
|
98
100
|
'sandbox/templates/fivem/README.txt',
|
|
99
101
|
'sandbox/templates/fivem/cs/Client/Client.csproj',
|
|
100
102
|
'sandbox/templates/fivem/cs/Client/ClientMain.cs',
|