Files
ARTEX/server/fact_source_schema_test.go
T
BaaRai bc33db2735 server: add the record_fact source enum to existing databases
SeedTool is first-insert only and ToolResolve serves the stored tools row,
so on an existing database the record_fact schema never gained the source
property: the model never saw the enum, every fact was stored as inference,
and prove_goal's target_response refusal could not fire.

seedFactSourceSchema copies properties.source from the record_fact seed into
the stored schema, only when the stored schema lacks it (same pattern as
seedFindingEvidenceSchema). Operator edits to the description and to other
properties are preserved, and a second run changes nothing. It runs from
seedOrchestrationTools next to the other schema seeders.

The PG test edits the stored row (no source, custom description and
parameter text), runs seedOrchestrationTools twice, and checks the added
enum, the preserved operator text, the unchanged row on the second run,
and that ToolResolve serves the source property to the worker.

Refs #124 (F4).
2026-11-12 01:09:30 +01:00

109 lines
3.7 KiB
Go

package server
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/Autumn-27/artex/agent"
)
// PG: on an existing database whose stored record_fact row predates the source
// field (and was edited by the operator), the startup seeders add the source
// enum without touching the operator's text, a second run changes nothing, and
// ToolResolve then serves the source property to the model.
func TestRecordFactSourceSchemaUpgrade(t *testing.T) {
skipWithoutDSN(t)
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { m.Close() })
ctx, cancel := context.WithCancel(context.Background())
cancel()
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
t.Cleanup(func() { agent.ToolResolve = nil })
pg := m.pg
old, err := pg.GetTool("record_fact")
if err != nil || old == nil {
t.Fatal("missing record_fact", err)
}
bindings, _ := json.Marshal(old.Agents)
t.Cleanup(func() { pg.UpdateTool(old.Key, old.Description, old.Schema, bindings, old.Enabled) })
// Simulate a pre-provenance row customised by the operator: no source
// property, an edited description and an edited parameter description.
var schema map[string]any
if err := json.Unmarshal(old.Schema, &schema); err != nil {
t.Fatal(err)
}
props := schema["properties"].(map[string]any)
delete(props, "source")
props["confidence"].(map[string]any)["description"] = "operator confidence text"
custom, _ := json.Marshal(schema)
if err := pg.UpdateTool("record_fact", "operator description", custom, bindings, true); err != nil {
t.Fatal(err)
}
if src := resolvedRecordFactProps(t)["source"]; src != nil {
t.Fatalf("precondition: stored schema should not expose source yet: %v", src)
}
snapshot := func() (string, string, time.Time) {
var desc, sch string
var upd time.Time
if err := pg.QueryRow(`SELECT description, schema::text, updated_at FROM tools WHERE key='record_fact'`).Scan(&desc, &sch, &upd); err != nil {
t.Fatal(err)
}
return desc, sch, upd
}
s.seedOrchestrationTools()
desc1, sch1, upd1 := snapshot()
s.seedOrchestrationTools()
desc2, sch2, upd2 := snapshot()
if desc1 != desc2 || sch1 != sch2 || !upd1.Equal(upd2) {
t.Fatalf("second run changed the row: %q %s %v -> %q %s %v", desc1, sch1, upd1, desc2, sch2, upd2)
}
tool, err := pg.GetTool("record_fact")
if err != nil || tool == nil {
t.Fatal(err)
}
var got struct {
Properties map[string]map[string]any `json:"properties"`
}
if err := json.Unmarshal(tool.Schema, &got); err != nil {
t.Fatal(err)
}
wantEnum := "[target_response tool_output third_party_intel operator_input inference]"
if src := got.Properties["source"]; src == nil || fmt.Sprint(src["enum"]) != wantEnum {
t.Fatalf("upgrade did not add the source enum: %s", tool.Schema)
}
if tool.Description != "operator description" || got.Properties["confidence"]["description"] != "operator confidence text" || len(got.Properties) != len(props)+1 {
t.Fatalf("operator configuration overwritten: %q %s", tool.Description, tool.Schema)
}
resolved := resolvedRecordFactProps(t)
src, _ := resolved["source"].(map[string]any)
if src == nil || fmt.Sprint(src["enum"]) != wantEnum {
t.Fatalf("ToolResolve does not serve the source enum: %v", resolved)
}
}
// resolvedRecordFactProps returns the properties of record_fact as ToolResolve
// serves it to the worker.
func resolvedRecordFactProps(t *testing.T) map[string]any {
t.Helper()
tools := agent.ToolResolve(context.Background(), "worker", agent.NewToolSet(nil, "").WorkerTools())
for _, tl := range tools {
if tl.Name() == "record_fact" {
props, _ := tl.InputSchema()["properties"].(map[string]any)
return props
}
}
t.Fatal("worker lost record_fact")
return nil
}