исправлены находки ревью проектной стороны

- манифест читается так, как записан: решётка внутри строки не открывает
  комментарий, скобка внутри комментария не закрывает массив, имя внутри
  комментария не становится подпиской; новый ключ встаёт после массива,
  а не внутрь него
- всё записываемое проходит через manifest.Quote — обратный слэш в пути
  делал файл, который инструмент сам не читает
- маркер локальной части переехал в doc и пропускает огороженные блоки:
  процитированный в примере маркер больше не считается границей, а копия
  без маркера не перезаписывается молча
- лишний позиционный аргумент отсекается: flag прекращал разбор и прятал
  флаги после себя, из-за чего pull, list и check игнорировали --for
- заведены тесты проверок копий, включая молчание на исправной копии
This commit is contained in:
av
2026-07-27 21:16:29 +03:00
parent 23d88c4048
commit b6b0976c19
19 changed files with 904 additions and 197 deletions
+96
View File
@@ -1,9 +1,12 @@
package manifest_test
import (
"slices"
"strings"
"testing"
"github.com/BurntSushi/toml"
"git.vakhrushev.me/av/convy/internal/manifest"
)
@@ -96,3 +99,96 @@ func TestAddTableGoesLast(t *testing.T) {
t.Errorf("a second table of the same name went through")
}
}
// The array is read the way the manifest wrote it. A # inside a string opens no
// comment, a bracket inside a comment closes no array, and a name inside a
// comment is not a subscription — turning any of the three into data is the one
// mistake there is no way back from.
func TestAddToListReadsCommentsAsComments(t *testing.T) {
cases := []struct {
name string
src string
add string
values []string
remains string
}{{
name: "a name quoted inside a comment",
src: "[c.app]\ntopics = [\n \"errors\",\n # \"config\" is not taken yet\n]\n",
add: "time",
values: []string{"errors", "time"},
remains: `# "config" is not taken yet`,
}, {
name: "a bracket inside a comment",
src: "[c.app]\ntopics = [\n \"errors\", # [enough for now]\n \"db-schema\",\n]\n",
add: "config",
values: []string{"errors", "db-schema", "config"},
remains: `"errors", # [enough for now]`,
}, {
name: "a comment trailing a one-line array",
src: "[c.app]\ntopics = [\"git\"] # only git for now\n",
add: "time",
values: []string{"git", "time"},
remains: `topics = ["git", "time"] # only git for now`,
}, {
name: "a hash inside a value",
src: "[c.app]\ntopics = [\"a#b\"]\n",
add: "time",
values: []string{"a#b", "time"},
remains: `topics = ["a#b", "time"]`,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := manifest.AddToList([]byte(tc.src), "c.app", "topics", tc.add)
if err != nil {
t.Fatal(err)
}
got := decodeTopics(t, out)
if !slices.Equal(got, tc.values) {
t.Errorf("the array holds %q, expected %q:\n%s", got, tc.values, out)
}
if !strings.Contains(string(out), tc.remains) {
t.Errorf("what the author wrote is gone — no %q:\n%s", tc.remains, out)
}
})
}
}
// decodeTopics reads the array back with the parser the tool itself uses.
func decodeTopics(t *testing.T, source []byte) []string {
t.Helper()
var got struct {
C map[string]struct{ Topics []string } `toml:"c"`
}
if _, err := toml.Decode(string(source), &got); err != nil {
t.Fatalf("the manifest the tool wrote does not parse: %v\n%s", err, source)
}
return got.C["app"].Topics
}
// A key is not always one line. Appending after the first line of an array
// written down a column would land the new entry inside it.
func TestAddToListAppendsPastAMultilineNeighbour(t *testing.T) {
src := "[c.app]\ndir = \"docs\"\nstack = [\n \"sqlite\",\n \"postgres\",\n]\n"
out, err := manifest.AddToList([]byte(src), "c.app", "topics", "errors")
if err != nil {
t.Fatal(err)
}
body := string(out)
if !strings.Contains(body, " \"postgres\",\n]\ntopics = [\"errors\"]") {
t.Errorf("the new key did not land past the array:\n%s", body)
}
}
// Whatever the tool writes into a manifest, it has to be able to read back.
func TestQuoteSurvivesTheRoundTrip(t *testing.T) {
for _, value := range []string{`docs\conventions`, `a "quoted" name`, `both\ "kinds"`} {
out, err := manifest.AddToList([]byte("[c.app]\ntopics = []\n"), "c.app", "topics", value)
if err != nil {
t.Fatal(err)
}
if list := decodeTopics(t, out); len(list) != 1 || list[0] != value {
t.Errorf("%q came back as %q", value, list)
}
}
}