Files
DeepSeek-Reasonix/internal/assembly/boot/effect_fileview_page_test.go
SivanCola 3d27a1a4e1 fix(builtin): bind file views to complete returned reads
Problem: whole-file rewrites can discard changes hidden by a later overlay
version, a paged read, or an oversized read that clears the previous guard.

Root cause: read_file renders one read and records another. Missing and
incomplete observations also collapse into the permissive never-seen path.

Fix: observe the renderer's decoded stream with bounded capture, refresh only
complete returned reads, retain older guards, and track first partial reads
explicitly. Keep never-seen compatibility and disabled protection behavior.

Verification: compiling old-owner controls fail five behavior regressions;
fixed focused race tests and real boot.Build checks pass. Full builtin,
root-package tests, go vet, make check and full lint are recorded in the PR.
Written bilingual security bounds and release notes accompany this slice.

A first partial read refuses replacement with the file's line count and the
read_file limit that certifies it. Without that, a default-limited read of a
file longer than 2000 lines can never be certified by rereading as told.

Refs #14025
Refs #14133
2026-10-25 21:21:51 -07:00

78 lines
2.5 KiB
Go

package boot
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"reasonix/internal/contract/event"
"reasonix/internal/contract/provider"
"reasonix/internal/session/control"
)
func TestEffectPagedReadDoesNotRefreshUnseenFileContent(t *testing.T) {
t.Parallel()
home, dir := statedBootHome(t), robustTempDir(t)
target := filepath.Join(dir, "notes.md")
writeFile(t, dir, "notes.md", "header\noriginal tail\n")
const current = "header\nthe user's new tail\n"
rec := &browserScriptProvider{rounds: []func(string) *provider.ToolCall{
func(string) *provider.ToolCall {
return browserCall("whole", "read_file", map[string]any{"path": target})
},
func(last string) *provider.ToolCall {
if !strings.Contains(last, "original tail") {
t.Errorf("whole read did not reach the provider: %q", last)
}
if err := os.WriteFile(target, []byte(current), 0600); err != nil {
t.Error(err)
}
return browserCall("page", "read_file", map[string]any{"path": target, "limit": 1})
},
func(last string) *provider.ToolCall {
if !strings.Contains(last, "header") || strings.Contains(last, "new tail") {
t.Errorf("page did not reach the provider as a partial read: %q", last)
}
return browserCall("overwrite", "write_file", map[string]any{"path": target, "content": "changed header\noriginal tail\n"})
},
}}
kind := "boot-fileview-" + strings.ToLower(t.Name())
provider.Register(kind, func(provider.Config) (provider.Provider, error) { return rec, nil })
writeFile(t, dir, "reasonix.toml", `
default_model = "test-model"
[codegraph]
enabled = false
[[providers]]
name = "test-model"
kind = "`+kind+`"
model = "x"
`)
approveWorkspaceAt(t, home, dir)
ctrl, err := Build(t.Context(), Options{Home: home, WorkspaceRoot: dir, Sink: event.Discard})
if err != nil {
t.Fatal(err)
}
defer ctrl.Close()
ctrl.SetToolApprovalMode(control.ToolApprovalYolo)
runErr := ctrl.Run(context.Background(), "update the header in notes.md")
got, err := os.ReadFile(target)
if err != nil || string(got) != current {
t.Fatalf("unseen user edit was overwritten: %q; read=%v; run=%v", got, err, runErr)
}
if runErr != nil {
t.Fatal(runErr)
}
reqs := rec.requests()
if len(reqs) < 4 {
t.Fatalf("missing requests: %d", len(reqs))
}
results := effectToolResults(reqs[len(reqs)-1])
if len(results) != 3 || !strings.Contains(results[2], "modified after you last read or wrote it") {
t.Fatalf("stale overwrite refusal did not reach the provider: %q", results)
}
}