stopevidence, hostclaim: tests follow main's FDHolders lstat rule and the enrollment parameter
The two KNOWN GAP skips come off now that main lstats the entry after an ENOENT stat (9cf9f18f); the Reconstruct case creates the database so identities are compared; the closed-between-reads case makes the entry vanish too. Main's C7 contract test passes the source enrollment through BeginMove's new parameter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+20
−24
internal/hostclaim/relocating_test.go+1 −5internal/stopevidence/fdholders_alias_linux_test.go+11 −13internal/stopevidence/reconstruct_linux_test.go+8 −6
diff --git a/internal/hostclaim/relocating_test.go b/internal/hostclaim/relocating_test.go| index d7a13ab81..0ee31ca2a 100644 |
| --- a/internal/hostclaim/relocating_test.go |
| +++ b/internal/hostclaim/relocating_test.go |
| @@ -267,11 +267,7 @@ func TestC7ConvertsAClaimBeginMoveProduced(t *testing.T) { |
| o, err = Update(ctx, s, o, func(c *Claim) error { |
| c.Op = Op{Kind: KindServing, Exec: "adi-knitting"} |
| c.Obligations = nil |
| - if err := BeginMove("move-1", "member", "adi-knitting", "bri-knitting", "uk", time.Now())(c); err != nil { |
| - return err |
| - } |
| - c.Op.Intent["source_enrollment"] = ref.Enrollment |
| - return nil |
| + return BeginMove("move-1", "member", "adi-knitting", ref.Enrollment, "bri-knitting", "uk", time.Now())(c) |
| }) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/stopevidence/fdholders_alias_linux_test.go b/internal/stopevidence/fdholders_alias_linux_test.go| index 9ee3a2b44..6c9971286 100644 |
| --- a/internal/stopevidence/fdholders_alias_linux_test.go |
| +++ b/internal/stopevidence/fdholders_alias_linux_test.go |
| @@ -223,12 +223,20 @@ func TestFDIdentityFailsClosed(t *testing.T) { |
| } |
| }, true}, |
| {"fd closed between the reads", func(o *fdOps) { |
| + // The descriptor is gone: both the stat through the link and the |
| + // entry itself answer ENOENT. |
| o.stat = func(p string) (os.FileInfo, error) { |
| if p == fdLink { |
| return nil, syscall.ENOENT |
| } |
| return real.stat(p) |
| } |
| + o.lstat = func(p string) (os.FileInfo, error) { |
| + if p == fdLink { |
| + return nil, syscall.ENOENT |
| + } |
| + return real.lstat(p) |
| + } |
| }, false}, |
| {"fd link refused", func(o *fdOps) { |
| o.readlink = func(string) (string, error) { return "", syscall.EACCES } |
| @@ -278,16 +286,9 @@ func TestFDIdentityFailsClosed(t *testing.T) { |
| } |
| } |
| -// The stricter reading fix round 1 had: a descriptor whose stat says |
| -// ENOENT while its /proc entry is still there is unidentified, so the scan |
| -// is incomplete. Main's FDHolders reads stat ENOENT as "the descriptor |
| -// closed" without re-checking the entry. On a real /proc the stat follows |
| -// the kernel's magic link to the open file and does not answer ENOENT for |
| -// a descriptor that is still open, so this is a gap only where a stat can |
| -// say ENOENT for a live entry (not seen on Linux). Reported in |
| -// task-3-report.md ("Dedupe after main merge"), not patched here: main's |
| -// helper belongs to the claim code. The test skips while the gap is |
| -// present, and passes, unchanged, the day main closes it. |
| +// A descriptor whose stat through /proc answers ENOENT while its entry |
| +// remains is unreadable, not closed: main's FDHolders lstats the entry after |
| +// such a stat (9cf9f18f) and fails the scan with ErrIncomplete. |
| func TestFDHoldersStatGoneWhileTheEntryRemains(t *testing.T) { |
| root, fdLink, _, files := aliasFaultFixture(t) |
| ops := realFDOps() |
| @@ -299,9 +300,6 @@ func TestFDHoldersStatGoneWhileTheEntryRemains(t *testing.T) { |
| return real(p) |
| } |
| got, err := fdHolders(context.Background(), root, files, ops) |
| - if err == nil && len(got) == 0 { |
| - t.Skip("KNOWN GAP (main FDHolders): stat ENOENT on a descriptor whose /proc entry remains is read as closed; see task-3-report.md") |
| - } |
| if !errors.Is(err, ErrIncomplete) || got != nil { |
| t.Fatalf("%v %v, want ErrIncomplete", got, err) |
| } |
diff --git a/internal/stopevidence/reconstruct_linux_test.go b/internal/stopevidence/reconstruct_linux_test.go| index c15e5aa6d..f533a71af 100644 |
| --- a/internal/stopevidence/reconstruct_linux_test.go |
| +++ b/internal/stopevidence/reconstruct_linux_test.go |
| @@ -370,16 +370,18 @@ func TestReconstructDatabaseHolders(t *testing.T) { |
| // A descriptor whose file cannot be identified (its stat says ENOENT while |
| // its entry remains: here a dangling fixture link) should not pass step 3. |
| -// Main's FDHolders reads that as a closed descriptor; this is the gap |
| -// TestFDHoldersStatGoneWhileTheEntryRemains documents, seen through |
| -// Reconstruct. It skips while the gap is present. |
| +// Main's FDHolders lstats the entry after an ENOENT stat (9cf9f18f), so an |
| +// entry that remains but cannot be followed is ErrIncomplete, seen here |
| +// through Reconstruct. |
| func TestReconstructUnidentifiedDescriptorIsNotAPass(t *testing.T) { |
| f := newBoxFixture(t) |
| + // The database exists, so FDHolders compares identities; a descriptor |
| + // whose target cannot be followed is then unreadable, not closed. |
| + if err := os.WriteFile(f.db, nil, 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| f.holdDB(t, 5150, filepath.Join(filepath.Dir(f.db), "vanished.db")) |
| rep, err := reconstruct(context.Background(), f.inputs(), opener(&fakeTree{}, nil)) |
| - if err == nil && rep.Steps[moves.StepFDs].OK { |
| - t.Skip("KNOWN GAP (main FDHolders): stat ENOENT on a descriptor whose /proc entry remains is read as closed; see task-3-report.md") |
| - } |
| if !errors.Is(err, ErrIncomplete) { |
| t.Fatalf("unidentifiable descriptor: %+v %v", rep.Steps, err) |
| } |