All checks were successful
release-tag / release-image (push) Successful in 2m43s
189 lines
6.1 KiB
Go
189 lines
6.1 KiB
Go
package ingest
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/graph"
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
)
|
|
|
|
func TestKnowledgeScannerIncludesAIThinkStaging(t *testing.T) {
|
|
root := t.TempDir()
|
|
prod := filepath.Join(root, "knowledge")
|
|
stage := filepath.Join(root, "staging")
|
|
if err := os.MkdirAll(prod, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(stage, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(prod, "vpn.json"), []byte(`{"id":"KB-VPN","title":"VPN","text":"Gateway","categories":["Netzwerk"],"keywords":["VPN"],"source":"internal-kb"}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stage, "think.json"), []byte(`{"id":"KB-AI-1","title":"VPN Zusammenhang","text":"Synthese","categories":["AI-THINK"],"keywords":["VPN"],"source":"Neural Brain","ai_think":{"subtype":"knowledge_synthesis","generation_depth":2,"source_node_ids":["a","b","c"]}}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, _ := graph.Open(filepath.Join(root, "data"))
|
|
s := KnowledgeScanner{Graph: g, ProductionDirs: []string{prod}, StagingDirs: []string{stage}}
|
|
if _, err := s.Scan(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snap := g.Snapshot()
|
|
var prodOK, thinkOK bool
|
|
for _, n := range snap.Nodes {
|
|
if n.ExternalID == "KB-VPN" && n.Status == "production" {
|
|
prodOK = true
|
|
}
|
|
if n.ExternalID == "KB-AI-1" && n.Kind == "ai-think" && n.Status == "staging" {
|
|
thinkOK = true
|
|
if n.Metadata["subtype"] != "knowledge_synthesis" || n.Metadata["generation_depth"].(float64) != 2 {
|
|
t.Fatalf("AI synthesis metadata missing: %#v", n.Metadata)
|
|
}
|
|
}
|
|
}
|
|
if !prodOK || !thinkOK {
|
|
t.Fatalf("missing nodes: prod=%v think=%v", prodOK, thinkOK)
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeScannerSkipsUnchangedGraphReplacement(t *testing.T) {
|
|
root := t.TempDir()
|
|
prod := filepath.Join(root, "prod")
|
|
if err := os.MkdirAll(prod, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(prod, "one.json"), []byte(`{"id":"KB-ONE","title":"One","text":"Same","categories":["Test"]}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, err := graph.Open(filepath.Join(root, "data"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scanner := KnowledgeScanner{Graph: g, ProductionDirs: []string{prod}}
|
|
if _, err := scanner.Scan(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v1 := g.Version()
|
|
if _, err := scanner.Scan(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v2 := g.Version(); v2 != v1 {
|
|
t.Fatalf("unchanged scan changed graph version: %d -> %d", v1, v2)
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeScannerUsesManifestFastPath(t *testing.T) {
|
|
root := t.TempDir()
|
|
prod := filepath.Join(root, "prod")
|
|
if err := os.MkdirAll(prod, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(prod, "one.json"), []byte(`{"id":"KB-ONE","title":"One","text":"Same","categories":["Test"]}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, err := graph.Open(filepath.Join(root, "data"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scanner := KnowledgeScanner{Graph: g, ProductionDirs: []string{prod}}
|
|
first, err := scanner.ScanDetailed()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.FastPath {
|
|
t.Fatalf("first scan must build the model: %+v", first)
|
|
}
|
|
version := g.Version()
|
|
second, err := scanner.ScanDetailed()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !second.FastPath || second.Count != first.Count {
|
|
t.Fatalf("expected manifest fast path: first=%+v second=%+v", first, second)
|
|
}
|
|
if g.Version() != version {
|
|
t.Fatalf("fast path changed graph version: %d -> %d", version, g.Version())
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeManifestFingerprintTracksRelevantFileMetadata(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "one.json")
|
|
if err := os.WriteFile(path, []byte(`{"id":"one"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := knowledgeManifestFingerprint([]string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := knowledgeManifestFingerprint([]string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first != second {
|
|
t.Fatal("unchanged manifest fingerprint is unstable")
|
|
}
|
|
stamp := time.Now().Add(2 * time.Second)
|
|
if err := os.Chtimes(path, stamp, stamp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
third, err := knowledgeManifestFingerprint([]string{root}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if third == first {
|
|
t.Fatal("mtime change did not invalidate manifest fingerprint")
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeScannerPreservesRuntimeArticleProvenanceEdges(t *testing.T) {
|
|
root := t.TempDir()
|
|
prod := filepath.Join(root, "prod")
|
|
stage := filepath.Join(root, "stage")
|
|
if err := os.MkdirAll(prod, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(stage, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(prod, "source.json"), []byte(`{"id":"KB-SOURCE","title":"Source","text":"Source text","categories":["Test"]}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stage, "article.json"), []byte(`{"id":"KB-AI-ARTICLE","title":"Article","text":"Draft","categories":["AI-THINK"],"ai_think":{"subtype":"knowledge_synthesis","source_node_ids":["unused"]}}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, err := graph.Open(filepath.Join(root, "data"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scanner := KnowledgeScanner{Graph: g, ProductionDirs: []string{prod}, StagingDirs: []string{stage}}
|
|
if _, err := scanner.ScanDetailed(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
articleID := graph.ID("knowledge", "KB-AI-ARTICLE")
|
|
sourceID := graph.ID("knowledge", "KB-SOURCE")
|
|
legacy := model.Edge{Source: articleID, Target: sourceID, Type: "synthesized_from", Origin: "knowledge-staging", Status: "staging", Confidence: .9}
|
|
g.UpsertEdge(legacy)
|
|
stamp := time.Now().Add(2 * time.Second)
|
|
if err := os.Chtimes(filepath.Join(stage, "article.json"), stamp, stamp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := scanner.ScanDetailed(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wanted := graph.EdgeID(articleID, sourceID, "synthesized_from", "knowledge-staging")
|
|
found := false
|
|
for _, edge := range g.Snapshot().Edges {
|
|
if edge.ID == wanted {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("runtime article provenance was deleted by staging reconciliation")
|
|
}
|
|
}
|