|
| 1 | +package form_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/stretchr/testify/assert" |
| 8 | + "github.com/stretchr/testify/require" |
| 9 | + |
| 10 | + "github.com/jack-work/figaro/api/form" |
| 11 | +) |
| 12 | + |
| 13 | +// Each test here fails against the flat implementation. They are the argument |
| 14 | +// for the change, stated as things that either work or do not. |
| 15 | + |
| 16 | +// A key whose own name contains a dot is addressable alongside a key that is |
| 17 | +// its prefix. Gluck's store holds both: skills.howto is a skill, and |
| 18 | +// skills.howto.md is a different skill. |
| 19 | +func TestCollidingKeysAreBothAddressable(t *testing.T) { |
| 20 | + s := form.FromMap(map[string]json.RawMessage{ |
| 21 | + "skills.howto": json.RawMessage(`{"filePath":"/a.md"}`), |
| 22 | + "skills.howto.md": json.RawMessage(`{"filePath":"/b.md"}`), |
| 23 | + "succession": json.RawMessage(`["one"]`), |
| 24 | + "succession.plan": json.RawMessage(`"handover"`), |
| 25 | + }) |
| 26 | + for k, want := range map[string]string{ |
| 27 | + "skills.howto": `{"filePath":"/a.md"}`, |
| 28 | + "skills.howto.md": `{"filePath":"/b.md"}`, |
| 29 | + "succession": `["one"]`, |
| 30 | + "succession.plan": `"handover"`, |
| 31 | + } { |
| 32 | + got, ok := s.Get(k) |
| 33 | + require.True(t, ok, "%s is unreachable", k) |
| 34 | + assert.JSONEq(t, want, string(got), k) |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +// One field of a large value changes without rewriting the value. The credo on |
| 39 | +// Gluck's board is 6.5KB and was rewritten whole to change one character. |
| 40 | +func TestOneFieldOfALargeValueIsItsOwnChange(t *testing.T) { |
| 41 | + big := make([]byte, 6000) |
| 42 | + for i := range big { |
| 43 | + big[i] = 'x' |
| 44 | + } |
| 45 | + prev := form.FromMap(map[string]json.RawMessage{ |
| 46 | + "system.credo": mustJSON(t, map[string]string{ |
| 47 | + "content": string(big), "filePath": "/credo.md", "frontmatter": "name: figaro", |
| 48 | + }), |
| 49 | + }) |
| 50 | + next := prev.SetPath("system.credo.filePath", json.RawMessage(`"/moved.md"`)) |
| 51 | + |
| 52 | + p := next.Diff(prev) |
| 53 | + es := p.Entries() |
| 54 | + require.Len(t, es, 1, "one field changed, so one entry: %v", es) |
| 55 | + assert.Equal(t, "system.credo.filePath", es[0].Key) |
| 56 | + |
| 57 | + // The 6KB of content is not in the patch. |
| 58 | + raw, err := json.Marshal(p) |
| 59 | + require.NoError(t, err) |
| 60 | + assert.Less(t, len(raw), 300, "the patch carries the untouched content") |
| 61 | + |
| 62 | + // And it still applies. |
| 63 | + got, ok := prev.Apply(p).Get("system.credo.filePath") |
| 64 | + require.True(t, ok) |
| 65 | + assert.JSONEq(t, `"/moved.md"`, string(got)) |
| 66 | +} |
| 67 | + |
| 68 | +// Protection sees a write at any depth. A flat patch could only name top-level |
| 69 | +// keys, so a write into a protected subtree was invisible to CheckWritable. |
| 70 | +func TestProtectionSeesANestedWrite(t *testing.T) { |
| 71 | + var managed string |
| 72 | + for _, k := range form.WellKnownKeys() { |
| 73 | + if k.Mode == form.KeySystemManaged { |
| 74 | + managed = k.Key |
| 75 | + break |
| 76 | + } |
| 77 | + } |
| 78 | + require.NotEmpty(t, managed) |
| 79 | + |
| 80 | + p := form.Build(form.Snapshot{}, map[string]json.RawMessage{ |
| 81 | + managed: json.RawMessage(`"forged"`), |
| 82 | + }, nil) |
| 83 | + assert.Error(t, form.CheckWritable(p, false), |
| 84 | + "a write to %s must be refused however deep it sits", managed) |
| 85 | +} |
| 86 | + |
| 87 | +// A patch can be undone from itself, with no snapshot in hand. |
| 88 | +func TestAPatchCarriesItsOwnUndo(t *testing.T) { |
| 89 | + before := form.FromMap(map[string]json.RawMessage{ |
| 90 | + "mantra": json.RawMessage(`"first"`), |
| 91 | + "system.model": json.RawMessage(`"opus"`), |
| 92 | + "doomed": json.RawMessage(`"here"`), |
| 93 | + }) |
| 94 | + after := before. |
| 95 | + SetPath("mantra", json.RawMessage(`"second"`)). |
| 96 | + SetPath("system.cwd", json.RawMessage(`"/tmp"`)). |
| 97 | + DeletePath("doomed") |
| 98 | + |
| 99 | + p := after.Diff(before) |
| 100 | + restored := after.Apply(p.Inverse()) |
| 101 | + |
| 102 | + for _, k := range []string{"mantra", "system.model", "doomed"} { |
| 103 | + want, _ := before.Get(k) |
| 104 | + got, ok := restored.Get(k) |
| 105 | + require.True(t, ok, "%s was not restored", k) |
| 106 | + assert.JSONEq(t, string(want), string(got), k) |
| 107 | + } |
| 108 | + _, hasNew := restored.Get("system.cwd") |
| 109 | + assert.False(t, hasNew, "undo must remove what the patch added") |
| 110 | +} |
| 111 | + |
| 112 | +// Two writers touching different fields of one parent do not clobber each |
| 113 | +// other. This is what dropped system.credo when an outfit set system.model. |
| 114 | +func TestSiblingWritesUnderOneParentCompose(t *testing.T) { |
| 115 | + a := form.Build(form.Snapshot{}, map[string]json.RawMessage{"system.credo": json.RawMessage(`"largo"`)}, nil) |
| 116 | + b := form.Build(form.Snapshot{}, map[string]json.RawMessage{"system.model": json.RawMessage(`"opus"`)}, nil) |
| 117 | + |
| 118 | + board := form.Snapshot{}.Apply(a).Apply(b) |
| 119 | + credo, ok := board.Get("system.credo") |
| 120 | + require.True(t, ok, "the second write clobbered the first") |
| 121 | + assert.JSONEq(t, `"largo"`, string(credo)) |
| 122 | + model, ok := board.Get("system.model") |
| 123 | + require.True(t, ok) |
| 124 | + assert.JSONEq(t, `"opus"`, string(model)) |
| 125 | +} |
| 126 | + |
| 127 | +// A value that is not valid JSON is refused, not silently dropped. |
| 128 | +func TestAnInvalidValueIsRefusedNotDropped(t *testing.T) { |
| 129 | + s := form.FromMap(map[string]json.RawMessage{"bad": json.RawMessage(`{not json`)}) |
| 130 | + _, err := json.Marshal(s) |
| 131 | + assert.Error(t, err, "a board that cannot be serialised must say so") |
| 132 | +} |
| 133 | + |
| 134 | +func mustJSON(t *testing.T, v any) json.RawMessage { |
| 135 | + t.Helper() |
| 136 | + b, err := json.Marshal(v) |
| 137 | + require.NoError(t, err) |
| 138 | + return b |
| 139 | +} |
0 commit comments