CI / CI (push) Successful in 4m48s
## Changes - `activity/synthesis.go` — 5 synthesis pipeline activities - `activity/synthesis_test.go` — 10 unit tests ## Activities 1. **ChunkAndEmbedActivity** — deterministic chunk ID + memory service ingest 2. **ExtractEntitiesActivity** — wiki-link, proper noun, technical term extraction 3. **ExtractFactsActivity** — 6 verb patterns with entity-boosted confidence 4. **DetectContradictionsActivity** — query + pre-filter + severity classification 5. **PersistSynthesisActivity** — save entities + facts to memory service ## Validation - 10 unit tests pass - `go build ./...` clean - Full suite: 35 packages pass, 0 failures --------- Co-authored-by: poimen <[email protected]> Reviewed-on: #13
184 lines
5.4 KiB
Go
184 lines
5.4 KiB
Go
package activity
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
var testCtx = context.Background()
|
|
|
|
func TestExtractEntities_WikiLinks(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil) // No client needed for extraction
|
|
entities, err := sa.ExtractEntitiesActivity(testCtx, "chunk-1", "Deploy [[Kubernetes]] with [[ArgoCD]]")
|
|
assert.NoError(t, err)
|
|
|
|
names := entityNames(entities)
|
|
assert.Contains(t, names, "Kubernetes")
|
|
assert.Contains(t, names, "ArgoCD")
|
|
|
|
// Wiki links get high confidence
|
|
for _, e := range entities {
|
|
if e.Name == "Kubernetes" || e.Name == "ArgoCD" {
|
|
assert.Equal(t, 0.95, e.Confidence)
|
|
assert.Equal(t, "reference", e.EntityType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractEntities_ProperNouns(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
entities, err := sa.ExtractEntitiesActivity(testCtx, "chunk-2", "Redis runs on Ubuntu Server")
|
|
assert.NoError(t, err)
|
|
|
|
names := entityNames(entities)
|
|
assert.Contains(t, names, "Redis")
|
|
assert.Contains(t, names, "Ubuntu Server")
|
|
}
|
|
|
|
func TestExtractEntities_TechnicalTerms(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
entities, err := sa.ExtractEntitiesActivity(testCtx, "chunk-3", "Set MAX_RETRIES and use camelCase variables")
|
|
assert.NoError(t, err)
|
|
|
|
names := entityNames(entities)
|
|
assert.Contains(t, names, "MAX_RETRIES")
|
|
assert.Contains(t, names, "camelCase")
|
|
}
|
|
|
|
func TestExtractEntities_Deduplication(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
entities, err := sa.ExtractEntitiesActivity(testCtx, "chunk-4", "[[Redis]] uses Redis for caching")
|
|
assert.NoError(t, err)
|
|
|
|
count := 0
|
|
for _, e := range entities {
|
|
if e.Name == "Redis" {
|
|
count++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, count, "Redis should appear only once")
|
|
}
|
|
|
|
func TestExtractFacts_VerbPatterns(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
entities := []ExtractedEntity{
|
|
{Name: "Kubernetes", EntityType: "tool"},
|
|
{Name: "Docker", EntityType: "tool"},
|
|
}
|
|
facts, err := sa.ExtractFactsActivity(testCtx, "chunk-5",
|
|
"Kubernetes uses Docker for container runtime. Redis depends on TCP",
|
|
entities)
|
|
assert.NoError(t, err)
|
|
assert.Greater(t, len(facts), 0)
|
|
|
|
// Find the "uses" fact
|
|
found := false
|
|
for _, f := range facts {
|
|
if f.Predicate == "uses" && f.Subject == "Kubernetes" {
|
|
found = true
|
|
assert.Greater(t, f.Confidence, 0.7) // Boosted by known entities
|
|
}
|
|
}
|
|
assert.True(t, found, "should find Kubernetes uses Docker fact")
|
|
}
|
|
|
|
func TestExtractFacts_EmptyText(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
facts, err := sa.ExtractFactsActivity(testCtx, "chunk-6", "", nil)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, facts)
|
|
}
|
|
|
|
func TestContradicts(t *testing.T) {
|
|
assert.True(t, contradicts("Kubernetes uses port 8080", ExtractedFact{
|
|
Subject: "Kubernetes", Predicate: "uses_port", Object: "9090",
|
|
}))
|
|
|
|
assert.False(t, contradicts("Kubernetes uses port 8080", ExtractedFact{
|
|
Subject: "Kubernetes", Predicate: "uses_port", Object: "8080",
|
|
}))
|
|
}
|
|
|
|
func TestContainsSubject(t *testing.T) {
|
|
assert.True(t, containsSubject("Kubernetes runs on Linux", "kubernetes"))
|
|
assert.False(t, containsSubject("Docker runs on Linux", "kubernetes"))
|
|
}
|
|
|
|
func TestIsCommonWord(t *testing.T) {
|
|
assert.True(t, isCommonWord("The"))
|
|
assert.True(t, isCommonWord("This"))
|
|
assert.False(t, isCommonWord("Kubernetes"))
|
|
assert.False(t, isCommonWord("Redis"))
|
|
}
|
|
|
|
func TestClassifyEntity(t *testing.T) {
|
|
assert.Equal(t, "tool", classifyEntity("Kubernetes"))
|
|
assert.Equal(t, "tool", classifyEntity("Docker"))
|
|
assert.Equal(t, "tool", classifyEntity("Redis"))
|
|
assert.Equal(t, "concept", classifyEntity("SomeRandomThing"))
|
|
}
|
|
|
|
// --- Tests for ExtractFactsActivity Validation ---
|
|
|
|
func TestExtractFacts_WithValidation(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
// Test with very long object that should be truncated
|
|
longText := "Kubernetes uses " + strings.Repeat("very long object name that should be truncated ", 20)
|
|
entities := []ExtractedEntity{}
|
|
|
|
facts, err := sa.ExtractFactsActivity(testCtx, "chunk-1", longText, entities)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify no fact has object > 500 chars
|
|
for _, f := range facts {
|
|
assert.LessOrEqual(t, len(f.Object), 500, "object should be truncated to 500 chars")
|
|
}
|
|
}
|
|
|
|
func TestExtractFacts_SkipsEmpty(t *testing.T) {
|
|
sa := NewSynthesisActivities(nil)
|
|
// Text with empty patterns that would extract nothing
|
|
text := "Something uses and other things"
|
|
entities := []ExtractedEntity{}
|
|
|
|
facts, err := sa.ExtractFactsActivity(testCtx, "chunk-1", text, entities)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify no empty facts
|
|
for _, f := range facts {
|
|
assert.NotEmpty(t, f.Subject, "subject should not be empty")
|
|
assert.NotEmpty(t, f.Object, "object should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestPersistSynthesis_EmptyInput(t *testing.T) {
|
|
// Test that empty input is handled (no entities or facts to persist)
|
|
// Note: This test requires a mock memory service; for now we just test structure
|
|
input := PersistInput{
|
|
ChunkID: "chunk-123",
|
|
Project: "test",
|
|
Source: "test://1",
|
|
Kind: "L1",
|
|
Entities: []ExtractedEntity{}, // Empty
|
|
Facts: []ExtractedFact{}, // Empty
|
|
Contradictions: []ContradictionResult{},
|
|
}
|
|
|
|
// Verify input structure is valid
|
|
assert.Equal(t, "chunk-123", input.ChunkID)
|
|
assert.Equal(t, 0, len(input.Entities))
|
|
assert.Equal(t, 0, len(input.Facts))
|
|
}
|
|
|
|
// helper
|
|
func entityNames(entities []ExtractedEntity) []string {
|
|
names := make([]string, len(entities))
|
|
for i, e := range entities {
|
|
names[i] = e.Name
|
|
}
|
|
return names
|
|
}
|