返回 DeepSeek-Reasonix
semantic_test.go
根目录 / internal / capability / semantic_test.go
1 package capability
2
3 import (
4 "context"
5 "testing"
6
7 "reasonix/internal/event"
8 "reasonix/internal/provider"
9 )
10
11 type fakeStreamProvider struct {
12 chunks []provider.Chunk
13 }
14
15 func (f *fakeStreamProvider) Name() string { return "fake" }
16
17 func (f *fakeStreamProvider) Stream(_ context.Context, _ provider.Request) (<-chan provider.Chunk, error) {
18 ch := make(chan provider.Chunk, len(f.chunks))
19 for _, c := range f.chunks {
20 ch <- c
21 }
22 close(ch)
23 return ch, nil
24 }
25
26 type captureSink struct{ events []event.Event }
27
28 func (c *captureSink) Emit(e event.Event) { c.events = append(c.events, e) }
29
30 func TestSemanticRouterRecordsPricedUsage(t *testing.T) {
31 prov := &fakeStreamProvider{chunks: []provider.Chunk{
32 {Type: provider.ChunkText, Text: `["skill:review"]`},
33 {Type: provider.ChunkUsage, Usage: &provider.Usage{PromptTokens: 1000, CompletionTokens: 10}},
34 {Type: provider.ChunkDone},
35 }}
36 audit := &Audit{}
37 sink := &captureSink{}
38 pricing := &provider.Pricing{Input: 1, Output: 2}
39 r := &SemanticRouter{Provider: prov, Sink: sink, Model: "deepseek/deepseek-v4-flash", Pricing: pricing, Audit: audit}
40 catalog := Catalog{Entries: []Entry{{
41 ID: "skill:review", Kind: KindSkill, Name: "review",
42 Description: "review code changes", Status: StatusReady,
43 }}}
44
45 decision := r.RouteSemantic(context.Background(), "please review the code", catalog, RouteDecision{})
46 if len(decision.Candidates) == 0 {
47 t.Fatalf("semantic route produced no candidates: %+v", decision)
48 }
49 snap := audit.Snapshot()
50 if snap.RouterPromptTokens != 1000 || snap.RouterCompletionTokens != 10 {
51 t.Fatalf("router token counters not recorded: prompt=%d completion=%d", snap.RouterPromptTokens, snap.RouterCompletionTokens)
52 }
53 if snap.RouterCost <= 0 {
54 t.Fatalf("router cost must be priced, got %v", snap.RouterCost)
55 }
56 if snap.RouterLatencyMs < 0 {
57 t.Fatalf("router latency negative: %v", snap.RouterLatencyMs)
58 }
59 var usageEvent *event.Event
60 for _, e := range sink.events {
61 if e.Kind == event.Usage && e.Pricing == pricing {
62 copy := e
63 usageEvent = &copy
64 }
65 }
66 if usageEvent == nil {
67 t.Fatalf("usage event missing Pricing: %+v", sink.events)
68 }
69 if usageEvent.ModelRef != "deepseek/deepseek-v4-flash" {
70 t.Fatalf("usage event model ref = %q", usageEvent.ModelRef)
71 }
72 }
73
74 func TestMergeSemanticIDsSkipsUnavailableEntries(t *testing.T) {
75 catalog := Catalog{Entries: []Entry{
76 {ID: "mcp-tool:failed", Kind: KindMCPTool, Status: StatusFailed},
77 {ID: "mcp-server:disabled", Kind: KindMCPServer, Status: StatusDisabled},
78 {ID: "skill:ready", Kind: KindSkill, Status: StatusReady},
79 }}
80 decision := mergeSemanticIDs(RouteDecision{}, catalog, []string{
81 "mcp-tool:failed", "mcp-server:disabled", "skill:ready",
82 }, "test")
83 if len(decision.Candidates) != 1 || decision.Candidates[0].Entry.ID != "skill:ready" {
84 t.Fatalf("semantic candidates = %+v, want only ready entry", decision.Candidates)
85 }
86 }
87
87 lines GO