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) } } }