144 lines
7.6 KiB
Go
144 lines
7.6 KiB
Go
package membank
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/store"
|
||
)
|
||
|
||
// memory_f1_test.go covers the F1 content-hash (D8): memoryVersion is a pure function of
|
||
// the frozen APPROVED rows + the algorithm versions. A change to the approved glossary
|
||
// re-pins loudly; auto/draft churn does not; the row id never leaks in.
|
||
|
||
func TestMemoryVersionStableAndSensitive(t *testing.T) {
|
||
base := []store.GlossaryEntry{
|
||
gl("阿Q", "А-кью", "name", "approved"),
|
||
gl("鲁镇", "Лучжэнь", "", "approved"),
|
||
}
|
||
v0 := ComputeVersion(base, false)
|
||
|
||
// Identical rows → identical version (idempotent; drift-proof content hash).
|
||
if ComputeVersion(base, false) != v0 {
|
||
t.Fatal("memoryVersion is not stable for identical rows")
|
||
}
|
||
|
||
// Changing an APPROVED dst → different version (a loud --resnapshot).
|
||
changed := []store.GlossaryEntry{
|
||
gl("阿Q", "А-Кью (изменено)", "name", "approved"),
|
||
gl("鲁镇", "Лучжэнь", "", "approved"),
|
||
}
|
||
if ComputeVersion(changed, false) == v0 {
|
||
t.Error("changing an approved dst did not change memoryVersion — F1 would silently regress")
|
||
}
|
||
|
||
// Adding an alias to an approved row → different version (aliases affect matching).
|
||
withAlias := []store.GlossaryEntry{
|
||
func() store.GlossaryEntry {
|
||
e := gl("阿Q", "А-кью", "name", "approved")
|
||
e.Aliases = []store.GlossaryAlias{alias("阿桂")}
|
||
return e
|
||
}(),
|
||
gl("鲁镇", "Лучжэнь", "", "approved"),
|
||
}
|
||
if ComputeVersion(withAlias, false) == v0 {
|
||
t.Error("adding an alias to an approved row did not change memoryVersion")
|
||
}
|
||
|
||
// PACK-20 CONTRACT CHANGE (D39.42 п.3), superseding the D8 approved-only fold. An AUTO row now MOVES
|
||
// the version, because in the auto mode it is CONTENT the model is shown: it changes the rendered
|
||
// injection, hence content_hash, hence the resume fast-path — while the snapshot (and with it
|
||
// projectRebill's re-payment projection, the Р6 consent contour) stayed blind. Folding it makes a bank
|
||
// edit of ANY status a loud --resnapshot instead of a silent per-chunk re-translate.
|
||
withAuto := append(append([]store.GlossaryEntry{}, base...), gl("小D", "Малыш Дэ", "", "auto"))
|
||
if ComputeVersion(withAuto, false) == v0 {
|
||
t.Error("adding an auto row must move memoryVersion — its content reaches the wire (D39.42 п.3)")
|
||
}
|
||
withAuto2 := append(append([]store.GlossaryEntry{}, base...), gl("小D", "Другой Дэ", "", "auto"))
|
||
if ComputeVersion(withAuto, false) == ComputeVersion(withAuto2, false) {
|
||
t.Error("editing an auto row must move memoryVersion — the injected bytes changed")
|
||
}
|
||
// The other half of the same contract, and the reason shipping this re-bills nobody who had nothing
|
||
// unverified: a bank that is entirely APPROVED hashes exactly as it did before pack-20.
|
||
if ComputeVersion(base, false) != v0 {
|
||
t.Error("an all-approved bank must hash unchanged — else every existing book re-pins for nothing")
|
||
}
|
||
}
|
||
|
||
// TestMemoryVersionIgnoresID: the autoincrement glossary.id is fresh on every replace;
|
||
// it must NOT feed the hash (else a re-seed of identical content would false-resnapshot).
|
||
func TestMemoryVersionIgnoresID(t *testing.T) {
|
||
a := []store.GlossaryEntry{{BookID: "b", ID: 1, Src: "甲", Dst: "А", Status: "approved"}}
|
||
b := []store.GlossaryEntry{{BookID: "b", ID: 999, Src: "甲", Dst: "А", Status: "approved"}}
|
||
if ComputeVersion(a, false) != ComputeVersion(b, false) {
|
||
t.Error("memoryVersion depends on glossary.id — a re-seed would false-resnapshot")
|
||
}
|
||
}
|
||
|
||
// TestMaterializeMemoryVersionMatches: the bank's Version() equals ComputeVersion
|
||
// of its input rows (the runner hashes the same thing it matches on).
|
||
func TestMaterializeMemoryVersionMatches(t *testing.T) {
|
||
rows := specGlossary()
|
||
b := Materialize(rows, false)
|
||
if b.Version() != ComputeVersion(rows, false) {
|
||
t.Error("bank Version() diverges from ComputeVersion of its rows")
|
||
}
|
||
}
|
||
|
||
// TestMemoryVersionGateFoldsUnapprovedDecl covers self-review #4: when the post-check
|
||
// hard gate is ON, editing a DRAFT/auto row's decl (which changes the gate's resolved
|
||
// disposition but is in NEITHER content_hash NOR the approved-only hash) MUST change
|
||
// memoryVersion — else a $0 resume silently flips a chunk. Gate OFF: it must NOT change
|
||
// it (D8 approved-only holds; decl affects only the recomputed retrieval-state).
|
||
func TestMemoryVersionGateFoldsUnapprovedDecl(t *testing.T) {
|
||
mk := func(declForm string) []store.GlossaryEntry {
|
||
e := gl("鈴木", "Судзуки", "", "draft")
|
||
e.Decl = declJSON(false, declForm)
|
||
return []store.GlossaryEntry{gl("阿Q", "А-кью", "", "approved"), e}
|
||
}
|
||
a := mk("Судзуки")
|
||
b := mk("Судзуку") // only the DRAFT decl form changed (dst unchanged, status draft)
|
||
|
||
// PACK-20 (D39.42 п.3): the fold no longer depends on the gate — EVERY row's content is hashed on
|
||
// both settings. The self-review #4 hole this test was written for is therefore closed unconditionally,
|
||
// not only when the gate happens to be on.
|
||
if ComputeVersion(a, false) == ComputeVersion(b, false) {
|
||
t.Error("gate OFF: a draft decl change must ALSO move memoryVersion now (D39.42 п.3)")
|
||
}
|
||
if ComputeVersion(a, true) == ComputeVersion(b, true) {
|
||
t.Error("gate ON: a draft decl change MUST move memoryVersion (else a resume silently flips the chunk)")
|
||
}
|
||
// Toggling the gate itself also changes the version (a mode change is a loud re-pin).
|
||
if ComputeVersion(a, false) == ComputeVersion(a, true) {
|
||
t.Error("toggling the post-check gate must change memoryVersion")
|
||
}
|
||
}
|
||
|
||
// TestMemoryVersionPinsTheUnverifiedRenderRevision is a GOLDEN pin, and it is the only shape that can hold
|
||
// this contract: the render-revision marker exists to differ from a PREVIOUS BINARY, so no comparison
|
||
// between two live calls can see it. Pack-20 gave the editor block a second, separately-headed section for
|
||
// non-Confirmed rows. For a book whose post-check gate was already ON, the row set folded is identical
|
||
// before and after — same version, same snapshot — while the wire bytes changed, and projectRebill skips a
|
||
// unit whose snapshot is unchanged. That is a silent re-payment outside the Р6 consent contour. The marker
|
||
// moves exactly the books that render the section, so their units re-pin ($0 where the bytes did not
|
||
// change) instead of being re-bought without consent.
|
||
//
|
||
// If this literal has to change, the render of the unverified section changed with it — and every book
|
||
// holding a non-Confirmed row will re-pin on the next run. That is the intended, and the only honest,
|
||
// price. Bumping RenderFormatVersion instead would move EVERY book, including those that render no such
|
||
// section, and a format bump is not a bank-only move, so none of them could re-pin at $0.
|
||
func TestMemoryVersionPinsTheUnverifiedRenderRevision(t *testing.T) {
|
||
withDraft := []store.GlossaryEntry{
|
||
{Src: "方源", Dst: "Фан Юань", Type: "name", Status: "approved"},
|
||
{Src: "花家", Dst: "клан Хуа", Type: "name", Status: "draft"},
|
||
}
|
||
const wantWithDraft = "d3193be2a471468a4cf4e047da7fbb081f2578f3052538d91e1fed41605736f2"
|
||
if got := ComputeVersionScoped(withDraft, true, false); got != wantWithDraft {
|
||
t.Errorf("the unverified-render revision is part of the version:\n got %s\nwant %s", got, wantWithDraft)
|
||
}
|
||
// A bank with nothing unverified renders no such section and must hash as if the marker did not exist —
|
||
// this is what keeps the change from re-billing books that had nothing to re-render.
|
||
const wantAllApproved = "ecb625f264bd56a840bd3a940b01fb2a86d16f22641efcb413cdbfbdb8388b72"
|
||
if got := ComputeVersionScoped(withDraft[:1], true, false); got != wantAllApproved {
|
||
t.Errorf("an all-approved bank must be untouched by the marker:\n got %s\nwant %s", got, wantAllApproved)
|
||
}
|
||
}
|