Artifact c8e18149d1631e9f5763914ca2366d8646b9e78f8bb479ea15a7a57d9d3aed91:
- File
internal/manifest/submanifest_write_test.go
— part of check-in
[14f17e675e]
at
2026-10-18 16:44:14
on branch feat/submanifest-reader
— feat(manifest): write tree check-ins behind enable-submanifests
Checkin now honours draft-fossil-submanifests-00 §8.1: with enable-submanifests on and a pinned submanifest-k it stores the canonical tree first, marks each sub-manifest unsent, records the seen-submanifest indicator that arms the §7.2 gate, and writes a manifest of S cards with no F card and never a B card. The setting is off by default, so every commit is byte-identical to before. A stored k outside 1..12 falls back to a flat manifest and logs it (§6.3); unchanged sub-manifests are shared by content addressing.
EnableSubmanifests is §8.1's local request: a valid pin wins and a different explicit k is refused as a re-tune; with no valid pin an explicit k is pinned, or §6.3's selection runs over the current check-in; a request that cannot be met changes neither setting. RetuneSubmanifests is the manual §6.4 re-tune. After a tree commit the §6.4 gate re-pins k while submanifest-auto-retune is on, and only advises otherwise.
A sync run reports avoid-submanifests in SyncResult, and CheckinOpts.AvoidSubmanifests keeps a commit made in that sync flat (§7.2). The settings' names and parsers move to internal/manifest, which sync now uses. Repo.EnableSubmanifests and Repo.RetuneSubmanifests expose the two requests, the first returning the operator warning. A test shows fossil 2.28 never receives a tree this writer made. (user: [email protected], size: 12386) [annotate] [blame] [check-ins using]
package manifest import ( "errors" "fmt" "strings" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" ) // These tests drive the writer of draft-fossil-submanifests-00 through // Checkin, the one place check-in manifests are made: the emission gate // (§8.1), the tree stored beside its manifest (§6.2), pinning and selecting // k (§6.3, §8.1) and re-tuning it (§6.4). // sha3Repo is a test repository that names artifacts as the vectors do. func sha3Repo(t *testing.T) *repo.Repo { t.Helper() r := setupTestRepo(t) if err := r.SetHashPolicy("sha3"); err != nil { t.Fatal(err) } return r } func a1CheckinFiles() []File { return []File{ {Name: "README.md", Content: []byte("# Example\n")}, {Name: "src/main.c", Content: []byte("int main(void){return 0;}\n")}, {Name: "src/node11.c", Content: []byte("/* node11 */\n")}, {Name: "src/util.c", Content: []byte("/* util */\n")}, } } func setSettings(t *testing.T, r *repo.Repo, kv ...string) { t.Helper() for i := 0; i+1 < len(kv); i += 2 { if err := r.SetConfig(kv[i], kv[i+1]); err != nil { t.Fatal(err) } } } var writeTime = time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC) func commit(t *testing.T, r *repo.Repo, opts CheckinOpts) (libfossil.FslID, *deck.Deck) { t.Helper() if opts.User == "" { opts.User = "drh" } if opts.Time.IsZero() { opts.Time = writeTime } rid, _, err := Checkin(r, opts) if err != nil { t.Fatalf("Checkin: %v", err) } d, err := GetManifest(r, rid) if err != nil { t.Fatal(err) } return rid, d } func seen(t *testing.T, r *repo.Repo) bool { t.Helper() ok, err := SeenSubmanifest(r.DB()) if err != nil { t.Fatal(err) } return ok } // §8.1: emission is off by default, and the manifest stays flat. func TestCheckinFlatByDefault(t *testing.T) { r := sha3Repo(t) _, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "flat"}) if len(d.S) != 0 || len(d.F) != 4 || seen(t, r) { t.Fatalf("default commit: %d S cards, %d F cards, seen %v", len(d.S), len(d.F), seen(t, r)) } } // §6.2: with emission on at k = 3 the A.1 files go to the two leaves of // Appendix A.2, the manifest carries their S cards and no F card, the tree // walks back to the four files, and the indicator of §8.1 is set. func TestCheckinEmitsTree(t *testing.T) { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") rid, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "tree"}) if len(d.F) != 0 || len(d.S) != 2 || d.S[0].UUID != "aa9d404c14fe4dadd2ab398961fb0dfe650a877c061f650d8233a5888e920acf" || d.S[1].UUID != "a50a722b465ed67b42ee0fbe61e4a19d6b1d22940ba5740e99e450921e3f52ea" { t.Fatalf("manifest S %+v F %d", d.S, len(d.F)) } files, err := ListFiles(r, rid) if err != nil || len(files) != 4 || files[3].Name != "src/util.c" { t.Fatalf("ListFiles = %+v, %v", files, err) } if !seen(t, r) { t.Fatal("seen-submanifest not recorded") } for _, s := range d.S { leaf, _ := blob.Exists(r.DB(), s.UUID) var n int r.DB().QueryRow("SELECT count(*) FROM unsent WHERE rid=?", leaf).Scan(&n) if n != 1 { t.Fatalf("leaf %s is not marked unsent", s.UUID[:10]) } } } // §4.4: a tree check-in carries no B card even when a delta is asked for; // a flat construction under emission still may be a delta. func TestCheckinTreeIsNeverDelta(t *testing.T) { r := sha3Repo(t) parent, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "base"}) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") files := a1CheckinFiles() files[1].Content = []byte("int main(void){return 1;}\n") _, d := commit(t, r, CheckinOpts{Files: files, Comment: "edit", Parent: parent, Delta: true, Time: writeTime.Add(time.Hour)}) if d.B != "" || len(d.S) != 2 { t.Fatalf("B %q with %d S cards", d.B, len(d.S)) } } // §6.3: a stored k outside 1..12 falls back to flat; §7.2: a commit made in // a session whose server sent avoid-submanifests is flat. func TestCheckinFlatFallbacks(t *testing.T) { for _, tc := range []struct { name, k string avoid bool }{{"k 13", "13", false}, {"k 0", "0", false}, {"k unset", "", false}, {"avoid", "3", true}} { t.Run(tc.name, func(t *testing.T) { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on") if tc.k != "" { setSettings(t, r, SettingSubmanifestK, tc.k) } _, d := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "c", AvoidSubmanifests: tc.avoid}) if len(d.S) != 0 || len(d.F) != 4 || seen(t, r) { t.Fatalf("%d S cards, %d F cards", len(d.S), len(d.F)) } }) } } // subCount counts the sub-manifests r holds. func subCount(t *testing.T, r *repo.Repo) int { t.Helper() subs := 0 rows, err := r.DB().Query("SELECT uuid FROM blob WHERE size>=0") if err != nil { t.Fatal(err) } var uuids []string for rows.Next() { var u string rows.Scan(&u) uuids = append(uuids, u) } rows.Close() for _, u := range uuids { rid, _ := blob.Exists(r.DB(), u) d, err := GetManifest(r, rid) if err != nil { continue } if d.Type == deck.Submanifest { subs++ } } return subs } // §6.2: content addressing shares every unchanged sub-manifest, so editing // one file adds exactly one leaf. func TestCheckinReusesUnchangedLeaves(t *testing.T) { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") parent, first := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "one"}) files := a1CheckinFiles() files[3].Content = []byte("/* util 2 */\n") _, second := commit(t, r, CheckinOpts{Files: files, Comment: "two", Parent: parent, Time: writeTime.Add(time.Hour)}) if second.S[0] != first.S[0] || second.S[1] == first.S[1] { t.Fatalf("S runs %+v then %+v", first.S, second.S) } if n := subCount(t, r); n != 3 { t.Fatalf("%d sub-manifests stored, want 3", n) } } func settingValue(t *testing.T, r *repo.Repo, name string) string { t.Helper() v, err := r.Config(name) if err != nil { return "" } return v } // §8.1: a local enable request pins k by the rules of §6.3, and one that // cannot be met changes nothing. func TestEnableSubmanifests(t *testing.T) { r := sha3Repo(t) one, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles()[:1], Comment: "one file"}) four, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "four", Parent: one, Time: writeTime.Add(time.Hour)}) unchanged := func(what string) { t.Helper() if settingValue(t, r, SettingEnableSubmanifests) != "" || settingValue(t, r, SettingSubmanifestK) != "" { t.Fatalf("%s left settings behind", what) } } // Appendix A.5: over one name no k is a candidate, so the request fails. if _, err := EnableSubmanifests(r, one, 0); !errors.Is(err, errNoSelection) { t.Fatalf("enable over one file: %v", err) } unchanged("selection without a result") if _, err := EnableSubmanifests(r, 0, 0); !errors.Is(err, errNoSelection) { t.Fatalf("enable with no current check-in: %v", err) } unchanged("no current check-in") for _, k := range []int{-1, 13} { if _, err := EnableSubmanifests(r, four, k); err == nil { t.Fatalf("enable --k %d accepted", k) } unchanged(fmt.Sprintf("k %d", k)) } // Selection over A.1: k = 1, 2 and 3 tie, and the lowest wins. if k, err := EnableSubmanifests(r, four, 0); err != nil || k != 1 { t.Fatalf("enable = %d, %v", k, err) } if settingValue(t, r, SettingEnableSubmanifests) != "on" || settingValue(t, r, SettingSubmanifestK) != "1" { t.Fatal("settings not written") } // The pin takes precedence; a different explicit k is a re-tune. if k, err := EnableSubmanifests(r, four, 1); err != nil || k != 1 { t.Fatalf("re-enable at the pin = %d, %v", k, err) } if _, err := EnableSubmanifests(r, four, 3); err == nil || !strings.Contains(err.Error(), "re-tune") { t.Fatalf("enable --k 3 over pin 1: %v", err) } // An out-of-range pin is no valid pin: an explicit value replaces it. setSettings(t, r, SettingEnableSubmanifests, "off", SettingSubmanifestK, "13") if k, err := EnableSubmanifests(r, four, 2); err != nil || k != 2 { t.Fatalf("enable --k 2 over pin 13 = %d, %v", k, err) } } // §8.1: a persistence failure partway through enabling leaves neither // setting applied. func TestEnableSubmanifestsAtomic(t *testing.T) { r := sha3Repo(t) four, _ := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "four"}) if _, err := r.DB().Exec(`CREATE TRIGGER fail_enable BEFORE INSERT ON config WHEN NEW.name='enable-submanifests' BEGIN SELECT RAISE(ABORT, 'injected'); END`); err != nil { t.Fatal(err) } if _, err := EnableSubmanifests(r, four, 3); err == nil { t.Fatal("enable succeeded through an injected failure") } if settingValue(t, r, SettingSubmanifestK) != "" || settingValue(t, r, SettingEnableSubmanifests) != "" { t.Fatal("a setting was applied by a failed enable") } } // §6.4: a manual re-tune governs the next commit; leaves whose bytes are the // same under both constructions are shared, and history is untouched. func TestRetuneGovernsNextCommit(t *testing.T) { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") parent, first := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "one"}) if err := RetuneSubmanifests(r, 13); err == nil { t.Fatal("re-tune to 13 accepted") } if err := RetuneSubmanifests(r, 4); err != nil { t.Fatal(err) } _, second := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "two", Parent: parent, Time: writeTime.Add(time.Hour)}) if len(second.S) != 0 || len(second.F) != 4 { t.Fatalf("k=4 commit has %d S cards; A.1 is flat at k = 4", len(second.S)) } if again, _ := GetManifest(r, parent); len(again.S) != len(first.S) { t.Fatal("the parent changed") } if err := RetuneSubmanifests(r, 2); err != nil { t.Fatal(err) } _, third := commit(t, r, CheckinOpts{Files: a1CheckinFiles(), Comment: "three", Parent: parent, Time: writeTime.Add(2 * time.Hour)}) if len(third.S) != 2 || third.S[0] != first.S[0] || third.S[1] != first.S[1] { t.Fatalf("k=2 shares A.1's partition, so its leaves: %+v vs %+v", third.S, first.S) } } // retuneFiles is a file set on which k = 1 is far from the optimum. func retuneFiles() []File { var out []File for i := 0; i < 300; i++ { name := fmt.Sprintf("dir%02d/file%03d.txt", i%20, i) out = append(out, File{Name: name, Content: []byte(name + "\n")}) } return out } // §6.4: only while submanifest-auto-retune is on does a commit re-pin k, and // then only past the margin; off, the pin stays. func TestAutoRetune(t *testing.T) { var cards []deck.FileCard for _, f := range retuneFiles() { cards = append(cards, deck.FileCard{Name: f.Name, UUID: strings.Repeat("0", 64)}) } sortCards(cards) target, ok := retuneTarget(partitionCosts(cards), 1) if !ok { t.Fatal("fixture: k = 1 shows no drift") } for _, auto := range []string{"off", "on"} { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "1", SettingAutoRetune, auto) commit(t, r, CheckinOpts{Files: retuneFiles(), Comment: "c"}) want := "1" if auto == "on" { want = fmt.Sprint(target) } if got := settingValue(t, r, SettingSubmanifestK); got != want { t.Fatalf("auto-retune %s: k %s, want %s", auto, got, want) } } } func sortCards(cards []deck.FileCard) { for i := 1; i < len(cards); i++ { for j := i; j > 0 && cards[j-1].Name > cards[j].Name; j-- { cards[j-1], cards[j] = cards[j], cards[j-1] } } } // A duplicate name is refused rather than built into a tree. func TestCheckinTreeRefusesDuplicateName(t *testing.T) { r := sha3Repo(t) setSettings(t, r, SettingEnableSubmanifests, "on", SettingSubmanifestK, "3") files := append(a1CheckinFiles(), File{Name: "README.md", Content: []byte("again\n")}) if _, _, err := Checkin(r, CheckinOpts{Files: files, Comment: "dup", User: "u"}); err == nil { t.Fatal("duplicate name accepted") } var n int if err := r.WithTx(func(tx *db.Tx) error { return tx.QueryRow("SELECT count(*) FROM event").Scan(&n) }); err != nil || n != 0 { t.Fatalf("%d events after a refused commit (%v)", n, err) } }