test(ingestion): behavioural race tests for raw/ reads; tighten the lint
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user