mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
0cc033edd8
POST /workspaces/{ws}/items/{ref}/patch {edits:[{old,new,line_hint?}],
expected_seq?} replaces exact text in the body. It runs through updateItem,
so it takes every PATCH guard; the body is computed inside the store
transaction from the row it replaces. Each old must occur exactly once;
edits apply in order, all or nothing; every patch writes a version. The
answer carries a unified diff of what changed.
expected_seq does not refuse: a moved seq is applied over when the
anchors still match, and answered rebased with the intervening writes,
read from a seq chain stamped into item activity entries ({prev_seq, seq,
version_id}). complete is decided at runtime, so any write that left no
stamp reads incomplete; a guard test classifies every seq-moving store
statement. Refusals write nothing: patch_anchor_missing,
patch_anchor_ambiguous, patch_room_open (an open editor; step 2).
CLI: pad item patch, pad item show --lines. MCP 0.87 (additive):
pad_item.patch, get lines, three allow-listed codes. The timeline treats
the chain and patch keys as bookkeeping, not a change.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
58 lines
2.0 KiB
Go
58 lines
2.0 KiB
Go
package bodypatch
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestApply(t *testing.T) {
|
|
body := "alpha\nbeta\ngamma beta\n"
|
|
for _, c := range []struct {
|
|
name string
|
|
edits []Edit
|
|
want string
|
|
err any
|
|
}{
|
|
{"single unique", []Edit{{Old: "alpha", New: "ALPHA"}}, "ALPHA\nbeta\ngamma beta\n", nil},
|
|
{"edits apply in order to the evolving body", []Edit{{Old: "alpha", New: "x"}, {Old: "x\nbeta", New: "y"}}, "y\ngamma beta\n", nil},
|
|
{"missing", []Edit{{Old: "delta", New: "d"}}, "", &AnchorMissingError{}},
|
|
{"ambiguous without hint", []Edit{{Old: "beta", New: "b"}}, "", &AnchorAmbiguousError{}},
|
|
{"hint picks a match", []Edit{{Old: "beta", New: "B", LineHint: 3}}, "alpha\nbeta\ngamma B\n", nil},
|
|
{"hint on no match is still ambiguous", []Edit{{Old: "beta", New: "B", LineHint: 1}}, "", &AnchorAmbiguousError{}},
|
|
{"hint ignored when unique", []Edit{{Old: "gamma", New: "G", LineHint: 1}}, "alpha\nbeta\nG beta\n", nil},
|
|
{"empty old refused", []Edit{{Old: "alpha", New: "a"}, {Old: "", New: "z"}}, "", &EmptyAnchorError{}},
|
|
{"overlapping occurrences are ambiguous", []Edit{{Old: "aa", New: "b"}}, "", &AnchorAmbiguousError{}},
|
|
{"a later failure applies nothing", []Edit{{Old: "alpha", New: "a"}, {Old: "nope", New: "z"}}, "", &AnchorMissingError{}},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
in := body
|
|
if c.name == "overlapping occurrences are ambiguous" {
|
|
in = "aaa"
|
|
}
|
|
got, err := Apply(in, c.edits)
|
|
if c.err != nil {
|
|
target := reflect.New(reflect.TypeOf(c.err).Elem()).Interface()
|
|
if err == nil || !errors.As(err, &target) {
|
|
t.Fatalf("err = %v, want %T", err, c.err)
|
|
}
|
|
if got != "" {
|
|
t.Fatalf("a refused patch returned a body: %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || got != c.want {
|
|
t.Fatalf("got %q, %v; want %q", got, err, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAmbiguousNamesEveryLine(t *testing.T) {
|
|
_, err := Apply("x\ny x\nx", []Edit{{Old: "x", New: "z"}})
|
|
var amb *AnchorAmbiguousError
|
|
if !errors.As(err, &amb) || !reflect.DeepEqual(amb.Lines, []int{1, 2, 3}) {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
}
|