Files
knowledge-governance/internal/qaopsmirror/ai_work_integration_test.go
T
姚章浩 65653e5c01
build / build (push) Successful in 1m14s
fix(qa): 已覆盖正确性改为问题答案二元评估
已覆盖只允许正确或错误,未覆盖可待确认,缺引用不再阻止正确结论。
复用统一事务写入与不可变审计重评旧待确认,保留原问答和人工结论。
验证:全仓 make verify、真实隔离 PostgreSQL 全包与 race 回归通过。
2026-10-24 16:47:09 +08:00

154 lines
7.0 KiB
Go

package qaopsmirror
import (
"context"
"encoding/json"
"knowledge-governance/internal/dobotaiops"
pg "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/qaopsai"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestAIManualBatchPersistsProcessesOnlyMissingFieldsAndReplaysWithoutExtraCalls(t *testing.T) {
pool := mirrorFixture(t)
ctx := context.Background()
tenant := "fixture-ai-work"
keyFile := filepath.Join(t.TempDir(), "master-key")
if err := os.WriteFile(keyFile, []byte(strings.Repeat("a", 32)), 0600); err != nil {
t.Fatal(err)
}
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
var body map[string]any
_ = json.NewDecoder(r.Body).Decode(&body)
messages, _ := body["messages"].([]any)
probe := len(messages) > 0 && strings.Contains(messages[0].(map[string]any)["content"].(string), "connection probe")
result := `{"ok":true}`
if !probe {
result = `{"fields":{"is_correct":{"value":"否","basis":"semantic_only","reason":"回答未给出所问的连接配置步骤","quotes":[]},"problem_type":{"value":"使用配置","basis":"semantic_only","reason":"问题询问连接配置","quotes":[]},"root_cause":{"value":"","basis":"insufficient_evidence","reason":"无法从现有信息确定根因","quotes":[]}}}`
}
if !probe && strings.Contains(messages[0].(map[string]any)["content"].(string), "翻成中文") {
result = `{"question_chinese":"MG400 如何连接?"}`
}
_ = json.NewEncoder(w).Encode(map[string]any{"model": "fixture-actual-model", "choices": []any{map[string]any{"message": map[string]string{"content": result}, "finish_reason": "stop"}}})
}))
defer server.Close()
ai := qaopsai.NewStore(pool, tenant, qaopsai.Options{KeyFile: keyFile, AllowPrivate: true})
if _, err := ai.SaveSettings(ctx, qaopsai.SaveSettings{BaseURL: server.URL + "/v1", Model: "fixture", KeyAction: "replace", APIKey: "fixture-only"}, "fixture-admin"); err != nil {
t.Fatal(err)
}
master := NewStore(pool, tenant)
entry := fixtureEntry("work-record", "fixture answer")
var wire map[string]any
_ = json.Unmarshal(entry.Payload, &wire)
wire["question"] = "How do I connect MG400?"
wire["master_fields"] = map[string]string{"product_category": "MG400"}
entry.Payload, _ = json.Marshal(wire)
job := claimMirrorJob(t, master, pg.NewStore(pool), "work-seed")
if err := master.HandleSync(ctx, *job, func(context.Context) (Source, error) {
return &fixtureSource{rows: []dobotaiops.MirrorRecord{entry}}, nil
}); err != nil {
t.Fatal(err)
}
preview, err := ai.Preview(ctx, qaopsai.Scope{Kind: "evaluation", IDs: []string{"work-record"}}, "fixture-admin")
if err != nil || preview.Eligible != 1 {
t.Fatalf("eligible preview not produced: %+v %v", preview, err)
}
batch, err := ai.Start(ctx, preview.ID, strings.Repeat("n", 24), "fixture-admin")
if err != nil || batch.Total != 1 {
t.Fatal("frozen persistent batch not created", err)
}
workerCtx, stopWorker := context.WithCancel(ctx)
defer stopWorker()
workerDone := make(chan struct{})
go func() { ai.Run(workerCtx, func(error) {}); close(workerDone) }()
deadline := time.Now().Add(5 * time.Second)
for {
current, e := ai.GetBatch(ctx, batch.ID)
if e != nil {
t.Fatal(e)
}
if current.Succeeded == 1 {
break
}
if time.Now().After(deadline) {
t.Fatal("background loop did not consume work after the initiating request returned")
}
time.Sleep(20 * time.Millisecond)
}
stopWorker()
select {
case <-workerDone:
case <-time.After(5 * time.Second):
t.Fatal("background loop did not stop cleanly")
}
var worked bool
batch, err = ai.GetBatch(ctx, batch.ID)
if err != nil || batch.Succeeded != 1 || batch.Queued != 0 || batch.Failed != 0 || calls != 2 {
t.Fatalf("wrong real work counters: %+v err=%v calls=%d", batch, err, calls)
}
replay, err := ai.Start(ctx, preview.ID, strings.Repeat("n", 24), "fixture-admin")
if err != nil || replay.ID != batch.ID {
t.Fatal("batch nonce was not idempotent", err)
}
worked, err = ai.ProcessOne(ctx)
if err != nil || worked || calls != 2 {
t.Fatal("completed work was called again", err)
}
projected, err := master.GetMasterRecord(ctx, tenant, "work-record")
if err != nil || projected.MasterFields["is_correct"] != "否" || projected.MasterFields["problem_type"] != "使用配置" || !strings.Contains(string(projected.BackgroundTasks), "succeeded") {
t.Fatal("saved model assessment not consumed by master list/detail projection", err)
}
projectedViews, err := master.ReadMasterQualityViews(ctx, []dobotaiops.QualityKey{{ID: projected.ID, InputHash: projected.QualityInputHash}})
if err != nil || projectedViews[projected.ID].Effective.ProblemType != "使用配置" {
t.Fatal("master quality/dashboard projection does not consume model output", err)
}
noMore, err := ai.Preview(ctx, qaopsai.Scope{Kind: "evaluation", IDs: []string{"work-record"}}, "fixture-admin")
if err != nil || noMore.Eligible != 0 {
t.Fatal("legitimately empty root was repeatedly requeued", err)
}
translationPreview, err := ai.Preview(ctx, qaopsai.Scope{Kind: "translation", IDs: []string{"work-record"}}, "fixture-admin")
if err != nil || translationPreview.Eligible != 1 {
t.Fatal("independent translation preview missing", err)
}
translatedBatch, err := ai.Start(ctx, translationPreview.ID, strings.Repeat("t", 24), "fixture-admin")
if err != nil {
t.Fatal(err)
}
if _, err = pool.Exec(ctx, `UPDATE qa_ops_ai_runtime SET next_request_at=clock_timestamp()-interval '1 second' WHERE tenant_key=$1`, tenant); err != nil {
t.Fatal(err)
}
worked, err = ai.ProcessOne(ctx)
if err != nil || !worked {
t.Fatal("translation work did not run", err)
}
translatedBatch, err = ai.GetBatch(ctx, translatedBatch.ID)
if err != nil || translatedBatch.Succeeded != 1 || calls != 3 {
t.Fatalf("translation mixed with analysis counters: %+v %v calls=%d", translatedBatch, err, calls)
}
projected, err = master.GetMasterRecord(ctx, tenant, "work-record")
if err != nil || projected.MasterFields["question_chinese"] != "MG400 如何连接?" || projected.Question != "How do I connect MG400?" {
t.Fatal("translation not read back or original question overwritten", err)
}
page, err := master.ListMasterRecords(ctx, dobotaiops.RecordQuery{Window: dobotaiops.Window{TenantKey: tenant, Start: projected.AskedAt.Add(-time.Second), End: projected.AskedAt.Add(time.Second)}, Keyword: "如何连接", Limit: 100})
if err != nil || page.Total != 1 || len(page.Items) != 1 {
t.Fatal("Chinese search does not consume saved translation", err)
}
var payload []byte
if err = pool.QueryRow(ctx, `SELECT payload FROM qa_ops_master_records WHERE tenant_key=$1 AND id='work-record'`, tenant).Scan(&payload); err != nil {
t.Fatal(err)
}
var unchanged map[string]any
_ = json.Unmarshal(payload, &unchanged)
if unchanged["master_fields"].(map[string]any)["product_category"] != "MG400" || strings.Contains(string(payload), "待确认") {
t.Fatal("AI overwrote original stored fields instead of using an audited effective layer")
}
}