конвенции: перенести механизируемое в golangci-lint и internal/archrules
Правило, которое проверяет машина, не должно оставаться прозой: файл конвенций на сотни строк размазывает внимание по тривиальному — модель добросовестно проверит именование полей лога и не дойдёт до формы решения. Включены sloglint (константный msg, стиль ключ-значение), forbidigo (fmt.Print*, os.Getenv, time.Now мимо store.Now), errorlint (сравнение ошибок), depguard (сторонние пакеты ошибок). internal/archrules — сканеры на то, что линтером не выражается: направление зависимостей ядро↔транспорты, AUTOINCREMENT и серверное время в новых миграциях, матчинг ошибки по тексту. Код приведён к правилам: logging.StartCall как единая точка отсчёта длительности внешних вызовов, store.Now вместо time.Now в httpapi и часах воркера, slog.DiscardHandler в тестах. Перенесённое вычеркнуто из docs/conventions/* и openspec/config.yaml — прозой осталось только то, что правилом не выражается. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// Package archrules — тесты-сканеры исходников для правил, которые не
|
||||
// выражаются линтером: структура проекта и SQL миграций.
|
||||
//
|
||||
// Каждое правило здесь — бывшая строка прозаической конвенции: у него есть
|
||||
// детерминированный оракул, поэтому ему место в конвейере сборки, а не в
|
||||
// промпте ревью (см. .claude/skills/review-pipeline/references/promote.md).
|
||||
package archrules
|
||||
|
||||
import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const modulePath = "git.vakhrushev.me/av/jellybit"
|
||||
|
||||
// repoRoot — корень репозитория относительно каталога пакета.
|
||||
const repoRoot = "../.."
|
||||
|
||||
// Транспорты — тонкие обёртки над ядром: не знают друг о друге и никем из ядра
|
||||
// не импортируются (CLAUDE.md, «Единое ядро, тонкие транспорты»).
|
||||
var transports = map[string]bool{
|
||||
"internal/httpapi": true,
|
||||
"internal/tgbot": true,
|
||||
}
|
||||
|
||||
func TestТранспортыНеЗависятДругОтДруга(t *testing.T) {
|
||||
for pkg, imports := range internalImports(t) {
|
||||
if !transports[pkg] {
|
||||
continue
|
||||
}
|
||||
for _, imp := range imports {
|
||||
if transports[imp] && imp != pkg {
|
||||
t.Errorf("%s импортирует транспорт %s: транспорты не знают друг о друге, общая логика живёт в ядре", pkg, imp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestЯдроНеЗависитОтТранспортов(t *testing.T) {
|
||||
for pkg, imports := range internalImports(t) {
|
||||
if transports[pkg] || pkg == "cmd/jellybit" {
|
||||
continue
|
||||
}
|
||||
for _, imp := range imports {
|
||||
if transports[imp] {
|
||||
t.Errorf("%s импортирует транспорт %s: зависимость направлена не туда, ядро не знает о доставке", pkg, imp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lastLegacyMigration — последняя миграция, написанная до того, как конвенция
|
||||
// сложилась: 0001 заводила AUTOINCREMENT и DEFAULT datetime('now'), 0006 и 0008
|
||||
// как раз уводили схему на ULID и RFC 3339 и потому упоминают старую форму.
|
||||
// Миграции неизменяемы, переписывать их нельзя — правило действует на новые.
|
||||
const lastLegacyMigration = 8
|
||||
|
||||
// docs/conventions/database.md: PK — TEXT ULID через internal/ident, время
|
||||
// генерирует приложение (store.Now), а не SQLite.
|
||||
func TestМиграцииБезAutoincrementИСерверногоВремени(t *testing.T) {
|
||||
forbidden := []struct {
|
||||
re *regexp.Regexp
|
||||
why string
|
||||
}{
|
||||
{regexp.MustCompile(`(?i)autoincrement`), "PK — TEXT ULID через internal/ident, без AUTOINCREMENT"},
|
||||
{regexp.MustCompile(`(?i)default\s*\(?\s*(datetime\s*\(\s*'now'|current_timestamp)`), "время генерирует приложение через store.Now(), а не DEFAULT в схеме (fail-loud при забытой вставке)"},
|
||||
}
|
||||
dir := filepath.Join(repoRoot, "internal/store/migrations")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("читаю каталог миграций: %v", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || migrationNumber(t, e.Name()) <= lastLegacyMigration {
|
||||
continue
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("читаю %s: %v", e.Name(), err)
|
||||
}
|
||||
for _, f := range forbidden {
|
||||
if loc := f.re.FindIndex(body); loc != nil {
|
||||
t.Errorf("%s: строка %d — %s", e.Name(), lineOf(body, loc[0]), f.why)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// docs/conventions/errors.md: сравнение ошибок — errors.Is/errors.As, никогда
|
||||
// по тексту. errorlint ловит `err == ErrX` и приведение типа, но не матчинг
|
||||
// подстрокой — его ловим здесь.
|
||||
func TestОшибкиНеМатчатсяПоТексту(t *testing.T) {
|
||||
re := regexp.MustCompile(`(strings\.(Contains|HasPrefix|HasSuffix|EqualFold)\([^)]*\.Error\(\)|\.Error\(\)\s*==)`)
|
||||
for _, path := range goFiles(t) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("читаю %s: %v", path, err)
|
||||
}
|
||||
if loc := re.FindIndex(body); loc != nil {
|
||||
rel, _ := filepath.Rel(repoRoot, path)
|
||||
t.Errorf("%s:%d — ошибку матчим через errors.Is/errors.As, а не по тексту сообщения", rel, lineOf(body, loc[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// internalImports возвращает карту «пакет репозитория → его внутренние импорты»
|
||||
// (пути относительно корня модуля).
|
||||
func internalImports(t *testing.T) map[string][]string {
|
||||
t.Helper()
|
||||
out := map[string][]string{}
|
||||
fset := token.NewFileSet()
|
||||
for _, path := range goFiles(t) {
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
t.Fatalf("разбираю %s: %v", path, err)
|
||||
}
|
||||
rel, err := filepath.Rel(repoRoot, filepath.Dir(path))
|
||||
if err != nil {
|
||||
t.Fatalf("отношу путь %s: %v", path, err)
|
||||
}
|
||||
for _, imp := range f.Imports {
|
||||
p := strings.Trim(imp.Path.Value, `"`)
|
||||
if after, ok := strings.CutPrefix(p, modulePath+"/"); ok {
|
||||
out[rel] = append(out[rel], after)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// goFiles — все нетестовые .go файлы репозитория (без tmp и вендорных каталогов).
|
||||
func goFiles(t *testing.T) []string {
|
||||
t.Helper()
|
||||
var files []string
|
||||
err := filepath.WalkDir(repoRoot, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
switch d.Name() {
|
||||
case "tmp", "vendor", ".git", "node_modules":
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("обхожу репозиторий: %v", err)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// migrationNumber достаёт числовой префикс имени миграции (0009_… → 9).
|
||||
func migrationNumber(t *testing.T, name string) int {
|
||||
t.Helper()
|
||||
prefix, _, ok := strings.Cut(name, "_")
|
||||
if !ok {
|
||||
t.Fatalf("имя миграции без числового префикса: %s", name)
|
||||
}
|
||||
n, err := strconv.Atoi(prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("нечисловой префикс миграции %s: %v", name, err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func lineOf(body []byte, offset int) int {
|
||||
return 1 + strings.Count(string(body[:offset]), "\n")
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -50,7 +49,7 @@ func (a actionReviewer) Refine(_ context.Context, _ string, hint string) error {
|
||||
func testRouterAction(t *testing.T, r stubReader, rv Reviewer, cmd Commander, lv stubLive) http.Handler {
|
||||
t.Helper()
|
||||
h, err := NewRouter(Deps{
|
||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Reader: r,
|
||||
Reviewer: rv,
|
||||
Commander: cmd,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/naming"
|
||||
"git.vakhrushev.me/av/jellybit/internal/recognize"
|
||||
@@ -134,7 +133,7 @@ func (s *server) buildDownloadView(id string, rd *worker.ReviewData) downloadDet
|
||||
// как в порядке и карточках списка); неразбираемое время просто опускаем.
|
||||
if t, ok := addedTime(d); ok {
|
||||
view.Added = fmtDate(t, s.deps.Loc)
|
||||
view.AddedAgo = humanizeAge(t, time.Now())
|
||||
view.AddedAgo = humanizeAge(t, store.Now())
|
||||
}
|
||||
|
||||
if rd.Recognition != nil {
|
||||
|
||||
@@ -303,7 +303,7 @@ func (s *server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
layoutSizes = nil // деградируем: размер уедет в фолбэк «—», страница не падает
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := store.Now()
|
||||
for _, d := range downloads {
|
||||
view.Downloads = append(view.Downloads, s.buildCardView(d, now, layoutSizes[d.ID]))
|
||||
}
|
||||
@@ -500,7 +500,7 @@ func (s *server) renderCardFragment(w http.ResponseWriter, r *http.Request, id s
|
||||
s.deps.Logger.Error("layout sizes", "download_id", id, "error", err)
|
||||
sizes = nil // деградируем: размер уедет в фолбэк «—», фрагмент не падает
|
||||
}
|
||||
v := s.buildCardView(*d, time.Now(), sizes[id])
|
||||
v := s.buildCardView(*d, store.Now(), sizes[id])
|
||||
if actionErr != nil {
|
||||
v.ActionError = userErr(r, actionErr, id)
|
||||
}
|
||||
@@ -841,7 +841,7 @@ func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
start := time.Now()
|
||||
start := time.Now() //nolint:forbidigo // измеряем длительность запроса, а не метку времени в БД
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (f *fakeReader) LayoutSizeByDownload(_ context.Context, _ []string) (map[st
|
||||
func newServer(t *testing.T, d httpapi.Deps) *httptest.Server {
|
||||
t.Helper()
|
||||
if d.Logger == nil {
|
||||
d.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
d.Logger = slog.New(slog.DiscardHandler)
|
||||
}
|
||||
h, err := httpapi.NewRouter(d)
|
||||
if err != nil {
|
||||
|
||||
@@ -113,7 +113,7 @@ func (s *server) handleFragCard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// layoutSize 0: у catched раскладки нет; в downloading размер берётся из
|
||||
// живого снимка внутри buildCardView.
|
||||
s.render(w, "card", s.buildCardView(*d, time.Now(), 0))
|
||||
s.render(w, "card", s.buildCardView(*d, store.Now(), 0))
|
||||
}
|
||||
|
||||
// handleFragSeeding отдаёт партиал секции «Раздача» (htmx-поллинг).
|
||||
|
||||
@@ -2,7 +2,6 @@ package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -77,7 +76,7 @@ func testRouter(t *testing.T, r stubReader, rv stubReviewer) http.Handler {
|
||||
func testRouterLive(t *testing.T, r stubReader, rv stubReviewer, lv stubLive) http.Handler {
|
||||
t.Helper()
|
||||
h, err := NewRouter(Deps{
|
||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Reader: r,
|
||||
Reviewer: rv,
|
||||
Live: lv,
|
||||
|
||||
@@ -3,7 +3,6 @@ package ingest
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -79,7 +78,7 @@ func (r *raceStore) UpgradeCatchedMagnetToTorrent(_ context.Context, _ string, _
|
||||
}
|
||||
|
||||
func newService(st Store) *Service {
|
||||
return New(st, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
return New(st, slog.New(slog.DiscardHandler))
|
||||
}
|
||||
|
||||
// Быстрый приём: сохраняем загрузку в catched и сразу отвечаем; qBittorrent и
|
||||
|
||||
@@ -81,7 +81,7 @@ func (c *Client) RefreshLibraries(ctx context.Context) error {
|
||||
req.Header.Set("X-Emby-Token", c.apiKey)
|
||||
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceJellyfin, Operation: "library/refresh", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceJellyfin, "library/refresh")
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
call.Failure(log, err)
|
||||
|
||||
@@ -128,12 +128,8 @@ func (c *openAICompat) Complete(ctx context.Context, req Request) (Response, err
|
||||
}
|
||||
}
|
||||
|
||||
call := logging.ExtCall{
|
||||
Service: logging.ServiceLLM,
|
||||
Operation: "chat.completions",
|
||||
Start: time.Now(),
|
||||
Attempt: attempt,
|
||||
}
|
||||
call := logging.StartCall(logging.ServiceLLM, "chat.completions")
|
||||
call.Attempt = attempt
|
||||
resp, retryable, err := c.do(ctx, body)
|
||||
if err == nil {
|
||||
call.Success(log, "model", resp.Model,
|
||||
|
||||
@@ -26,6 +26,14 @@ type ExtCall struct {
|
||||
Attempt int // номер попытки; >0 — пишем поле retry (поле и метод Retry конфликтовали бы)
|
||||
}
|
||||
|
||||
// StartCall заводит запись о начинающемся вызове внешнего сервиса, засекая
|
||||
// время. Единая точка отсчёта длительности: клиентам не нужен собственный
|
||||
// time.Now, а конвенция «время генерирует store.Now()» остаётся без исключений
|
||||
// (здесь это не метка времени, а измерение — см. docs/conventions/logging.md).
|
||||
func StartCall(service, operation string) ExtCall {
|
||||
return ExtCall{Service: service, Operation: operation, Start: time.Now()} //nolint:forbidigo // единственная точка отсчёта длительности внешних вызовов
|
||||
}
|
||||
|
||||
func (c ExtCall) attrs(extra ...any) []any {
|
||||
a := make([]any, 0, 10+len(extra))
|
||||
a = append(a,
|
||||
|
||||
@@ -76,7 +76,7 @@ func postJSON(ctx context.Context, hc *http.Client, log *slog.Logger, service, o
|
||||
// при отсутствии — переданный fallback.
|
||||
func doJSON(ctx context.Context, hc *http.Client, log *slog.Logger, service, operation string, req *http.Request, out any) error {
|
||||
log = logctx.FromOr(ctx, log)
|
||||
call := logging.ExtCall{Service: service, Operation: operation, Start: time.Now()}
|
||||
call := logging.StartCall(service, operation)
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
// Транспортный сбой несёт *url.Error с полным URL, а у TMDB api_key —
|
||||
|
||||
@@ -126,7 +126,7 @@ func (t *TVDB) rawGet(ctx context.Context, operation, path, token string) (int,
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
log := logctx.FromOr(ctx, t.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceTVDB, Operation: operation, Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceTVDB, operation)
|
||||
resp, err := t.hc.Do(req)
|
||||
if err != nil {
|
||||
call.Failure(log, err)
|
||||
|
||||
@@ -3,7 +3,6 @@ package naming
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
@@ -11,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func testLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
return slog.New(slog.DiscardHandler)
|
||||
}
|
||||
|
||||
// fakeProvider отдаёт заранее заданные ответы по очереди; считает вызовы.
|
||||
|
||||
+6
-6
@@ -141,7 +141,7 @@ func (c *Client) login(ctx context.Context) error {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", c.base.String()) // qBit проверяет Referer/Host
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "auth/login", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "auth/login")
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
call.Failure(log, err)
|
||||
@@ -221,7 +221,7 @@ func (c *Client) Add(ctx context.Context, ar AddRequest) error {
|
||||
payload := buf.Bytes()
|
||||
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "torrents/add", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "torrents/add")
|
||||
resp, err := c.do(ctx, func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
c.endpoint("/api/v2/torrents/add"), bytes.NewReader(payload))
|
||||
@@ -278,7 +278,7 @@ func (c *Client) Delete(ctx context.Context, hashes []string, deleteFiles bool)
|
||||
body := form.Encode()
|
||||
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "torrents/delete", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "torrents/delete")
|
||||
resp, err := c.do(ctx, func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
c.endpoint("/api/v2/torrents/delete"), strings.NewReader(body))
|
||||
@@ -319,7 +319,7 @@ func (c *Client) RenameTorrent(ctx context.Context, hash, name string) error {
|
||||
body := form.Encode()
|
||||
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "torrents/rename", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "torrents/rename")
|
||||
resp, err := c.do(ctx, func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
c.endpoint("/api/v2/torrents/rename"), strings.NewReader(body))
|
||||
@@ -350,7 +350,7 @@ func (c *Client) RenameTorrent(ctx context.Context, hash, name string) error {
|
||||
// Torrents возвращает задачи указанной категории (пустая — все).
|
||||
func (c *Client) Torrents(ctx context.Context, category string) ([]Torrent, error) {
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "torrents/info", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "torrents/info")
|
||||
resp, err := c.do(ctx, func() (*http.Request, error) {
|
||||
u := c.endpoint("/api/v2/torrents/info")
|
||||
if category != "" {
|
||||
@@ -386,7 +386,7 @@ func (c *Client) Torrents(ctx context.Context, category string) ([]Torrent, erro
|
||||
// распознаванию как один из сигналов; абсолютный путь — join(save_path, Name).
|
||||
func (c *Client) Files(ctx context.Context, hash string) ([]File, error) {
|
||||
log := logctx.FromOr(ctx, c.log)
|
||||
call := logging.ExtCall{Service: logging.ServiceQBittorrent, Operation: "torrents/files", Start: time.Now()}
|
||||
call := logging.StartCall(logging.ServiceQBittorrent, "torrents/files")
|
||||
resp, err := c.do(ctx, func() (*http.Request, error) {
|
||||
u := c.endpoint("/api/v2/torrents/files?hash=" + url.QueryEscape(hash))
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
|
||||
@@ -2,7 +2,6 @@ package recognize_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -43,7 +42,7 @@ func TestIntegration_RecognizeSeries(t *testing.T) {
|
||||
t.Fatalf("llm.New: %v", err)
|
||||
}
|
||||
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
log := slog.New(slog.DiscardHandler)
|
||||
r := recognize.New(provider, nil, recognize.Config{MaxRetries: 2}, log)
|
||||
|
||||
const dir = "Аватар Легенда об Аанге.Книга 2.Земля(Avatar The Last Airbender The book 2.Earth)/"
|
||||
|
||||
@@ -3,7 +3,6 @@ package recognize
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -37,7 +36,7 @@ func (f *fakeLLM) Complete(_ context.Context, req llm.Request) (llm.Response, er
|
||||
}
|
||||
|
||||
func testLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
return slog.New(slog.DiscardHandler)
|
||||
}
|
||||
|
||||
func TestRecognize_Movie(t *testing.T) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package tgbot
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -147,7 +146,7 @@ func newTestBot(t *testing.T, allowed []int64) (*Bot, *fakeAPI, *fakeIngestor, *
|
||||
ing := &fakeIngestor{res: ingest.Result{DownloadID: tid, State: store.StateDownloading}}
|
||||
rev := &fakeReviewer{data: reviewData(store.StateReview)}
|
||||
b := New(api, ing, rev, Config{AllowedUserIDs: allowed, WebBaseURL: "http://host:8080"},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
slog.New(slog.DiscardHandler))
|
||||
return b, api, ing, rev
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -753,7 +752,7 @@ func (f *fakeRecognizer) Director(_ context.Context, _ recognize.MediaType, _, _
|
||||
|
||||
func testWorkerWith(st Store, qb QBittorrent, rec Recognizer, lay Layouter) *Worker {
|
||||
w := New(st, qb, rec, lay, Config{Category: "jellybit"},
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
slog.New(slog.DiscardHandler))
|
||||
n := 0
|
||||
w.newID = func() string { n++; return "batch-" + itoa(n) }
|
||||
return w
|
||||
|
||||
@@ -281,7 +281,7 @@ func New(st Store, qb QBittorrent, rec Recognizer, lay Layouter, cfg Config, log
|
||||
layouter: lay,
|
||||
cfg: cfg,
|
||||
log: log,
|
||||
now: time.Now,
|
||||
now: store.Now,
|
||||
newID: defaultBatchID,
|
||||
failNotified: map[string]time.Time{},
|
||||
live: map[string]Live{},
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -360,7 +359,7 @@ func newTestWorker(st *fakeStore, qb *fakeQbt) *Worker {
|
||||
SavePath: "/srv/media/downloads",
|
||||
MagnetTimeout: 30 * time.Minute,
|
||||
StuckAfter: time.Hour,
|
||||
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
}, slog.New(slog.DiscardHandler))
|
||||
w.now = func() time.Time { return time.Date(2026, 6, 14, 10, 0, 0, 0, time.UTC) }
|
||||
return w
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user