carlos / platform Public

host: the C8 files-manifest provider, BuildPrepared and Establish (Task 6)

movesFiles is the provider the claim's ClaimFilesManifest contract names
(claim section 6.2a P6-D3, moves spec section 7.6, index C8; Q3 ruling):

- BuildPrepared writes an installation's prepared manifest under
  files/g-<P>/ in the location: every regular file as a create-only
  object at sequence 0 keyed by digest, the env file and the wake state
  as platform items, one create-only sequence-0 manifest; the binding
  {location, P, op, stamp} with the manifest's digest is returned for the
  op journal. A replay reuses objects by digest and the manifest by key.
  buildPreparedManifest runs it over the install's own versioned store
  (createOnlyStore: PutIfAbsent, current reads, never a replace).
- Establish runs on every claim-on serving wake: the baseline is P's own
  prepared manifest for the probe's admission (in.Prepared), no manifest
  for the claim's empty bootstrap (in.Empty, after EmptyAuthority), and
  otherwise the newest valid manifest of the admitted predecessor chain
  read by exact version (regions through regionname.SameRegion; a
  predecessor in another region ends the walk, P6-D3). With no manifest
  anywhere the only empty start is a pin that evidences a host that
  never published files (hostfiles.PinnedFiles.NeverPublished through
  Evidence); otherwise hostfiles.ErrNoEvidence, never an empty-files
  bootstrap (claim section 6.4a). Under the start's work it stages the
  env file's exact bytes on disk, drops the adoption item, commits
  (create-only for a new generation, compare-and-swap on P) and removes
  the local adoption record. It never writes the env file.

Evidence is nil on this branch: region HA's pin reader (ReadPin) is not
on main, so a chain without a manifest refuses every wake here. Nothing
registers the provider yet; the registration seam follows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5f3eaaef60d60577f940228b2d884e6dfbc727a0 parent f8504dc
2 files changed, +996 −0
  • internal/host/movesfiles.go +429 −0
  • internal/host/movesfiles_test.go +567 −0
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go
new file mode 100644
index 000000000..6d1b81b40
--- /dev/null
+++ b/internal/host/movesfiles.go
@@ -0,0 +1,429 @@
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/genselect"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/instancekey"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/regionname"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// movesFiles is the C8 files-manifest provider (claim §6.2a P6-D3, moves
+// spec §7.6, index C8; Q3 ruling). It is two pieces of region HA's files
+// publisher API, on hostfiles:
+//
+// - BuildPrepared, the prepared-manifest build an installation runs in
+// `local` before any session exists: create-only content objects plus
+// one create-only sequence-0 manifest under files/g-<P>/ in the
+// location, bound to {location, P, op, stamp}, replayed by digest and
+// key (hostfiles.Prepared).
+// - Establish, the reader a cold admission establishes its files manifest
+// with: the newest valid manifest of the admitted predecessor chain,
+// read by exact version, copied create-only into the admitted
+// generation with the env file's bytes on disk staged and the adoption
+// item dropped (hostfiles.NewFromCopy; instance key O1').
+//
+// The session-bound publisher (sequence 1 onwards under a session) stays
+// region HA's; moves does not need it, the cut archive is the transfer.
+//
+// Missing or malformed data is an error, never an empty-files bootstrap
+// (claim §6.4a). The two empty starts are the claim's own bootstrap
+// (in.Empty, proved by EmptyAuthority before Establish runs) and a pin
+// that evidences a host that never published files (region HA stage B's
+// rule, hostfiles.PinnedFiles.NeverPublished, read through Evidence).
+// Absence of a manifest, of a pin, or of a reader is never evidence: with
+// Evidence nil, a chain without a manifest refuses every wake.
+type movesFiles struct {
+ // Files is this box's data bucket as a release.Store: current reads
+ // with the object's etag, create-only and compare-and-swap writes
+ // (the S3 store's Put with If-None-Match and If-Match). Every write
+ // this provider makes goes through it.
+ Files release.Store
+ // Data is the same bucket read by exact version (the claim's
+ // hostbox.RequestStore in production): the chain's manifests are read
+ // at the version the listing names, never by key.
+ Data genselect.Reader
+ // Bucket is the data bucket's name, HomeRegion the fleet's signed home
+ // region, Region this box's engine region (park.PlacementRegion of its
+ // placement) and Box its id. Every region comparison goes through
+ // regionname.SameRegion with HomeRegion as the alias.
+ Bucket, HomeRegion, Region, Box string
+ // FilesDir is the host's files directory and env file on this box
+ // (moveCfg.FilesDir).
+ FilesDir func(host string) (dir, envPath string)
+ // Evidence reads the claim pin's files evidence for a host, when the
+ // claim carries a pin: region HA's ReadPin over the op journal's pin
+ // reference. false means the claim carries no pin. nil means no pin
+ // reader is wired on this box, which reads as no evidence.
+ Evidence func(ctx context.Context, o hostclaim.Observed) (hostfiles.PinnedFiles, bool, error)
+}
+
+var _ ClaimFilesManifest = (*movesFiles)(nil)
+
+// preparedManifest is the files_manifest journal step's value: the
+// binding {location, P, op, stamp} of the prepared manifest an install
+// wrote, with the digest of its exact bytes. Key is
+// <location>/files/g-<P>/manifest.json, which names the location and P.
+type preparedManifest struct {
+ Key string `json:"key"`
+ SHA256 string `json:"sha256"`
+ Op string `json:"op"`
+ Stamp uint64 `json:"stamp"`
+}
+
+// movesFilesPrefix is a generation's files prefix in its location,
+// <location>/files/g-<generation>/, the rule pinFilesLiveSet and the park
+// view's files live set share (spec §7.6).
+func movesFilesPrefix(location, generation string) string {
+ return strings.TrimSuffix(location, "/") + "/files/g-" + generation + "/"
+}
+
+// movesFilesGeneration accepts a generation name as the claim does
+// (claimFreshGeneration's shape), never the origin or a path.
+func movesFilesGeneration(g string) bool {
+ return g != "" && g != "." && g != ".." && g != hostclaim.RootGeneration && len(g) <= 64 && !strings.ContainsAny(g, "/\\\x00\r\n")
+}
+
+// createOnlyStore is a release.Store over a versioned store for one
+// bucket: current reads by the listing's latest version, create-only
+// writes (PutIfAbsent), never a replace. BuildPrepared writes through it,
+// so an install needs only the box's versioned store.
+type createOnlyStore struct {
+ data lease.VersionedStore
+ bucket string
+}
+
+func (s createOnlyStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ vs, err := s.data.ListVersions(ctx, s.bucket, key)
+ if err != nil {
+ return nil, "", err
+ }
+ for _, v := range vs {
+ if v.Key == key && v.IsLatest {
+ if v.DeleteMarker || v.VersionID == "" || v.VersionID == "null" {
+ return nil, "", fmt.Errorf("host: %s is not a readable version: %w", key, os.ErrNotExist)
+ }
+ body, err := s.data.GetVersion(ctx, s.bucket, key, v.VersionID)
+ return body, v.VersionID, err
+ }
+ }
+ return nil, "", fmt.Errorf("host: %s: %w", key, os.ErrNotExist)
+}
+
+func (s createOnlyStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if ifMatch != "" {
+ return "", errors.New("host: the prepared manifest build never replaces an object")
+ }
+ id, err := s.data.PutIfAbsent(ctx, s.bucket, key, body)
+ if errors.Is(err, lease.ErrExists) {
+ return "", fmt.Errorf("%w: %s", release.ErrCASConflict, key)
+ }
+ return id, err
+}
+
+func (createOnlyStore) List(context.Context, string) ([]string, error) {
+ return nil, errors.New("host: the prepared manifest build does not list")
+}
+func (createOnlyStore) ListDirs(context.Context, string) ([]string, error) {
+ return nil, errors.New("host: the prepared manifest build does not list")
+}
+func (createOnlyStore) Delete(context.Context, string) error {
+ return errors.New("host: the prepared manifest build never deletes")
+}
+
+// buildPreparedManifest is BuildPrepared over a versioned store: the
+// install's own (ir.versioned), which needs no compare-and-swap.
+func buildPreparedManifest(ctx context.Context, data lease.VersionedStore, bucket, location, generation, op string, stamp uint64, dir, envPath string, wake hostfiles.WakeState) (preparedManifest, error) {
+ if data == nil || bucket == "" {
+ return preparedManifest{}, hostwrite.ErrUnavailable
+ }
+ return buildPrepared(ctx, createOnlyStore{data: data, bucket: bucket}, location, generation, op, stamp, dir, envPath, wake)
+}
+
+// BuildPrepared writes the prepared manifest of generation (the
+// installation's prepared destination generation P, g-<move id> or
+// g-<rollback id>, never holder.generation: R19-1) under location: every
+// regular file under dir as a create-only object at sequence 0 (a file
+// over hostfiles.MaxFileBytes is recorded as skipped; anything not a
+// regular file is left out, as SyncDir leaves it), the env file at envPath
+// as the EnvItem when it exists, the wake state as the WakeItem, then the
+// one create-only sequence-0 manifest. It returns the binding for the op
+// journal: the key, the manifest's digest, op and stamp. A replay reuses
+// every object by digest and reads the manifest back by key; a different
+// manifest at the key is hostfiles.ErrManifestExists.
+//
+// The caller runs it as op.exec in a phase that permits installation,
+// after the install's own claim sync and stamp capture (local_).
+func (f *movesFiles) BuildPrepared(ctx context.Context, location, generation, op string, stamp uint64, dir, envPath string, wake hostfiles.WakeState) (preparedManifest, error) {
+ if f == nil || f.Files == nil {
+ return preparedManifest{}, hostwrite.ErrUnavailable
+ }
+ return buildPrepared(ctx, f.Files, location, generation, op, stamp, dir, envPath, wake)
+}
+
+func buildPrepared(ctx context.Context, s release.Store, location, generation, op string, stamp uint64, dir, envPath string, wake hostfiles.WakeState) (preparedManifest, error) {
+ if ctx == nil || location == "" || !movesFilesGeneration(generation) || op == "" || stamp == 0 || dir == "" || envPath == "" {
+ return preparedManifest{}, fmt.Errorf("%w: a prepared manifest names its location, generation, op, stamp, files directory and env path", hostwrite.ErrUnavailable)
+ }
+ prefix := movesFilesPrefix(location, generation)
+ p, err := hostfiles.NewPrepared(s, prefix)
+ if err != nil {
+ return preparedManifest{}, err
+ }
+ err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ if path == dir && errors.Is(err, fs.ErrNotExist) {
+ return fs.SkipAll // no files directory: an empty image
+ }
+ return err
+ }
+ if path == dir || d.IsDir() || !d.Type().IsRegular() {
+ return nil
+ }
+ rel, err := filepath.Rel(dir, path)
+ if err != nil {
+ return err
+ }
+ info, err := d.Info()
+ if err != nil {
+ return err
+ }
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ return p.Stage(ctx, filepath.ToSlash(rel), body, uint32(info.Mode().Perm()))
+ })
+ if err != nil {
+ return preparedManifest{}, fmt.Errorf("prepared manifest of %s: %w", dir, err)
+ }
+ env, err := os.ReadFile(envPath)
+ switch {
+ case err == nil:
+ if err := p.StagePlatform(ctx, hostfiles.EnvItem, env); err != nil {
+ return preparedManifest{}, err
+ }
+ case !errors.Is(err, fs.ErrNotExist):
+ return preparedManifest{}, err
+ }
+ body, err := json.Marshal(wake)
+ if err != nil {
+ return preparedManifest{}, fmt.Errorf("marshal wake state: %w", err)
+ }
+ if err := p.StagePlatform(ctx, hostfiles.WakeItem, body); err != nil {
+ return preparedManifest{}, err
+ }
+ _, raw, err := p.Commit(ctx)
+ if err != nil {
+ return preparedManifest{}, err
+ }
+ sum := sha256.Sum256(raw)
+ return preparedManifest{Key: hostfiles.ManifestKey(prefix), SHA256: hex.EncodeToString(sum[:]), Op: op, Stamp: stamp}, nil
+}
+
+// movesFilesChain is the admitted predecessor chain whose manifests are in
+// this region's bucket, newest first: the holder's generation, then each
+// cold admission's predecessor while it is a named generation in this
+// region (regionname.SameRegion against the home alias). A predecessor in
+// another region ends the walk: its objects are not here, and a target's
+// manifest never references a source-region object (P6-D3). Warm
+// admissions keep their generation and are passed over.
+func movesFilesChain(h *hostclaim.Holder, home, region string) []string {
+ if h == nil || !movesFilesGeneration(h.Generation) {
+ return nil
+ }
+ cur := h.Generation
+ chain := []string{cur}
+ seen := map[string]bool{cur: true}
+ for i := len(h.Admissions) - 1; i >= 0; i-- {
+ a := h.Admissions[i]
+ if a.Generation != cur || a.Mode != "cold" {
+ continue
+ }
+ next := a.Predecessor
+ if !movesFilesGeneration(next) || seen[next] || !regionname.SameRegion(home, a.PredecessorRegion, region) {
+ break
+ }
+ chain = append(chain, next)
+ seen[next] = true
+ cur = next
+ }
+ return chain
+}
+
+// manifestVersion is the current version of a manifest key as the listing
+// names it: "" when the key has no current object. A delete marker or an
+// unversioned current object is an error.
+func (f *movesFiles) manifestVersion(ctx context.Context, key string) (string, error) {
+ vs, err := f.Data.ListVersions(ctx, f.Bucket, key)
+ if err != nil {
+ return "", err
+ }
+ for _, v := range vs {
+ if v.Key != key || !v.IsLatest {
+ continue
+ }
+ if v.DeleteMarker || v.VersionID == "" || v.VersionID == "null" {
+ return "", fmt.Errorf("host: the files manifest %s is not a readable version", key)
+ }
+ return v.VersionID, nil
+ }
+ return "", nil
+}
+
+// newestManifest is the newest valid manifest of the chain, read by exact
+// version, with the chain's prefixes as the origins its entries may name.
+// A generation without a manifest is passed over; the first manifest found
+// is validated and is the baseline, malformed or not (a malformed newest
+// manifest refuses, never an older one in its place). found is false when
+// no generation of the chain has a manifest.
+func (f *movesFiles) newestManifest(ctx context.Context, location string, chain []string) (hostfiles.Manifest, []string, bool, error) {
+ origins := make([]string, 0, len(chain))
+ for _, g := range chain {
+ origins = append(origins, movesFilesPrefix(location, g))
+ }
+ for _, prefix := range origins {
+ key := hostfiles.ManifestKey(prefix)
+ version, err := f.manifestVersion(ctx, key)
+ if err != nil {
+ return hostfiles.Manifest{}, nil, false, err
+ }
+ if version == "" {
+ continue
+ }
+ m, found, err := hostfiles.Load(ctx, versionStore{data: f.Data, bucket: f.Bucket, key: key, version: version}, prefix)
+ if err != nil {
+ return hostfiles.Manifest{}, nil, false, err
+ }
+ if !found {
+ return hostfiles.Manifest{}, nil, false, fmt.Errorf("host: the files manifest %s@%s is not readable", key, version)
+ }
+ if err := hostfiles.ValidateManifest(m, origins); err != nil {
+ return hostfiles.Manifest{}, nil, false, fmt.Errorf("host: the files manifest %s@%s: %w", key, version, err)
+ }
+ return m, origins, true, nil
+ }
+ return hostfiles.Manifest{}, nil, false, nil
+}
+
+// emptyManifest is the base of an empty start.
+func emptyManifest() hostfiles.Manifest {
+ return hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}
+}
+
+// Establish is ClaimFilesManifest.Establish. It runs once per start under
+// the start's captured selection, after the launch source has minted the
+// instance key into <host>.env and before the app executes.
+//
+// The baseline:
+// - in.Prepared (the probe's admission opening P): P's own prepared
+// manifest, read current with its etag so the env revision replaces
+// exactly it (a new version; the sequence-0 version stays), validated
+// against P's prefix alone (destination-local objects, P6-D3);
+// - in.Empty (the claim's empty bootstrap, EmptyAuthority already run):
+// no manifest;
+// - otherwise the newest valid manifest of the chain, read by exact
+// version; with none, the pin's never-published evidence or an error
+// (hostfiles.ErrNoEvidence), never an empty bootstrap.
+//
+// Then, under work: the env file's exact bytes on disk are staged as the
+// EnvItem, the adoption item is dropped, the revision is committed
+// (create-only for a new generation; compare-and-swap on P), and the local
+// adoption record is removed. Establish never writes the env file. An
+// error, a bucket that cannot be reached included, fails the wake.
+func (f *movesFiles) Establish(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
+ if f == nil || f.Files == nil || f.Data == nil || f.Bucket == "" || f.Region == "" || f.Box == "" || f.FilesDir == nil || ctx == nil || work == nil {
+ return hostwrite.ErrUnavailable
+ }
+ if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Host == "" || in.Identity.Config == "" || in.Identity.Release == "" {
+ return hostbox.ErrChanged
+ }
+ h := o.Claim.Holder
+ gen := in.Admission.NewGeneration
+ if in.Admission.Box != f.Box || !regionname.SameRegion(f.HomeRegion, in.Admission.Region, f.Region) || !movesFilesGeneration(gen) || h.Location == "" {
+ return hostbox.ErrChanged
+ }
+ _, envPath := f.FilesDir(o.Host)
+ env, err := os.ReadFile(envPath)
+ if err != nil {
+ return fmt.Errorf("host: the env file the files manifest must carry: %w", err)
+ }
+ prefix := movesFilesPrefix(h.Location, gen)
+ var p *hostfiles.Publisher
+ switch {
+ case in.Prepared:
+ p, err = hostfiles.Open(ctx, f.Files, prefix)
+ if err != nil {
+ return err
+ }
+ m, found, err := hostfiles.Load(ctx, f.Files, prefix)
+ if err != nil {
+ return err
+ }
+ if !found {
+ return fmt.Errorf("host: the prepared files manifest %s is missing: %w", hostfiles.ManifestKey(prefix), os.ErrNotExist)
+ }
+ if err := hostfiles.ValidateManifest(m, []string{prefix}); err != nil {
+ return fmt.Errorf("host: the prepared files manifest %s: %w", hostfiles.ManifestKey(prefix), err)
+ }
+ case in.Empty:
+ p, err = hostfiles.NewFromCopy(f.Files, prefix, emptyManifest(), nil)
+ if err != nil {
+ return err
+ }
+ default:
+ base, origins, found, err := f.newestManifest(ctx, h.Location, movesFilesChain(h, f.HomeRegion, f.Region))
+ if err != nil {
+ return err
+ }
+ if !found {
+ never := false
+ if f.Evidence != nil {
+ pin, ok, err := f.Evidence(ctx, o)
+ if err != nil {
+ return fmt.Errorf("host: the claim pin's files evidence: %w", err)
+ }
+ never = ok && pin.NeverPublished()
+ }
+ if !never {
+ return fmt.Errorf("%w: no files manifest in the admitted predecessor chain of %s (%s)", hostfiles.ErrNoEvidence, o.Host, h.Generation)
+ }
+ base, origins = emptyManifest(), nil
+ }
+ p, err = hostfiles.NewFromCopy(f.Files, prefix, base, origins)
+ if err != nil {
+ return err
+ }
+ }
+ return work.With(ctx, in.Identity, func() error {
+ if err := p.StagePlatform(ctx, hostfiles.EnvItem, env); err != nil {
+ return err
+ }
+ if err := p.StagePlatformDelete(instancekey.AdoptionItem); err != nil {
+ return err
+ }
+ if _, err := p.Commit(ctx); err != nil {
+ return err
+ }
+ adoption := filepath.Join(filepath.Dir(envPath), o.Host+instancekey.AdoptionSuffix)
+ if err := os.Remove(adoption); err != nil && !errors.Is(err, fs.ErrNotExist) {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/internal/host/movesfiles_test.go b/internal/host/movesfiles_test.go
new file mode 100644
index 000000000..712f8f407
--- /dev/null
+++ b/internal/host/movesfiles_test.go
@@ -0,0 +1,567 @@
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/instancekey"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// casBucketStore is a release.Store over one leasetest bucket: current
+// reads with the latest version id as the etag, create-only and
+// compare-and-swap writes. It stands in for the S3 store the production
+// provider writes the data bucket through.
+type casBucketStore struct {
+ b *leasetest.VersionedBucket
+ bucket string
+}
+
+func (s casBucketStore) Get(_ context.Context, key string) ([]byte, string, error) {
+ body, ok := s.b.Current(s.bucket, key)
+ if !ok {
+ return nil, "", os.ErrNotExist
+ }
+ return body, s.b.LatestID(s.bucket, key), nil
+}
+
+func (s casBucketStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if ifMatch == "" {
+ id, err := s.b.PutIfAbsent(ctx, s.bucket, key, body)
+ if errors.Is(err, lease.ErrExists) {
+ return "", release.ErrCASConflict
+ }
+ return id, err
+ }
+ if s.b.LatestID(s.bucket, key) != ifMatch {
+ return "", release.ErrCASConflict
+ }
+ return s.b.Put(s.bucket, key, body), nil
+}
+
+func (s casBucketStore) List(ctx context.Context, prefix string) ([]string, error) {
+ vs, err := s.b.ListVersions(ctx, s.bucket, prefix)
+ if err != nil {
+ return nil, err
+ }
+ var out []string
+ for _, v := range vs {
+ if v.IsLatest && !v.DeleteMarker {
+ out = append(out, v.Key)
+ }
+ }
+ return out, nil
+}
+
+func (casBucketStore) ListDirs(context.Context, string) ([]string, error) {
+ return nil, errors.New("test: not implemented")
+}
+func (casBucketStore) Delete(context.Context, string) error {
+ return errors.New("test: not implemented")
+}
+
+// recordingReader records every exact-version read.
+type recordingReader struct {
+ lease.VersionedStore
+ mu sync.Mutex
+ reads []string // key@version
+}
+
+func (r *recordingReader) GetVersion(ctx context.Context, bucket, key, version string) ([]byte, error) {
+ r.mu.Lock()
+ r.reads = append(r.reads, key+"@"+version)
+ r.mu.Unlock()
+ return r.VersionedStore.GetVersion(ctx, bucket, key, version)
+}
+
+type filesFixture struct {
+ t *testing.T
+ bucket *leasetest.VersionedBucket
+ data *recordingReader
+ files casBucketStore
+ dir string
+ f *movesFiles
+ // work records whether the start's work ran and can refuse.
+ workRan int
+ workErr error
+}
+
+const (
+ filesHost = "shop.example.com"
+ filesLoc = "instances/" + filesHost + "/" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+)
+
+func (x *filesFixture) With(_ context.Context, _ hostwrite.Identity, fn func() error) error {
+ x.workRan++
+ if x.workErr != nil {
+ return x.workErr
+ }
+ return fn()
+}
+
+func newFilesFixture(t *testing.T) *filesFixture {
+ t.Helper()
+ b := leasetest.NewVersionedBucket()
+ b.AddBucket("data")
+ x := &filesFixture{t: t, bucket: b, data: &recordingReader{VersionedStore: b}, files: casBucketStore{b: b, bucket: "data"}, dir: t.TempDir()}
+ x.f = &movesFiles{Files: x.files, Data: x.data, Bucket: "data", HomeRegion: "ie", Region: "uk", Box: "uk-box",
+ FilesDir: func(host string) (string, string) {
+ return filepath.Join(x.dir, host), filepath.Join(x.dir, host+".env")
+ }}
+ return x
+}
+
+func (x *filesFixture) envPath() string { return filepath.Join(x.dir, filesHost+".env") }
+
+func (x *filesFixture) writeEnv(body string) {
+ x.t.Helper()
+ if err := os.WriteFile(x.envPath(), []byte(body), 0o600); err != nil {
+ x.t.Fatal(err)
+ }
+}
+
+// publish writes a manifest revision for generation gen through the
+// ordinary publisher (a predecessor's session output) and returns it.
+func (x *filesFixture) publish(gen string, stage func(p *hostfiles.Publisher)) hostfiles.Manifest {
+ x.t.Helper()
+ p, err := hostfiles.Open(context.Background(), x.files, movesFilesPrefix(filesLoc, gen))
+ if err != nil {
+ x.t.Fatal(err)
+ }
+ stage(p)
+ if _, err := p.Commit(context.Background()); err != nil {
+ x.t.Fatal(err)
+ }
+ m, found, err := hostfiles.Load(context.Background(), x.files, movesFilesPrefix(filesLoc, gen))
+ if err != nil || !found {
+ x.t.Fatal(err, found)
+ }
+ return m
+}
+
+func (x *filesFixture) manifest(gen string) (hostfiles.Manifest, bool) {
+ x.t.Helper()
+ m, found, err := hostfiles.Load(context.Background(), x.files, movesFilesPrefix(filesLoc, gen))
+ if err != nil {
+ x.t.Fatal(err)
+ }
+ return m, found
+}
+
+// serving is a serving claim on uk-box whose holder is at gen with the
+// cold admission chain given, newest last: each entry is
+// "<generation>:<region>:<predecessor>:<predecessor region>".
+func servingObserved(gen string, chain ...string) hostclaim.Observed {
+ h := &hostclaim.Holder{Family: "instance", Key: "k", Account: "bab", App: "jam", Incarnation: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", Location: filesLoc, Generation: gen, Session: 3, WriterBox: "uk-box", Stamp: 7}
+ for _, c := range chain {
+ p := strings.Split(c, ":")
+ h.Admissions = append(h.Admissions, hostclaim.AdmissionRecord{Generation: p[0], Region: p[1], Predecessor: p[2], PredecessorRegion: p[3], Mode: "cold", Session: 3, Box: "uk-box"})
+ }
+ return hostclaim.Observed{Host: filesHost, ETag: "e1", Claim: &hostclaim.Claim{V: 1, Host: filesHost, Gen: 9, Holder: h, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "uk-box"}}}
+}
+
+func launchInputs(newGen string) ClaimLaunchInputs {
+ return ClaimLaunchInputs{Identity: hostwrite.Identity{Config: "sha256:c", Release: "sha256:r"}, Admission: hostclaim.Admission{Box: "uk-box", Mode: "cold", NewGeneration: newGen, Region: "uk"}}
+}
+
+func sha(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
+
+// The prepared manifest (spec §7.6, R19-1; claim rev 6.30 R18-1): one
+// create-only manifest at sequence 0 under files/g-<P>/ in the location,
+// every content object create-only at sequence 0 under the same prefix,
+// the env file and the wake state as platform items, and the binding
+// {location, P, op, stamp} returned for the op journal. A different build
+// of the same generation is refused and leaves the manifest as it was.
+func TestPreparedManifestIsCreateOnlySequenceZeroAndBound(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ dir := filepath.Join(x.dir, filesHost)
+ for rel, body := range map[string]string{"a.txt": "A", "uploads/b.bin": "BB"} {
+ if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, rel)), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, rel), []byte(body), 0o640); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.Symlink("a.txt", filepath.Join(dir, "link")); err != nil {
+ t.Fatal(err)
+ }
+ x.writeEnv("K=1\n")
+ wake := hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{{Name: "tick", Kind: "cron", Spec: "* * * * *", NextAt: 5}}, NextDue: 5, Replication: "hot"}
+ pm, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prefix := movesFilesPrefix(filesLoc, "mv1")
+ if pm.Key != hostfiles.ManifestKey(prefix) || pm.Op != "mv1" || pm.Stamp != 7 || len(pm.SHA256) != 64 {
+ t.Fatalf("binding: %+v", pm)
+ }
+ raw, ok := x.bucket.Current("data", pm.Key)
+ if !ok || sha(raw) != pm.SHA256 {
+ t.Fatalf("the manifest at %s is not the journalled bytes: %v", pm.Key, ok)
+ }
+ m, found := x.manifest("mv1")
+ if !found || m.Seq != 0 {
+ t.Fatalf("manifest found %v seq %d", found, m.Seq)
+ }
+ if err := hostfiles.ValidateManifest(m, []string{prefix}); err != nil {
+ t.Fatal(err)
+ }
+ if len(m.Entries) != 2 || m.Entries["a.txt"].Mode != 0o640 || m.Entries["uploads/b.bin"].SHA256 != sha([]byte("BB")) {
+ t.Fatalf("entries: %+v", m.Entries)
+ }
+ for _, e := range m.Entries {
+ if e.Seq != 0 || !strings.HasPrefix(e.Object, prefix+"objects/0000000000000000-") {
+ t.Fatalf("entry object: %+v", e)
+ }
+ }
+ if e := m.Platform[hostfiles.EnvItem]; e.SHA256 != sha([]byte("K=1\n")) || e.Seq != 0 {
+ t.Fatalf("env item: %+v", e)
+ }
+ ws, found, err := hostfiles.LoadWake(ctx, x.files, prefix, m, nil)
+ if err != nil || !found || len(ws.Timers) != 1 || ws.NextDue != 5 || ws.Replication != "hot" {
+ t.Fatalf("wake: %+v %v %v", ws, found, err)
+ }
+ // A different build of the same generation: refused, the manifest kept.
+ if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("changed"), 0o640); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake); !errors.Is(err, hostfiles.ErrManifestExists) {
+ t.Fatalf("second build: %v", err)
+ }
+ if again, _ := x.bucket.Current("data", pm.Key); sha(again) != pm.SHA256 {
+ t.Fatal("the prepared manifest was replaced")
+ }
+}
+
+// A replay of the build (a crash after the objects, before or after the
+// manifest) reuses every object it finds by digest and the manifest by
+// key: the same binding, one version of each object, no new object.
+func TestPreparedManifestReplayReusesObjects(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ dir := filepath.Join(x.dir, filesHost)
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("A"), 0o640); err != nil {
+ t.Fatal(err)
+ }
+ x.writeEnv("K=1\n")
+ wake := hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{}}
+ first, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake)
+ if err != nil {
+ t.Fatal(err)
+ }
+ second, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake)
+ if err != nil {
+ t.Fatalf("replay: %v", err)
+ }
+ if first != second {
+ t.Fatalf("replay binding differs: %+v %+v", first, second)
+ }
+ vs, err := x.bucket.ListVersions(ctx, "data", movesFilesPrefix(filesLoc, "mv1"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(vs) != 4 { // three objects (a.txt, env, wake) and the manifest, one version each
+ t.Fatalf("%d versions under the prepared prefix after a replay: %+v", len(vs), vs)
+ }
+}
+
+// A cold admission establishes its generation's files manifest from the
+// newest valid manifest of its admitted predecessor chain, read by the
+// exact version the listing names: here g2 (the predecessor) never
+// published a manifest, so g1's current revision is the baseline. The copy
+// keeps every object origin-qualified under g1, continues the sequence,
+// and is create-only under g3.
+func TestEstablishCopiesTheNewestValidManifestByExactVersion(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ x.writeEnv("K=1\n")
+ x.publish("g1", func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "old.txt", []byte("old"), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ })
+ g1 := x.publish("g1", func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ if err := p.StagePlatform(ctx, hostfiles.EnvItem, []byte("K=1\n")); err != nil {
+ t.Fatal(err)
+ }
+ })
+ if g1.Seq != 3 {
+ t.Fatalf("g1 at seq %d", g1.Seq)
+ }
+ o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk")
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil {
+ t.Fatal(err)
+ }
+ if x.workRan != 1 {
+ t.Fatalf("work ran %d times", x.workRan)
+ }
+ m, found := x.manifest("g3")
+ if !found {
+ t.Fatal("no manifest under g3")
+ }
+ if m.Seq != 3 || len(m.Entries) != 2 || m.Entries["a.txt"].Object != g1.Entries["a.txt"].Object || m.Entries["old.txt"].Object != g1.Entries["old.txt"].Object {
+ t.Fatalf("copied manifest: %+v", m)
+ }
+ if e := m.Platform[hostfiles.EnvItem]; e.Object != g1.Platform[hostfiles.EnvItem].Object {
+ t.Fatalf("the unchanged env file took a new object: %+v", e)
+ }
+ want := hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g1")) + "@" + x.bucket.LatestID("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g1")))
+ found = false
+ for _, r := range x.data.reads {
+ found = found || r == want
+ }
+ if !found {
+ t.Fatalf("g1's manifest was not read at its exact latest version %s: %v", want, x.data.reads)
+ }
+ if _, has := x.manifest("g2"); has {
+ t.Fatal("g2 gained a manifest")
+ }
+ // A repeat (a retry before the app executed) reads its own bytes back.
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil {
+ t.Fatalf("repeat: %v", err)
+ }
+ if again, _ := x.manifest("g3"); again.Seq != 3 {
+ t.Fatalf("the repeat replaced the manifest: seq %d", again.Seq)
+ }
+}
+
+// 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
+// record is removed under the start's work. Establish never writes the
+// env file. A work that refuses leaves everything as it was.
+func TestEstablishCarriesTheEnvBytesAndDropsTheAdoptionItem(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ x.publish("g1", func(p *hostfiles.Publisher) {
+ if err := p.StagePlatform(ctx, hostfiles.EnvItem, []byte("K=old\n")); err != nil {
+ t.Fatal(err)
+ }
+ if err := p.StagePlatform(ctx, instancekey.AdoptionItem, []byte(`{"v":2}`)); err != nil {
+ t.Fatal(err)
+ }
+ })
+ x.writeEnv("K=new\nCARLOS_INSTANCE_KEY=abc\n")
+ adoption := filepath.Join(x.dir, filesHost+instancekey.AdoptionSuffix)
+ if err := os.WriteFile(adoption, []byte(`{"v":2}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ o := servingObserved("g1", "g1:uk:@root:uk")
+ x.workErr = hostbox.ErrChanged
+ if err := x.f.Establish(ctx, o, launchInputs("g2"), x); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatalf("refused work: %v", err)
+ }
+ if _, found := x.manifest("g2"); found {
+ t.Fatal("a refused work wrote the manifest")
+ }
+ if _, err := os.Stat(adoption); err != nil {
+ t.Fatal("a refused work removed the adoption record")
+ }
+ x.workErr = nil
+ if err := x.f.Establish(ctx, o, launchInputs("g2"), x); err != nil {
+ t.Fatal(err)
+ }
+ m, found := x.manifest("g2")
+ if !found {
+ t.Fatal("no manifest under g2")
+ }
+ e := m.Platform[hostfiles.EnvItem]
+ if e.SHA256 != sha([]byte("K=new\nCARLOS_INSTANCE_KEY=abc\n")) || !strings.HasPrefix(e.Object, movesFilesPrefix(filesLoc, "g2")+"objects/") {
+ t.Fatalf("env item: %+v", e)
+ }
+ got, err := hostfiles.ReadPlatform(ctx, x.files, movesFilesPrefix(filesLoc, "g2"), m, hostfiles.EnvItem, nil)
+ if err != nil || string(got) != "K=new\nCARLOS_INSTANCE_KEY=abc\n" {
+ t.Fatalf("env bytes: %q %v", got, err)
+ }
+ if _, has := m.Platform[instancekey.AdoptionItem]; has {
+ t.Fatal("the adoption item was carried into the new generation")
+ }
+ if _, err := os.Stat(adoption); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("the local adoption record remains: %v", err)
+ }
+ if disk, _ := os.ReadFile(x.envPath()); string(disk) != "K=new\nCARLOS_INSTANCE_KEY=abc\n" {
+ t.Fatalf("Establish wrote the env file: %q", disk)
+ }
+ // Without an env file on disk there is nothing to carry: an error, and
+ // no manifest.
+ if err := os.Remove(x.envPath()); err != nil {
+ t.Fatal(err)
+ }
+ if err := x.f.Establish(ctx, servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk"), launchInputs("g3"), x); err == nil {
+ t.Fatal("established without an env file")
+ }
+ if _, found := x.manifest("g3"); found {
+ t.Fatal("a manifest was written without the env file")
+ }
+}
+
+// Missing or malformed data is an error, never an empty-files bootstrap
+// (claim §6.4a): a chain with no manifest anywhere, with no pin evidence
+// or with evidence of published files, refuses; a malformed newest
+// manifest refuses rather than falling back to an older one; a chain that
+// leaves this region is not followed into another region's bucket. Nothing
+// is written in any of these.
+func TestEstablishNeverBootstrapsEmpty(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ x.writeEnv("K=1\n")
+ o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk")
+ // No manifest anywhere, no evidence.
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("no manifest, no pin: %v", err)
+ }
+ // A pin that evidences published files is not evidence of none.
+ x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) {
+ return hostfiles.PinnedFiles{SHA256: "ab", Retained: "retained/ab"}, true, nil
+ }
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("no manifest, published pin: %v", err)
+ }
+ // A pin reader that fails is an error, not absence.
+ x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) {
+ return hostfiles.PinnedFiles{}, false, errors.New("pin unreadable")
+ }
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err == nil || errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("unreadable pin: %v", err)
+ }
+ x.f.Evidence = nil
+ // The newest manifest is malformed: refused, not skipped.
+ x.publish("g1", func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ })
+ x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g2")), []byte(`{"v":2,"seq":1,"entries":{"x":{"object":"elsewhere/objects/0000000000000001-`+sha(nil)+`","sha256":"`+sha(nil)+`","seq":1}},"platform":{}}`))
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrOrigin) {
+ t.Fatalf("malformed newest manifest: %v", err)
+ }
+ // The predecessor lives in another region: its manifest is not in this
+ // bucket and the chain is not followed there.
+ far := servingObserved("g5", "g5:uk:g1:ie")
+ if err := x.f.Establish(ctx, far, launchInputs("g6"), x); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("predecessor in another region: %v", err)
+ }
+ for _, gen := range []string{"g3", "g6"} {
+ if _, found := x.manifest(gen); found {
+ t.Fatalf("a manifest was written under %s", gen)
+ }
+ }
+ if x.workRan != 0 {
+ t.Fatalf("work ran %d times with nothing to establish", x.workRan)
+ }
+}
+
+// The one empty start besides the claim's own bootstrap (in.Empty after
+// EmptyAuthority): the pin evidences a host that never published files
+// (region HA stage B's rule, hostfiles.PinnedFiles). Then the generation
+// starts from no manifest, carrying only the env file. The claim's empty
+// bootstrap reads no chain at all.
+func TestEstablishHostThatNeverPublishedFiles(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ x.writeEnv("K=1\n")
+ x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) {
+ return hostfiles.PinnedFiles{}, true, nil
+ }
+ o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk")
+ if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil {
+ t.Fatal(err)
+ }
+ m, found := x.manifest("g3")
+ if !found || m.Seq != 1 || len(m.Entries) != 0 || len(m.Platform) != 1 || m.Platform[hostfiles.EnvItem].SHA256 != sha([]byte("K=1\n")) {
+ t.Fatalf("never-published bootstrap: %+v %v", m, found)
+ }
+ // The claim's own empty bootstrap: a created incarnation before its
+ // first writer, admitted after EmptyAuthority.
+ x.f.Evidence = nil
+ empty := servingObserved("")
+ empty.Claim.Holder.Session, empty.Claim.Holder.WriterBox = 0, ""
+ in := launchInputs("g1")
+ in.Empty = true
+ if err := x.f.Establish(ctx, empty, in, x); err != nil {
+ t.Fatal(err)
+ }
+ if m, found := x.manifest("g1"); !found || m.Seq != 1 || len(m.Entries) != 0 || len(x.data.reads) != 0 {
+ t.Fatalf("empty bootstrap: %+v %v reads %v", m, found, x.data.reads)
+ }
+}
+
+// The probe's admission opens the prepared generation itself (in.Prepared,
+// NewGeneration = P): its baseline is P's own prepared manifest at
+// sequence 0, which must name destination-local objects only; the env file
+// on disk (now carrying the minted key) is staged over it at sequence 1,
+// a new version of the same key, and the sequence-0 version stays
+// readable by exact version. A prepared manifest naming another
+// generation's objects refuses.
+func TestEstablishOnThePreparedGenerationContinuesFromSequenceZero(t *testing.T) {
+ x := newFilesFixture(t)
+ ctx := context.Background()
+ dir := filepath.Join(x.dir, filesHost)
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("A"), 0o640); err != nil {
+ t.Fatal(err)
+ }
+ x.writeEnv("K=1\n")
+ pm, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ seq0 := x.bucket.LatestID("data", pm.Key)
+ x.writeEnv("K=1\nCARLOS_INSTANCE_KEY=abc\n")
+ o := servingObserved("cur", "cur:ie:@root:ie")
+ in := launchInputs("mv1")
+ in.Prepared = true
+ if err := x.f.Establish(ctx, o, in, x); err != nil {
+ t.Fatal(err)
+ }
+ m, found := x.manifest("mv1")
+ if !found || m.Seq != 1 || m.Entries["a.txt"].Seq != 0 || m.Platform[hostfiles.EnvItem].SHA256 != sha([]byte("K=1\nCARLOS_INSTANCE_KEY=abc\n")) {
+ t.Fatalf("after the probe's establishment: %+v %v", m, found)
+ }
+ raw, err := x.bucket.GetVersion(ctx, "data", pm.Key, seq0)
+ if err != nil || sha(raw) != pm.SHA256 {
+ t.Fatalf("the sequence-0 version is gone: %v", err)
+ }
+ // A prepared manifest that names a foreign object (P6-D3) refuses.
+ foreign := hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{"x": {Object: movesFilesPrefix(filesLoc, "cur") + "objects/0000000000000000-" + sha(nil), SHA256: sha(nil), Seq: 0}}, Platform: map[string]hostfiles.Entry{}}
+ body, _ := json.Marshal(foreign)
+ x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "mv2")), body)
+ in = launchInputs("mv2")
+ in.Prepared = true
+ if err := x.f.Establish(ctx, o, in, x); !errors.Is(err, hostfiles.ErrOrigin) {
+ t.Fatalf("foreign prepared manifest: %v", err)
+ }
+ // No prepared manifest at P: an error, never a fresh start.
+ in = launchInputs("mv3")
+ in.Prepared = true
+ if err := x.f.Establish(ctx, o, in, x); err == nil || errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("missing prepared manifest: %v", err)
+ }
+ if _, found := x.manifest("mv3"); found {
+ t.Fatal("a manifest was written for a missing prepared generation")
+ }
+}