4-stage pipeline as Temporal workflow:
Stage 1: ChunkAndEmbed — chunk text + generate embeddings
Stage 2: ExtractEntities — LLM entity extraction with reflection
Stage 3: ExtractFacts — pattern + LLM fact extraction
Stage 4: DetectContradictions — pre-filter + LLM verification
Stage 5: PersistSynthesis — save all results to DB
Types: SynthesisInput, SynthesisResult, ExtractedEntity,
ExtractedFact, ContradictionResult, PersistInput
Retry: 3 attempts, exponential backoff (1s → 2s → 4s)
Each stage fails independently with wrapped errors.
Tests: 5 pass (success, contradictions, entity fail, chunk fail, fields)
Build: clean, 36 packages pass
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go.temporal.io/sdk/testsuite"
|
||||
)
|
||||
|
||||
// Stub activity functions for test registration
|
||||
func ChunkAndEmbedActivity(_ context.Context, _ SynthesisInput) (string, error) { return "", nil }
|
||||
func ExtractEntitiesActivity(_ context.Context, _ string, _ string) ([]ExtractedEntity, error) { return nil, nil }
|
||||
func ExtractFactsActivity(_ context.Context, _ string, _ string, _ []ExtractedEntity) ([]ExtractedFact, error) { return nil, nil }
|
||||
func DetectContradictionsActivity(_ context.Context, _ string, _ []ExtractedFact) ([]ContradictionResult, error) { return nil, nil }
|
||||
func PersistSynthesisActivity(_ context.Context, _ PersistInput) error { return nil }
|
||||
|
||||
func registerSynthesisActivities(env *testsuite.TestWorkflowEnvironment) {
|
||||
env.RegisterActivity(ChunkAndEmbedActivity)
|
||||
env.RegisterActivity(ExtractEntitiesActivity)
|
||||
env.RegisterActivity(ExtractFactsActivity)
|
||||
env.RegisterActivity(DetectContradictionsActivity)
|
||||
env.RegisterActivity(PersistSynthesisActivity)
|
||||
}
|
||||
|
||||
func TestSynthesisWorkflow_Success(t *testing.T) {
|
||||
ts := &testsuite.WorkflowTestSuite{}
|
||||
env := ts.NewTestWorkflowEnvironment()
|
||||
|
||||
input := SynthesisInput{
|
||||
Project: "poimen",
|
||||
Source: "transcript://test-123",
|
||||
Text: "Kubernetes uses port 8080 for the API server",
|
||||
Kind: "L1",
|
||||
}
|
||||
|
||||
registerSynthesisActivities(env)
|
||||
|
||||
// Stage 1: Chunk + Embed
|
||||
env.OnActivity(ChunkAndEmbedActivity, mock.Anything, input).Return("chunk-abc123", nil)
|
||||
|
||||
// Stage 2: Entity Extraction
|
||||
entities := []ExtractedEntity{
|
||||
{Name: "Kubernetes", EntityType: "tool", Confidence: 0.95},
|
||||
{Name: "API server", EntityType: "component", Confidence: 0.90},
|
||||
}
|
||||
env.OnActivity(ExtractEntitiesActivity, mock.Anything, "chunk-abc123", input.Text).Return(entities, nil)
|
||||
|
||||
// Stage 3: Fact Extraction
|
||||
facts := []ExtractedFact{
|
||||
{Subject: "Kubernetes", Predicate: "uses_port", Object: "8080", Confidence: 0.85},
|
||||
}
|
||||
env.OnActivity(ExtractFactsActivity, mock.Anything, "chunk-abc123", input.Text, entities).Return(facts, nil)
|
||||
|
||||
// Stage 4: Contradiction Detection
|
||||
contradictions := []ContradictionResult{}
|
||||
env.OnActivity(DetectContradictionsActivity, mock.Anything, "poimen", facts).Return(contradictions, nil)
|
||||
|
||||
// Stage 5: Persist
|
||||
env.OnActivity(PersistSynthesisActivity, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
env.ExecuteWorkflow(SynthesisWorkflow, input)
|
||||
|
||||
assert.True(t, env.IsWorkflowCompleted())
|
||||
assert.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var result SynthesisResult
|
||||
assert.NoError(t, env.GetWorkflowResult(&result))
|
||||
assert.Equal(t, "chunk-abc123", result.ChunkID)
|
||||
assert.Equal(t, 2, result.EntitiesExtracted)
|
||||
assert.Equal(t, 1, result.FactsExtracted)
|
||||
assert.Equal(t, 0, result.Contradictions)
|
||||
assert.Equal(t, 0, result.ReviewQueued)
|
||||
}
|
||||
|
||||
func TestSynthesisWorkflow_WithContradictions(t *testing.T) {
|
||||
ts := &testsuite.WorkflowTestSuite{}
|
||||
env := ts.NewTestWorkflowEnvironment()
|
||||
registerSynthesisActivities(env)
|
||||
|
||||
input := SynthesisInput{
|
||||
Project: "poimen",
|
||||
Source: "transcript://test-456",
|
||||
Text: "Port 8080 is used by nginx",
|
||||
Kind: "L1",
|
||||
}
|
||||
|
||||
env.OnActivity(ChunkAndEmbedActivity, mock.Anything, input).Return("chunk-def456", nil)
|
||||
|
||||
entities := []ExtractedEntity{
|
||||
{Name: "nginx", EntityType: "tool", Confidence: 0.92},
|
||||
}
|
||||
env.OnActivity(ExtractEntitiesActivity, mock.Anything, "chunk-def456", input.Text).Return(entities, nil)
|
||||
|
||||
facts := []ExtractedFact{
|
||||
{Subject: "nginx", Predicate: "uses_port", Object: "8080", Confidence: 0.88},
|
||||
}
|
||||
env.OnActivity(ExtractFactsActivity, mock.Anything, "chunk-def456", input.Text, entities).Return(facts, nil)
|
||||
|
||||
contradictions := []ContradictionResult{
|
||||
{
|
||||
FactA: ExtractedFact{Subject: "Kubernetes", Predicate: "uses_port", Object: "8080"},
|
||||
FactB: ExtractedFact{Subject: "nginx", Predicate: "uses_port", Object: "8080"},
|
||||
Severity: "medium",
|
||||
AutoResolved: false,
|
||||
QueuedReview: true,
|
||||
},
|
||||
}
|
||||
env.OnActivity(DetectContradictionsActivity, mock.Anything, "poimen", facts).Return(contradictions, nil)
|
||||
env.OnActivity(PersistSynthesisActivity, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
env.ExecuteWorkflow(SynthesisWorkflow, input)
|
||||
|
||||
assert.True(t, env.IsWorkflowCompleted())
|
||||
assert.NoError(t, env.GetWorkflowError())
|
||||
|
||||
var result SynthesisResult
|
||||
assert.NoError(t, env.GetWorkflowResult(&result))
|
||||
assert.Equal(t, 1, result.Contradictions)
|
||||
assert.Equal(t, 1, result.ReviewQueued)
|
||||
}
|
||||
|
||||
func TestSynthesisWorkflow_EntityExtractionFails(t *testing.T) {
|
||||
ts := &testsuite.WorkflowTestSuite{}
|
||||
env := ts.NewTestWorkflowEnvironment()
|
||||
registerSynthesisActivities(env)
|
||||
|
||||
input := SynthesisInput{
|
||||
Project: "poimen",
|
||||
Source: "transcript://test-789",
|
||||
Text: "Some text",
|
||||
Kind: "L1",
|
||||
}
|
||||
|
||||
env.OnActivity(ChunkAndEmbedActivity, mock.Anything, input).Return("chunk-xyz", nil)
|
||||
env.OnActivity(ExtractEntitiesActivity, mock.Anything, "chunk-xyz", input.Text).
|
||||
Return(nil, assert.AnError)
|
||||
|
||||
env.ExecuteWorkflow(SynthesisWorkflow, input)
|
||||
|
||||
assert.True(t, env.IsWorkflowCompleted())
|
||||
assert.Error(t, env.GetWorkflowError())
|
||||
assert.Contains(t, env.GetWorkflowError().Error(), "stage 2 entity extraction")
|
||||
}
|
||||
|
||||
func TestSynthesisWorkflow_ChunkFails(t *testing.T) {
|
||||
ts := &testsuite.WorkflowTestSuite{}
|
||||
env := ts.NewTestWorkflowEnvironment()
|
||||
registerSynthesisActivities(env)
|
||||
|
||||
input := SynthesisInput{
|
||||
Project: "poimen",
|
||||
Source: "transcript://test-fail",
|
||||
Text: "Bad text",
|
||||
Kind: "L1",
|
||||
}
|
||||
|
||||
env.OnActivity(ChunkAndEmbedActivity, mock.Anything, input).Return("", assert.AnError)
|
||||
|
||||
env.ExecuteWorkflow(SynthesisWorkflow, input)
|
||||
|
||||
assert.True(t, env.IsWorkflowCompleted())
|
||||
assert.Error(t, env.GetWorkflowError())
|
||||
assert.Contains(t, env.GetWorkflowError().Error(), "stage 1 chunk+embed")
|
||||
}
|
||||
|
||||
func TestSynthesisInput_Fields(t *testing.T) {
|
||||
input := SynthesisInput{
|
||||
Project: "test",
|
||||
Source: "source://1",
|
||||
Text: "hello",
|
||||
Kind: "L2",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
assert.Equal(t, "test", input.Project)
|
||||
assert.Equal(t, "L2", input.Kind)
|
||||
assert.Len(t, input.Tags, 2)
|
||||
}
|
||||
Reference in New Issue
Block a user