приём и чтение записей сведены к одному контракту приложения

- адреса приложения переехали в своё пространство `/app/`, опрос готовности
  убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи,
  текст — отдельным адресом названного вида
- заведена единая точка отображения доменной ошибки и слой, приводящий к той же
  форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением
- у записи появились имя файла отправителя, длительность и размер своими
  колонками, а у ленты владельца — свой индекс: без него страница сканировала
  весь архив сервиса
This commit is contained in:
av
2026-08-15 13:51:23 +03:00
parent 79ff12548f
commit 3a2da3004b
55 changed files with 5506 additions and 466 deletions
+52
View File
@@ -260,6 +260,39 @@ func TestУКаждогоРабочегоРубежаЕстьШаг(t *testing.T
// Обратное направление того же правила: шаг, написанный под рубеж, которого в
// дескрипторе нет, недостижим — захват такую запись не выдаст никогда.
// Отбор списка — очередной потребитель словаря рубежей, и перечислять их у него
// строкой запроса нельзя: рубеж, добавленный конвейером, молча поменял бы состав
// всех трёх состояний отбора, а заметить это было бы нечем.
//
// Правило смотрит, что выборка списка берёт рубежи у дескриптора, а не пишет их
// литералом. Инвариант проекта «Рубеж объявляется одним дескриптором» компилятор
// не проверяет — проверяет оно.
func TestОтборСпискаБерётРубежиУДескриптора(t *testing.T) {
const listFile = repoPkg + "/record_list.go"
body := readFile(t, listFile)
for _, value := range declaredStateValues(t) {
if strings.Contains(body, `"`+value+`"`) {
t.Errorf(
"отбор списка называет рубеж %q строкой: рубеж, добавленный "+
"дескриптором, молча не попадёт ни в одно состояние отбора",
value,
)
}
}
for _, fn := range []string{"entity.WorkingStages()", "entity.TerminalStages()"} {
if !strings.Contains(body, fn) {
t.Errorf(
"отбор списка не зовёт %s: перечень рубежей обязан приходить из "+
"дескриптора, а не собираться по месту",
fn,
)
}
}
}
func TestШагиОбъявленыРубежамиДескриптора(t *testing.T) {
body := funcBody(t, serviceFile, "func (s *TranscribeService) stepFor(")
declared := map[string]bool{}
@@ -363,6 +396,25 @@ func stageDescriptor(t *testing.T) (all []string, working []string) {
}
// declaredStates — константы рубежей, объявленные доменом.
// declaredStateValues — **значения** рубежей, а не имена их констант: правило
// отбора ищет строковый литерал в чужом файле, и сравнивать его надо со
// значением.
//
// declaredStates рядом отдаёт имена констант — им пользуются правила, читающие
// код, а не строки.
func declaredStateValues(t *testing.T) []string {
t.Helper()
out := []string{}
re := regexp.MustCompile(`(?m)^\tState\w+\s*=\s*"([^"]+)"`)
for _, m := range re.FindAllStringSubmatch(readFile(t, stateFile), -1) {
out = append(out, m[1])
}
if len(out) == 0 {
t.Fatalf("в %s не объявлено ни одного рубежа: правило потеряло предмет", stateFile)
}
return out
}
func declaredStates(t *testing.T) map[string]bool {
t.Helper()
out := map[string]bool{}