File size: 56,206 Bytes
35437d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
from __future__ import annotations

import base64
from pathlib import Path
import re
import shutil
import subprocess

import pytest


ROOT = Path(__file__).resolve().parents[1]
STORE = ROOT / "plugins" / "_agent_editor" / "webui" / "agent-editor-store.js"
MODAL = ROOT / "plugins" / "_agent_editor" / "webui" / "main.html"
SWITCHER = (
    ROOT
    / "plugins"
    / "_model_config"
    / "extensions"
    / "webui"
    / "chat-input-progress-start"
    / "model-switcher.html"
)
SWITCHER_MIXIN = ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"


def test_agent_editor_surface_has_normative_entry_points_and_accessible_controls() -> None:
    modal = MODAL.read_text(encoding="utf-8")
    store = STORE.read_text(encoding="utf-8")
    switcher = SWITCHER.read_text(encoding="utf-8")
    tool_section = re.search(
        r'data-agent-editor-section="3".*?(?=<section x-show="\$store\.agentEditor\.section === \'4\'")',
        modal,
        re.DOTALL,
    ).group(0)
    mcp_section = re.search(
        r'data-agent-editor-section="4".*?(?=<section x-show="\$store\.agentEditor\.section === \'5\'")',
        modal,
        re.DOTALL,
    ).group(0)
    skill_section = re.search(
        r'data-agent-editor-section="5".*?(?=<section x-show="\$store\.agentEditor\.section === \'6\'")',
        modal,
        re.DOTALL,
    ).group(0)
    easy_surface = modal.split('<div class="agent-advanced"', 1)[0]

    assert "Create agent" not in switcher
    assert "Manage agents" in switcher
    assert '<div class="agent-profile-row"' in switcher
    assert 'class="agent-profile-row-edit"' in switcher
    assert ':aria-label="`Edit ${profile.label || profile.key}`"' in switcher
    assert "profileId: profile.key" in switcher
    assert "<span>Edit</span>" in switcher
    assert "profile.customized" not in switcher
    assert 'class="model-switcher-item agent-profile-edit"' not in switcher
    assert "min-height: 24px" in re.search(
        r"\.agent-profile-row-edit\s*\{([^}]*)\}", switcher
    ).group(1)
    assert "createAgentProfileChat" not in switcher
    assert all(
        label in modal
        for label in (
            "Identity",
            "Prompt files",
            "Tools",
            "MCPs",
            "Skills",
            "Review",
            "Save & test",
        )
    )
    assert 'aria-label="Editor mode"' in modal
    assert "Allow selected" not in modal and "Block selected" not in modal
    assert "No optional tools" not in modal
    assert 'class="agent-model-preset-picker"' in modal
    assert modal.count('x-model="$store.agentEditor.draft.modelPreset"') == 2
    assert modal.count('$el.value = $store.agentEditor.draft.modelPreset') == 2
    assert 'id="agent-editor-easy-model-preset"' in easy_surface
    easy_identity = easy_surface[easy_surface.index('<section class="agent-easy-identity">'):easy_surface.index('<section class="agent-easy-field">')]
    assert easy_identity.index('id="agent-editor-name"') < easy_identity.index('id="agent-editor-easy-model-preset"')
    assert easy_surface.index('id="agent-editor-easy-model-preset"') < easy_surface.index('id="agent-editor-instructions"')
    assert '`Use current preset (${$store.agentEditor.state.model_preset.effective})`' in modal
    assert 'x-for="preset in $store.agentEditor.state.model_presets"' in modal
    assert modal.count("Edit Presets") == 2
    assert "Manage presets" not in modal
    assert "model-preset-row" not in modal
    assert easy_surface.count('<details class="capability-accordion">') == 3
    assert re.findall(r'<summary><span>(Choose individual (?:tools|MCPs|skills))</span><x-icon name="expand_more"></x-icon></summary>', easy_surface) == ["Choose individual tools", "Choose individual MCPs", "Choose individual skills"]
    assert '<details class="capability-accordion" open' not in easy_surface
    assert 'x-for="tool in $store.agentEditor.standardToolCatalog"' in easy_surface
    assert 'x-for="skill in $store.agentEditor.skillCatalog"' in easy_surface
    assert 'x-for="tool in $store.agentEditor.mcpCatalog"' in easy_surface
    assert "Choose tools in Advanced" not in modal
    assert "Use standard tool access" not in modal
    assert "Use standard skill access" not in modal
    assert 'class="policy-lists"' not in modal
    assert "policy-transfer-actions" not in modal
    assert tool_section.count('class="policy-group"') == 1
    assert mcp_section.count('class="policy-group"') == 1
    assert skill_section.count('class="policy-group"') == 1
    assert '<label>Category ' not in tool_section
    assert "indeterminate" not in modal
    assert modal.count('class="policy-state-control" role="group"') == 6
    assert modal.count(':aria-pressed="$store.agentEditor.policyItemState') == 18
    assert '<details class="policy-description"' not in tool_section
    assert '<details class="policy-description"' not in mcp_section
    assert '<details class="policy-description"' not in skill_section
    assert tool_section.count('class="policy-item-description"') == 1
    assert mcp_section.count('class="policy-item-description"') == 1
    assert skill_section.count('class="policy-item-description"') == 1
    assert "Your changes override the built-in profile. The original files stay unchanged." in modal
    assert 'x-model="$store.agentEditor.projectName"' in modal
    assert 'x-init="$nextTick(() => $el.value = $store.agentEditor.projectName)"' in modal
    assert '@change="$store.agentEditor.onScopeChanged()"' in modal
    assert '<option value="">Global</option>' in modal
    assert 'x-for="project in $store.agentEditor.projects"' in modal
    assert 'x-show="profile.deletable"' in modal
    assert 'class="agent-customized-indicator"' not in modal
    assert 'class="button agent-manager-create"' in modal
    assert 'class="active-agent-display"' in modal
    assert 'class="button icon-button" title="Edit"' in modal
    assert 'class="text-button agent-manager-inline-action"' in modal
    inline_action_style = re.search(
        r"\.agent-editor \.agent-manager-inline-action\s*\{([^}]*)\}", modal
    ).group(1)
    assert "color:var(--color-message-text)" in inline_action_style
    assert ".agent-editor .agent-manager-inline-action:hover:not(:disabled)" in modal
    assert ':disabled="!!$store.agentEditor.duplicatingProfile"' in modal
    assert ':aria-label="`Duplicate ${profile.title || profile.id}`"' in modal
    assert "$store.agentEditor.duplicateProfile(profile)" in modal
    assert 'x-show="profile.scope_has_overrides && !profile.deletable"' in modal
    assert ':aria-label="`Reset ${profile.title || profile.id} to default`"' in modal
    assert "Restore original" not in modal
    assert modal.count("Reset to default") >= 6
    assert "$store.agentEditor.restoreProfile(profile)" in modal
    assert "$store.agentEditor.restoreProfile($store.agentEditor.state.profile)" in modal
    assert 'class="agent-editor-topbar-actions"' in modal
    topbar = modal.split('class="agent-editor-topbar-actions"', 1)[1].split("</header>", 1)[0]
    footer = modal.split('class="modal-footer agent-editor-footer"', 1)[1]
    assert "Reset to default" not in topbar
    assert "$store.agentEditor.restoreProfile($store.agentEditor.state.profile)" in footer
    assert 'class="btn btn-cancel agent-reset-profile"' in footer
    assert '<x-icon name="restore"></x-icon><span>Reset to default</span>' not in footer
    assert "x-show=\"$store.agentEditor.draft && !$store.agentEditor.draft.creating" in footer
    assert ':disabled="$store.agentEditor.saving || !$store.agentEditor.state?.profile?.scope_has_overrides"' in footer
    avatar_reset = 'x-show="$store.agentEditor.state.profile.metadata.avatar.has_override || $store.agentEditor.draft.avatar" @click="$store.agentEditor.resetAvatar()">Reset</button>'
    assert modal.count(avatar_reset) == 2
    assert '@click="$store.agentEditor.resetAvatar()">Remove</button>' not in modal
    assert 'class="btn btn-cancel" @click="window.closeModal?.()">Cancel</button>' not in modal
    assert 'class="toggle agent-profile-availability"' in modal
    assert ':disabled="$store.agentEditor.profileAvailabilitySaving"' in modal
    assert "Default is always available" not in modal
    assert "$store.agentEditor.setProfileEnabled(profile, $event.target.checked)" in modal
    assert "agent-profile-availability" not in store
    assert "activateProfile(profile.id)" not in modal
    assert 'class="button cancel icon-button" x-show="profile.deletable"' in modal
    assert "project_override_active" not in modal
    assert "profile.origin === 'Custom'" not in modal
    assert "Agent scope" not in modal
    assert "Create agents and customize inherited profiles" not in modal
    assert "Unavailable — kept in your settings" in modal
    assert "Customize this file" not in modal
    assert "Choose a file and edit its prompt." in modal
    assert "inherited version next to your version" not in modal
    assert 'role="tablist" aria-label="Prompt view"' not in modal
    assert "Your version" not in modal and ">Compare</button>" not in modal
    assert "Find in file" not in modal and "prompt-match-action" not in modal
    assert "No prompt files match your search." in modal
    assert "Saving will change exactly these files — nothing else." in modal
    assert "Review & test" not in modal
    assert "Refresh change plan" not in modal
    assert "Back to Easy" not in modal
    assert "This agent has a detailed prompt" not in modal
    assert "Replace with simple instructions" not in modal
    assert "easyInstructionsEditable" not in modal
    assert '<textarea id="agent-editor-instructions"' in modal
    assert "<h2" not in modal
    assert "Section 1" not in modal
    assert '<textarea id="agent-editor-description" rows="2"' in modal
    assert "Allow newly installed" not in modal
    assert modal.count("<strong>Allow tools by default</strong>") == 2
    assert modal.count("<strong>Allow MCPs by default</strong>") == 2
    assert modal.count("<strong>Allow skills by default</strong>") == 2
    assert "Allow tools and MCPs by default" not in modal
    assert "Applies to Tools and MCPs left on Default." not in modal
    assert "Applies to Skills left on Default." not in modal
    assert "No blocked tools" not in modal and "No blocked skills" not in modal
    assert "policy-description" not in modal and "-webkit-line-clamp:2" not in modal
    assert 'class="prompt-file-list" role="region" aria-label="Prompt file list" tabindex="0"' in modal
    assert 'class="prompt-editor" role="region" aria-label="Selected prompt file" tabindex="0"' in modal
    assert "Preview combined prompt" not in modal
    assert 'class="prompt-customization-path"' in modal
    assert '<strong x-text="$store.agentEditor.selectedPrompt"></strong>\n                    <div class="prompt-customization-path">' in modal
    assert "source-chain" not in modal and "promptSourceChain" not in store
    assert "$store.agentEditor.promptCustomizationPath()" in modal
    assert 'class="review-identity"' in modal
    assert 'aria-label="Agent identity"' in modal
    assert 'id="review-identity-title"' not in modal
    review_identity = modal[modal.index('class="review-identity"'):modal.index('class="change-plan"')]
    assert "draft.profileId\"></code>" not in review_identity
    assert "width:92vw" in modal
    assert ".modal-inner.agent-editor-advanced .modal-scroll { max-height:none; }" in modal
    assert '.agent-advanced-content > section[data-agent-editor-section="2"] { height:100%; min-height:0; }' in modal
    assert ".prompt-workspace { flex:1 1 auto;" in modal
    assert 'width:1.5rem; height:1.5rem' in modal
    assert 'id="agent-editor-prompt-ace"' in modal
    assert 'id="agent-editor-prompt-text"' not in modal
    assert "globalThis.ace.edit(container)" in store
    assert 'editor.session.setMode("ace/mode/markdown")' in store
    assert "editor.session.setUseWrapMode(true)" in store
    assert "showPrintMargin: false" in store and "useWorker: false" in store
    assert "findInPrompt" not in store and "promptTextSearch" not in store
    assert 'class="agent-editor-heading"' in modal
    assert ':aria-invalid=' in modal
    assert modal.count('role="alert"') >= 4
    assert "Fix ${$store.agentEditor.validationIssues().length}" in modal
    assert modal.count("$store.agentEditor.validationIssues().length > 0") == 2
    assert "$store.agentEditor.view === 'editor' && !$store.agentEditor.pendingMutation" in modal
    assert 'x-show="$store.agentEditor.view === \'editor\' && !$store.agentEditor.draft?.creating"' in modal
    assert 'class="btn btn-ok" x-show="$store.agentEditor.view === \'editor\'"' in modal
    assert "Delete all customizations in" in modal
    assert '.agent-editor input[type="checkbox"]' not in modal
    assert 'promptDisplayState(prompt)' in modal
    assert "Will reset to default on save." in modal
    assert "metadataProvenance('description')" not in modal
    assert 'x-show="$store.agentEditor.metadataProvenance(\'title\')"' in modal
    assert 'metadataProvenance(\'context\')' not in modal
    assert ':title="prompt.filename"' not in modal
    assert "promptEditPending($store.agentEditor.selectedPromptDraft)" in modal
    assert 'aria-label="Discard current edit"' in modal
    assert 'aria-label="Accept current edit"' in modal
    assert ':readonly="!$store.agentEditor.isPromptEditing' not in modal
    assert ".prompt-ace { flex:1; min-height:0;" in modal
    store_source = STORE.read_text(encoding="utf-8")
    assert "cannot be recovered" in store_source
    assert "deletionImpactHtml" not in store_source
    assert "agent-profile-avatar" in switcher
    assert '<button type="button" class="model-switcher-item agent-profile-item"' in switcher
    assert '<div class="model-switcher-item agent-profile-item"' not in switcher
    switcher_mixin = SWITCHER_MIXIN.read_text(encoding="utf-8")
    assert "avatar_url" in switcher_mixin
    assert "BUILT_IN_AGENT_COLORS" in switcher_mixin
    assert '!["_example", "default"].includes(profile.id)' in switcher_mixin
    assert 'activeKey !== "default"' in switcher_mixin
    assert "customized: !!profile.has_user_overrides" not in switcher_mixin
    assert 'x-for="profile in $store.agentEditor.visibleProfiles"' in modal
    assert "$store.agentEditor.activeProfile().id !== 'default'" in modal
    assert 'name="palette"' in modal and 'name="add_photo_alternate"' in modal
    assert ".capability-accordion .policy-items { max-height:none; overflow:visible;" in modal
    assert ".capability-policy-group + .capability-policy-group { border-top:1px solid var(--color-border);" in modal
    assert "font-weight:500" in re.search(
        r"\.capability-accordion summary\s*\{([^}]*)\}", modal
    ).group(1)
    assert ".capability-accordion[open] summary x-icon { transform:rotate(180deg); }" in modal
    assert "toolCategory" not in store_source
    assert "selectedAllowed" not in store_source and "selectedBlocked" not in store_source
    assert "moveAllVisible" not in store_source and "confirmBulkMove" not in store_source
    assert "setPolicyItemState" in store_source and "collapsePolicy" in store_source
    assert '.agent-editor [aria-invalid="true"]' not in modal
    assert "color-scheme:dark" not in modal
    assert "#fff 82%" not in modal
    assert ".agent-manager-name strong,.agent-manager-copy p { overflow-wrap:anywhere; }" in modal
    assert ".field-error { display:block; color:var(--color-text)" in modal
    assert ".prompt-pane" not in modal
    assert 'callJsonApi("/plugins/_agent_editor/agent_editor"' in switcher_mixin
    assert "profile.enabled !== false" in switcher_mixin
    assert 'x-show="!$store.modelConfig.agentProfilesLoading"' in switcher
    assert "@keydown.ctrl.s.prevent" in modal
    assert "@media (max-width: 760px)" in modal
    assert "tri-state-item" not in modal
    assert "policy-state-legend" not in modal
    assert "policy-item-state" not in modal
    assert modal.count("Default (${$store.agentEditor.policyDefault") == 6
    assert "border-radius:var(--border-radius-sm)" in modal
    assert "cyclePolicyItem" not in store_source and "policyAriaChecked" not in store_source
    assert ".agent-manager-card { grid-template-columns:3rem minmax(0,1fr) auto; align-items:start; }" in modal
    assert ".agent-manager-actions { grid-column:auto; align-self:stretch; display:grid;" in modal
    assert ".agent-manager-actions .agent-profile-availability { grid-column:1/-1; }" in modal
    assert ".footer-actions { width:auto; max-width:100%; flex-wrap:wrap; justify-content:flex-end; }" in modal
    assert ".footer-actions .btn { flex:0 1 auto; text-align:center; }" in modal
    assert modal.count("data-modal-footer") == 1
    assert ">Close</button>" not in modal


def test_agent_editor_store_has_no_conversational_or_model_builder_path() -> None:
    source = STORE.read_text(encoding="utf-8")
    modal = MODAL.read_text(encoding="utf-8")
    switcher_source = (
        ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"
    ).read_text(encoding="utf-8")

    assert 'createStore("agentEditor", model)' in source
    assert "CREATE_AGENT_PROFILE_PROMPT" not in switcher_source
    assert "a0-create-agent" not in switcher_source
    assert "save_agent_data" not in source
    assert not re.search(r"utility.?model|call.?model|generate", source, re.IGNORECASE)
    assert "Advanced <span" not in modal
    assert 'class="toast-action-row"' in source
    assert 'class="button confirm"' in source
    assert "toast-link" not in source and ".toast-link" not in modal


@pytest.mark.skipif(not shutil.which("node"), reason="node is required")
def test_local_slugging_and_fresh_chat_profile_selection_are_deterministic() -> None:
    source = STORE.read_text(encoding="utf-8")
    source = re.sub(r"^import .*?;\n", "", source, flags=re.MULTILINE)
    harness = r"""
const calls = [];
const confirmations = [];
const toasts = [];
let setEnabledHandler = null;
let loadHandler = null;
let profilesHandler = null;
let confirmResult = false;
const createStore = (_name, value) => value;
const callJsonApi = async (endpoint, payload) => {
  calls.push({ endpoint, payload });
  if (payload?.action === "load" && loadHandler) return loadHandler(payload);
  if (payload?.action === "list") return profilesHandler
    ? profilesHandler(payload)
    : { ok: true, profiles: [] };
  if (payload?.action === "set_enabled") return setEnabledHandler
    ? setEnabledHandler(payload)
    : { ok: true, active_profile: "default", active_profile_label: "Default" };
  if (payload?.action === "plan") return {
    ok: true,
    written: [payload.project_name
      ? `usr/projects/${payload.project_name}/.a0proj/agents/${payload.patch.profile_id}/agent.yaml`
      : `usr/agents/${payload.patch.profile_id}/agent.yaml`],
    deleted: [],
    warnings: [],
  };
  if (payload?.action === "duplicate") return {
    ok: true,
    profile_id: "researcher-1",
    title: "Researcher 1",
    profiles: [{ id: "researcher" }, { id: "researcher-1" }],
  };
  if (endpoint === "/agent_profile_set") return {
    ok: true,
    agent_profile: payload.agent_profile,
    agent_profile_label: "Researcher",
  };
  return endpoint === "/chat_create" ? { ok: true, ctxid: "fresh-chat" } : { ok: true };
};
const fetchApi = async () => ({ ok: true, json: async () => ({}) });
const closeModal = async () => {};
const openModal = async () => {};
const showConfirmDialog = async options => { confirmations.push(options); return confirmResult; };
const chatsStore = {
  selected: "old-chat",
  selectedContext: { project: { name: "demo" }, agent_profile: "researcher" },
  selectChat: async (id) => {
    calls.push({ endpoint: "selectChat", payload: id });
    chatsStore.selected = id;
    chatsStore.selectedContext = { agent_profile: "default", agent_profile_label: "Default" };
  },
};
const modelConfigStore = {
  loadAgentProfiles: async force => calls.push({ endpoint: "loadAgentProfiles", payload: force }),
  openPresetEditor: async preset => calls.push({ endpoint: "openPresetEditor", payload: preset }),
  selectAgentProfile: async (contextId, profileId) => {
    calls.push({ endpoint: "selectAgentProfile", payload: { contextId, profileId } });
    return true;
  },
  getAgentProfileVisual: (_id, label) => ({ color: "#123456", url: "", initials: label?.[0] || "A" }),
};
const aceState = { change: null, find: null, destroyed: false, value: "" };
const aceContainer = { textContent: "" };
const aceSession = {
  setMode: value => { aceState.mode = value; },
  setUseWrapMode: value => { aceState.wrap = value; },
  on: (name, callback) => { if (name === "change") aceState.change = callback; },
  off: (name, callback) => { if (name === "change" && aceState.change === callback) aceState.change = null; },
};
const aceEditor = {
  container: aceContainer,
  session: aceSession,
  textInput: { getElement: () => ({ setAttribute: (key, value) => { aceState[key] = value; } }) },
  setTheme: value => { aceState.theme = value; },
  setOptions: value => { aceState.options = value; },
  setValue: value => { aceState.value = value; aceState.change?.(); },
  getValue: () => aceState.value,
  find: (query, options) => { aceState.find = { query, options }; },
  focus: () => { aceState.focused = true; },
  resize: () => { aceState.resized = true; },
  destroy: () => { aceState.destroyed = true; },
};
globalThis.ace = { edit: container => { aceState.container = container; return aceEditor; } };
globalThis.window = globalThis;
globalThis.document = {
  dispatchEvent: (event) => calls.push({ endpoint: "event", payload: event.type }),
  createElement: () => ({ textContent: "", get innerHTML() { return this.textContent; } }),
  addEventListener: () => {},
  removeEventListener: () => {},
};
globalThis.CustomEvent = class { constructor(type) { this.type = type; } };
globalThis.sessionStorage = { setItem: () => {}, getItem: () => "", removeItem: () => {} };
globalThis.localStorage = { setItem: () => {}, getItem: () => "" };
globalThis.requestAnimationFrame = callback => callback();
globalThis.confirm = () => true;
globalThis.justToast = (...args) => toasts.push(args);
"""
    checks = r"""
if (slugifyProfileName("  Crème Brûlée__Lab  ") !== "creme-brulee-lab") throw new Error("slug mismatch");
if (slugifyProfileName("東京") !== "") throw new Error("unsupported slug mismatch");
store.draft = { title: "stale" };
store.initialDraft = { title: "clean" };
store.intent = { view: "manage", contextId: "" };
const modalTitle = { textContent: "" };
const modalElement = { querySelector: selector => selector === ".modal-title" ? modalTitle : null };
const modalInner = { classList: { toggle: () => {} } };
await store.mount({
  closest: selector => selector === ".modal" ? modalElement : selector === ".modal-inner" ? modalInner : null,
  querySelector: () => null,
});
if (store.draft !== null || store.initialDraft !== null || store.dirty || store.loading) throw new Error("manage mount kept stale draft state");
if (modalTitle.textContent !== "Manage agents") throw new Error("manage mount title mismatch");
store.state = {
  profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: {} } },
  prompts: [
    { filename: "agent.system.main.specifics.md", group: "2.1", group_label: "Agent instructions", effective: "", inherited: "", source_chain: [] },
    { filename: "agent.system.main.communication.md", group: "2.4", group_label: "Communication", effective: "Inherited comm", inherited: "Inherited comm", source_chain: ["Framework", "Researcher"], state: "Inherited", has_override: false },
  ],
  model_preset: { has_override: false },
  tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
  skills: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [] },
};
store.makeDraft(true);
if (await store.previewPlan()) throw new Error("invalid plan unexpectedly succeeded");
if (store.planStatus !== "blocked" || store.error || store.validationIssues().length !== 2) throw new Error("blocked plan state mismatch");
if (store.fieldIssue("name")?.message !== "Agent name is required.") throw new Error("inline name issue missing");
if (store.fieldIssue("instructions")?.field !== "agent-editor-instructions") throw new Error("Easy validation targeted the wrong input");
await store.save();
if (store.error) throw new Error("validation leaked into dismissible error banner");
store.state.profile.id = "new-agent";
store.state.profile.origin = "Built-in";
store.state.profile.metadata.title = { inherited_source: "agents/new-agent/agent.yaml" };
if (store.metadataProvenance("title") !== "") throw new Error("new profile showed misleading provenance");
store.draft.creating = false;
if (store.metadataProvenance("title") !== "Using the default") throw new Error("default provenance mismatch");
store.profiles = [{ id: "researcher", title: "Researcher" }, { id: "default", title: "Default" }];
if (store.visibleProfiles.length !== 1 || store.visibleProfiles[0].id !== "researcher") throw new Error("Default profile remained selectable in Agent Editor");
store.state.profile.metadata.title = { inherited_source: "agents/researcher/agent.yaml" };
if (store.metadataProvenance("title") !== "Inherited from Researcher") throw new Error("inherited provenance mismatch");
store.state.profile.metadata.title.has_override = true;
if (store.metadataProvenance("title") !== "Customized by you") throw new Error("custom provenance mismatch");
store.draft.creating = true;
store.draft.profileId = "researcher";
store.selectedPrompt = "agent.system.main.specifics.md";
store.projectName = "";
if (store.promptCustomizationPath() !== "usr/agents/researcher/prompts/agent.system.main.specifics.md") throw new Error("Global prompt customization path mismatch");
store.projectName = "demo";
if (store.promptCustomizationPath() !== "usr/projects/demo/.a0proj/agents/researcher/prompts/agent.system.main.specifics.md") throw new Error("project prompt customization path mismatch");
store.projectName = "";
store.state.model_preset.effective = "Current";
store.state.model_presets = [{ name: "Codex" }];
store.draft.modelPreset = "Codex";
if (store.buildPatch().model_preset?.name !== "Codex") throw new Error("Easy model preset was not saved");
loadHandler = () => ({ ok: true, state: { model_presets: [{ name: "Codex" }] } });
await store.openPresetManager();
loadHandler = null;
if (!calls.some(call => call.endpoint === "openPresetEditor" && call.payload === "Codex")) throw new Error("Easy preset editor action did not reuse Model Configuration");
store.draft.modelPreset = "";
store.state.tools.catalog = [
  { id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
  { id: "local:gone", name: "gone", label: "Gone", origin: "Unavailable", available: false },
  { id: "mcp:docs:read", name: "read", label: "Docs read", origin: "MCP", available: true },
];
store.draft.toolPolicy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
store.initialDraft.toolPolicy = clone(store.draft.toolPolicy);
if (store.standardToolCatalog.length !== 1 || store.mcpCatalog.length !== 1 || store.toolCatalog.length !== 2) throw new Error("Easy tool/MCP grouping mismatch");
if (store.filteredTools("tool").length !== 2 || store.filteredTools("mcp").length !== 1) throw new Error("Advanced retained catalog grouping mismatch");
if (store.policyItemState("tool", "local:shell") !== "default") throw new Error("initial segmented state mismatch");
store.setPolicyItem("tool", "local:shell", "allow");
if (store.policyItemState("tool", "local:shell") !== "allow" || !store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("On selection failed");
store.setPolicyItem("tool", "local:shell", "block");
if (store.policyItemState("tool", "local:shell") !== "block" || !store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("Off selection failed");
if (JSON.stringify(store.skillWarnings({ allowed_tools: ["shell"] })) !== JSON.stringify(["shell"])) throw new Error("live skill warning missing");
store.setPolicyItem("tool", "local:shell", "default");
if (store.draft.toolPolicy.mode !== "inherit" || store.policyItemState("tool", "local:shell") !== "default") throw new Error("segmented undo did not collapse to inherit");
store.setPolicyDefault("tool", "block");
store.setPolicyItem("tool", "local:shell", "allow");
store.setPolicyDefault("tool", "allow");
if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("explicit On was lost when the default changed");
store.setPolicyItem("tool", "local:shell", "block");
store.setPolicyDefault("tool", "block");
if (!store.draft.toolPolicy.blocked.includes("local:shell")) throw new Error("explicit Off was lost when the default changed");
store.setPolicyItem("tool", "local:shell", "default");
store.setPolicyDefault("tool", "allow");
if (store.draft.toolPolicy.mode !== "inherit") throw new Error("default undo did not collapse to inherit");
store.setPolicyDefault("mcp", "block");
if (store.policyDefault("tool") !== "allow" || store.policyDefault("mcp") !== "block") throw new Error("tool and MCP defaults were not independent");
if (!store.isToolAllowed(store.state.tools.catalog[0]) || store.isToolAllowed(store.state.tools.catalog[2])) throw new Error("MCP default affected the wrong catalog group");
store.setPolicyItem("mcp", "mcp:docs:read", "allow");
store.setPolicyDefault("mcp", "allow");
if (!store.draft.toolPolicy.allowed.includes("mcp:docs:read")) throw new Error("explicit MCP On was lost when its default changed");
store.setPolicyItem("mcp", "mcp:docs:read", "default");
if (store.draft.toolPolicy.mode !== "inherit") throw new Error("MCP default undo did not collapse to inherit");
store.projectName = "demo";
store.intent = { ...store.intent, projectName: "demo" };
if (!store.currentChatUsesScope() || !store.isProfileActive("researcher")) throw new Error("active project profile state mismatch");
store.profiles = [{ id: "researcher", title: "Researcher", enabled: true }, { id: "default", title: "Default", enabled: true }];
if (store.activeProfile()?.id !== "researcher") throw new Error("active profile summary mismatch");
await store.setProfileEnabled(store.profiles[0], false);
if (!calls.some(item => item.payload?.action === "set_enabled" && item.payload.profile_id === "researcher") || chatsStore.selectedContext.agent_profile !== "default") throw new Error("profile availability did not reconcile the active chat");
if (!calls.some(item => item.endpoint === "loadAgentProfiles" && item.payload === true)) throw new Error("profile switcher did not refresh eagerly");
let releaseFirstToggle;
const firstToggleResponse = new Promise(resolve => { releaseFirstToggle = resolve; });
let toggleRequest = 0;
setEnabledHandler = () => {
  toggleRequest += 1;
  return toggleRequest === 1
    ? firstToggleResponse
    : Promise.resolve({ ok: true });
};
const rapidProfiles = [
  { id: "agent0", title: "Agent 0", enabled: true },
  { id: "developer", title: "Developer", enabled: true },
];
store.profiles = rapidProfiles;
const firstToggle = store.setProfileEnabled(rapidProfiles[0], false);
while (!toggleRequest) await Promise.resolve();
const secondToggle = store.setProfileEnabled(rapidProfiles[1], false);
await secondToggle;
if (toggleRequest !== 1 || rapidProfiles[1].enabled !== true || !store.profileAvailabilitySaving) throw new Error("availability saves were not serialized");
releaseFirstToggle({ ok: true });
await firstToggle;
if (store.profiles !== rapidProfiles || rapidProfiles[0].enabled || rapidProfiles[1].enabled !== true || store.profileAvailabilitySaving) throw new Error("first availability save did not settle cleanly");
await store.setProfileEnabled(rapidProfiles[1], false);
if (toggleRequest !== 2 || rapidProfiles[1].enabled || store.profileAvailabilitySaving) throw new Error("availability gate did not reopen after save");
setEnabledHandler = null;
await store.duplicateProfile({ id: "researcher", title: "Researcher" });
if (!calls.some(item => item.payload?.action === "duplicate" && item.payload.profile_id === "researcher") || !store.profiles.some(profile => profile.id === "researcher-1")) throw new Error("profile duplication failed");
calls.length = 0;
confirmResult = false;
await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
if (calls.length || confirmations.at(-1)?.title !== "Reset Researcher to default?") throw new Error("restore cancellation failed");
confirmResult = true;
profilesHandler = () => ({
  ok: true,
  profiles: [{ id: "researcher", title: "Researcher", scope_has_overrides: false }],
});
await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
if (!calls.some(item => item.payload?.action === "remove_changes" && item.payload.profile_id === "researcher" && item.payload.destructive === false)) throw new Error("reset to default did not use sparse removal");
if (!calls.some(item => item.endpoint === "loadAgentProfiles" && item.payload === true) || store.saving) throw new Error("reset to default did not refresh profile state");
if (store.profiles[0]?.scope_has_overrides || toasts.at(-1)?.[0] !== "Researcher reset to default.") throw new Error("reset to default kept its action or omitted feedback");
profilesHandler = null;
let editorReload = null;
const loadEditor = store.loadEditor;
const setMode = store.setMode;
store.view = "editor";
store.mode = "advanced";
store.section = "3";
store.loadEditor = async (...args) => { editorReload = { args }; };
store.setMode = (...args) => { editorReload.mode = args; };
await store.restoreProfile({ id: "researcher", title: "Researcher", scope_has_overrides: true, deletable: false });
if (editorReload?.args.join(":") !== "researcher:false" || editorReload.mode.join(":") !== "advanced:3:false") throw new Error("editor reset did not restore its surface");
store.loadEditor = loadEditor;
store.setMode = setMode;
store.view = "manage";
confirmations.length = 0;
confirmResult = false;
store.projectName = "other";
if (store.currentChatUsesScope() || store.isProfileActive("default")) throw new Error("foreign project profile appeared active");
store.projectName = "demo";
store.state.tools.effective_policy = { mode: "custom", default: "allow", mcp_default: "allow", allowed: [], blocked: ["local:shell"] };
if (store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope ignored inherited tool restriction");
store.setPolicyItem("tool", "local:shell", "default");
if (store.draft.toolPolicy.mode !== "custom" || !store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not customize inherited policy");
store.setPolicyItem("tool", "local:shell", "allow");
if (!store.draft.toolPolicy.allowed.includes("local:shell")) throw new Error("project scope did not pin explicit On");
store.setPolicyItem("tool", "local:shell", "block");
if (store.draft.toolPolicy.mode !== "inherit" || store.isToolAllowed(store.state.tools.catalog[0])) throw new Error("project scope did not restore inherited policy");
store.projectName = "";
store.state.tools.effective_policy = { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] };
store.state.skills.catalog = [
  { name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research sources", available: true, tags: [], allowed_tools: [] },
  { name: "Gone", path: "skills/gone/SKILL.md", origin: "Unavailable", description: "Missing skill", available: false, tags: [], allowed_tools: [] },
];
store.draft.skillPolicy = { mode: "inherit", default: "allow", allowed: [], blocked: [] };
store.initialDraft.skillPolicy = clone(store.draft.skillPolicy);
if (store.skillCatalog.length !== 1 || store.filteredSkills().length !== 2) throw new Error("Easy/Advanced skill catalog mismatch");
store.setPolicyItem("skill", "Research", "allow");
if (!store.draft.skillPolicy.allowed.includes("Research")) throw new Error("skill On selection failed");
store.setPolicyItem("skill", "Research", "block");
if (!store.draft.skillPolicy.blocked.includes("Research")) throw new Error("skill Off selection failed");
store.setPolicyItem("skill", "Research", "default");
if (store.draft.skillPolicy.mode !== "inherit") throw new Error("skill sparse undo did not collapse");
store.draft.title = "Preserved Agent";
store.onNameInput();
store.instructions.value = "Preserved instructions";
store.draft.description = "Created description";
store.draft.context = "Delegate created work";
const createdMetadata = store.buildPatch().metadata.set;
if (createdMetadata.description !== "Created description" || createdMetadata.context !== "Delegate created work") throw new Error("Advanced create metadata was omitted");
store.mode = "advanced";
store.instructions.value = "";
const callsBeforeInvalidAdvancedCreate = calls.length;
if (!store.fieldIssue("instructions")) throw new Error("Advanced create accepted empty instructions");
if (store.fieldIssue("instructions")?.field !== "agent-editor-prompt-ace") throw new Error("Advanced validation targeted the wrong editor");
await store.save();
if (calls.length !== callsBeforeInvalidAdvancedCreate) throw new Error("Advanced create submitted empty instructions");
store.instructions.value = "Preserved instructions";
store.draft.creating = false;
store.mode = "easy";
store.instructions.initialValue = "Preserved instructions";
store.instructions.value = "";
if (!store.fieldIssue("instructions")) throw new Error("existing empty Easy instructions were accepted");
store.instructions.value = "Preserved instructions";
if (store.fieldIssue("instructions")) throw new Error("corrected Easy instructions kept a validation error");
store.mode = "advanced";
store.instructions.value = "";
if (store.fieldIssue("instructions")) throw new Error("Advanced empty instructions were rejected");
store.instructions.value = "Preserved instructions";
store.markPromptSet("agent.system.main.specifics.md");
if (store.instructions.reset || store.promptEditPending(store.instructions)) throw new Error("Easy instructions did not update its edit checkpoint");
store.restoreInstructions();
if (!store.instructions.reset || store.instructions.value !== "" || store.promptEditPending(store.instructions)) throw new Error("default instructions were not restored");
store.state = {
  profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: { effective: { kind: "color", value: "#111111" } } } },
  prompts: [
    { filename: "agent.system.main.specifics.md", group: "2.1", group_label: "Agent instructions", effective: "", inherited: "Old instructions", source: "old-source", source_chain: ["Old"] },
    { filename: "agent.system.main.communication.md", group: "2.4", group_label: "Communication", effective: "Old comm", inherited: "Old comm", source: "old-source", source_chain: ["Old"], state: "Inherited", has_override: false },
  ],
  model_preset: { has_override: false, effective: "Default" },
  model_presets: [],
  tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
    { id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
    { id: "local:old", name: "old", label: "Old", origin: "Old scope", available: true },
  ] },
  skills: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
    { name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research", available: true, tags: [], allowed_tools: [] },
    { name: "Old skill", path: "skills/old/SKILL.md", origin: "Old scope", description: "Old", available: true, tags: [], allowed_tools: [] },
  ] },
};
store.view = "editor";
store.intent = { ...store.intent, view: "create", projectName: "" };
store.makeDraft(true);
store.root = {
  querySelector: selector => selector === "#agent-editor-prompt-ace" ? aceContainer : null,
  contains: node => node === aceContainer,
  closest: () => null,
};
store.mode = "advanced";
store.section = "2";
const draftBeforeAce = JSON.stringify(store.draft);
store.initPromptEditor();
if (aceState.mode !== "ace/mode/markdown" || aceState.wrap !== true || aceState.options?.showPrintMargin !== false || aceState.options?.useWorker !== false) throw new Error("ACE configuration mismatch");
if (aceState["aria-label"] !== "Prompt Markdown" || JSON.stringify(store.draft) !== draftBeforeAce) throw new Error("ACE initialization created a false edit");
aceState.value = "Edited in ACE";
aceState.change();
if (store.instructions.value !== "Edited in ACE" || !store.promptEditPending(store.instructions)) throw new Error("ACE change did not update the prompt draft");
store.selectPrompt("agent.system.main.communication.md");
if (aceState.value !== "Old comm" || store.instructions.value !== "Edited in ACE") throw new Error("ACE file switch lost a draft");
store.selectPrompt("agent.system.main.specifics.md");
store.destroyPromptEditor();
if (!aceState.destroyed || aceState.change) throw new Error("ACE instance was not destroyed cleanly");
store.draft.title = "Scoped Agent";
store.onNameInput();
store.draft.description = "Scoped description";
store.draft.context = "Use for scoped work";
store.instructions.value = "Authored instructions";
store.markPromptSet("agent.system.main.specifics.md");
store.draft.prompts["agent.system.main.communication.md"].value = "Authored communication";
store.acceptPromptEdit("agent.system.main.communication.md");
store.chooseAvatarColor("#ABCDEF");
store.setPolicyItem("tool", "local:shell", "block");
store.setPolicyDefault("tool", "block");
store.setPolicyItem("skill", "Research", "block");
const projectState = {
  profile: { id: "new-agent", avatar_url: "", metadata: { title: {}, description: {}, context: {}, avatar: { effective: { kind: "color", value: "#222222" } } } },
  prompts: [
    { filename: "agent.system.main.specifics.md", group: "2.1", group_label: "Agent instructions", effective: "", inherited: "Project instructions", source: "project-source", source_chain: ["Project"] },
    { filename: "agent.system.main.communication.md", group: "2.4", group_label: "Communication", effective: "Inherited comm", inherited: "Inherited comm", source: "project-source", source_chain: ["Project"], state: "Inherited", has_override: false },
  ],
  model_preset: { has_override: false, effective: "Default" },
  model_presets: [],
  tools: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", mcp_default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
    { id: "local:shell", name: "shell", label: "Shell", origin: "Agent Zero", available: true },
    { id: "local:new", name: "new", label: "New", origin: "Project", available: true },
  ] },
  skills: { policy: { mode: "inherit" }, effective_policy: { mode: "inherit", default: "allow", allowed: [], blocked: [] }, has_override: false, catalog: [
    { name: "Research", path: "skills/research/SKILL.md", origin: "Agent Zero", description: "Research", available: true, tags: [], allowed_tools: [] },
    { name: "New skill", path: "skills/new/SKILL.md", origin: "Project", description: "New", available: true, tags: [], allowed_tools: [] },
  ] },
};
loadHandler = () => ({ ok: true, state: projectState });
store.mode = "advanced";
store.section = "6";
store.projectName = "demo";
store.intent = { ...store.intent, projectName: "" };
calls.length = 0;
await store.onScopeChanged();
if (store.state !== projectState || store.draft.title !== "Scoped Agent" || store.draft.profileId !== "scoped-agent") throw new Error("create scope rebase lost identity");
if (store.draft.description !== "Scoped description" || store.draft.context !== "Use for scoped work") throw new Error("create scope rebase lost authored metadata");
if (store.instructions.value !== "Authored instructions" || store.instructions.source !== "project-source") throw new Error("create scope rebase kept stale prompt provenance");
if (store.draft.avatar?.value !== "#ABCDEF" || store.isToolAllowed(projectState.tools.catalog[0]) || store.isToolAllowed(projectState.tools.catalog[1]) || store.draft.toolPolicy.default !== "block") throw new Error("create scope rebase lost avatar or tool policy");
if (store.isSkillAllowed(projectState.skills.catalog[0]) || !store.isSkillAllowed(projectState.skills.catalog[1])) throw new Error("create scope rebase lost explicit skill decision");
if (store.draft.prompts["agent.system.main.communication.md"].value !== "Authored communication" || store.draft.prompts["agent.system.main.communication.md"].source !== "project-source" || store.promptEditPending(store.draft.prompts["agent.system.main.communication.md"])) throw new Error("create scope rebase lost an accepted prompt edit or kept stale provenance");
if (calls.at(-1)?.payload?.action !== "plan" || calls.at(-1)?.payload?.project_name !== "demo" || store.planStatus !== "ready" || !store.plan.written[0].startsWith("usr/projects/demo/.a0proj/agents/scoped-agent/")) throw new Error("Review plan was not recomputed after scope change");
loadHandler = null;
store.projectName = "";
store.intent = { ...store.intent, projectName: "" };
const communication = store.draft.prompts["agent.system.main.communication.md"];
communication.value = communication.initialValue;
communication.reset = false;
store.acceptPromptEdit(communication.filename);
if (store.filteredPromptFiles("2.4")[0] !== communication) throw new Error("grouped prompt filter mismatch");
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Default") throw new Error("default prompt checkpoint mismatch");
communication.value += "\nNew rule";
store.onPromptInput(communication.filename);
if (!store.promptEditPending(communication)) throw new Error("prompt edit actions did not appear");
if (!store.buildPatch().prompts.set[communication.filename].endsWith("New rule")) throw new Error("pending prompt edit missing from sparse patch");
store.discardPromptEdit(communication.filename);
if (store.promptEditPending(communication) || communication.value !== "Inherited comm") throw new Error("prompt edit was not discarded");
if (store.buildPatch().prompts.set[communication.filename]) throw new Error("discarded prompt edit remained in sparse patch");
communication.value += "\nNew rule";
store.onPromptInput(communication.filename);
store.acceptPromptEdit(communication.filename);
if (store.promptEditPending(communication)) throw new Error("prompt edit was not accepted");
if (store.promptDisplayState(communication) !== "Customized by you") throw new Error("customized state missing");
store.resetPrompt(communication.filename);
if (store.promptEditPending(communication) || store.promptDisplayState(communication) !== "Will use the default") throw new Error("reset state mismatch");
const draftBeforeModes = JSON.stringify(store.draft);
store.setMode("advanced", "2");
store.setMode("easy");
if (store.section !== "2" || JSON.stringify(store.draft) !== draftBeforeModes) throw new Error("mode switch lost draft");
store.setMode("advanced", "6");
await Promise.resolve();
await Promise.resolve();
if (store.planStatus !== "ready" || calls.at(-1).payload.action !== "plan") throw new Error("review plan was not computed on entry");
calls.length = 0;
store.intent = { contextId: "source-chat" };
await store.openFreshChat("researcher", true);
const endpoints = calls.map((item) => item.endpoint);
const expected = ["/chat_create", "/projects", "/agent_profile_set", "/plugins/_model_config/model_override", "selectChat", "event"];
if (JSON.stringify(endpoints) !== JSON.stringify(expected)) throw new Error(JSON.stringify(calls));
if (calls[1].payload.action !== "deactivate") throw new Error("global test chat kept a project");
if (calls[2].payload.agent_profile !== "researcher") throw new Error("profile not selected");
if (calls[3].payload.action !== "clear") throw new Error("chat preset override not cleared");
if (store.readyNoteContext !== "fresh-chat") throw new Error("ready note missing");
if (chatsStore.selectedContext.agent_profile !== "researcher" || chatsStore.selectedContext.agent_profile_label !== "Researcher") throw new Error("fresh chat showed a stale profile");
calls.length = 0;
store.projectName = "demo";
await store.openFreshChat("researcher", false);
if (calls[1].endpoint !== "/projects" || calls[1].payload.action !== "activate" || calls[1].payload.name !== "demo") throw new Error("project test chat did not activate selected scope");
store.draft.title = `${store.draft.title} dirty`;
calls.length = 0;
if (await store.planRemoval(true)) throw new Error("removal plan accepted unsaved edits");
if (calls.length || !store.error.includes("Save or discard")) throw new Error("dirty removal plan was not blocked locally");
store.initialDraft = clone(store.draft);
store.error = "";
await store.planRemoval(true);
if (!store.pendingMutation?.destructive || store.section !== "6" || store.planStatus !== "ready") throw new Error("removal plan was replaced");
if (calls.at(-1).payload.action !== "plan_remove_changes") throw new Error("removal plan request missing");
if (calls.at(-1).payload.project_name !== "demo") throw new Error("removal request lost selected scope");
const callsBeforePendingSave = calls.length;
if (await store.save() || calls.length !== callsBeforePendingSave) throw new Error("ordinary save ran over a pending removal plan");
store.draft.title += " changed after planning";
if (await store.applyPendingMutation() !== false || confirmations.length || calls.length !== callsBeforePendingSave || !store.error.includes("before applying")) throw new Error("removal plan discarded edits made after planning");
store.draft.title = store.initialDraft.title;
store.error = "";
store.plan = { written: ["usr/agents/researcher/agent.yaml"], deleted: ["usr/agents/researcher/prompts/old.md"], warnings: [] };
confirmResult = true;
loadHandler = () => ({ ok: true, state: store.state });
await store.applyPendingMutation();
if (confirmations.length !== 1 || confirmations[0].type !== "danger") throw new Error("danger confirmation missing");
if (!confirmations[0].message.includes("agent.yaml") || !confirmations[0].message.includes("old.md")) throw new Error("planned paths missing from confirmation");
if (confirmations[0].title !== "Delete all customizations for this profile?") throw new Error("cleanup confirmation title mismatch");
const removalCall = calls.find(item => item.payload?.action === "remove_changes");
if (!removalCall || removalCall.payload.confirm !== true || removalCall.payload.destructive !== true) throw new Error("destructive removal omitted explicit confirmation");
loadHandler = null;
confirmResult = false;
confirmations.length = 0;
calls.length = 0;
store.projectName = "";
await store.deleteProfile("custom-agent");
if (calls.length) throw new Error("cancelled deletion made an API request");
if (confirmations.length !== 1 || confirmations[0].title !== "Delete custom-agent?") throw new Error("delete confirmation mismatch");
if (confirmations[0].message !== "<p>This agent profile will be permanently deleted from Global and cannot be recovered.</p>") throw new Error("delete confirmation is not concise");
confirmResult = true;
store.view = "editor";
store.mode = "advanced";
store.draft = { title: "dirty", avatar: null, avatarToken: "", metadataResets: [] };
store.initialDraft = { title: "clean", avatar: null, avatarToken: "", metadataResets: [] };
store.promptEditBaselines = { stale: { value: "stale", reset: false } };
await store.deleteProfile("custom-agent");
if (store.view !== "manage" || store.mode !== "easy" || store.draft !== null || store.initialDraft !== null || Object.keys(store.promptEditBaselines).length) throw new Error("delete did not enter a clean Manage state");
store.view = "editor";
store.mode = "advanced";
store.draft = { title: "dirty" };
store.initialDraft = { title: "clean" };
store.promptEditBaselines = { stale: { value: "stale", reset: false } };
store.error = "stale";
store.showManager();
if (store.view !== "manage" || store.mode !== "easy" || store.draft !== null || store.initialDraft !== null || store.error || Object.keys(store.promptEditBaselines).length) throw new Error("Back did not discard editor state before Manage");
"""
    module_source = harness + "\n" + source + "\n" + checks
    module_url = "data:text/javascript;base64," + base64.b64encode(
        module_source.encode("utf-8")
    ).decode("ascii")
    subprocess.run(
        ["node", "--input-type=module", "-e", f"await import('{module_url}')"],
        check=True,
        text=True,
    )


@pytest.mark.skipif(not shutil.which("node"), reason="node is required")
def test_agent_profile_loads_dedupe_same_context_and_ignore_stale_context() -> None:
    source = SWITCHER_MIXIN.read_text(encoding="utf-8")
    source = re.sub(r"^import .*?;\n", "", source, flags=re.MULTILINE)
    harness = r"""
const pending = [];
const callJsonApi = async () => await new Promise(resolve => pending.push(resolve));
const fetchApi = async () => ({ ok: true, json: async () => ({}) });
let selectedContextId = "ctx";
globalThis.window = { Alpine: { store: () => ({ selected: selectedContextId }) } };
"""
    checks = r"""
const store = { ...switcherState, ...switcherMethods };
const older = store.loadAgentProfiles(true);
const newer = store.loadAgentProfiles(true);
if (pending.length !== 1 || !store.agentProfilesLoading) throw new Error("same-context profile loads were not deduplicated");
pending[0]({ profiles: [
  { id: "default", title: "Default", enabled: true },
  { id: "new", title: "New", enabled: true },
] });
await Promise.all([older, newer]);
if (store.agentProfiles[0]?.key !== "new" || store.agentProfilesLoading || !store.agentProfilesLoaded) throw new Error("newest profile load did not settle");
if (store.agentProfiles.length !== 1 || store.getAgentProfileList("default", "Default").some(profile => profile.key === "default")) throw new Error("Default profile remained selectable in the chat popover");
selectedContextId = "older-context";
const stale = store.loadAgentProfiles(true);
selectedContextId = "newer-context";
const fresh = store.loadAgentProfiles(true);
if (pending.length !== 3) throw new Error("different-context profile loads were incorrectly deduplicated");
pending[2]({ profiles: [{ id: "fresh", title: "Fresh", enabled: true }] });
await fresh;
pending[1]({ profiles: [{ id: "stale", title: "Stale", enabled: true }] });
await stale;
if (store.agentProfiles[0]?.key !== "fresh" || store.agentProfilesLoading) throw new Error("stale profile load replaced newer state");
const requestCount = pending.length;
await store.loadAgentProfiles();
if (pending.length !== requestCount) throw new Error("cached profile catalog unexpectedly reloaded");
"""
    module_source = harness + "\n" + source + "\n" + checks
    module_url = "data:text/javascript;base64," + base64.b64encode(
        module_source.encode("utf-8")
    ).decode("ascii")
    subprocess.run(
        ["node", "--input-type=module", "-e", f"await import('{module_url}')"],
        check=True,
        text=True,
    )


def test_profile_slash_effects_reuse_the_agent_editor_entry_points() -> None:
    slash_store = (
        ROOT / "plugins" / "_commands" / "webui" / "commands-slash-store.js"
    ).read_text(encoding="utf-8")

    assert 'type === "open_agent_editor"' in slash_store
    assert "globalThis.openAgentEditor?." in slash_store
    assert 'type === "test_agent_profile"' in slash_store
    assert "globalThis.testAgentProfile?." in slash_store
    assert "String(effect.project_name || \"\")" in slash_store