129 lines
6 KiB
Go
129 lines
6 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/membank"
|
||
"textmachine/backend/internal/miner"
|
||
"textmachine/backend/internal/store"
|
||
"textmachine/backend/internal/text"
|
||
)
|
||
|
||
// minerseed_contract_test.go: the CROSS-SUBSYSTEM contract the split has to keep provable — what the
|
||
// miner EMITS must be loadable by the bank's REAL loader, with zero fail-louds. It is the pre-condition
|
||
// of the bank-mining reseed: the stop writes a delta the owner signs, the next run loads it through
|
||
// loadGlossarySeed, and a delta that does not lint aborts a paid run instead of a $0 test.
|
||
//
|
||
// It lives in the composite root because that is the only package that sees BOTH sides (the subsystems
|
||
// deliberately cannot import each other). Pre-split this was seedlint_test.go's
|
||
// TestSeedLintEmittedMinedDelta; the seed-lint fixtures moved with the loader into membank and this one
|
||
// belongs here, with them.
|
||
|
||
// minerContrast is a tiny general-zh reference for the emitted-delta fixture (mirrors the miner's own
|
||
// smallContrast: common chars carry high freq, the domain formant is absent).
|
||
func minerContrast(t *testing.T) *miner.Contrast {
|
||
t.Helper()
|
||
const data = "的 100000 uj\n是 80000 v\n人 50000 n\n出 9000 v\n现 9000 v\n" +
|
||
"月 4000 n\n光 3800 n\n希 900 n\n望 3000 v\n影 800 n\n在 20000 p\n此 1200 r\n" +
|
||
"古 400 nr\n方 900 nr\n源 300 n\n族 500 n\n长 6000 a\n青 700 n\n茅 20 n\n山 5000 n\n" +
|
||
"四 8000 m\n代 3000 n\n甲 300 n\n等 12000 u\n正 4000 a\n"
|
||
c, err := miner.LoadContrast(strings.NewReader(data))
|
||
if err != nil {
|
||
t.Fatalf("LoadContrast: %v", err)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// writeMinedDelta writes an emitted delta to a temp seed file and returns its path.
|
||
func writeMinedDelta(t *testing.T, body string) string {
|
||
t.Helper()
|
||
p := filepath.Join(t.TempDir(), "mined-delta.yaml")
|
||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return p
|
||
}
|
||
|
||
func TestSeedLintEmittedMinedDelta(t *testing.T) {
|
||
// The emitted mined delta must be loadable by the REAL loader (0 fail-louds) — the pre-condition for
|
||
// the bank-mining reseed.
|
||
// The fixture must actually PROPOSE terms — the pre-split version of this test used a corpus the
|
||
// detector types nothing in, so it linted an EMPTY delta and proved nothing. This corpus is the one
|
||
// the miner's own reject fixture relies on (方源 + 花家 both proposed).
|
||
src := text.NormalizeSourceKey(strings.Repeat("方源来到青茅山。方源很强。花家很大。花家的人。", 6))
|
||
chunks := []miner.Chunk{{Chapter: 1, ChunkIdx: 0, NSource: src}}
|
||
seed := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}}
|
||
mined := miner.MineBank(chunks, minerContrast(t), seed, nil, miner.FrozenConfig(), testLangPack(t))
|
||
if len(mined) == 0 {
|
||
t.Fatal("test premise broken: the fixture must propose at least one term (else the contract is vacuous)")
|
||
}
|
||
yamlStr, err := miner.DeltaYAML(mined, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
p := writeMinedDelta(t, yamlStr)
|
||
if err := membank.SeedLint(p); err != nil {
|
||
t.Fatalf("the emitted mined delta must lint clean (loadable), got:\n%v\n--- delta ---\n%s", err, yamlStr)
|
||
}
|
||
// …and the loader must actually materialize the emitted terms (a delta that lints but loads to
|
||
// nothing would pass the lint above while the reseed silently gains no terms).
|
||
entries, err := membank.LoadGlossarySeed(p)
|
||
if err != nil {
|
||
t.Fatalf("the emitted delta must load: %v", err)
|
||
}
|
||
if len(entries) != len(mined) {
|
||
t.Errorf("loader returned %d entries for %d mined terms — the emission/loader schemas diverged", len(entries), len(mined))
|
||
}
|
||
}
|
||
|
||
// TestSeedLintEmittedMinedDeltaWithProposedDst extends the same contract to the WHAT-carrying form
|
||
// (D39.36 fix): a delta whose terms arrived with a translator-proposed dst must ALSO load through the
|
||
// real loader. The dst travels as evidence on a status:auto term, and a sign map that the loader
|
||
// refuses would be worse than the bare one it replaced.
|
||
func TestSeedLintEmittedMinedDeltaWithProposedDst(t *testing.T) {
|
||
src := text.NormalizeSourceKey(strings.Repeat("方源来到青茅山。方源很强。花家很大。花家的人。", 6))
|
||
chunks := []miner.Chunk{{Chapter: 1, ChunkIdx: 0, NSource: src}}
|
||
seedRows := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}}
|
||
mined := miner.MineBank(chunks, minerContrast(t), seedRows, nil, miner.FrozenConfig(), testLangPack(t))
|
||
if len(mined) == 0 {
|
||
t.Fatal("test premise broken: the fixture must propose at least one term")
|
||
}
|
||
proposals := map[string][]miner.DstProposal{}
|
||
for _, m := range mined {
|
||
proposals[text.NormalizeSourceKey(m.Src)] = []miner.DstProposal{
|
||
{Dst: "Предложенный вариант", Type: "name", Chunks: 2},
|
||
{Dst: "Другой вариант", Type: "name", Chunks: 1},
|
||
}
|
||
}
|
||
yamlStr, err := miner.DeltaYAML(mined, proposals)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(yamlStr, "Предложенный вариант") {
|
||
t.Fatalf("the winning proposal must reach the sign map:\n%s", yamlStr)
|
||
}
|
||
if !strings.Contains(yamlStr, "NOT approved") {
|
||
t.Fatalf("the sign map must say the dst is a PROPOSAL, not a canon:\n%s", yamlStr)
|
||
}
|
||
p := writeMinedDelta(t, yamlStr)
|
||
if err := membank.SeedLint(p); err != nil {
|
||
t.Fatalf("a dst-carrying mined delta must lint clean, got:\n%v\n--- delta ---\n%s", err, yamlStr)
|
||
}
|
||
entries, err := membank.LoadGlossarySeed(p)
|
||
if err != nil {
|
||
t.Fatalf("the emitted delta must load: %v", err)
|
||
}
|
||
if len(entries) != len(mined) {
|
||
t.Errorf("loader returned %d entries for %d mined terms", len(entries), len(mined))
|
||
}
|
||
// The proposal must NOT arrive pre-trusted: status stays auto, so the trust gate keeps it out of
|
||
// the injection until the owner signs. This is the whole safety boundary of the fix.
|
||
for _, e := range entries {
|
||
if e.Status != "auto" {
|
||
t.Errorf("term %q loaded with status %q — a PROPOSED dst must never arrive approved", e.Src, e.Status)
|
||
}
|
||
}
|
||
}
|