@dotdrelle/wiki-manager 0.8.2 → 0.10.4

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.
@@ -1,7 +1,51 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
+ import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
3
4
  import { startRuntimeServer } from './server.js';
4
5
 
6
+ test('runtime server checks bearer and x-runtime-token credentials', async (t) => {
7
+ let handle;
8
+ try {
9
+ handle = await startRuntimeServer({
10
+ host: '127.0.0.1',
11
+ port: 0,
12
+ token: 'runtime-secret',
13
+ store: {
14
+ dbPath: ':memory:',
15
+ getState: () => ({ status: 'idle' }),
16
+ listEvents: () => [],
17
+ },
18
+ session: {},
19
+ run: async () => {},
20
+ });
21
+ } catch (err) {
22
+ if (err?.code === 'EPERM') {
23
+ t.skip('network listen is not permitted in this sandbox');
24
+ return;
25
+ }
26
+ throw err;
27
+ }
28
+
29
+ try {
30
+ const url = `http://127.0.0.1:${handle.port}/health`;
31
+
32
+ const missing = await fetch(url);
33
+ assert.equal(missing.status, 401);
34
+
35
+ const bearer = await fetch(url, {
36
+ headers: { authorization: 'Bearer runtime-secret' },
37
+ });
38
+ assert.equal(bearer.status, 200);
39
+
40
+ const legacy = await fetch(url, {
41
+ headers: { 'x-runtime-token': 'runtime-secret' },
42
+ });
43
+ assert.equal(legacy.status, 200);
44
+ } finally {
45
+ await handle.close();
46
+ }
47
+ });
48
+
5
49
  test('runtime server accepts only one active run', async (t) => {
6
50
  let releaseRun;
7
51
  let runCount = 0;
@@ -102,6 +146,58 @@ test('runtime server returns the accepted run id and passes it to the runner', a
102
146
  }
103
147
  });
104
148
 
149
+ test('runtime server state exposes active run identity while running', async (t) => {
150
+ let releaseRun;
151
+ const context = {
152
+ workspace: 'juno',
153
+ session: { workspace: 'juno' },
154
+ running: false,
155
+ currentAbortController: null,
156
+ currentRunId: null,
157
+ };
158
+ let acceptedRun;
159
+ let handle;
160
+ try {
161
+ handle = await startRuntimeServer({
162
+ host: '127.0.0.1',
163
+ port: 0,
164
+ store: {
165
+ dbPath: ':memory:',
166
+ getState: () => ({ status: 'idle', plan: [] }),
167
+ listEvents: () => [],
168
+ },
169
+ getContext: async () => context,
170
+ run: async () => {
171
+ await new Promise((resolve) => { releaseRun = resolve; });
172
+ },
173
+ });
174
+ } catch (err) {
175
+ if (err?.code === 'EPERM') {
176
+ t.skip('network listen is not permitted in this sandbox');
177
+ return;
178
+ }
179
+ throw err;
180
+ }
181
+
182
+ try {
183
+ const runResponse = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=juno`, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ input: 'build' }),
187
+ });
188
+ acceptedRun = await runResponse.json();
189
+ const stateResponse = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=juno`);
190
+ const state = await stateResponse.json();
191
+ assert.equal(state.status, 'running');
192
+ assert.equal(state.running, true);
193
+ assert.equal(state.runId, acceptedRun.runId);
194
+ assert.equal(state.workspace, 'juno');
195
+ } finally {
196
+ releaseRun?.();
197
+ await handle.close();
198
+ }
199
+ });
200
+
105
201
  test('runtime server isolates active runs by workspace', async (t) => {
106
202
  const releases = new Map();
107
203
  const runWorkspaces = [];
@@ -222,6 +318,52 @@ test('runtime server filters state and events by workspace', async (t) => {
222
318
  }
223
319
  });
224
320
 
321
+ test('runtime server exposes a correlated audit trail endpoint', async (t) => {
322
+ let auditArgs = null;
323
+ let handle;
324
+ try {
325
+ handle = await startRuntimeServer({
326
+ host: '127.0.0.1',
327
+ port: 0,
328
+ store: {
329
+ dbPath: ':memory:',
330
+ getState: () => ({ status: 'idle' }),
331
+ listEvents: () => [],
332
+ listAuditTrail: (options) => {
333
+ auditArgs = options;
334
+ return [{ sequence: 1, type: 'tool_call_started', runId: options.runId, taskId: 'task-a' }];
335
+ },
336
+ },
337
+ getContext: async (workspace) => ({
338
+ workspace,
339
+ session: { workspace },
340
+ running: false,
341
+ currentAbortController: null,
342
+ }),
343
+ run: async () => {},
344
+ });
345
+ } catch (err) {
346
+ if (err?.code === 'EPERM') {
347
+ t.skip('network listen is not permitted in this sandbox');
348
+ return;
349
+ }
350
+ throw err;
351
+ }
352
+
353
+ try {
354
+ const response = await fetch(`http://127.0.0.1:${handle.port}/audit?workspace=docs&runId=run-1`);
355
+ assert.equal(response.status, 200);
356
+ const body = await response.json();
357
+ assert.equal(body.ok, true);
358
+ assert.equal(body.workspace, 'docs');
359
+ assert.equal(body.runId, 'run-1');
360
+ assert.deepEqual(body.audit, [{ sequence: 1, type: 'tool_call_started', runId: 'run-1', taskId: 'task-a' }]);
361
+ assert.deepEqual(auditArgs, { workspace: 'docs', runId: 'run-1' });
362
+ } finally {
363
+ await handle.close();
364
+ }
365
+ });
366
+
225
367
  test('runtime server exposes manual resume endpoint', async (t) => {
226
368
  let resumedWorkspace = null;
227
369
  let handle;
@@ -306,3 +448,522 @@ test('runtime server exposes approval endpoint', async (t) => {
306
448
  await handle.close();
307
449
  }
308
450
  });
451
+
452
+ test('runtime server exposes control status and explanation', async (t) => {
453
+ const session = {
454
+ workspace: 'juno',
455
+ controlQueue: [{ id: 'control-1', workspace: 'juno', status: 'queued', input: 'later' }],
456
+ };
457
+ let handle;
458
+ try {
459
+ handle = await startRuntimeServer({
460
+ host: '127.0.0.1',
461
+ port: 0,
462
+ store: {
463
+ dbPath: ':memory:',
464
+ getState: () => ({
465
+ status: 'idle',
466
+ plan: [{ step: 1, description: 'Check status', status: 'pending' }],
467
+ queue: [],
468
+ controlQueue: session.controlQueue,
469
+ approvals: [],
470
+ summary: null,
471
+ }),
472
+ listEvents: () => [],
473
+ },
474
+ getContext: async () => ({
475
+ workspace: 'juno',
476
+ session,
477
+ running: false,
478
+ currentAbortController: null,
479
+ }),
480
+ run: async () => {},
481
+ });
482
+ } catch (err) {
483
+ if (err?.code === 'EPERM') {
484
+ t.skip('network listen is not permitted in this sandbox');
485
+ return;
486
+ }
487
+ throw err;
488
+ }
489
+
490
+ try {
491
+ const status = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`);
492
+ assert.equal(status.status, 200);
493
+ const statusBody = await status.json();
494
+ assert.equal(statusBody.status, 'idle');
495
+ assert.equal(statusBody.running, false);
496
+ assert.equal(statusBody.controlQueue[0].id, 'control-1');
497
+
498
+ const explain = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
499
+ method: 'POST',
500
+ headers: { 'Content-Type': 'application/json' },
501
+ body: JSON.stringify({ action: 'explain' }),
502
+ });
503
+ assert.equal(explain.status, 200);
504
+ assert.match((await explain.json()).explanation, /control request/);
505
+ } finally {
506
+ await handle.close();
507
+ }
508
+ });
509
+
510
+ test('runtime server control enqueue emits events but does not patch an active plan or start a run', async (t) => {
511
+ const session = {
512
+ workspace: 'juno',
513
+ controlQueue: [],
514
+ };
515
+ const events = [];
516
+ session._onAgentEvent = (event) => events.push(event);
517
+ let runCount = 0;
518
+ let handle;
519
+ try {
520
+ handle = await startRuntimeServer({
521
+ host: '127.0.0.1',
522
+ port: 0,
523
+ store: {
524
+ dbPath: ':memory:',
525
+ getState: () => ({
526
+ status: 'running',
527
+ plan: [{ step: 1, description: 'Active step', status: 'running' }],
528
+ queue: [],
529
+ controlQueue: session.controlQueue,
530
+ approvals: [],
531
+ summary: null,
532
+ }),
533
+ listEvents: () => [],
534
+ },
535
+ getContext: async () => ({
536
+ workspace: 'juno',
537
+ session,
538
+ running: true,
539
+ currentAbortController: new AbortController(),
540
+ }),
541
+ run: async () => { runCount += 1; },
542
+ });
543
+ } catch (err) {
544
+ if (err?.code === 'EPERM') {
545
+ t.skip('network listen is not permitted in this sandbox');
546
+ return;
547
+ }
548
+ throw err;
549
+ }
550
+
551
+ try {
552
+ const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
553
+ method: 'POST',
554
+ headers: { 'Content-Type': 'application/json' },
555
+ body: JSON.stringify({ action: 'enqueue', input: 'run this after current work' }),
556
+ });
557
+ assert.equal(response.status, 202);
558
+ const body = await response.json();
559
+ assert.equal(body.accepted, true);
560
+ assert.equal(body.item.status, 'queued');
561
+ assert.equal(body.controlQueue.length, 1);
562
+ assert.equal(body.plan[0].description, 'Active step');
563
+ assert.equal(session.controlQueue[0].input, 'run this after current work');
564
+ assert.equal(events.filter((event) => event.type === 'control_enqueued').length, 1);
565
+ assert.equal(runCount, 0);
566
+ } finally {
567
+ await handle.close();
568
+ }
569
+ });
570
+
571
+ test('runtime server control message observes an active run without enqueueing', async (t) => {
572
+ const session = {
573
+ workspace: 'juno',
574
+ controlQueue: [],
575
+ };
576
+ let runCount = 0;
577
+ let handle;
578
+ try {
579
+ handle = await startRuntimeServer({
580
+ host: '127.0.0.1',
581
+ port: 0,
582
+ store: {
583
+ dbPath: ':memory:',
584
+ getState: () => ({
585
+ status: 'running',
586
+ plan: [{ step: 1, description: 'Build documents', status: 'running' }],
587
+ queue: [],
588
+ approvals: [],
589
+ summary: null,
590
+ }),
591
+ listEvents: () => [],
592
+ },
593
+ getContext: async () => ({
594
+ workspace: 'juno',
595
+ session,
596
+ running: true,
597
+ currentAbortController: new AbortController(),
598
+ }),
599
+ run: async () => { runCount += 1; },
600
+ });
601
+ } catch (err) {
602
+ if (err?.code === 'EPERM') {
603
+ t.skip('network listen is not permitted in this sandbox');
604
+ return;
605
+ }
606
+ throw err;
607
+ }
608
+
609
+ try {
610
+ const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
611
+ method: 'POST',
612
+ headers: { 'Content-Type': 'application/json' },
613
+ body: JSON.stringify({ action: 'message', input: 'Où en est le build ?' }),
614
+ });
615
+ assert.equal(response.status, 200);
616
+ const body = await response.json();
617
+ assert.equal(body.kind, 'observe');
618
+ assert.match(body.explanation, /Build documents/);
619
+ assert.equal(session.controlQueue.length, 0);
620
+ assert.equal(runCount, 0);
621
+ } finally {
622
+ await handle.close();
623
+ }
624
+ });
625
+
626
+ test('runtime server control message records active plan mutation as a proposal', async (t) => {
627
+ const session = {
628
+ workspace: 'juno',
629
+ controlQueue: [],
630
+ };
631
+ dispatchAgentEvent(session, createAgentEvent('run_started', {
632
+ origin: 'runtime',
633
+ runId: 'run-mutate',
634
+ workspace: 'juno',
635
+ }));
636
+ dispatchAgentEvent(session, createAgentEvent('plan_set', {
637
+ origin: 'tool',
638
+ runId: 'run-mutate',
639
+ workspace: 'juno',
640
+ payload: {
641
+ steps: [{ step: 1, id: 'generate', description: 'Generate', status: 'running' }],
642
+ },
643
+ }));
644
+ let runCount = 0;
645
+ let handle;
646
+ try {
647
+ handle = await startRuntimeServer({
648
+ host: '127.0.0.1',
649
+ port: 0,
650
+ store: {
651
+ dbPath: ':memory:',
652
+ getState: () => ({
653
+ ...session.agentProjection,
654
+ status: session.agentProjection?.status ?? 'running',
655
+ queue: [],
656
+ runs: [{ id: 'run-mutate', workspace: 'juno', status: 'running' }],
657
+ runId: 'run-mutate',
658
+ }),
659
+ listEvents: () => [],
660
+ },
661
+ getContext: async () => ({
662
+ workspace: 'juno',
663
+ session,
664
+ running: true,
665
+ currentAbortController: new AbortController(),
666
+ currentRunId: 'run-mutate',
667
+ }),
668
+ run: async () => { runCount += 1; },
669
+ });
670
+ } catch (err) {
671
+ if (err?.code === 'EPERM') {
672
+ t.skip('network listen is not permitted in this sandbox');
673
+ return;
674
+ }
675
+ throw err;
676
+ }
677
+
678
+ try {
679
+ const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
680
+ method: 'POST',
681
+ headers: { 'Content-Type': 'application/json' },
682
+ body: JSON.stringify({ action: 'message', input: 'Ajoute un envoi après chaque génération' }),
683
+ });
684
+ assert.equal(response.status, 202);
685
+ const body = await response.json();
686
+ assert.equal(body.kind, 'mutate');
687
+ assert.equal(body.proposal.status, 'proposed');
688
+ assert.equal(body.proposal.input, 'Ajoute un envoi après chaque génération');
689
+ assert.equal(session.agentProjection.planPatches[0].status, 'proposed');
690
+ assert.ok(session.agentEvents.some((event) => event.type === 'control_message_received'));
691
+ assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_proposed'));
692
+ const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
693
+ method: 'POST',
694
+ headers: { 'Content-Type': 'application/json' },
695
+ body: JSON.stringify({ action: 'approve_patch', patchId: body.proposal.id }),
696
+ });
697
+ assert.equal(approveResponse.status, 202);
698
+ const approved = await approveResponse.json();
699
+ assert.equal(approved.kind, 'approve_patch');
700
+ // plan_set (revision 0->1) then the approved patch application (1->2).
701
+ assert.equal(session.agentProjection.planRevision, 2);
702
+ assert.deepEqual(session.agentProjection.plan.map((step) => step.id), ['generate', session.agentProjection.plan[1].id]);
703
+ assert.deepEqual(session.agentProjection.plan[1].dependsOn, ['generate']);
704
+ assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_approved'));
705
+ assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_applied'));
706
+ assert.equal(session.controlQueue.length, 0);
707
+ assert.equal(runCount, 0);
708
+ } finally {
709
+ await handle.close();
710
+ }
711
+ });
712
+
713
+ test('runtime server control message reports ambiguity without starting a run', async (t) => {
714
+ const session = {
715
+ workspace: 'juno',
716
+ controlQueue: [],
717
+ };
718
+ let runCount = 0;
719
+ let handle;
720
+ try {
721
+ handle = await startRuntimeServer({
722
+ host: '127.0.0.1',
723
+ port: 0,
724
+ store: {
725
+ dbPath: ':memory:',
726
+ getState: () => ({
727
+ status: 'running',
728
+ plan: [{ step: 1, description: 'Generate', status: 'running' }],
729
+ queue: [],
730
+ approvals: [],
731
+ summary: null,
732
+ }),
733
+ listEvents: () => [],
734
+ },
735
+ getContext: async () => ({
736
+ workspace: 'juno',
737
+ session,
738
+ running: true,
739
+ currentAbortController: new AbortController(),
740
+ }),
741
+ run: async () => { runCount += 1; },
742
+ });
743
+ } catch (err) {
744
+ if (err?.code === 'EPERM') {
745
+ t.skip('network listen is not permitted in this sandbox');
746
+ return;
747
+ }
748
+ throw err;
749
+ }
750
+
751
+ try {
752
+ const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
753
+ method: 'POST',
754
+ headers: { 'Content-Type': 'application/json' },
755
+ body: JSON.stringify({ action: 'message', input: 'Lance aussi la publication' }),
756
+ });
757
+ assert.equal(response.status, 200);
758
+ const body = await response.json();
759
+ assert.equal(body.kind, 'ambiguous');
760
+ assert.equal(body.choices.length, 3);
761
+ assert.equal(session.controlQueue.length, 0);
762
+ assert.equal(runCount, 0);
763
+ } finally {
764
+ await handle.close();
765
+ }
766
+ });
767
+
768
+ test('runtime server drains queued control requests when idle', async (t) => {
769
+ const session = {
770
+ workspace: 'juno',
771
+ controlQueue: [],
772
+ };
773
+ const events = [];
774
+ session._onAgentEvent = (event) => events.push(event);
775
+ let receivedBody = null;
776
+ let handle;
777
+ try {
778
+ handle = await startRuntimeServer({
779
+ host: '127.0.0.1',
780
+ port: 0,
781
+ store: {
782
+ dbPath: ':memory:',
783
+ getState: () => ({
784
+ status: 'idle',
785
+ plan: [],
786
+ queue: [],
787
+ controlQueue: session.controlQueue,
788
+ approvals: [],
789
+ summary: null,
790
+ }),
791
+ listEvents: () => [],
792
+ },
793
+ getContext: async () => ({
794
+ workspace: 'juno',
795
+ session,
796
+ running: false,
797
+ currentAbortController: null,
798
+ }),
799
+ run: async (_context, body) => {
800
+ receivedBody = body;
801
+ },
802
+ });
803
+ } catch (err) {
804
+ if (err?.code === 'EPERM') {
805
+ t.skip('network listen is not permitted in this sandbox');
806
+ return;
807
+ }
808
+ throw err;
809
+ }
810
+
811
+ try {
812
+ const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
813
+ method: 'POST',
814
+ headers: { 'Content-Type': 'application/json' },
815
+ body: JSON.stringify({ action: 'enqueue', input: 'run from control queue' }),
816
+ });
817
+ assert.equal(response.status, 202);
818
+ await new Promise((resolve) => setImmediate(resolve));
819
+ assert.equal(receivedBody.input, 'run from control queue');
820
+ assert.equal(receivedBody.workspace, 'juno');
821
+ assert.match(receivedBody.runId, /^[0-9a-f-]{36}$/);
822
+ assert.equal(session.controlQueue[0].status, 'running');
823
+ assert.equal(session.controlQueue[0].runId, receivedBody.runId);
824
+ assert.deepEqual(events.map((event) => event.type), ['control_enqueued', 'control_started']);
825
+ } finally {
826
+ await handle.close();
827
+ }
828
+ });
829
+
830
+ test('runtime server handle drains a pre-existing hydrated control request', async (t) => {
831
+ const session = {
832
+ workspace: 'juno',
833
+ controlQueue: [{ id: 'control-existing', workspace: 'juno', status: 'queued', input: 'resume queued control' }],
834
+ };
835
+ const events = [];
836
+ session._onAgentEvent = (event) => events.push(event);
837
+ const context = {
838
+ workspace: 'juno',
839
+ session,
840
+ running: false,
841
+ currentAbortController: null,
842
+ };
843
+ let receivedBody = null;
844
+ let handle;
845
+ try {
846
+ handle = await startRuntimeServer({
847
+ host: '127.0.0.1',
848
+ port: 0,
849
+ store: {
850
+ dbPath: ':memory:',
851
+ getState: () => ({ status: 'idle' }),
852
+ listEvents: () => [],
853
+ },
854
+ getContext: async () => context,
855
+ run: async (_context, body) => {
856
+ receivedBody = body;
857
+ },
858
+ });
859
+ } catch (err) {
860
+ if (err?.code === 'EPERM') {
861
+ t.skip('network listen is not permitted in this sandbox');
862
+ return;
863
+ }
864
+ throw err;
865
+ }
866
+
867
+ try {
868
+ assert.equal(handle.drainControl(context), true);
869
+ await new Promise((resolve) => setImmediate(resolve));
870
+ assert.equal(receivedBody.input, 'resume queued control');
871
+ assert.equal(session.controlQueue[0].status, 'running');
872
+ assert.equal(events[0].type, 'control_started');
873
+ } finally {
874
+ await handle.close();
875
+ }
876
+ });
877
+
878
+ test('runtime server exposes config profile list and switch endpoints', async (t) => {
879
+ const context = {
880
+ workspace: 'juno',
881
+ session: { workspace: 'juno', wikirc: { profile: 'default' } },
882
+ running: false,
883
+ currentAbortController: null,
884
+ };
885
+ let switchedProfile = null;
886
+ let handle;
887
+ try {
888
+ handle = await startRuntimeServer({
889
+ host: '127.0.0.1',
890
+ port: 0,
891
+ store: {
892
+ dbPath: ':memory:',
893
+ getState: () => ({ status: 'idle' }),
894
+ listEvents: () => [],
895
+ },
896
+ getContext: async () => context,
897
+ run: async () => {},
898
+ configProfiles: async () => ({ profiles: ['default', 'vpn'], active: context.session.wikirc.profile }),
899
+ useConfigProfile: async (_context, profile) => {
900
+ switchedProfile = profile;
901
+ context.session.wikirc.profile = profile;
902
+ return { ok: true, active: profile, config: { llm: { model: 'model-vpn' } } };
903
+ },
904
+ });
905
+ } catch (err) {
906
+ if (err?.code === 'EPERM') {
907
+ t.skip('network listen is not permitted in this sandbox');
908
+ return;
909
+ }
910
+ throw err;
911
+ }
912
+
913
+ try {
914
+ const profiles = await fetch(`http://127.0.0.1:${handle.port}/config/profiles?workspace=juno`);
915
+ assert.equal(profiles.status, 200);
916
+ assert.deepEqual(await profiles.json(), { profiles: ['default', 'vpn'], active: 'default' });
917
+
918
+ const use = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=juno`, {
919
+ method: 'POST',
920
+ headers: { 'Content-Type': 'application/json' },
921
+ body: JSON.stringify({ profile: 'vpn' }),
922
+ });
923
+ assert.equal(use.status, 200);
924
+ assert.equal(switchedProfile, 'vpn');
925
+ assert.deepEqual(await use.json(), { ok: true, active: 'vpn', config: { llm: { model: 'model-vpn' } } });
926
+ } finally {
927
+ await handle.close();
928
+ }
929
+ });
930
+
931
+ test('runtime server rejects config switching while a run is active', async (t) => {
932
+ let handle;
933
+ try {
934
+ handle = await startRuntimeServer({
935
+ host: '127.0.0.1',
936
+ port: 0,
937
+ store: {
938
+ dbPath: ':memory:',
939
+ getState: () => ({ status: 'running' }),
940
+ listEvents: () => [],
941
+ },
942
+ getContext: async () => ({
943
+ workspace: 'juno',
944
+ session: { workspace: 'juno' },
945
+ running: true,
946
+ currentAbortController: null,
947
+ }),
948
+ run: async () => {},
949
+ useConfigProfile: async () => ({ ok: true }),
950
+ });
951
+ } catch (err) {
952
+ if (err?.code === 'EPERM') {
953
+ t.skip('network listen is not permitted in this sandbox');
954
+ return;
955
+ }
956
+ throw err;
957
+ }
958
+
959
+ try {
960
+ const response = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=juno`, {
961
+ method: 'POST',
962
+ headers: { 'Content-Type': 'application/json' },
963
+ body: JSON.stringify({ profile: 'vpn' }),
964
+ });
965
+ assert.equal(response.status, 409);
966
+ } finally {
967
+ await handle.close();
968
+ }
969
+ });