Repository navigation
Expand file tree
/
Copy pathsettings_test.go
More file actions
1502 lines (1423 loc) · 56.1 KB
/
Copy pathsettings_test.go
File metadata and controls
1502 lines (1423 loc) · 56.1 KB
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
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package config
import (
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"
"github.com/Agent-Field/codeaf/internal/automation"
"github.com/Agent-Field/codeaf/internal/cell/celltest"
"github.com/Agent-Field/codeaf/internal/pool/poolcfg"
"github.com/Agent-Field/codeaf/internal/taxonomy"
)
func registry(t *testing.T, dir string) *Settings {
t.Helper()
return NewSettings(SettingsOptions{
ProfileDir: dir,
ModelValue: func(slot string) string { return slot + "/model" },
SetModel: func(string, string) error { return nil },
SplitPct: func() int { return 0 },
})
}
func TestSlackApplicationRowShowsOnlyThePersonsAnswer(t *testing.T) {
dir := t.TempDir()
t.Setenv("SLACK_OAUTH_CLIENT", "")
row, found := registry(t, dir).Row(KeySlackOAuthClient)
if !found {
t.Fatal("the Slack application row is not registered")
}
if row.Label != "slack app id" || row.Env != "SLACK_OAUTH_CLIENT" || row.EmptyLabel != "not set" {
t.Errorf("Slack row = %+v", row)
}
if got := row.Value(); got != "not set" {
t.Errorf("an unanswered row shows %q, want not set", got)
}
if got := SlackOAuthClientAt(dir); got != defaultSlackOAuthClient {
t.Errorf("shipped application = %q", got)
}
if err := row.Apply("an-internal-application"); err != nil {
t.Fatalf("Apply: %v", err)
}
if got := SlackOAuthClientAt(dir); got != "an-internal-application" {
t.Errorf("persisted application = %q", got)
}
t.Setenv("SLACK_OAUTH_CLIENT", "an-environment-application")
if got := SlackOAuthClientAt(dir); got != "an-environment-application" {
t.Errorf("environment application = %q", got)
}
}
// The pool key row: the trusted word a fetched index is checked under. Empty
// is the ordinary answer and reads as the key the binary carries; a stored
// word is the one the install trusts, and the pin outranks it.
func TestThePoolKeyRowReadsBuiltInAndFollowsItsStoredWordAndItsPin(t *testing.T) {
dir := t.TempDir()
t.Setenv("CODEAF_MODEL_POOL_PUBLIC_KEY", "")
t.Setenv("CODEAF_MODEL_POOL", "")
rows := registry(t, dir)
row, ok := rows.Row(KeyModelPoolPublicKey)
if !ok {
t.Fatal("the pool key is not registered")
}
if row.Category != CategoryModels || row.Kind != SettingText || row.Label != "pool key" {
t.Fatalf("pool key row = %+v", row)
}
if row.Value() != "built in" {
t.Fatalf("an unset key row reads %q, want built in", row.Value())
}
if got := ModelPoolAt(dir); got.PublicKey != "" || got.Source.PublicKey != "default" {
t.Fatalf("an untouched profile resolved %+v", got)
}
if err := row.Apply("A-base64-word="); err != nil {
t.Fatal(err)
}
reread, _ := registry(t, dir).Row(KeyModelPoolPublicKey)
if reread.Value() != "A-base64-word=" {
t.Fatalf("the reread row lost the persisted word: %q", reread.Value())
}
if got := ModelPoolAt(dir); got.PublicKey != "A-base64-word=" || got.Source.PublicKey != "setting" {
t.Fatalf("the stored key did not take: %+v", got)
}
t.Setenv("CODEAF_MODEL_POOL_PUBLIC_KEY", "a-pinned-key")
pinned, _ := registry(t, dir).Row(KeyModelPoolPublicKey)
if name, isPinned := pinned.PinnedBy(); !isPinned || name != "CODEAF_MODEL_POOL_PUBLIC_KEY" {
t.Fatalf("the key row did not report its pin: %q %v", name, isPinned)
}
if got := ModelPoolAt(dir); got.PublicKey != "a-pinned-key" || got.Source.PublicKey != "env" {
t.Fatalf("the pin lost: %+v", got)
}
if err := pinned.Apply("another"); err == nil || !strings.Contains(err.Error(), "CODEAF_MODEL_POOL_PUBLIC_KEY") {
t.Fatalf("a pinned key row accepted an edit: %v", err)
}
}
// The registry is the completeness gate: a user-tunable environment pin added
// anywhere in the tree has to arrive as a row here, or land on the explicit
// operator-plumbing allowlist. Until then this test fails the build.
func TestRegistryCoversEveryUserFacingEnvironmentPin(t *testing.T) {
registered := map[string]bool{}
for _, row := range registry(t, t.TempDir()).Rows() {
if row.Env != "" {
registered[row.Env] = true
}
if row.EnvDefault != "" {
registered[row.EnvDefault] = true
}
}
for _, name := range OperatorEnvPins {
registered[name] = true
}
pattern := regexp.MustCompile(`CODEAF_[A-Z0-9_]+`)
root := repositoryRoot(t)
seen := map[string]string{}
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
// .claude holds other branches' worktrees; their pins register
// in their own settings.go, and reading them here fails this
// branch for a variable it cannot see.
if entry.Name() == ".git" || entry.Name() == ".claude" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
for _, name := range pattern.FindAllString(string(raw), -1) {
if _, ok := seen[name]; !ok {
seen[name] = path
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
if len(seen) < 10 {
t.Fatalf("the environment scan found only %d pins; it is not reading the tree", len(seen))
}
for name, path := range seen {
if strings.HasPrefix(name, "CODEAF_TEST_") || registered[name] {
continue
}
t.Fatalf("%s (%s) is neither a settings row nor operator plumbing — register it in settings.go",
name, path)
}
}
func repositoryRoot(t *testing.T) string {
t.Helper()
dir, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("no go.mod above the config package")
}
dir = parent
}
}
func TestRegistryGroupsEveryCategoryAndEveryModelSlot(t *testing.T) {
rows := registry(t, t.TempDir())
groups := rows.Groups()
if len(groups) != len(SettingCategories) {
t.Fatalf("groups = %d, want %d", len(groups), len(SettingCategories))
}
for index, group := range groups {
if group.Title != SettingCategories[index] {
t.Fatalf("group %d = %q, want %q", index, group.Title, SettingCategories[index])
}
if len(group.Rows) == 0 {
t.Fatalf("category %q is empty", group.Title)
}
}
for _, slot := range ModelSlots() {
row, ok := rows.Row(ModelSettingKey(slot.Slot))
if !ok || row.Kind != SettingModel || row.Slot != slot.Slot {
t.Fatalf("model slot %q is missing from the registry", slot.Slot)
}
if !slot.Held {
// A role nothing holds a client for reads as what it follows —
// never as the engine's answer for a word it does not know.
if row.Value() != "follows "+slot.Follows {
t.Fatalf("unheld role %q reads %q", slot.Slot, row.Value())
}
continue
}
if slot.Role == "" {
// A CAPABILITY SLOT IS READ OUT OF THE PROFILE, not out of a
// running engine (docs/MULTIMODAL.md Decision 5): its value is a
// choice written down here, and an untouched profile has written
// nothing, which reads as the word the resolver will act on.
if row.Value() != "automatic" {
t.Fatalf("capability slot %q reads %q in an untouched profile", slot.Slot, row.Value())
}
continue
}
if row.Value() != slot.Slot+"/model" {
t.Fatalf("model row %q reads %q", slot.Slot, row.Value())
}
}
seen := map[string]bool{}
for _, row := range rows.Rows() {
if seen[row.Key] {
t.Fatalf("duplicate settings key %q", row.Key)
}
seen[row.Key] = true
if row.Label == "" || row.Category == "" || row.Hint == "" {
t.Fatalf("row %q is missing plain language: %+v", row.Key, row)
}
}
}
func TestSettingsResolveEnvironmentThenFileThenDefault(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{
"CODEAF_DAILY_BUDGET", "CODEAF_DOC_ENGINE", "CODEAF_VISION_MODEL",
} {
t.Setenv(name, "")
}
rows := registry(t, dir)
defaults := map[string]string{
KeyDailyBudget: formatDollars(DefaultDailyBudgetUSD),
KeyDocumentEngine: "auto",
KeyVisionModel: "automatic",
}
for key, want := range defaults {
row, ok := rows.Row(key)
if !ok {
t.Fatalf("%s is not registered", key)
}
if got := row.Value(); got != want {
t.Fatalf("%s default = %q, want %q", key, got, want)
}
}
changes := map[string]string{
KeyDailyBudget: "35.50",
KeyDocumentEngine: "local",
KeyVisionModel: "seer/vision",
}
for key, raw := range changes {
row, _ := rows.Row(key)
if err := row.Apply(raw); err != nil {
t.Fatalf("apply %s=%q: %v", key, raw, err)
}
}
reread := registry(t, dir)
persisted := map[string]string{
KeyDailyBudget: "$35.5",
KeyDocumentEngine: "local",
KeyVisionModel: "seer/vision",
}
for key, want := range persisted {
row, _ := reread.Row(key)
if got := row.Value(); got != want {
t.Fatalf("%s persisted = %q, want %q", key, got, want)
}
}
// One file, one writer: the budget rail and the new rows share it, and an
// unrelated key already in the object survives every write.
if err := WriteDailyBudgetUSD(dir, 12); err != nil {
t.Fatal(err)
}
if got, err := DailyBudgetUSDAt(dir); err != nil || got != 12 {
t.Fatalf("daily budget = %v err=%v", got, err)
}
if got, _ := reread.Row(KeyDocumentEngine); got.Value() != "local" {
t.Fatalf("a budget write erased the document engine: %q", got.Value())
}
t.Setenv("CODEAF_DOC_ENGINE", "ocr")
t.Setenv("CODEAF_VISION_MODEL", "pinned/vision")
pinned := registry(t, dir)
for key, want := range map[string]string{
KeyDocumentEngine: "ocr", KeyVisionModel: "pinned/vision",
} {
row, _ := pinned.Row(key)
if got := row.Value(); got != want {
t.Fatalf("%s under the environment = %q, want %q", key, got, want)
}
name, isPinned := row.PinnedBy()
if !isPinned || name == "" {
t.Fatalf("%s did not report its environment pin", key)
}
if err := row.Apply("auto"); err == nil || !strings.Contains(err.Error(), name) {
t.Fatalf("%s accepted an edit while pinned: %v", key, err)
}
}
}
// The persisted layer is what Load reads on the next launch; the sheet writing
// a value has to be the same thing the process reads back.
func TestLoadReadsPersistedSettings(t *testing.T) {
dir := t.TempDir()
rows := registry(t, dir)
for key, raw := range map[string]string{
KeyDailyBudget: "6",
KeyDocumentEngine: "free",
KeyVisionModel: "seer/vision",
} {
row, _ := rows.Row(key)
if err := row.Apply(raw); err != nil {
t.Fatalf("apply %s: %v", key, err)
}
}
t.Setenv("OPENROUTER_API_KEY", "test-key")
t.Setenv("CODEAF_BASE_URL", "http://127.0.0.1:1")
t.Setenv("CODEAF_PROFILE_DIR", dir)
for _, name := range []string{
"CODEAF_DAILY_BUDGET", "CODEAF_DOC_ENGINE", "CODEAF_VISION_MODEL",
} {
t.Setenv(name, "")
}
loaded, err := Load()
if err != nil {
t.Fatal(err)
}
if loaded.DailyBudgetUSD != 6 || loaded.DocumentEngine != "free" ||
loaded.VisionModel != "seer/vision" {
t.Fatalf("persisted settings did not reach Load: %+v", loaded)
}
// The environment still wins over everything written here.
t.Setenv("CODEAF_DOC_ENGINE", "ocr")
t.Setenv("CODEAF_DAILY_BUDGET", "2")
loaded, err = Load()
if err != nil {
t.Fatal(err)
}
if loaded.DocumentEngine != "ocr" || loaded.DailyBudgetUSD != 2 {
t.Fatalf("environment lost to the persisted file: %+v", loaded)
}
}
func TestSettingEditorsRefuseNonsenseInPlainLanguage(t *testing.T) {
rows := registry(t, t.TempDir())
for key, raw := range map[string]string{
KeyDailyBudget: "twenty dollars",
KeyTaskParallel: "many",
KeyDocumentEngine: "tesseract",
} {
row, ok := rows.Row(key)
if !ok {
t.Fatalf("%s is not registered", key)
}
err := row.Apply(raw)
if err == nil {
t.Fatalf("%s accepted %q", key, raw)
}
if message := err.Error(); message == "" || strings.Contains(message, "strconv") {
t.Fatalf("%s error is not plain language: %v", key, err)
}
}
}
// THE ATTRIBUTION ROW defaults on, turns signing off when requested, and obeys
// the shell pin.
func TestTheAttributionRowDefaultsOnAndTurnsSigningOff(t *testing.T) {
dir := t.TempDir()
t.Setenv("CODEAF_ATTRIBUTION", "")
rows := registry(t, dir)
row, ok := rows.Row(KeyAttribution)
if !ok {
t.Fatalf("%s is not registered", KeyAttribution)
}
if KeyAttribution != "attribution" || row.Label != "attribution" ||
row.Env != "CODEAF_ATTRIBUTION" || row.Category != CategoryInterface || row.Kind != SettingBool {
t.Fatalf("the attribution row = %+v", row)
}
if want := "On, codeaf signs the commits, pull requests, issues and comments it writes; off, it adds no marks."; row.Hint != want || AttributionHint != want {
t.Fatalf("hint = %q, want %q", row.Hint, want)
}
if !PreferenceKey(KeyAttribution) {
t.Fatal("attribution is not a preference key")
}
if row.SelfService() {
t.Fatal("attribution must not be self-service")
}
if row.Value() != "on" || !AttributionAt(dir) {
t.Fatalf("attribution is not on by default: %q", row.Value())
}
if err := row.Apply("off"); err != nil {
t.Fatal(err)
}
if AttributionAt(dir) {
t.Fatal("off still leaves attribution enabled")
}
if reread, _ := registry(t, dir).Row(KeyAttribution); reread.Value() != "off" {
t.Fatalf("the reread row lost the persisted choice: %q", reread.Value())
}
if err := row.Apply("on"); err != nil {
t.Fatal(err)
}
if !AttributionAt(dir) {
t.Fatal("on did not turn attribution back on")
}
if err := row.Apply("off"); err != nil {
t.Fatal(err)
}
t.Setenv("CODEAF_ATTRIBUTION", "on")
if !AttributionAt(dir) {
t.Fatal("the environment lost to the persisted file")
}
pinned, _ := registry(t, dir).Row(KeyAttribution)
if name, isPinned := pinned.PinnedBy(); !isPinned || name != "CODEAF_ATTRIBUTION" {
t.Fatalf("the attribution row did not report its pin: %q", name)
}
t.Setenv("CODEAF_ATTRIBUTION", "off")
if AttributionAt(dir) {
t.Fatal("CODEAF_ATTRIBUTION=off did not turn attribution off")
}
t.Setenv("CODEAF_ATTRIBUTION", "sure")
if !AttributionAt(dir) {
t.Fatal("a malformed pin did not fall back to the default")
}
}
// THE MODEL'S NAME IN THE `Assisted-by` LINE is on until someone says
// otherwise, off leaves the line bare, and the shell still wins.
func TestTheModelNameRowDefaultsOnAndTurnsOnlyTheNameOff(t *testing.T) {
dir := t.TempDir()
t.Setenv("CODEAF_ATTRIBUTION_MODEL", "")
rows := registry(t, dir)
row, ok := rows.Row(KeyAttributionModel)
if !ok {
t.Fatalf("%s is not registered", KeyAttributionModel)
}
if KeyAttributionModel != "attribution.model" || row.Env != "CODEAF_ATTRIBUTION_MODEL" ||
row.Category != CategoryInterface || row.Kind != SettingBool {
t.Fatalf("the model-name row = %+v", row)
}
if want := "On, commits say `Assisted-by: CodeAF (<model>)`; off, `Assisted-by: CodeAF`."; row.Hint != want {
t.Fatalf("hint = %q, want %q", row.Hint, want)
}
const model = "deepseek/deepseek-v4-flash"
if row.Value() != "on" || AssistedByModelAt(dir, model) != model {
t.Fatalf("the model's name is not on by default: %q", row.Value())
}
if err := row.Apply("off"); err != nil {
t.Fatal(err)
}
if got := AssistedByModelAt(dir, model); got != "" {
t.Fatalf("off still hands the line a model: %q", got)
}
if reread, _ := registry(t, dir).Row(KeyAttributionModel); reread.Value() != "off" {
t.Fatalf("the reread row lost the persisted choice: %q", reread.Value())
}
t.Setenv("CODEAF_ATTRIBUTION_MODEL", "on")
if AssistedByModelAt(dir, model) != model {
t.Fatal("the environment lost to the persisted file")
}
pinned, _ := registry(t, dir).Row(KeyAttributionModel)
if name, isPinned := pinned.PinnedBy(); !isPinned || name != "CODEAF_ATTRIBUTION_MODEL" {
t.Fatalf("the model-name row did not report its pin: %q", name)
}
t.Setenv("CODEAF_ATTRIBUTION_MODEL", "sure")
if !AttributionModelAt(dir) {
t.Fatal("a malformed pin did not fall back to the default")
}
}
func TestSplitPercentClampsAndSavesThroughTheRegistry(t *testing.T) {
saved := 0
rows := NewSettings(SettingsOptions{
ProfileDir: t.TempDir(),
SplitPct: func() int { return saved },
SaveSplitPct: func(pct int) { saved = pct },
})
row, ok := rows.Row(KeySplitPct)
if !ok {
t.Fatal("the divider is not registered")
}
if got := row.Value(); got != formatPercent(DefaultSplitPct) {
t.Fatalf("unset divider reads %q", got)
}
if err := row.Apply("60"); err != nil || saved != 60 {
t.Fatalf("apply 60 saved=%d err=%v", saved, err)
}
if err := row.Apply("95"); err == nil {
t.Fatal("the divider accepted a share outside its band")
}
}
// And the other half of that row: a caller with no divider to move does not get
// a divider row. It used to get one that read a plausible percentage, took a
// new one and answered "chat width is unavailable here" — a control whose only
// behaviour was to refuse, which is the shape this codebase leaves OFF rather
// than shipping broken. The v3 chat is that caller: its roster is a fixed
// column, not a share of the frame.
func TestTheDividerRowIsAbsentWithoutSomewhereToSaveIt(t *testing.T) {
rows := NewSettings(SettingsOptions{
ProfileDir: t.TempDir(),
// The read seam alone, which is what a surface that can only DRAW a
// divider would hand over.
SplitPct: func() int { return 60 },
})
if _, ok := rows.Row(KeySplitPct); ok {
t.Fatal("a surface that cannot save the divider was still given the row")
}
for _, row := range rows.Rows() {
if row.Key == KeySplitPct {
t.Fatal("the divider reached the sheet through Rows()")
}
}
// With the seam it is back, and it is back WHERE IT WAS: at the head of the
// interface group, ahead of the task-column row it has always sat above.
full := NewSettings(SettingsOptions{
ProfileDir: t.TempDir(),
SplitPct: func() int { return 60 },
SaveSplitPct: func(int) {},
})
divider, taskcol := -1, -1
for index, row := range full.Rows() {
switch row.Key {
case KeySplitPct:
divider = index
case KeyTaskColumn:
taskcol = index
}
}
if divider < 0 {
t.Fatal("a surface that CAN save the divider was not given the row")
}
if divider > taskcol {
t.Fatalf("the divider moved: it is row %d and the task column is row %d", divider, taskcol)
}
}
// ── the web-search rows ─────────────────────────────────────────────────────
// The provider row is a choice with a working default: a person who has never
// opened the sheet searches, and a value nobody can parse still searches.
func TestSearchProviderDefaultsToAutoAndRefusesAPlugItDoesNotKnow(t *testing.T) {
dir := t.TempDir()
rows := registry(t, dir)
row, ok := rows.Row(KeySearchProvider)
if !ok {
t.Fatal("the search provider is not registered")
}
if row.Category != CategoryModels || row.Kind != SettingChoice || row.Label != "searching" {
t.Fatalf("search provider row = %+v", row)
}
if row.Value() != SearchProviderAuto || SearchProviderAt(dir) != SearchProviderAuto {
t.Fatalf("the search provider does not default to auto: %q", row.Value())
}
if _, pinned := row.PinnedBy(); pinned {
t.Fatal("the provider row is pinned by an environment variable; it is a preference, not a secret")
}
if err := row.Apply("exa"); err != nil {
t.Fatal(err)
}
if SearchProviderAt(dir) != "exa" {
t.Fatalf("the pin did not persist: %q", SearchProviderAt(dir))
}
reread, _ := registry(t, dir).Row(KeySearchProvider)
if reread.Value() != "exa" {
t.Fatalf("the reread row lost the persisted pin: %q", reread.Value())
}
if err := reread.Apply("kagi"); err == nil {
t.Fatal("the row accepted a plug this build does not have")
}
if SearchProviderAt(dir) != "exa" {
t.Fatalf("a refused edit still moved the row: %q", SearchProviderAt(dir))
}
// A value written by hand — an older build's plug, a typo — reads as auto
// rather than as an error. Search must not be takeable away by a stale row.
if err := writeProfileValue(dir, KeySearchProvider, "yahoo!"); err != nil {
t.Fatal(err)
}
if SearchProviderAt(dir) != SearchProviderAuto {
t.Fatalf("a stale pin did not fall back to auto: %q", SearchProviderAt(dir))
}
}
// V3 and V5: the public rows feed one options mapping, and the provider choices
// follow the deliberate built-in order rather than registry init order.
func TestSearchRowsMapToOptionsAndProvidersHaveOneOrder(t *testing.T) {
for _, env := range []string{"EXA_API_KEY", "FIRECRAWL_API_KEY", "JINA_API_KEY"} {
t.Setenv(env, "")
}
if got, want := strings.Join(SearchProviders, ","), "auto,firecrawl,duckduckgo,exa,jina-search"; got != want {
t.Fatalf("SearchProviders = %q, want %q", got, want)
}
dir := t.TempDir()
rows := registry(t, dir)
for key, value := range map[string]string{
KeySearchProvider: "exa",
KeyExaKey: "exa-sheet",
KeyFirecrawlKey: "firecrawl-sheet",
KeyJinaKey: "jina-sheet",
} {
row, ok := rows.Row(key)
if !ok {
t.Fatalf("row %q is absent", key)
}
if err := row.Apply(value); err != nil {
t.Fatalf("Apply(%s): %v", key, err)
}
}
opts := SearchOptionsAt(dir)
if opts.Provider != "exa" || opts.ExaKey != "exa-sheet" || opts.FirecrawlKey != "firecrawl-sheet" || opts.JinaKey != "jina-sheet" {
t.Fatalf("SearchOptionsAt = %+v", opts)
}
auto := t.TempDir()
if got := SearchOptionsAt(auto).Provider; got != "" {
t.Fatalf("an untouched auto row became pin %q", got)
}
}
// V2, V4, and V6: the searching row explains the live next-call state, quotes
// the pinned-key failure exactly, and no search row still promises a session
// restart.
func TestSearchHintsDescribeTheNextCall(t *testing.T) {
for _, env := range []string{"EXA_API_KEY", "FIRECRAWL_API_KEY", "JINA_API_KEY"} {
t.Setenv(env, "")
}
dir := t.TempDir()
rows := registry(t, dir)
if got, want := SearchProviderHintAt(dir), "now firecrawl, keyless — set search.exaKey or search.firecrawlKey to raise it"; got != want {
t.Fatalf("auto hint = %q, want %q", got, want)
}
provider, _ := rows.Row(KeySearchProvider)
if err := provider.Apply("exa"); err != nil {
t.Fatal(err)
}
if got, want := SearchProviderHintAt(dir), "exa is pinned but search.exaKey is not set — every search answers \"Search failed (exa): no API key\". Choose auto, or set the key."; got != want {
t.Fatalf("missing-key hint = %q, want %q", got, want)
}
exa, _ := rows.Row(KeyExaKey)
if err := exa.Apply("exa-sheet"); err != nil {
t.Fatal(err)
}
if got, want := SearchProviderHintAt(dir), "exa, with your key"; got != want {
t.Fatalf("keyed hint = %q, want %q", got, want)
}
for _, key := range []string{KeySearchProvider, KeyExaKey, KeyFirecrawlKey, KeyJinaKey} {
row, _ := rows.Row(key)
if strings.Contains(strings.ToLower(row.Hint), "next session") || !strings.Contains(strings.ToLower(row.Hint), "next search") {
t.Errorf("%s hint does not describe the next search: %q", key, row.Hint)
}
}
}
// V3: All search credentials are optional, masked when they read, and pinned by the
// vendors' own environment variables.
func TestTheSearchKeysAreOptionalMaskedAndEnvironmentPinned(t *testing.T) {
for _, credential := range []struct {
key string
env string
label string
read func(string) string
}{
{KeyExaKey, "EXA_API_KEY", "exa key", ExaKeyAt},
{KeyFirecrawlKey, "FIRECRAWL_API_KEY", "firecrawl key", FirecrawlKeyAt},
{KeyJinaKey, "JINA_API_KEY", "jina key", JinaKeyAt},
} {
t.Run(credential.key, func(t *testing.T) {
dir := t.TempDir()
t.Setenv(credential.env, "")
rows := registry(t, dir)
row, ok := rows.Row(credential.key)
if !ok {
t.Fatalf("%s is not registered", credential.label)
}
if row.Category != CategoryModels || row.Kind != SettingText || !row.Secret {
t.Fatalf("%s row = %+v, want a secret text row", credential.label, row)
}
if row.Label != credential.label {
t.Fatalf("row label = %q, want %q", row.Label, credential.label)
}
// Unset is a working configuration and reads as one: the empty
// label, never a row of bullets standing in for nothing.
if row.Value() != "not set" || credential.read(dir) != "" {
t.Fatalf("an unset key reads %q", row.Value())
}
if err := row.Apply("secret-key-abcdefgh1234"); err != nil {
t.Fatal(err)
}
if got := credential.read(dir); got != "secret-key-abcdefgh1234" {
t.Fatalf("the key did not persist whole: %q", got)
}
reread, _ := registry(t, dir).Row(credential.key)
masked := reread.Value()
if strings.Contains(masked, "secret-key") {
t.Fatalf("the row rendered the key: %q", masked)
}
if !strings.HasSuffix(masked, "1234") || !strings.HasPrefix(masked, "••••") {
t.Fatalf("the row did not mask to bullets plus a tail: %q", masked)
}
// The edit that must not destroy anything: open the row, save what
// it displayed, keep the key.
if err := reread.Apply(masked); err != nil {
t.Fatal(err)
}
if got := credential.read(dir); got != "secret-key-abcdefgh1234" {
t.Fatalf("saving the mask overwrote the key: %q", got)
}
// Clearing is still possible, and is the only thing that clears.
if err := reread.Apply(""); err != nil {
t.Fatal(err)
}
if got := credential.read(dir); got != "" {
t.Fatalf("an emptied row kept the key: %q", got)
}
// The vendor's variable wins, is named, and holds the row.
t.Setenv(credential.env, "from-the-shell-wxyz")
if got := credential.read(dir); got != "from-the-shell-wxyz" {
t.Fatalf("the environment did not win: %q", got)
}
pinned, _ := registry(t, dir).Row(credential.key)
name, isPinned := pinned.PinnedBy()
if !isPinned || name != credential.env {
t.Fatalf("%s did not report its pin: %q", credential.label, name)
}
if value := pinned.Value(); !strings.HasSuffix(value, "wxyz") || strings.Contains(value, "from-the-shell") {
t.Fatalf("the pinned row rendered unmasked: %q", value)
}
if err := pinned.Apply("another-key"); err == nil || !strings.Contains(err.Error(), name) {
t.Fatalf("a pinned key accepted an edit: %v", err)
}
})
}
}
// The mask shows enough to tell two keys apart and never enough to use, and it
// never reports a length.
func TestMaskCredentialHidesTheKeyAndItsLength(t *testing.T) {
short := maskCredential("sk-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
long := maskCredential("sk-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccc")
if len([]rune(short)) != len([]rune(long)) {
t.Fatalf("the mask leaks the key's length: %q vs %q", short, long)
}
if short == long {
t.Fatalf("two different keys mask identically: %q", short)
}
if maskCredential("") != "" {
t.Fatalf("an unset value masked to %q, want nothing", maskCredential(""))
}
// Too short to be a key: show none of it rather than most of it.
if got := maskCredential("abc"); strings.Contains(got, "abc") {
t.Fatalf("a short value was rendered: %q", got)
}
}
// The three throttle rows read their defaults, take a person's answer, and hand
// it back to the accessor the session door calls — which is the whole of what a
// settings row has to do.
// THE TWO ROWS THAT ARE NOT CREW SEATS SHIP POINTED AT A MODEL, and the two
// answers a person can give one are different from each other: never touching
// it is this build's own choice, emptying it on purpose is "follow the
// conversation". The crew's three seats ship with NO model: unpinned, a seat is
// routed, and a profile with no provider connected has nothing to route to.
func TestEveryTierShipsWithAModelAndCanStillBeCleared(t *testing.T) {
dir := t.TempDir()
row := mustRow(t, registry(t, dir), KeyTierReflexModel)
if got := TierModelAt(dir, ModelTierReflex); got != DefaultReflexModel {
t.Fatalf("an untouched profile resolves the reflex tier to %q, want %q", got, DefaultReflexModel)
}
if got := row.Value(); got != DefaultReflexModel {
t.Fatalf("the reflex row reads %q in an untouched profile, want %q", got, DefaultReflexModel)
}
if got := TierModelAt(dir, ModelTierLow); got != DefaultLowModel {
t.Fatalf("the low tier resolves %q in an untouched profile, want %q", got, DefaultLowModel)
}
for _, tier := range []string{ModelTierWorker, ModelTierHigh, ModelTierMastermind} {
if seat := TierSeatAt(dir, tier); seat.Source != SeatRouted {
t.Fatalf("the %s tier reads %+v in an untouched profile, want a routed seat", tier, seat)
}
}
if err := row.Apply("vendor/tiny"); err != nil {
t.Fatal(err)
}
if got := TierModelAt(dir, ModelTierReflex); got != "vendor/tiny" {
t.Fatalf("the reflex tier resolves %q after a person wrote vendor/tiny", got)
}
if got := mustRow(t, registry(t, dir), KeyTierReflexModel).Value(); got != "vendor/tiny" {
t.Fatalf("the reflex row reads %q on the next launch", got)
}
// Cleared is an ANSWER: the row goes back to its empty label and the tier
// falls to internal/roles' floor, the model the person is talking to.
if err := row.Apply(""); err != nil {
t.Fatalf("clearing the reflex row: %v", err)
}
if got := TierModelAt(dir, ModelTierReflex); got != "" {
t.Fatalf("a cleared reflex row resolves %q, want nothing", got)
}
if got := mustRow(t, registry(t, dir), KeyTierReflexModel).Value(); got != "follows the conversation" {
t.Fatalf("a cleared reflex row reads %q, want its empty label", got)
}
}
// C27.7 exercises this behaviour without an opt-in switch.
func TestTheTaskThrottleRowsRoundTrip(t *testing.T) {
celltest.Available(t)
dir := t.TempDir()
rows := registry(t, dir)
// NO LIMIT IS THE DEFAULT, and the row says it in words rather than in a
// zero: "0 tasks at once" reads like a switch that is off.
parallel, ok := rows.Row(KeyTaskParallel)
if !ok {
t.Fatal("the parallel row is not registered")
}
if got := parallel.Value(); got != "no limit" {
t.Fatalf("unset task.parallel reads %q, want its empty label", got)
}
if TaskParallelAt(dir) != DefaultTaskParallel {
t.Fatalf("unset task.parallel resolves %d, want %d", TaskParallelAt(dir), DefaultTaskParallel)
}
if err := parallel.Apply("3"); err != nil {
t.Fatal(err)
}
if got := TaskParallelAt(dir); got != 3 {
t.Fatalf("task.parallel resolves %d after a person wrote 3", got)
}
if got := mustRow(t, registry(t, dir), KeyTaskParallel).Value(); got != "3" {
t.Fatalf("task.parallel reads %q after a person wrote 3", got)
}
// And clearing it is asking for the empty label back, not a parse error.
if err := parallel.Apply(""); err != nil {
t.Fatalf("clearing task.parallel: %v", err)
}
if TaskParallelAt(dir) != 0 {
t.Fatalf("a cleared task.parallel resolves %d, want no limit", TaskParallelAt(dir))
}
// The load row is a figure and not a count: 1.5 must survive being written.
load := mustRow(t, rows, KeyTaskMaxLoad)
if got := load.Value(); got != "1.5" {
t.Fatalf("unset task.max_load reads %q, want 1.5", got)
}
if err := load.Apply("2.25"); err != nil {
t.Fatal(err)
}
if got := TaskMaxLoadAt(dir); got != 2.25 {
t.Fatalf("task.max_load resolves %v after a person wrote 2.25", got)
}
if err := load.Apply("busy"); err == nil {
t.Fatal("task.max_load accepted a word")
}
if err := load.Apply("0"); err != nil || TaskMaxLoadAt(dir) != 0 {
t.Fatalf("turning the load check off: %v, %v", err, TaskMaxLoadAt(dir))
}
// The cell budget takes a fraction of a GB, and shows it back the same way.
budget := mustRow(t, rows, KeyCellBudget)
if got := budget.Value(); got != "20" {
t.Fatalf("unset cell.budget_gb reads %q, want 20", got)
}
if err := budget.Apply("0.5"); err != nil || CellBudgetGBAt(dir) != 0.5 {
t.Fatalf("cell.budget_gb after 0.5: %v, %v", err, CellBudgetGBAt(dir))
}
if err := budget.Apply("lots"); err == nil {
t.Fatal("cell.budget_gb accepted a word")
}
memory := mustRow(t, rows, KeyTaskMinFreeMB)
if got := memory.Value(); got != "1536" {
t.Fatalf("unset task.min_free_mb reads %q", got)
}
if err := memory.Apply("512"); err != nil {
t.Fatal(err)
}
if got := TaskMinFreeMBAt(dir); got != 512 {
t.Fatalf("task.min_free_mb resolves %d after a person wrote 512", got)
}
if err := memory.Apply("-1"); err == nil {
t.Fatal("task.min_free_mb accepted a negative floor")
}
}
func mustRow(t *testing.T, rows *Settings, key string) Setting {
t.Helper()
row, ok := rows.Row(key)
if !ok {
t.Fatalf("%s is not registered", key)
}
return row
}
// ── auto update ─────────────────────────────────────────────────────────────
// THE AUTO UPDATE ROW IS FILED WITH WHAT THE MACHINE DOES ON ITS OWN, rather
// than with how the surface draws itself. It used to be filed under the
// interface group and drawn on the Display tab, which is about appearance and
// typing; a person keeping an installation current looks where the machine's
// own work is kept.
func TestTheAutoUpdateRowIsFiledWithThePracticeRows(t *testing.T) {
registry := NewSettings(SettingsOptions{ProfileDir: t.TempDir()})
row, ok := registry.Row(KeyUpdateAuto)
if !ok {
t.Fatal("the registry has no auto update row")
}
if row.Category != CategoryPractice {
t.Fatalf("auto update is filed under %q, want %q", row.Category, CategoryPractice)
}
// THE FIRST SENTENCE SAYS WHAT THE ROW DOES, and the rest says what off
// means: manual installation, while the one launch check still runs.
for _, want := range []string{
"keeps a released codeaf current: a newer stable release installs as codeaf opens",
"Dev, staging, rc and source builds are never updated on their own.",
"Off, installation is manual",
"still checks once",
} {
if !strings.Contains(row.Hint, want) {
t.Fatalf("the hint does not say %q: %s", want, row.Hint)
}
}
}
// ── the retired task-start word ─────────────────────────────────────────────
// retiredTaskStartWord is the spelling a profile written before the ONE ROAD
// wave may still be holding. It is a literal here, and deliberately: the whole
// mechanism is that this word exists nowhere in internal/config's own code, so a
// constant offered back to the package would be the thing these tests exist to
// prove is gone.
const retiredTaskStartWord = "adaptive"
// ONE ROAD. `task.start` used to have an answer that opened a planned graph
// before anybody started, and it is gone: a chat turn takes one road for
// ordinary work now, and the two words left both start ONE worker — the
// difference between them is only whether the brief is read for width first.
func TestTaskStartOffersOnlyTheTwoSurvivingWords(t *testing.T) {
if got := strings.Join(TaskStartModes, ","); got != "sized,single" {
t.Fatalf("the modes are %q, want the two surviving words with the default first", got)
}
row, ok := registry(t, t.TempDir()).Row(KeyTaskStart)
if !ok {
t.Fatal("the sheet has no starting-a-task row")
}
if strings.Join(row.Choices, ",") != strings.Join(TaskStartModes, ",") {
t.Fatalf("the row offers %v, which is not the list every other reader takes", row.Choices)
}
// AND THE HINT DOES NOT DESCRIBE A ROAD NOBODY CAN TAKE. The hint is the
// whole of what a person reads before answering, so a sentence about
// planning the pieces up front would be the sheet advertising a word the
// row itself refuses.
if strings.Contains(row.Hint, retiredTaskStartWord) {
t.Errorf("the hint still describes the retired word: %s", row.Hint)
}
// A WRITE OF IT IS REFUSED, in the words the row accepts in, and the refusal
// does not name it either.
err := row.Apply(retiredTaskStartWord)
if err == nil {
t.Fatal("the row accepted the retired word")
}
if strings.Contains(err.Error(), retiredTaskStartWord) {
t.Errorf("the refusal offers the retired word back: %v", err)
}
}
// A PREFERENCE SET MONTHS AGO MAY NOT BECOME AN ERROR. Somebody who chose the
// planned road once has a config.json still holding the word; the retirement is
// paid for by the resolver, which reads a word this build does not know as no
// answer at all — so the profile loads clean, the row reads `sized`, and nobody
// is told anything about a decision they made and forgot.
func TestAProfileStillHoldingTheRetiredWordReadsAsSized(t *testing.T) {
dir := t.TempDir()
if err := writeProfileValue(dir, KeyTaskStart, retiredTaskStartWord); err != nil {