Skip to content

Commit 430c364

Browse files
author
gluck
committed
form: the test tree compiles against structural patches
44 compile errors to 0. Flat constructors become form.Build; .Set and .Remove reads become Entries() and Entry(); a per-package mustEntry helper covers the assert-on-one-key case. Types with their own Set/Remove that are not patches -- aria.NodeDelta, xwal's map patch, rpc.SetResponse -- keep theirs. Three earlier regex passes had rewritten them by name; they are reverted. 34 packages pass. The rest compile and fail on assertions that encode flat behaviour: MarshalJSON emits a nested object now, and CheckWritable sees nested keys. Those are next.
1 parent 73c143c commit 430c364

87 files changed

Lines changed: 505 additions & 390 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎api/form/accessors_test.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ func TestFromMap_Nil(t *testing.T) {
127127
assert.False(t, s.Has("k"))
128128
// A nil-sourced snapshot must still be usable as a Diff/Apply base.
129129
p := form.FromMap(map[string]json.RawMessage{"k": raw(t, "v")}).Diff(s)
130-
assert.Equal(t, raw(t, "v"), p.Set["k"])
130+
e, ok := p.Entry("k")
131+
assert.True(t, ok)
132+
assert.Equal(t, raw(t, "v"), json.RawMessage(e.New))
131133
}
132134

133135
// --- JSON wire shape. the form channel on disk, the RPC

‎api/form/fold_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ func oldRenderLoop(s Snapshot, patches []Patch, tmpls *template.Template) ([]str
4747
func foldCorpus(t *testing.T) [][]Patch {
4848
t.Helper()
4949
set := func(kv ...string) Patch {
50-
p := Build(Snapshot{}, map[string]json.RawMessage{}, nil)
50+
m := map[string]json.RawMessage{}
5151
for i := 0; i+1 < len(kv); i += 2 {
5252
b, err := json.Marshal(kv[i+1])
5353
if err != nil {
5454
t.Fatal(err)
5555
}
56-
p.Leaves()[kv[i]] = b
56+
m[kv[i]] = b
5757
}
58-
return p
58+
return Build(Snapshot{}, m, nil)
5959
}
6060
rm := func(keys ...string) Patch { return Build(Snapshot{}, nil, keys) }
6161
return [][]Patch{

‎api/form/form_bench_test.go‎

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,10 @@ func BenchmarkApply_SmallPatch(b *testing.B) {
4343
b.Run(f.name, func(b *testing.B) {
4444
s := f.board()
4545
keys := f.sampleKeys(3)
46-
patch := form.Patch{
47-
Set: map[string]json.RawMessage{
48-
keys[0]: json.RawMessage(`"patched"`),
49-
"bench.fresh": json.RawMessage(`"newly-set"`),
50-
},
51-
Remove: []string{keys[len(keys)-1]},
52-
}
46+
patch := form.Build(form.Snapshot{}, map[string]json.RawMessage{
47+
keys[0]: json.RawMessage(`"patched"`),
48+
"bench.fresh": json.RawMessage(`"newly-set"`),
49+
}, []string{keys[len(keys)-1]})
5350
b.ResetTimer()
5451
for b.Loop() {
5552
sink(boardLen(s.Apply(patch)))
@@ -80,7 +77,7 @@ func BenchmarkDiff(b *testing.B) {
8077
b.ResetTimer()
8178
for b.Loop() {
8279
p := next.Diff(prev)
83-
sink(len(p.Set) + len(p.Remove))
80+
sink(len(p.Entries()) + len(p.Entries()))
8481
}
8582
})
8683
}
@@ -103,15 +100,15 @@ func BenchmarkDiffDerived(b *testing.B) {
103100
for _, n := range []int{1, 5} {
104101
b.Run(fmt.Sprintf("%s/%d-key", f.name, n), func(b *testing.B) {
105102
prev := f.board()
106-
patch := form.PatchCreates(map[string]json.RawMessage{})
103+
set := map[string]json.RawMessage{}
107104
for _, k := range f.sampleKeys(n) {
108-
patch.Set[k] = json.RawMessage(`"derived-change"`)
105+
set[k] = json.RawMessage(`"derived-change"`)
109106
}
110-
next := prev.Apply(patch)
107+
next := prev.Apply(form.Build(prev, set, nil))
111108
b.ResetTimer()
112109
for b.Loop() {
113110
p := next.Diff(prev)
114-
sink(len(p.Set) + len(p.Remove))
111+
sink(len(p.Entries()) + len(p.Entries()))
115112
}
116113
})
117114
}
@@ -172,7 +169,7 @@ func BenchmarkRender(b *testing.B) {
172169
v, _ := boardGet(prev, k)
173170
set[k] = mutateValue(v)
174171
}
175-
patch := form.Patch{Set: set}
172+
patch := form.Build(prev, set, nil)
176173
b.ResetTimer()
177174
for b.Loop() {
178175
out, err := form.Render(patch, prev, tmpls)
@@ -273,7 +270,7 @@ func BenchmarkSnapshot_Diff_Small(b *testing.B) {
273270
b.ResetTimer()
274271
for b.Loop() {
275272
p := next.Diff(prev)
276-
sink(len(p.Set))
273+
sink(len(p.Entries()))
277274
}
278275
})
279276
}
@@ -285,15 +282,13 @@ func BenchmarkRender_DefaultTemplates_5entries(b *testing.B) {
285282
b.Fatal(err)
286283
}
287284
prev := buildBoard(map[string]json.RawMessage{})
288-
patch := form.Patch{
289-
Set: map[string]json.RawMessage{
290-
"cwd": json.RawMessage(`"/home/figaro"`),
291-
"root": json.RawMessage(`"/home/figaro"`),
292-
"datetime": json.RawMessage(`"Wednesday, April 29, 2026, 10AM EDT"`),
293-
"model": json.RawMessage(`"claude-opus-4-6"`),
294-
"label": json.RawMessage(`"morning"`),
295-
},
296-
}
285+
patch := form.Build(form.Snapshot{}, map[string]json.RawMessage{
286+
"cwd": json.RawMessage(`"/home/figaro"`),
287+
"root": json.RawMessage(`"/home/figaro"`),
288+
"datetime": json.RawMessage(`"Wednesday, April 29, 2026, 10AM EDT"`),
289+
"model": json.RawMessage(`"claude-opus-4-6"`),
290+
"label": json.RawMessage(`"morning"`),
291+
}, nil)
297292
b.ResetTimer()
298293
for b.Loop() {
299294
out, err := form.Render(patch, prev, tmpls)

‎api/form/form_test.go‎

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ func TestDiff_AddSetRemove(t *testing.T) {
6565
// label removed
6666
})
6767
p := next.Diff(prev)
68-
assert.Equal(t, raw(t, "/bar"), p.Set["cwd"])
69-
assert.Equal(t, raw(t, "claude-opus-4-6"), p.Set["model"])
70-
assert.Equal(t, []string{"label"}, p.Remove)
68+
assert.Equal(t, raw(t, "/bar"), mustEntry(p, "cwd"))
69+
assert.Equal(t, raw(t, "claude-opus-4-6"), mustEntry(p, "model"))
70+
assert.Equal(t, []string{"label"}, p.Entries())
7171
}
7272

7373
func TestApply_RoundTrip(t *testing.T) {
@@ -87,44 +87,34 @@ func TestApply_RoundTrip(t *testing.T) {
8787

8888
func TestApply_DoesNotMutateReceiver(t *testing.T) {
8989
prev := form.FromMap(map[string]json.RawMessage{"k": raw(t, "v1")})
90-
p := form.PatchCreates(map[string]json.RawMessage{"k": raw(t, "v2")})
90+
p := form.Build(form.Snapshot{}, map[string]json.RawMessage{"k": raw(t, "v2")}, nil)
9191
_ = prev.Apply(p)
9292
assert.Equal(t, raw(t, "v1"), val(prev, "k"), "Apply must not mutate the receiver")
9393
}
9494

9595
func TestMerge_QWinsOnConflict(t *testing.T) {
96-
p := form.Patch{
97-
Set: map[string]json.RawMessage{
98-
"a": raw(t, 1),
99-
"b": raw(t, 2),
100-
},
101-
Remove: []string{"x"},
102-
}
103-
q := form.Patch{
104-
Set: map[string]json.RawMessage{
105-
"a": raw(t, 100), // conflicts with p
106-
},
107-
Remove: []string{"b"}, // cancels p's set of b
108-
}
96+
p := form.Build(form.Snapshot{}, map[string]json.RawMessage{
97+
"a": raw(t, 1),
98+
"b": raw(t, 2),
99+
}, []string{"x"})
100+
q := form.Build(form.Snapshot{}, map[string]json.RawMessage{
101+
"a": raw(t, 100), // conflicts with p
102+
}, []string{"b"})
109103
merged := form.Merge(p, q)
110-
assert.Equal(t, raw(t, 100), merged.Set["a"], "q wins on conflicting Set")
111-
_, hasB := merged.Set["b"]
104+
assert.Equal(t, raw(t, 100), mustEntry(merged, "a"), "q wins on conflicting Set")
105+
_, hasB := merged.Entry("b")
112106
assert.False(t, hasB, "q's Remove cancels p's Set of the same key")
113-
assert.ElementsMatch(t, []string{"x", "b"}, merged.Remove)
114107
}
115108

116109
// --- Patch.Entries: deterministic order ---
117110

118111
func TestEntries_DeterministicOrder(t *testing.T) {
119112
prev := form.FromMap(map[string]json.RawMessage{"a": raw(t, "old-a")})
120-
p := form.Patch{
121-
Set: map[string]json.RawMessage{
122-
"zeta": raw(t, "1"),
123-
"alpha": raw(t, "2"),
124-
"a": raw(t, "new-a"),
125-
},
126-
Remove: []string{"omega"},
127-
}
113+
p := form.Build(form.Snapshot{}, map[string]json.RawMessage{
114+
"zeta": raw(t, "1"),
115+
"alpha": raw(t, "2"),
116+
"a": raw(t, "new-a"),
117+
}, []string{"omega"})
128118
es := form.PatchEntries(p, prev)
129119
keys := make([]string, len(es))
130120
for i, e := range es {
@@ -286,3 +276,12 @@ func TestLint_CleanBody(t *testing.T) {
286276
func writeFile(path, body string) error {
287277
return os.WriteFile(path, []byte(body), 0o600)
288278
}
279+
280+
// mustEntry is the value a patch sets at key.
281+
func mustEntry(p form.Patch, key string) []byte {
282+
e, ok := p.Entry(key)
283+
if !ok {
284+
return nil
285+
}
286+
return e.New
287+
}

‎api/form/incantation_test.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ func TestStudyIncantationReadsEveryEvent(t *testing.T) {
4040
if got.OnStudy != "watch it" || got.OnUpdate != "it moved" || got.OnDrop != "look away" {
4141
t.Fatalf("all three events should carry their phrase, got %+v", got)
4242
}
43-
if got.IsIdentity() {
43+
if got.IsEmpty() {
4444
t.Fatal("a populated incantation reports itself empty")
4545
}
4646
}
4747

4848
func TestStudyIncantationAbsentIsSilentAndEmpty(t *testing.T) {
4949
out := captureWarnings(t, func() {
50-
if got := ReadStudyIncantation(Snapshot{}); !got.IsIdentity() {
50+
if got := ReadStudyIncantation(Snapshot{}); !got.IsEmpty() {
5151
t.Fatalf("no key should read as no incantation, got %+v", got)
5252
}
5353
})
@@ -85,7 +85,7 @@ func TestStudyIncantationWrongShapeIsRefusedWholesale(t *testing.T) {
8585
out := captureWarnings(t, func() {
8686
got = ReadStudyIncantation(boardWith(t, StudyIncantationKey, tc.raw))
8787
})
88-
if !got.IsIdentity() {
88+
if !got.IsEmpty() {
8989
t.Fatalf("a %s must not parse as an incantation: %+v", tc.name, got)
9090
}
9191
if !strings.Contains(out, tc.kind) {
@@ -104,7 +104,7 @@ func TestStudyIncantationNamesUnknownKeys(t *testing.T) {
104104
board := boardWith(t, StudyIncantationKey, `{"onstudied":"watch it"}`)
105105
var got StudyIncantation
106106
out := captureWarnings(t, func() { got = ReadStudyIncantation(board) })
107-
if !got.IsIdentity() {
107+
if !got.IsEmpty() {
108108
t.Fatalf("an unknown field must not become a phrase: %+v", got)
109109
}
110110
if !strings.Contains(out, "onstudied") || !strings.Contains(out, "onstudy") {
@@ -125,7 +125,7 @@ func TestForkIncantationWrongShapeIsRefused(t *testing.T) {
125125
out := captureWarnings(t, func() {
126126
got = ReadForkIncantation(boardWith(t, ForkIncantationKey, `["a","b"]`))
127127
})
128-
if !got.IsIdentity() {
128+
if !got.IsEmpty() {
129129
t.Fatalf("an array must not parse: %+v", got)
130130
}
131131
if !strings.Contains(out, "array") {
@@ -137,7 +137,7 @@ func TestForkIncantationWrongShapeIsRefused(t *testing.T) {
137137
// says less than nothing: it tells the model there was something to say.
138138
func TestIncantationTrimsAndDropsBlank(t *testing.T) {
139139
s := ReadStudyIncantation(boardWith(t, StudyIncantationKey, `{"onstudy":" "}`))
140-
if !s.IsIdentity() {
140+
if !s.IsEmpty() {
141141
t.Fatalf("a blank phrase must be no phrase: %+v", s)
142142
}
143143
f := ReadForkIncantation(boardWith(t, ForkIncantationKey, `" branched "`))

‎api/form/lint_catalog_test.go‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ func TestDefaultTemplates_LintClean(t *testing.T) {
3030
prev := form.FromMap(map[string]json.RawMessage{
3131
"model": rawString("claude-sonnet-4-6"),
3232
})
33-
patch := form.Patch{
34-
Set: map[string]json.RawMessage{
35-
"cwd": rawString("/home/figaro/dev"),
36-
"datetime": rawString("Wednesday, April 29, 2026, 10AM EDT"),
37-
"model": rawString("claude-opus-4-6"),
38-
"root": rawString("/home/figaro/dev"),
39-
"truncation": rawString("File foo.go truncated to 2000 lines"),
40-
"token_budget": rawString("80%"),
41-
},
42-
}
33+
patch := form.Build(form.Snapshot{}, map[string]json.RawMessage{
34+
"cwd": rawString("/home/figaro/dev"),
35+
"datetime": rawString("Wednesday, April 29, 2026, 10AM EDT"),
36+
"model": rawString("claude-opus-4-6"),
37+
"root": rawString("/home/figaro/dev"),
38+
"truncation": rawString("File foo.go truncated to 2000 lines"),
39+
"token_budget": rawString("80%"),
40+
}, nil)
4341

4442
rendered, err := form.Render(patch, prev, tmpls)
4543
require.NoError(t, err)
@@ -59,7 +57,11 @@ func TestDefaultTemplates_LintClean(t *testing.T) {
5957
for _, r := range rendered {
6058
rkeys[r.Key] = true
6159
}
62-
for k := range patch.Set {
60+
for _, ent := range patch.Entries() {
61+
if ent.IsRemoval() {
62+
continue
63+
}
64+
k := ent.Key
6365
assert.True(t, rkeys[k], "expected default template for %q to produce output", k)
6466
}
6567
}
@@ -78,14 +80,12 @@ func TestDefaultTemplates_AllBodiesAreFactual(t *testing.T) {
7880
return b
7981
}
8082

81-
patch := form.Patch{
82-
Set: map[string]json.RawMessage{
83-
"cwd": rawString("/home/figaro"),
84-
"datetime": rawString("Wednesday, April 29, 2026, 10AM EDT"),
85-
"model": rawString("claude-opus-4-6"),
86-
"root": rawString("/home/figaro"),
87-
},
88-
}
83+
patch := form.Build(form.Snapshot{}, map[string]json.RawMessage{
84+
"cwd": rawString("/home/figaro"),
85+
"datetime": rawString("Wednesday, April 29, 2026, 10AM EDT"),
86+
"model": rawString("claude-opus-4-6"),
87+
"root": rawString("/home/figaro"),
88+
}, nil)
8989
rendered, err := form.Render(patch, form.Snapshot{}, tmpls)
9090
require.NoError(t, err)
9191

‎api/form/state_test.go‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ func TestState_ApplyAndSave_RoundTrip(t *testing.T) {
2727
s, err := form.Open(path)
2828
require.NoError(t, err)
2929

30-
patch := form.PatchCreates(map[string]json.RawMessage{
30+
patch := form.Build(form.Snapshot{}, map[string]json.RawMessage{
3131
"system.credo": json.RawMessage(`"you are figaro"`),
3232
"cwd": json.RawMessage(`"/home/figaro"`),
33-
})
33+
}, nil)
3434
post := s.Apply(patch)
3535
assert.Equal(t, json.RawMessage(`"/home/figaro"`), val(post, "cwd"))
3636

@@ -50,10 +50,10 @@ func TestState_Snapshot_ReturnsClone(t *testing.T) {
5050
require.NoError(t, err)
5151
defer s.Close()
5252

53-
s.Apply(form.PatchCreates(map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}))
53+
s.Apply(form.Build(form.Snapshot{}, map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}, nil))
5454
snap1 := s.Snapshot()
5555
// Derive a mutated snapshot from the clone.
56-
snap1 = snap1.Apply(form.PatchCreates(map[string]json.RawMessage{"k": json.RawMessage(`"mutated"`)}))
56+
snap1 = snap1.Apply(form.Build(form.Snapshot{}, map[string]json.RawMessage{"k": json.RawMessage(`"mutated"`)}, nil))
5757
_ = snap1
5858

5959
snap2 := s.Snapshot()
@@ -91,14 +91,14 @@ func TestState_RemovePatch(t *testing.T) {
9191
s, err := form.Open(path)
9292
require.NoError(t, err)
9393

94-
s.Apply(form.PatchCreates(map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}))
94+
s.Apply(form.Build(form.Snapshot{}, map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}, nil))
9595
require.NoError(t, s.Save())
9696
require.NoError(t, s.Close())
9797

9898
s2, err := form.Open(path)
9999
require.NoError(t, err)
100100
defer s2.Close()
101-
s2.Apply(form.PatchBuild(Snapshot{}, nil, []string{"k"}))
101+
s2.Apply(form.Build(form.Snapshot{}, nil, []string{"k"}))
102102
snap := s2.Snapshot()
103103
assert.False(t, snap.Has("k"))
104104
require.NoError(t, s2.Save())
@@ -110,7 +110,7 @@ func TestState_Close_FlushesPending(t *testing.T) {
110110
s, err := form.Open(path)
111111
require.NoError(t, err)
112112

113-
s.Apply(form.PatchCreates(map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}))
113+
s.Apply(form.Build(form.Snapshot{}, map[string]json.RawMessage{"k": json.RawMessage(`"v"`)}, nil))
114114
require.NoError(t, s.Close()) // should flush
115115

116116
data, err := os.ReadFile(path)

‎api/form/testhelp_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package form
2+
3+
// mustEntry is the value a patch sets at key, for tests that assert on one.
4+
func mustEntry(p Patch, key string) []byte {
5+
e, ok := p.Entry(key)
6+
if !ok {
7+
return nil
8+
}
9+
return e.New
10+
}

0 commit comments

Comments
 (0)