Derive corpus-test data paths from the repo root and HOME instead of one machine's absolute paths, and document which tests stay dark without the stand data
This commit is contained in:
parent
f66518a860
commit
4d2d1449bc
4 changed files with 114 additions and 10 deletions
|
|
@ -47,7 +47,8 @@
|
|||
Стенд: WSL2 (localhost из-под прокси = 403 — для local-вызовов no-proxy транспорт). Книга-стенд: `/home/ubuntu/books/gu-zhenren/` (GB18030; тексты и производные ВНЕ git; парити-тесты майнера читают отсюда под `TM_MINER_PARITY=1`).
|
||||
|
||||
```bash
|
||||
go build ./... && go vet ./... && go test ./... -race # всё зелёное = норма
|
||||
go build ./... && go vet ./... && go test ./... -race # зелёное = норма ДЛЯ ГЕРМЕТИЧНОЙ ЧАСТИ (см. ниже)
|
||||
go test ./... -race -v | grep -- '--- SKIP' # ЧТО НЕ ВЫПОЛНЯЛОСЬ — вклеивать в отчёт, не опускать
|
||||
go run ./cmd/tmctl translate --config example/book.yaml # реальные вызовы — ключи в .env (пример: zh→ru)
|
||||
go run ./cmd/tmctl status --config example/book.yaml --json # $0, N/M+паспорта+деньги, живой прогон ок
|
||||
go run ./cmd/tmctl report --config example/book.yaml # $0, quality-report (KPI/rates)
|
||||
|
|
@ -56,6 +57,21 @@ go run ./cmd/tmctl export --config example/book.yaml # $0, экспор
|
|||
set -a; . ./.env; set +a; TM_LIVE=1 go test -tags live -run TestLive -v ./internal/pipeline/
|
||||
```
|
||||
|
||||
⚠ **«Зелёное» ≠ «всё проверено»: четыре теста — ИЗМЕРЕНИЯ на корпусе, а не юнит-тесты, и на чистом клоне они молча скипаются.** Голая батарея выше их НЕ выполняет, поэтому «батарея зелёная» без списка SKIP — неполный отчёт, а не приёмка.
|
||||
|
||||
| Что тёмное без данных | Флаг | Данные |
|
||||
|---|---|---|
|
||||
| `TestMinerFullBookParity` — единственная сверка Go-порта против Python-эталона на полной книге | `TM_MINER_PARITY=1` | jieba-словарь по внутрирепозиторному ПУТИ `eval/exp16/data/`, но ВНЕ git (`eval/.gitignore:4`) — клон его не получает, регенерируется из jieba 0.42.1 (SHA в `experiments/16-bank-mining.md:183`); плюс `~/books/gu-zhenren/{rerun/records.json, guzhenren-seed-v2.yaml}` |
|
||||
| `TestCheckerLabelsBaseline`, `TestCheckerLabelsCandidates`, `TestK6LabelsBaseline` — единственный замер precision/recall $0-гейтов | `TM_CHECKER_LABELS=1` | `~/books/gu-zhenren/labels/` |
|
||||
|
||||
Флаг переводит отсутствие данных из тихого скипа в громкий `Fatal` — то есть «я это гонял» становится проверяемым. Пути дефолтятся ОТ `$HOME` и от корня репозитория (не от `/home/ubuntu`), так что клон под любым пользователем и по любому пути находит то, что есть; точечные переопределения — `TM_CHECKER_LABELS_DIR`, `TM_MINER_PARITY_{CONTRAST,RECORDS,SEED}`.
|
||||
|
||||
Полная батарея на стенде:
|
||||
```bash
|
||||
go build ./... && go vet ./... && go vet -tags live ./... && test -z "$(gofmt -l .)" && \
|
||||
TM_MINER_PARITY=1 TM_CHECKER_LABELS=1 go test ./... -race -count=1
|
||||
```
|
||||
|
||||
**Golden-гард детерминизма** (`golden_test.go` + `testdata/golden/`): пинит бит-в-бит snapshotID, request_hash, wire-тела, вердикты и resume-байты. Красный golden = wire/вердикты изменились = `--resnapshot` = переоплата. Обновлять ТОЛЬКО на ратифицированной смене поведения: `TM_UPDATE_GOLDEN=1 go test ./internal/pipeline/ -run TestGolden`; при re-capture — **маскированный структурный дифф** (хеши/версии → плейсхолдер) обязан быть пустым, если смена версий не меняла вердиктов.
|
||||
|
||||
Финал каждой вехи — агентское адверсариальное селфревью (мандат CLAUDE.md 12.07); внешнее ревью — оркестратор.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
labelsDir = envOrChecks("TM_CHECKER_LABELS_DIR", "/home/ubuntu/books/gu-zhenren/labels")
|
||||
labelsDir = envOrChecks("TM_CHECKER_LABELS_DIR", standLabelsDir())
|
||||
langpackRoot = envOrChecks("TM_CHECKER_LANGPACK_ROOT", "../../configs/langpacks")
|
||||
)
|
||||
|
||||
|
|
@ -51,6 +51,18 @@ func envOrChecks(key, def string) string {
|
|||
return def
|
||||
}
|
||||
|
||||
// standLabelsDir derives the labelled-corpus root from $HOME rather than hardcoding /home/ubuntu, so the
|
||||
// default is correct for any user on any machine holding the stand data. An unresolvable HOME yields a
|
||||
// deliberately un-plausible path: "I could not find your home directory" must not be reported to the
|
||||
// operator as "the corpus is absent", because those two call for completely different actions.
|
||||
func standLabelsDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join("<home-not-found>", "books", "gu-zhenren", "labels")
|
||||
}
|
||||
return filepath.Join(home, "books", "gu-zhenren", "labels")
|
||||
}
|
||||
|
||||
// --- corpus / label record shapes -------------------------------------------------
|
||||
|
||||
type corpusUnit struct {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,20 @@ func envOrMem(key, def string) string {
|
|||
return def
|
||||
}
|
||||
|
||||
var memLabelsDir = envOrMem("TM_CHECKER_LABELS_DIR", "/home/ubuntu/books/gu-zhenren/labels")
|
||||
var memLabelsDir = envOrMem("TM_CHECKER_LABELS_DIR", memStandLabelsDir())
|
||||
|
||||
// memStandLabelsDir mirrors standLabelsDir in internal/checks — same root, same $HOME derivation, same
|
||||
// un-plausible path when HOME cannot be resolved (that is a broken environment, not an absent corpus).
|
||||
// The copy is a deliberate deferral, not a necessity: internal/chunk/chunktest shows this repo is willing
|
||||
// to host a shared test-only package, and these two halves plus miner's standFile are the third user —
|
||||
// enough to justify extracting one. Until then TM_CHECKER_LABELS_DIR is what actually keeps them in step.
|
||||
func memStandLabelsDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join("<home-not-found>", "books", "gu-zhenren", "labels")
|
||||
}
|
||||
return filepath.Join(home, "books", "gu-zhenren", "labels")
|
||||
}
|
||||
|
||||
type k6Pool struct {
|
||||
ID string `json:"id"`
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package miner
|
|||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -25,14 +27,22 @@ import (
|
|||
// they are absent (CI / a fresh checkout) and runs on the stand (or when TM_MINER_PARITY=1 forces it,
|
||||
// failing loud if the data is missing). $0, deterministic — Python is the reference; a divergence means
|
||||
// the Go port is wrong (fix Go), never the reference.
|
||||
// The stand-data paths default to the stand layout but are overridable via env so the test is not
|
||||
// pinned to one machine's absolute paths (test hygiene): TM_MINER_PARITY_{CONTRAST,RECORDS,SEED}.
|
||||
// The data is out of git (CLAUDE.md), so the test still SKIPS when a path is absent unless
|
||||
// TM_MINER_PARITY=1 forces it (then a missing path fails loud).
|
||||
// The three inputs are of TWO different kinds and their defaults are derived, never hardcoded to one
|
||||
// machine, so a clone at any path under any username finds whatever is actually present:
|
||||
// - the jieba contrast artifact sits at an in-repo PATH (eval/exp16/data) but is NOT in git — it falls
|
||||
// under the polygon's blanket raw-data rule (eval/.gitignore:4), so no clone ever receives it; it is
|
||||
// regenerated from jieba 0.42.1 (SHA in docs/experiments/16-bank-mining.md:183). The path is resolved
|
||||
// against the repo root because the old absolute default was additionally unfindable in every clone
|
||||
// not at /home/ubuntu/projects/textmachine — a portability bug ON TOP of the data gap, not instead of it;
|
||||
// - records.json and the seed are book derivatives and live OUT of git in the stand (~/books, CLAUDE.md),
|
||||
// so they are resolved from $HOME.
|
||||
//
|
||||
// All three stay overridable: TM_MINER_PARITY_{CONTRAST,RECORDS,SEED}. The test still SKIPS when a path
|
||||
// is absent unless TM_MINER_PARITY=1 forces it (then a missing path fails loud).
|
||||
var (
|
||||
minerParityContrast = envOr("TM_MINER_PARITY_CONTRAST", "/home/ubuntu/projects/textmachine/eval/exp16/data/jieba_dict_general_zh.txt")
|
||||
minerParityRecords = envOr("TM_MINER_PARITY_RECORDS", "/home/ubuntu/books/gu-zhenren/rerun/records.json")
|
||||
minerParitySeed = envOr("TM_MINER_PARITY_SEED", "/home/ubuntu/books/gu-zhenren/guzhenren-seed-v2.yaml")
|
||||
minerParityContrast = envOr("TM_MINER_PARITY_CONTRAST", repoFile("eval", "exp16", "data", "jieba_dict_general_zh.txt"))
|
||||
minerParityRecords = envOr("TM_MINER_PARITY_RECORDS", standFile("gu-zhenren", "rerun", "records.json"))
|
||||
minerParitySeed = envOr("TM_MINER_PARITY_SEED", standFile("gu-zhenren", "guzhenren-seed-v2.yaml"))
|
||||
)
|
||||
|
||||
// envOr returns the environment override for key, or def when it is unset/empty.
|
||||
|
|
@ -43,6 +53,59 @@ func envOr(key, def string) string {
|
|||
return def
|
||||
}
|
||||
|
||||
// repoRoot finds the repository root by MARKER (backend/go.mod) rather than by counting "..", so the
|
||||
// derivation cannot quietly go wrong. It starts from this file's own directory, and falls back to the
|
||||
// working directory when that path is not absolute — which is what -trimpath does: it rewrites
|
||||
// runtime.Caller to a module-relative path, and a naive "../../.." then yields a RELATIVE root that can
|
||||
// only ever miss, turning the parity test into a permanent silent skip on every machine including the
|
||||
// stand. `go test` always runs in the package directory, so the fallback is sound.
|
||||
func repoRoot() string {
|
||||
start := ""
|
||||
if _, self, _, ok := runtime.Caller(0); ok && filepath.IsAbs(self) {
|
||||
start = filepath.Dir(self)
|
||||
}
|
||||
if start == "" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
start = wd
|
||||
}
|
||||
for dir := start; ; {
|
||||
if _, err := os.Stat(filepath.Join(dir, "backend", "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return ""
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
// repoFile resolves parts against the repository root so an in-repo path is found wherever the clone
|
||||
// lives. When the root cannot be established the returned path is deliberately un-plausible: a missing
|
||||
// marker must read as "my derivation is broken", never as "the data is simply absent".
|
||||
func repoFile(parts ...string) string {
|
||||
root := repoRoot()
|
||||
if root == "" {
|
||||
return filepath.Join("<repo-root-not-found>", filepath.Join(parts...))
|
||||
}
|
||||
return filepath.Join(append([]string{root}, parts...)...)
|
||||
}
|
||||
|
||||
// standFile resolves parts against the out-of-git stand root ($HOME/books). Deriving the root from $HOME
|
||||
// instead of a literal /home/ubuntu keeps the default correct for any user. An unresolvable HOME yields a
|
||||
// deliberately un-plausible path for the same reason as repoFile: "I could not find your home directory"
|
||||
// must not be reported as "the stand data is absent".
|
||||
func standFile(parts ...string) string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join("<home-not-found>", "books", filepath.Join(parts...))
|
||||
}
|
||||
return filepath.Join(append([]string{home, "books"}, parts...)...)
|
||||
}
|
||||
|
||||
func TestMinerFullBookParity(t *testing.T) {
|
||||
force := os.Getenv("TM_MINER_PARITY") == "1"
|
||||
for _, p := range []string{minerParityContrast, minerParityRecords, minerParitySeed} {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue