Files
convy/internal/cli/rulelist_test.go
av 8331aa1ca5 suite rule и suite list
- suite rule дописывает правило: номер берётся следующим за наибольшим, блоки
  раскладываются в порядке норма, ПОЧЕМУ, ПРИМЕРЫ, --after ставит правило рядом
  с тем, которое оно уточняет
- ступень называется категорией (requirement, prohibition, ...), а не словом
  языка, поэтому вызывающему не нужно знать, на каком языке записан набор
- suite list показывает темы со слоями, а с осью — что возьмёт компонент; отбор
  слоёв вынесен в suite.Assemble, откуда его возьмут проектные команды
- слои темы теперь всегда возвращаются базовым вперёд
2026-07-27 11:36:19 +03:00

202 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cli_test
import (
"strings"
"testing"
"git.vakhrushev.me/av/convy/internal/cli"
)
func TestRuleTakesTheNextNumberAndKeepsTheOrderOfBlocks(t *testing.T) {
root := retirable(t)
code, out := run(t, root, "", false, "suite", "rule",
"--prefix", "TIME", "--title", "Точность носителя фиксируется",
"--modality", "requirement",
"--norm", "У колонки БД и у потока логов точность объявлена и не плавает.",
"--why", "Плавающая точность ломает сортировку выборочно и невоспроизводимо.")
if code != cli.OK {
t.Fatalf("adding a rule returned %d: %s", code, out)
}
if !strings.Contains(out, "TIME-3 added") {
t.Errorf("the number is not the next after the highest:\n%s", out)
}
body := read(t, root, "conventions/time.md")
want := "### TIME-3. Точность носителя фиксируется\n\n**ДОЛЖЕН.** У колонки БД и у потока логов точность объявлена и не плавает.\n\n**ПОЧЕМУ.** Плавающая точность ломает сортировку выборочно и невоспроизводимо."
if !strings.Contains(body, want) {
t.Errorf("the rule is not laid out as the language fixes it:\n%s", body)
}
checkClean(t, root)
}
// A number is never reused, so retiring a rule does not free its number: the
// next one still comes after the highest.
func TestRuleNumbersPastARetiredOne(t *testing.T) {
root := retirable(t)
run(t, root, "", false, "suite", "retire", "--rule", "TIME-2", "--reason", "не нужно", "--date", "2026-07-27")
code, out := run(t, root, "", false, "suite", "rule",
"--prefix", "TIME", "--title", "Третье", "--modality", "recommendation",
"--norm", "Норма.", "--why", "Причина.")
if code != cli.OK {
t.Fatalf("adding a rule returned %d: %s", code, out)
}
if !strings.Contains(out, "TIME-3 added") {
t.Errorf("the number of a retired rule was reused:\n%s", out)
}
checkClean(t, root)
}
// Order in a file goes by reading rather than by number, so a rule elaborating
// another has to be placeable next to it.
func TestRulePlacesAfterTheOneItElaborates(t *testing.T) {
root := retirable(t)
run(t, root, "", false, "suite", "rule",
"--prefix", "TIME", "--title", "Третье", "--modality", "recommendation",
"--norm", "Норма третьего.", "--why", "Причина третьего.")
code, out := run(t, root, "", false, "suite", "rule",
"--prefix", "TIME", "--title", "Уточнение первого", "--modality", "permission",
"--norm", "Норма четвёртого.", "--why", "Причина четвёртого.",
"--after", "TIME-1")
if code != cli.OK {
t.Fatalf("adding a rule returned %d: %s", code, out)
}
body := read(t, root, "conventions/time.md")
order := []string{"### TIME-1.", "### TIME-4.", "### TIME-2.", "### TIME-3."}
at := 0
for _, id := range order {
i := strings.Index(body[at:], id)
if i < 0 {
t.Fatalf("%s is missing or out of place:\n%s", id, body)
}
at += i
}
checkClean(t, root)
}
// The step is named by its category rather than by a word of any one language,
// so that a caller need not know which language the suite is written in. The
// word itself is taken too, for whoever has it at hand.
func TestRuleTakesTheStepByCategoryOrByWord(t *testing.T) {
root := retirable(t)
run(t, root, "", false, "suite", "rule",
"--prefix", "SLOG", "--title", "Через категорию", "--modality", "not-recommended",
"--norm", "Норма.", "--why", "Причина.")
run(t, root, "", false, "suite", "rule",
"--prefix", "SLOG", "--title", "Через слово", "--modality", "ДОПУСКАЕТСЯ",
"--norm", "Норма.", "--why", "Причина.")
body := read(t, root, "conventions/logging.md")
for _, want := range []string{"**НЕ СЛЕДУЕТ.** Норма.", "**ДОПУСКАЕТСЯ.** Норма."} {
if !strings.Contains(body, want) {
t.Errorf("the step did not render as %q:\n%s", want, body)
}
}
checkClean(t, root)
}
func TestRuleRefusesWhatItCannotDo(t *testing.T) {
root := retirable(t)
cases := []struct {
name string
args []string
want string
}{{
name: "a prefix the suite does not declare",
args: []string{"--prefix", "ZZZZ", "--title", "Т", "--modality", "requirement", "--norm", "Н", "--why", "П"},
want: "no live prefix ZZZZ",
}, {
name: "a step that is not on the scale",
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "maybe", "--norm", "Н", "--why", "П"},
want: "one of: requirement, prohibition",
}, {
name: "placing after a rule of another file",
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "requirement", "--norm", "Н", "--why", "П", "--after", "SLOG-1"},
want: "belongs to another file",
}, {
name: "a rationale left out",
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "requirement", "--norm", "Н"},
want: "--why",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
code, out := run(t, root, "", false, append([]string{"suite", "rule"}, tc.args...)...)
if code == cli.OK {
t.Fatalf("the command went through:\n%s", out)
}
if !strings.Contains(out, tc.want) {
t.Errorf("the refusal does not say %q:\n%s", tc.want, out)
}
})
}
checkClean(t, root)
}
func TestListShowsTheSuiteBaseLayerFirst(t *testing.T) {
root := retirable(t)
code, out := run(t, root, "", false, "suite", "list")
if code != cli.OK {
t.Fatalf("listing returned %d: %s", code, out)
}
for _, want := range []string{"time — время", "logging — логирование", "TIME", "GTIM", "lang=go", "base"} {
if !strings.Contains(out, want) {
t.Errorf("the listing lacks %q:\n%s", want, out)
}
}
if strings.Index(out, "conventions/time.md") > strings.Index(out, "conventions/lang/go/time.md") {
t.Errorf("a language layer came before the base one:\n%s", out)
}
}
// The selection is the read-only half of assembly: a layer travels when its
// axis keys agree with the component, and the base layer travels always.
func TestListSelectsWhatAComponentWouldTake(t *testing.T) {
root := retirable(t)
code, out := run(t, root, "", false, "suite", "list", "--topic", "time", "--lang", "go")
if code != cli.OK {
t.Fatalf("listing returned %d: %s", code, out)
}
if strings.Contains(out, "left out") {
t.Errorf("a component of the right language left a layer out:\n%s", out)
}
code, out = run(t, root, "", false, "suite", "list", "--topic", "time", "--lang", "python")
if code != cli.OK {
t.Fatalf("listing returned %d: %s", code, out)
}
if !strings.Contains(out, "conventions/lang/go/time.md") || !strings.Contains(out, "left out") {
t.Errorf("the go layer was not left out for a python component:\n%s", out)
}
if !strings.Contains(out, "conventions/time.md") {
t.Errorf("the base layer did not travel:\n%s", out)
}
}
func TestListShowsRetiredNames(t *testing.T) {
root := retirable(t)
code, out := run(t, root, "", false, "suite", "list", "--retired")
if code != cli.OK {
t.Fatalf("listing returned %d: %s", code, out)
}
if !strings.Contains(out, "nothing has been retired yet") {
t.Errorf("an untouched suite reported retirements:\n%s", out)
}
run(t, root, "", false, "suite", "retire", "--prefix", "SLOG", "--reason", "свёрнута", "--date", "2026-07-27")
_, out = run(t, root, "", false, "suite", "list", "--retired")
for _, want := range []string{"SLOG", "2026-07-27", "свёрнута", "ever handed out again"} {
if !strings.Contains(out, want) {
t.Errorf("the listing of retired names lacks %q:\n%s", want, out)
}
}
}