112 lines
5 KiB
Go
112 lines
5 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"textmachine/backend/internal/miner"
|
|
"textmachine/backend/internal/text"
|
|
)
|
|
|
|
// stopflag_test.go: unit pins of the §4.1 stop predicate. The end-to-end semantics live in
|
|
// bankchain_test.go (TestTheStopIsAFlagNotAGateOnDecisions) and miningstop_join_test.go; these pin the
|
|
// cluster geometry the harness cannot reach cheaply.
|
|
|
|
func TestAClusterSharingAnAliasWithTheMemoryReadsAsPresented(t *testing.T) {
|
|
// The representative of a cluster can CHANGE as the book grows — the first-ranked member may become
|
|
// an alias of the same entity. Memory by intersection is what keeps that from re-stopping: a term
|
|
// whose src is fresh but whose cluster touches a presented surface has been seen.
|
|
presented := map[string]bool{text.NormalizeSourceKey("方源"): true}
|
|
swapped := []miner.Term{{Src: "小方", Aliases: []string{"方源"}}}
|
|
if hasUnpresentedCluster(swapped, presented) {
|
|
t.Fatal("a cluster sharing a surface with the memory has been presented; a representative swap must not re-stop")
|
|
}
|
|
// …and the accepted harmful residue, recorded as the pin of a DECISION rather than of an accident: a
|
|
// genuinely new entity sharing an alias surface with a presented cluster is NOT presented either. It
|
|
// still reaches the editor as an unsigned auto row; changing this trade needs a new decision, not a
|
|
// refactor.
|
|
newEntity := []miner.Term{{Src: "老方", Aliases: []string{"方源", "方大人"}}}
|
|
if hasUnpresentedCluster(newEntity, presented) {
|
|
t.Fatal("intersection semantics: a cluster touching the memory reads as seen (the named two-sided residue)")
|
|
}
|
|
}
|
|
|
|
func TestAWhollyNewClusterTripsTheFlag(t *testing.T) {
|
|
presented := map[string]bool{text.NormalizeSourceKey("方源"): true}
|
|
fresh := []miner.Term{
|
|
{Src: "方源"}, // seen
|
|
{Src: "花家", Aliases: []string{"花家的人"}}, // wholly outside the memory
|
|
}
|
|
if !hasUnpresentedCluster(fresh, presented) {
|
|
t.Fatal("a map holding a cluster the memory has never seen must stop")
|
|
}
|
|
if hasUnpresentedCluster(nil, presented) {
|
|
t.Fatal("an empty map holds nothing to stop on")
|
|
}
|
|
}
|
|
|
|
func TestPresentedSurfacesFlattenEveryClusterMemberNormalized(t *testing.T) {
|
|
// What the memory stores is exactly what the next map is compared against: every member of every
|
|
// cluster, in the emission's own key form (NormalizeSourceKey — trad→simp, kana fold, casefold; NOT
|
|
// a whitespace trim, spaces are part of a key), blanks dropped. 陳博 arrives TRADITIONAL here (陳→陈)
|
|
// and must land simplified, or the memory would never match the miner's simplified candidate keys —
|
|
// the fixture char is chosen to actually cross the fold (review-workflow finding: the first fixture
|
|
// was an identity under normalization and the claim was vacuous).
|
|
got := presentedSurfaces([]miner.Term{
|
|
{Src: "陳博", Aliases: []string{"方大人", ""}},
|
|
{Src: "花家"},
|
|
})
|
|
if text.NormalizeSourceKey("陳博") == "陳博" {
|
|
t.Fatal("fixture premise broken: 陳博 no longer crosses the trad→simp fold")
|
|
}
|
|
want := map[string]bool{
|
|
text.NormalizeSourceKey("陳博"): true,
|
|
text.NormalizeSourceKey("方大人"): true,
|
|
text.NormalizeSourceKey("花家"): true,
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want the members of %v", got, want)
|
|
}
|
|
for _, s := range got {
|
|
if !want[s] {
|
|
t.Fatalf("surface %q is not in the normalized member set %v", s, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTheMemoryIsNotWrittenWhenTheMapCannotBe pins the §4.2-of-fix2 ORDER — the map on disk first, the
|
|
// memory second, never the reverse. The asymmetry is load-bearing: a crash between the two costs one
|
|
// benign re-stop, but a remembered surface whose map never reached disk is a SILENT LOSS — the run
|
|
// stops pointing the owner at a file that is not there, the clusters read as presented, and no later
|
|
// flag ever stops on them again. The acceptance planted «downgrade the map-write failure to a warning»
|
|
// and the battery stayed green in all three candidate packages; this is the pin that reddens on it.
|
|
func TestTheMemoryIsNotWrittenWhenTheMapCannotBe(t *testing.T) {
|
|
rec := &reqRec{}
|
|
srv := newJSONProvider(rec, miningStopProvider(t, rec, ""))
|
|
defer srv.Close()
|
|
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{})
|
|
r := newVerifyRunner(t, bookPath)
|
|
defer r.Close()
|
|
// The map's path is occupied by a non-empty directory: the atomic replace cannot land.
|
|
if err := os.MkdirAll(filepath.Join(r.signatureMapPath(), "occupied"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := r.TranslateBook(context.Background())
|
|
var stop *WaveSignatureStop
|
|
if errors.As(err, &stop) {
|
|
t.Fatalf("the run STOPPED pointing the owner at a map that never reached disk (%d terms)", stop.Terms)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("a run whose signature map cannot be written must fail loud, not proceed")
|
|
}
|
|
mem, merr := r.Store.StopPresentedSurfaces("test-book")
|
|
if merr != nil {
|
|
t.Fatal(merr)
|
|
}
|
|
if len(mem) != 0 {
|
|
t.Fatalf("surfaces were marked presented though their map never reached disk — no later flag will ever stop on them: %v", mem)
|
|
}
|
|
}
|