mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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).
109 lines
3.7 KiB
Go
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
|
|
}
|