test(ingestion): behavioural race tests for raw/ reads; tighten the lint
CI / Lint / Test / Vet (pull_request) Successful in 36s
CI / Mirror to GitHub (pull_request) Skipped

PR #156 round 3: a readRawNote rewrite to os.ReadFile or Root.ReadFile
passed the lint and brought back the escape (19k leaks in 4s) and the
FIFO hang. Listing forbidden APIs is whack-a-mole, so the control now
rests on behaviour:

- TestRawReadersLeakNothingUnderASymlinkSwapRace and
  TestRawReadersDoNotHangUnderAFIFOSwapRace swap a regular note, a
  symlink to an outside file, or a FIFO into raw/r.md while ListPending
  and ReadIngestFile loop for 1.5s; any leaked byte or a hang fails.
- TestProcessDir_DoesNotHangUnderAFIFOSwapRace does the same for the
  unattended watcher (read and archive copy).

Flipped: os.ReadFile -> leak + hang tests red; Root.ReadFile -> hang
test red; copyFromRoot back to Root.Open -> watcher test red.

The lint stays as a second layer: it also forbids ReadFile, matches the
selector (so a stored method value is caught), refuses a dot-import of
extract, and says it only covers the three files it parses.

Refs #153

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:07:58 +02:00
co-authored by Claude Opus 5.5
parent 809420070b
commit d5b2be0ecf
3 changed files with 179 additions and 6 deletions
+17 -6
View File
@@ -58,6 +58,9 @@ func TestNoPackageReferencesExtractTextOutsideExtract(t *testing.T) {
if imp.Name != nil {
name = imp.Name.Name
}
if name == "." {
t.Errorf("%s dot-imports extract; import it by name so its use stays checkable", path)
}
names[name] = true
}
}
@@ -77,13 +80,21 @@ func TestNoPackageReferencesExtractTextOutsideExtract(t *testing.T) {
// PR #156 review round 2: the FIFO fix (open non-blocking, check the handle)
// lives in OpenRegular, and reverting any caller to a plain Root.Open stayed
// green. In the raw/ readers, the only Open/OpenFile on anything is inside
// OpenRegular itself.
// green. In the three raw/ reader FILES parsed below (and only those: a
// wrapper in another file is not seen), no Open/OpenFile/ReadFile selector
// may appear outside OpenRegular, called or taken as a method value. This is
// a second layer; the behavioural race tests in internal/api and
// internal/watcher are what catch a rewrite through any API (round 3).
// osOpenAllowed names the functions in guarded files that may call os.Open*
// on a path that is not a raw/ read.
var osOpenAllowed = map[string]bool{"internal/watcher/watcher.go:appendWatcherLog": true}
var osOpenAllowed = map[string]bool{
"internal/watcher/watcher.go:appendWatcherLog": true, // ingest log append, not a raw/ read
// Text reads by path; outside this package only TextIn's temp copy reaches
// it (TestNoPackageReferencesExtractTextOutsideExtract enforces that).
"internal/extract/extract.go:Text": true,
}
func TestRawReadersOpenOnlyThroughOpenRegular(t *testing.T) {
func TestRawReaderFilesUseNoOpenOrReadFileOutsideOpenRegular(t *testing.T) {
guarded := []string{"internal/api/rawroot.go", "internal/extract/extract.go", "internal/watcher/watcher.go"}
files := parseNonTest(t)
for _, g := range guarded {
@@ -102,11 +113,11 @@ func TestRawReadersOpenOnlyThroughOpenRegular(t *testing.T) {
continue
}
ast.Inspect(fn, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
sel, ok := n.(*ast.SelectorExpr)
if !ok {
return true
}
if sel, ok := call.Fun.(*ast.SelectorExpr); ok && (sel.Sel.Name == "Open" || sel.Sel.Name == "OpenFile") {
if sel.Sel.Name == "Open" || sel.Sel.Name == "OpenFile" || sel.Sel.Name == "ReadFile" {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "os" && osOpenAllowed[g+":"+fn.Name.Name] {
return true // a named non-raw write (the ingest log append)
}