| index 4aaea8970..d9d8cc786 100644 |
| --- a/internal/hostrelocate/pin_test.go |
| +++ b/internal/hostrelocate/pin_test.go |
| @@ -7,6 +7,7 @@ import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| + "os" |
| "strings" |
| "testing" |
| "time" |
| @@ -26,13 +27,42 @@ func pinClaim() hostclaim.Observed { |
| } |
| |
| // keyed adds the host's instance.key (H6), as every manifest of a host |
| -// whose database is nonempty must carry (rev 18). |
| +// whose database is nonempty must carry (rev 18), and the platform items |
| +// every pinned manifest carries (withPlatformItems). |
| func keyed(m hostfiles.Manifest) hostfiles.Manifest { |
| sha := strings.Repeat("b", 64) |
| m.Entries["instance.key"] = hostfiles.Entry{Object: pinLocation + "/files/g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 32, Mode: 0o600, Seq: 1} |
| + return withPlatformItems(m) |
| +} |
| + |
| +// withPlatformItems adds the env file and the wake state, as a release |
| +// flush publishes them, where m has none (Task 8c: a pinned manifest |
| +// without either refuses). The pinner reads neither unless the key |
| +// evidence asks for the env. |
| +func withPlatformItems(m hostfiles.Manifest) hostfiles.Manifest { |
| + return withItem(withItem(m, hostfiles.EnvItem, "c"), hostfiles.WakeItem, "d") |
| +} |
| + |
| +func withItem(m hostfiles.Manifest, name, digit string) hostfiles.Manifest { |
| + if _, ok := m.Platform[name]; !ok { |
| + sha := strings.Repeat(digit, 64) |
| + m.Platform[name] = hostfiles.Entry{Object: pinLocation + "/files/g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 10, Mode: 0o600, Seq: 1} |
| + } |
| return m |
| } |
| |
| +// stagePlatformItems stages the env file and the wake state, as a release |
| +// flush does. |
| +func stagePlatformItems(t *testing.T, p *hostfiles.Publisher) { |
| + t.Helper() |
| + if err := p.StagePlatform(context.Background(), hostfiles.EnvItem, []byte("CARLOS_ADMIN_TOKEN=t\n")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := p.StagePlatform(context.Background(), hostfiles.WakeItem, []byte(`{"v":1,"host":"`+host+`","timers":[]}`)); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| func putManifest(t *testing.T, data release.Store, gen string, m hostfiles.Manifest) (string, []byte) { |
| t.Helper() |
| raw, _ := json.Marshal(m) |
| @@ -361,6 +391,7 @@ func TestPinKeepsAFileOriginTheAdmissionListTrimmed(t *testing.T) { |
| if err := p.Stage(ctx, "instance.key", []byte("the host's key"), 0o600, false); err != nil { |
| t.Fatal(err) |
| } |
| + stagePlatformItems(t, p) |
| if err := p.Stage(ctx, "changed.txt", []byte(gen), 0o644, false); err != nil { |
| t.Fatal(err) |
| } |
| @@ -427,10 +458,12 @@ func TestPreparedManifestIsSequenceZeroAndTheSessionPublishesOne(t *testing.T) { |
| } |
| for i := 1; i <= 7; i++ { |
| if i == 7 { |
| - // The seventh revision also publishes the host's key (H6). |
| + // The seventh revision also publishes the host's key (H6), and |
| + // its env file and wake state, as the release flush does. |
| if err := p.Stage(ctx, "instance.key", []byte("the host's key"), 0o600, false); err != nil { |
| t.Fatal(err) |
| } |
| + stagePlatformItems(t, p) |
| } |
| if err := p.Stage(ctx, "a.txt", []byte(fmt.Sprintf("revision %d", i)), 0o644, false); err != nil { |
| t.Fatal(err) |
| @@ -441,7 +474,7 @@ func TestPreparedManifestIsSequenceZeroAndTheSessionPublishesOne(t *testing.T) { |
| } |
| pinner := StorePinner{Claims: claims, Data: data, Release: routeRelease, Bucket: "data", Region: "ie", SelectDB: selectAt(41)} |
| ref, err := pinner.Pin(ctx, pinClaim(), time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)) |
| - if err != nil || ref.FilesSeq != 8 { // seven revisions of a.txt and the key: eight objects |
| + if err != nil || ref.FilesSeq != 10 { // seven revisions of a.txt, the key, the env and the wake state: ten objects |
| t.Fatalf("pin: %+v %v", ref, err) |
| } |
| obj, err := ReadPin(ctx, claims, ref) |
| @@ -522,7 +555,7 @@ func TestStorePinnerRefusesAKeylessManifestForANonemptyDatabase(t *testing.T) { |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| claims, data := release_NewDirStore(t), release_NewDirStore(t) |
| - m := hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 4, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}} |
| + m := withPlatformItems(hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 4, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}) |
| if tc.key { |
| m = keyed(m) |
| } |
| @@ -675,3 +708,45 @@ func TestReadPinNeedsTheEmptyEvidence(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +// Task 8c fix round 1, M6: the install's rule at pin time. A pinned |
| +// manifest without the env file or without the wake state is no evidence |
| +// that the host has neither (platform items have no tombstone, and a |
| +// failover's manifest carries no wake state), so the pin refuses before |
| +// any build: no pin object, no retained copy. A host that never published |
| +// files still pins empty (TestStorePinnerRefusesAnEmptyFilesPinAfterAWriter). |
| +func TestStorePinnerRefusesAManifestWithoutTheEnvOrWakeState(t *testing.T) { |
| + ctx := context.Background() |
| + at := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) |
| + for _, tc := range []struct { |
| + name string |
| + missing string |
| + want error |
| + }{ |
| + {"both", "", nil}, |
| + {"no env file", hostfiles.EnvItem, ErrFilesUnevidenced}, |
| + {"no wake state", hostfiles.WakeItem, ErrFilesUnevidenced}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| + m := keyed(hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 5, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}) |
| + delete(m.Platform, tc.missing) |
| + putManifest(t, data, "g3", m) |
| + o := pinClaim() |
| + p := StorePinner{Claims: claims, Data: data, Release: routeRelease, Bucket: "data", Region: "ie", SelectDB: selectAt(41)} |
| + _, err := p.Pin(ctx, o, at) |
| + if !errors.Is(err, tc.want) { |
| + t.Fatalf("got %v, want %v", err, tc.want) |
| + } |
| + if tc.want == nil { |
| + return |
| + } |
| + if _, _, err := claims.Get(ctx, PinKey(host, "rel-1", o.Claim.Gen)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("a pin object was written: %v", err) |
| + } |
| + if keys, err := claims.List(ctx, ""); err != nil || len(keys) != 0 { |
| + t.Fatalf("the refused pin wrote %v %v", keys, err) |
| + } |
| + }) |
| + } |
| +} |