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) } }