hop2 0.11.21 → 0.11.23
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/hay/apps/server/src/rooms.ts +9 -7
- package/hay/apps/web/src/App.tsx +57 -4
- package/hay/apps/web/src/components/SessionSwitcher.tsx +6 -4
- package/hay/apps/web/src/utils/scrollFlywheel.ts +177 -20
- package/hay/apps/web/test/scrollFlywheel.test.ts +212 -0
- package/hay-web/assets/index-COQpy7gK.js +134 -0
- package/hay-web/assets/index-COQpy7gK.js.map +1 -0
- package/hay-web/assets/index-CXZrhZBl.js +134 -0
- package/hay-web/assets/index-CXZrhZBl.js.map +1 -0
- package/hay-web/assets/index-Chtgn1Ot.js +134 -0
- package/hay-web/assets/index-Chtgn1Ot.js.map +1 -0
- package/hay-web/assets/index-DcF33Fu1.js +134 -0
- package/hay-web/assets/index-DcF33Fu1.js.map +1 -0
- package/hay-web/assets/index-JN4pNZh2.js +134 -0
- package/hay-web/assets/index-JN4pNZh2.js.map +1 -0
- package/hay-web/assets/index-tyqviMzG.js +134 -0
- package/hay-web/assets/index-tyqviMzG.js.map +1 -0
- package/hay-web/index.html +1 -1
- package/hop +177 -47
- package/package.json +1 -1
- package/scripts/hay-host.js +32 -1
- package/scripts/salvage-and-restart-host.js +6 -1
- package/tests/terminal-api.integration.test.cjs +129 -27
|
@@ -491,9 +491,10 @@ test('rename sticks across reconcile ticks and never manufactures a session', as
|
|
|
491
491
|
name: 'RenameOrigin', type: 'terminal', cwd: tempDir, startRuntime: true
|
|
492
492
|
});
|
|
493
493
|
assert.equal(create.status, 200);
|
|
494
|
+
const originId = create.data.internalName;
|
|
494
495
|
|
|
495
496
|
const renamed = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename', {
|
|
496
|
-
oldName: 'RenameOrigin', newName: 'RenameTarget', internalName:
|
|
497
|
+
oldName: 'RenameOrigin', newName: 'RenameTarget', internalName: originId
|
|
497
498
|
});
|
|
498
499
|
assert.equal(renamed.status, 200);
|
|
499
500
|
|
|
@@ -502,7 +503,7 @@ test('rename sticks across reconcile ticks and never manufactures a session', as
|
|
|
502
503
|
for (let i = 0; i < 4; i++) {
|
|
503
504
|
await delay(1200);
|
|
504
505
|
const list = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
505
|
-
const entry = list.data.sessions.find(s => s.internalName ===
|
|
506
|
+
const entry = list.data.sessions.find(s => s.internalName === originId);
|
|
506
507
|
seen.push(entry ? entry.displayName : '(missing)');
|
|
507
508
|
}
|
|
508
509
|
assert.deepEqual(seen, ['RenameTarget', 'RenameTarget', 'RenameTarget', 'RenameTarget'],
|
|
@@ -513,12 +514,12 @@ test('rename sticks across reconcile ticks and never manufactures a session', as
|
|
|
513
514
|
const after = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
514
515
|
const byTarget = after.data.sessions.filter(s => s.displayName === 'RenameTarget');
|
|
515
516
|
assert.equal(byTarget.length, 1, 'exactly one session may hold the new name');
|
|
516
|
-
assert.equal(byTarget[0].internalName,
|
|
517
|
-
assert.ok(!after.data.sessions.some(s => s.
|
|
518
|
-
'no phantom session may be created under the
|
|
517
|
+
assert.equal(byTarget[0].internalName, originId, 'the ORIGINAL session must be the one wearing it');
|
|
518
|
+
assert.ok(!after.data.sessions.some(s => s.displayName === 'RenameOrigin'),
|
|
519
|
+
'no phantom session may be created under the old name');
|
|
519
520
|
|
|
520
521
|
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete',
|
|
521
|
-
{ name: 'RenameTarget', internalName:
|
|
522
|
+
{ name: 'RenameTarget', internalName: originId });
|
|
522
523
|
});
|
|
523
524
|
|
|
524
525
|
// Regression: connecting by a name nothing owns must REFUSE, not create.
|
|
@@ -561,36 +562,38 @@ test('websocket attach to an unknown session refuses instead of creating one', a
|
|
|
561
562
|
// with 409 "Session name already in use", with no way to free the name short
|
|
562
563
|
// of restarting the daemon.
|
|
563
564
|
test('a display name freed by a later rename can be reused', async () => {
|
|
564
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
565
|
+
const one = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
565
566
|
{ name: 'AliasOne', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
566
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
567
|
+
const two = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
567
568
|
{ name: 'AliasTwo', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
569
|
+
const oneId = one.data.internalName;
|
|
570
|
+
const twoId = two.data.internalName;
|
|
568
571
|
await delay(600);
|
|
569
572
|
|
|
570
573
|
// AliasOne: Freed → Final, leaving the alias "Freed" behind.
|
|
571
574
|
for (const [from, to] of [['AliasOne', 'Freed'], ['Freed', 'Final']]) {
|
|
572
575
|
const r = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename',
|
|
573
|
-
{ oldName: from, newName: to, internalName:
|
|
576
|
+
{ oldName: from, newName: to, internalName: oneId });
|
|
574
577
|
assert.equal(r.status, 200, `rename ${from}→${to} failed: ${JSON.stringify(r.data)}`);
|
|
575
578
|
}
|
|
576
579
|
|
|
577
580
|
// "Freed" is now worn by nobody, so another session may take it.
|
|
578
581
|
const reuse = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename',
|
|
579
|
-
{ oldName: 'AliasTwo', newName: 'Freed', internalName:
|
|
582
|
+
{ oldName: 'AliasTwo', newName: 'Freed', internalName: twoId });
|
|
580
583
|
assert.equal(reuse.status, 200, `a freed name must be reusable, got: ${JSON.stringify(reuse.data)}`);
|
|
581
584
|
|
|
582
585
|
const list = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
583
586
|
const freed = list.data.sessions.filter(s => s.displayName === 'Freed');
|
|
584
587
|
assert.equal(freed.length, 1, 'exactly one session may wear the reused name');
|
|
585
|
-
assert.equal(freed[0].internalName,
|
|
588
|
+
assert.equal(freed[0].internalName, twoId);
|
|
586
589
|
|
|
587
590
|
// A name a LIVE session still wears stays protected.
|
|
588
591
|
const collide = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename',
|
|
589
|
-
{ oldName: 'Final', newName: 'Freed', internalName:
|
|
592
|
+
{ oldName: 'Final', newName: 'Freed', internalName: oneId });
|
|
590
593
|
assert.equal(collide.status, 409, 'a name in active use must still be refused');
|
|
591
594
|
|
|
592
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'Final', internalName:
|
|
593
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'Freed', internalName:
|
|
595
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'Final', internalName: oneId });
|
|
596
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'Freed', internalName: twoId });
|
|
594
597
|
});
|
|
595
598
|
|
|
596
599
|
// Regression: folder membership must survive every cache rebuild. The store
|
|
@@ -600,8 +603,9 @@ test('a display name freed by a later rename can be reused', async () => {
|
|
|
600
603
|
// the live-rooms loop) already knew. Nine of eleven folder assignments
|
|
601
604
|
// vanished from the UI after a host cycle while every meta on disk was right.
|
|
602
605
|
test('folder membership survives restore re-registration and a daemon restart', async () => {
|
|
603
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
606
|
+
const created = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
604
607
|
{ name: 'FolderKeeper', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
608
|
+
const keeperId = created.data.internalName;
|
|
605
609
|
await delay(600);
|
|
606
610
|
const folder = await requestJson(state.port, state.sessionSecret, 'POST', '/api/folders', { name: 'KeeperShelf' });
|
|
607
611
|
assert.equal(folder.status, 200, `folder create failed: ${JSON.stringify(folder.data)}`);
|
|
@@ -611,7 +615,7 @@ test('folder membership survives restore re-registration and a daemon restart',
|
|
|
611
615
|
assert.equal(moved.status, 200, `move failed: ${JSON.stringify(moved.data)}`);
|
|
612
616
|
|
|
613
617
|
const listedNow = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
614
|
-
assert.equal(listedNow.data.sessions.find(s => s.internalName ===
|
|
618
|
+
assert.equal(listedNow.data.sessions.find(s => s.internalName === keeperId)?.folderId, folderId,
|
|
615
619
|
'freshly moved session must list its folder');
|
|
616
620
|
|
|
617
621
|
// Restore re-registration recreates cached session definitions — the write
|
|
@@ -619,25 +623,26 @@ test('folder membership survives restore re-registration and a daemon restart',
|
|
|
619
623
|
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/restore', {});
|
|
620
624
|
await delay(1200);
|
|
621
625
|
const afterRestore = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
622
|
-
assert.equal(afterRestore.data.sessions.find(s => s.internalName ===
|
|
626
|
+
assert.equal(afterRestore.data.sessions.find(s => s.internalName === keeperId)?.folderId, folderId,
|
|
623
627
|
'folder membership must survive restore re-registration');
|
|
624
628
|
|
|
625
629
|
const previousState = state;
|
|
626
630
|
await stopDaemon(true);
|
|
627
631
|
await launchDaemon(previousState);
|
|
628
632
|
const afterRestart = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
629
|
-
assert.equal(afterRestart.data.sessions.find(s => s.internalName ===
|
|
633
|
+
assert.equal(afterRestart.data.sessions.find(s => s.internalName === keeperId)?.folderId, folderId,
|
|
630
634
|
'folder membership must survive a daemon restart');
|
|
631
635
|
|
|
632
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'FolderKeeper', internalName:
|
|
636
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'FolderKeeper', internalName: keeperId });
|
|
633
637
|
});
|
|
634
638
|
|
|
635
639
|
// Session names resolve case-insensitively everywhere a name addresses a
|
|
636
640
|
// session: exact-case wearers win, a UNIQUE folded match resolves, and two
|
|
637
641
|
// sessions differing only by case stay exact-only rather than guessed at.
|
|
638
642
|
test('session names resolve case-insensitively', async () => {
|
|
639
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
643
|
+
const caseCreate = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
640
644
|
{ name: 'CaseFold', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
645
|
+
const caseId = caseCreate.data.internalName;
|
|
641
646
|
await delay(600);
|
|
642
647
|
|
|
643
648
|
// A by-name API call accepts any casing — no internalName hint given.
|
|
@@ -651,20 +656,20 @@ test('session names resolve case-insensitively', async () => {
|
|
|
651
656
|
assert.equal(caseOnly.status, 200, `case-only rename must be allowed: ${JSON.stringify(caseOnly.data)}`);
|
|
652
657
|
|
|
653
658
|
const list = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
654
|
-
const worn = list.data.sessions.filter(s => s.internalName ===
|
|
659
|
+
const worn = list.data.sessions.filter(s => s.internalName === caseId);
|
|
655
660
|
assert.equal(worn.length, 1, 'still exactly one session — folded lookups must never mint twins');
|
|
656
661
|
assert.equal(worn[0].displayName, 'casekept', 'the case-only rename is the worn spelling');
|
|
657
662
|
|
|
658
663
|
// A name another session wears — in ANY casing — is a collision.
|
|
659
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
664
|
+
const other = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions',
|
|
660
665
|
{ name: 'CaseOther', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
661
666
|
await delay(400);
|
|
662
667
|
const collide = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename',
|
|
663
|
-
{ oldName: 'CaseOther', newName: 'CASEKEPT', internalName:
|
|
668
|
+
{ oldName: 'CaseOther', newName: 'CASEKEPT', internalName: other.data.internalName });
|
|
664
669
|
assert.equal(collide.status, 409, 'a differently-cased spelling of a worn name must be refused');
|
|
665
670
|
|
|
666
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'casekept', internalName:
|
|
667
|
-
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'CaseOther', internalName:
|
|
671
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'casekept', internalName: caseId });
|
|
672
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'CaseOther', internalName: other.data.internalName });
|
|
668
673
|
});
|
|
669
674
|
|
|
670
675
|
// Regression: an undeclared Bearer-token creation is automation, not a human.
|
|
@@ -676,7 +681,7 @@ test('bare token API creations default to agent origin', async () => {
|
|
|
676
681
|
{ name: 'BareTokenProbe', type: 'terminal', cwd: tempDir, startRuntime: true });
|
|
677
682
|
await delay(400);
|
|
678
683
|
const list = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
679
|
-
const s = list.data.sessions.find(x => x.
|
|
684
|
+
const s = list.data.sessions.find(x => x.displayName === 'BareTokenProbe');
|
|
680
685
|
assert.ok(s, 'session exists');
|
|
681
686
|
assert.equal(s.createdBy, 'agent', `undeclared token caller must be agent, got ${s.createdBy}`);
|
|
682
687
|
|
|
@@ -686,7 +691,7 @@ test('bare token API creations default to agent origin', async () => {
|
|
|
686
691
|
{ 'X-Hop-Via': 'cli' });
|
|
687
692
|
await delay(400);
|
|
688
693
|
const list2 = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
689
|
-
const s2 = list2.data.sessions.find(x => x.
|
|
694
|
+
const s2 = list2.data.sessions.find(x => x.displayName === 'CliUserProbe');
|
|
690
695
|
assert.equal(s2 && s2.createdBy, 'user', 'CLI-stamped caller stays user');
|
|
691
696
|
|
|
692
697
|
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: 'BareTokenProbe', internalName: 'BareTokenProbe' });
|
|
@@ -919,3 +924,100 @@ test('a user attach stamps lastUserSeenAt; a monitor tile does not', async () =>
|
|
|
919
924
|
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete',
|
|
920
925
|
{ name: 'seenprobe', internalName: internal });
|
|
921
926
|
});
|
|
927
|
+
|
|
928
|
+
// Identity is minted, names are labels. A session's internal id is opaque
|
|
929
|
+
// (s_hex), never derived from the display name, so a rename touches only
|
|
930
|
+
// the label — and a display name that some renamed-away session was BORN
|
|
931
|
+
// with is free for reuse (deriving ids used to make that collide).
|
|
932
|
+
test('sessions carry an opaque invariant id; renames touch only the label', async () => {
|
|
933
|
+
const create = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions', {
|
|
934
|
+
name: 'projectx', type: 'terminal', startRuntime: true, cwd: tempDir
|
|
935
|
+
});
|
|
936
|
+
assert.equal(create.status, 200);
|
|
937
|
+
assert.equal(create.data.displayName, 'projectx');
|
|
938
|
+
assert.match(create.data.internalName, /^s_[0-9a-f]{10}$/,
|
|
939
|
+
`id must be minted, got ${create.data.internalName}`);
|
|
940
|
+
const id = create.data.internalName;
|
|
941
|
+
await delay(400);
|
|
942
|
+
|
|
943
|
+
// Rename: same id, new label, and the entry the clients render says so.
|
|
944
|
+
const rename = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/rename', {
|
|
945
|
+
oldName: 'projectx', newName: 'projecty'
|
|
946
|
+
});
|
|
947
|
+
assert.equal(rename.status, 200, JSON.stringify(rename.data));
|
|
948
|
+
await delay(400);
|
|
949
|
+
const list = await requestJson(state.port, state.sessionSecret, 'GET', '/api/sessions');
|
|
950
|
+
const entry = list.data.sessions.find(s => s.internalName === id);
|
|
951
|
+
assert.ok(entry, 'the invariant id survives the rename');
|
|
952
|
+
assert.equal(entry.displayName, 'projecty');
|
|
953
|
+
assert.equal(entry.name, 'projecty', 'every user-facing name field is the display name');
|
|
954
|
+
|
|
955
|
+
// The freed display name is genuinely free — the latent collision.
|
|
956
|
+
const reuse = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions', {
|
|
957
|
+
name: 'projectx', type: 'terminal', startRuntime: true, cwd: tempDir
|
|
958
|
+
});
|
|
959
|
+
assert.equal(reuse.status, 200, `freed name must be reusable: ${JSON.stringify(reuse.data)}`);
|
|
960
|
+
assert.notEqual(reuse.data.internalName, id, 'the new session is its own identity');
|
|
961
|
+
|
|
962
|
+
for (const s of [id, reuse.data.internalName]) {
|
|
963
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete',
|
|
964
|
+
{ name: s, internalName: s });
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Restore's cwd arbiter must not trust a durable meta whose cwd sits in the
|
|
969
|
+
// transcript store. The meta is normally the room's last LIVE directory
|
|
970
|
+
// (the arbiter that catches a foreign claude's record) — but a shell left
|
|
971
|
+
// standing in ~/.claude/projects/* poisoned it, the arbiter then fired
|
|
972
|
+
// against the CORRECT record, and Accessibility-fork restored as a bare zsh
|
|
973
|
+
// in the transcript dir with its conversation gone.
|
|
974
|
+
test('restore does not let a transcript-store meta cwd override a good record', async () => {
|
|
975
|
+
const create = await requestJson(state.port, state.sessionSecret, 'POST', '/api/terminals', {
|
|
976
|
+
name: 'metapoison', cwd: tempDir
|
|
977
|
+
}, agentHeaders);
|
|
978
|
+
assert.equal(create.status, 200);
|
|
979
|
+
const internal = create.data.sessionName;
|
|
980
|
+
const dir = path.join(hopHome, 'claude-sessions');
|
|
981
|
+
await fs.mkdir(dir, { recursive: true });
|
|
982
|
+
// Good record: a real conversation, in the real workspace.
|
|
983
|
+
await fs.writeFile(path.join(dir, `${internal}.json`), JSON.stringify({
|
|
984
|
+
sessionId: '22222222-3333-4444-5555-666666666666', cwd: tempDir,
|
|
985
|
+
launchCmd: 'claude', updatedAt: new Date().toISOString()
|
|
986
|
+
}), { mode: 0o600 });
|
|
987
|
+
// Poisoned meta: cwd inside a transcript store.
|
|
988
|
+
const meta = JSON.parse(await fs.readFile(path.join(dir, `${internal}.meta`), 'utf8').catch(() => '{}'));
|
|
989
|
+
await fs.writeFile(path.join(dir, `${internal}.meta`), JSON.stringify({
|
|
990
|
+
...meta, internalName: internal, cwd: path.join(os.homedir(), '.claude', 'projects', '-Users-x')
|
|
991
|
+
}), { mode: 0o600 });
|
|
992
|
+
// Make the transcript "exist" where the record says it lives.
|
|
993
|
+
const tdir = path.join(os.homedir(), '.claude', 'projects', tempDir.replace(/[^A-Za-z0-9]/g, '-'));
|
|
994
|
+
await fs.mkdir(tdir, { recursive: true });
|
|
995
|
+
const tfile = path.join(tdir, '22222222-3333-4444-5555-666666666666.jsonl');
|
|
996
|
+
await fs.writeFile(tfile, '{"type":"user"}\n');
|
|
997
|
+
|
|
998
|
+
const plan = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/restore', { dryRun: true });
|
|
999
|
+
assert.equal(plan.status, 200, JSON.stringify(plan.data));
|
|
1000
|
+
const mine = (plan.data.planned || plan.data.sessions || []).find(p => p.internalName === internal);
|
|
1001
|
+
// It is live, so restore skips it — instead exercise the planner through
|
|
1002
|
+
// a stopped copy: delete the runtime, keep the records, plan again.
|
|
1003
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete', { name: internal, internalName: internal });
|
|
1004
|
+
await fs.writeFile(path.join(dir, `${internal}.json`), JSON.stringify({
|
|
1005
|
+
sessionId: '22222222-3333-4444-5555-666666666666', cwd: tempDir,
|
|
1006
|
+
launchCmd: 'claude', updatedAt: new Date().toISOString()
|
|
1007
|
+
}), { mode: 0o600 });
|
|
1008
|
+
await fs.writeFile(path.join(dir, `${internal}.meta`), JSON.stringify({
|
|
1009
|
+
internalName: internal, displayName: 'metapoison',
|
|
1010
|
+
cwd: path.join(os.homedir(), '.claude', 'projects', '-Users-x')
|
|
1011
|
+
}), { mode: 0o600 });
|
|
1012
|
+
const plan2 = await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/restore', { dryRun: true });
|
|
1013
|
+
const p2 = (plan2.data.restored || []).find(p => p.name === 'metapoison');
|
|
1014
|
+
assert.ok(p2, `planned: ${JSON.stringify(plan2.data).slice(0, 300)}`);
|
|
1015
|
+
// The poisoned meta must not have demoted this to a plain shell.
|
|
1016
|
+
assert.equal(p2.warning, null, `no wrong-conversation warning, got: ${p2.warning}`);
|
|
1017
|
+
assert.match(String(p2.command || ''), /--resume 22222222/, `must resume the conversation, got ${p2.command}`);
|
|
1018
|
+
void mine;
|
|
1019
|
+
|
|
1020
|
+
await fs.rm(tfile, { force: true });
|
|
1021
|
+
await fs.rm(path.join(dir, `${internal}.json`), { force: true });
|
|
1022
|
+
await fs.rm(path.join(dir, `${internal}.meta`), { force: true });
|
|
1023
|
+
});
|