| index 067e1cae9..fd428db89 100644 |
| --- a/internal/host/movesfiles_test.go |
| +++ b/internal/host/movesfiles_test.go |
| @@ -10,6 +10,7 @@ import ( |
| "os" |
| "path/filepath" |
| "reflect" |
| + "sort" |
| "strings" |
| "sync" |
| "testing" |
| @@ -1239,10 +1240,16 @@ func TestEstablishProvesAndCarriesOnlyReferencedOrigins(t *testing.T) { |
| t.Fatalf("g2 origins %v, want %v", m.Origins, want) |
| } |
| // One Proven(set) call per Establish (Plan 15): the deduplicated |
| - // referenced origins plus the admitted generation, in one batch. |
| - if len(proof.calls) != 1 || !reflect.DeepEqual(proof.calls[0], []string{"g1", "g2", "g5"}) { |
| + // referenced origins plus the admitted generation, in one batch (the |
| + // set's order is the caller's; the prefixes are what get sorted). |
| + if len(proof.calls) != 1 { |
| t.Fatalf("Proven calls %v, want one call over the referenced origins and the admitted generation", proof.calls) |
| } |
| + asked := append([]string(nil), proof.calls[0]...) |
| + sort.Strings(asked) |
| + if !reflect.DeepEqual(asked, []string{"g1", "g2", "g5"}) { |
| + t.Fatalf("Proven asked %v, want the referenced origins and the admitted generation", proof.calls[0]) |
| + } |
| // A referenced origin the proof refuses still refuses the whole. |
| x.f.Proof = &countingProof{proven: map[string]bool{"g1": true, "g2": true, "g3": true}} |
| if err := x.f.Establish(ctx, servingObserved("g3", "g1:uk:@root:uk", "g2:uk:g1:uk", "g3:uk:g2:uk"), launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrOrigin) { |
| @@ -1471,6 +1478,56 @@ func TestRecoveryRestartTakesTheNewestRecoveryManifest(t *testing.T) { |
| }) |
| } |
| |
| +// The proven set and the carried set order the same way (Astra's finding |
| +// on the Plan 15 switch): generation names and their files prefixes do |
| +// not sort alike ("mv1" before "mv1-old", but "g-mv1-old/" before |
| +// "g-mv1/"), so the prefixes, not the names, are sorted everywhere the |
| +// sets are compared. A prepared manifest for mv1 that also references an |
| +// object under mv1-old (a same-region retained reference) is established |
| +// once; a crash before the local effect leaves the key at sequence 1 |
| +// with the carried set; the genuine replay is accepted and writes nothing. |
| +func TestPreparedReplayOrdersOriginsLikeTheCarriedSet(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + x.writeEnv("K=1\n") |
| + oldObj := movesFilesPrefix(filesLoc, "mv1-old") + "objects/0000000000000001-" + sha([]byte("O")) |
| + x.bucket.Put("data", oldObj, []byte("O")) |
| + newObj := movesFilesPrefix(filesLoc, "mv1") + "objects/0000000000000000-" + sha([]byte("A")) |
| + x.bucket.Put("data", newObj, []byte("A")) |
| + envObj := movesFilesPrefix(filesLoc, "mv1") + "objects/0000000000000000-" + sha([]byte("K=1\n")) |
| + x.bucket.Put("data", envObj, []byte("K=1\n")) |
| + prepared := hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 0, |
| + Entries: map[string]hostfiles.Entry{"a.txt": {Object: newObj, SHA256: sha([]byte("A")), Size: 1, Mode: 0o600, Seq: 0}, "old.txt": {Object: oldObj, SHA256: sha([]byte("O")), Size: 1, Mode: 0o600, Seq: 1}}, |
| + Platform: map[string]hostfiles.Entry{hostfiles.EnvItem: {Object: envObj, SHA256: sha([]byte("K=1\n")), Size: 4, Mode: 0o600, Seq: 0}}, |
| + Origins: []string{movesFilesPrefix(filesLoc, "mv1-old"), movesFilesPrefix(filesLoc, "mv1")}} |
| + raw := mustJSON(t, prepared) |
| + key := hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "mv1")) |
| + version := x.bucket.Put("data", key, raw) |
| + x.journal("mv1", preparedManifest{Key: key, SHA256: sha(raw), Version: version, Op: "mv1", Stamp: 7}) |
| + x.writeEnv("K=1\nCARLOS_INSTANCE_KEY=abc\n") |
| + o := servingObserved("mv1", "cur:ie:@root:ie", "mv1:uk:cur:ie") |
| + o.Claim.Op = hostclaim.Op{Kind: hostclaim.KindLanding, ID: "mv1", Phase: "probing", Exec: "uk-box", Participants: []string{"ie-box", "uk-box"}, Intent: map[string]string{"to": "uk", "source": "ie-box", "target": "uk-box"}} |
| + o = x.observe(o, "mv1", "mv1-old") |
| + in := launchInputs("mv1") |
| + in.Prepared = true |
| + x.workErr, x.workErrAt = errors.New("crash before the local effect"), 3 |
| + if err := x.f.Establish(ctx, o, in, x); err == nil { |
| + t.Fatal("the crash was not reported") |
| + } |
| + x.workErr, x.workErrAt = nil, 0 |
| + m, found := x.manifest("mv1") |
| + if !found || m.Seq != 1 { |
| + t.Fatalf("after the crashed attempt: %+v %v", m, found) |
| + } |
| + seq1 := x.bucket.LatestID("data", key) |
| + if err := x.f.Establish(ctx, o, in, x); err != nil { |
| + t.Fatalf("the genuine replay: %v", err) |
| + } |
| + if x.bucket.LatestID("data", key) != seq1 { |
| + t.Fatal("the replay wrote a new revision") |
| + } |
| +} |
| + |
| // The instance key rule (O1'): the manifest Establish leaves carries the |
| // env file with exactly the bytes on disk when it runs, staged and |
| // committed before it returns, and no adoption item; the local adoption |