Files
ARTEX/agent/auto_scope_hygiene_test.go
BaaRai 37fab18809 fix(coverage): count auto-scope only under a declared root (F8)
Auto task_scope rows (source='auto') come from insert_assets and intent
asset_ids, i.e. from target-derived content. They now count toward the
coverage denominator and list_untested_assets only when they sit under a
root the same task declared (a non-auto row: goal target registered via
add_task_scope, or manual scope): the same or a deeper domain of a declared
root_domain/subdomain, or a net inside a declared ip/cidr.

The direct-source coverage view also stops counting an asset from an intent
anchor alone; fact anchors still count, so tested evidence is unchanged.
Without this, insert_assets put a planted host back into the backlog
through its intent anchor.

task_scope is described as "coverage scope, not enforced" in the code
comments, the add_task_scope description, the goal-decomposer tail and the
task overview UI, instead of "authorization boundary".

Audit F8, 0.7.0.
2026-11-12 07:37:58 +01:00

107 lines
3.5 KiB
Go

package agent
import (
"fmt"
"strings"
"testing"
"time"
"github.com/Autumn-27/artex/db"
)
// TestTaskScopeLabelIsHonest pins the task_scope wording while the RoE is not
// back: the tool text and the goals tail call it coverage scope, not enforced, and
// no longer claim an authorization boundary.
func TestTaskScopeLabelIsHonest(t *testing.T) {
texts := map[string]string{
"add_task_scope": NewToolSet(nil, "").addTaskScope().Description(),
"goalsScopeTail": goalsScopeTail,
}
for name, text := range texts {
if !strings.Contains(text, taskScopeLabel) {
t.Errorf("%s lacks %q", name, taskScopeLabel)
}
if strings.Contains(text, "authorization boundary") {
t.Errorf("%s still claims an authorization boundary", name)
}
}
}
// TestAutoScopeOutsideDeclaredRootExcluded: a worker inserts one host under the
// declared root and one host outside it. Both get an auto scope row and an intent
// anchor, but only the host under the declared root enters the coverage
// denominator and list_untested_assets.
func TestAutoScopeOutsideDeclaredRootExcluded(t *testing.T) {
d := testDB(t)
defer d.Close()
stamp := time.Now().UnixNano()
declared := fmt.Sprintf("app.f8-%d.invalid", stamp)
inside := "v1." + declared
outsideRoot := fmt.Sprintf("f8-planted-%d.invalid", stamp)
outside := "x." + outsideRoot
task, err := d.CreateTask("auto scope hygiene", "test "+declared, nil, 0, 0)
if err != nil {
t.Fatal(err)
}
assets := d.Assets()
t.Cleanup(func() {
_ = d.DeleteTask(task.ID)
_, _ = d.Exec(`DELETE FROM assets WHERE domain IN ($1,$2,$3) OR root_domain IN ($4,$5)`,
declared, inside, outside, fmt.Sprintf("f8-%d.invalid", stamp), outsideRoot)
})
// The goal target, as the goal decomposer registers it through add_task_scope.
if _, err := assets.AddAgentScope(task.ID, "subdomain", declared, "goal target", "agent"); err != nil {
t.Fatal(err)
}
store := d.Exploration(task.ExplorationID)
intentID, err := store.AddNode(db.KindIntent, map[string]any{"summary": "recon"}, 0, "open", "planner", nil)
if err != nil {
t.Fatal(err)
}
worker := NewToolSet(store, "worker")
worker.SetTaskID(task.ID)
worker.SetAssetStore(assets, d.Companies())
worker.SetOwnerNode(intentID)
out := callInsertAssets(t, worker, map[string]any{"assets": []any{
map[string]any{"type": "subdomain", "domain": inside},
map[string]any{"type": "subdomain", "domain": outside},
}})
if errs, _ := out["errors"].([]any); len(errs) > 0 {
t.Fatalf("insert_assets errors: %v", errs)
}
scope, err := assets.ListTaskScope(task.ID)
if err != nil {
t.Fatal(err)
}
autoRows := map[string]bool{}
for _, row := range scope {
if row.Source == "auto" {
autoRows[row.Domain] = true
}
}
if !autoRows[inside] || !autoRows[outside] {
t.Fatalf("both hosts should be auto-scoped: %+v", scope)
}
planner := NewToolSet(store, "planner")
planner.SetTaskID(task.ID)
planner.SetAssetStore(assets, d.Companies())
coverage, ok := planner.graphOverviewData()["coverage"].(map[string]any)
if !ok {
t.Fatal("coverage block missing from graph overview")
}
if got := coverage["denominator"]; got != 1 {
t.Fatalf("coverage denominator = %v, want 1 (only %s)", got, inside)
}
untested := fmt.Sprint(callReadJSON(t, planner.listUntestedAssets(), `{"page":1,"page_size":50}`))
if !strings.Contains(untested, inside) {
t.Fatalf("host under the declared root missing from list_untested_assets: %s", untested)
}
if strings.Contains(untested, outsideRoot) {
t.Fatalf("host outside the declared root listed as untested: %s", untested)
}
}