Files
knowledge-governance/internal/qaopsmirror/ai_automatic_integration_test.go
T
姚章浩 533db535f1
build / build (push) Successful in 1m12s
feat(qaops): 接入真实模型补缺任务与总表看板
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。

验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。

上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
2026-10-19 14:29:49 +08:00

116 lines
4.7 KiB
Go

package qaopsmirror
import (
"context"
"encoding/json"
pg "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/qaopsai"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestAIAutomationExcludesInitialHistoryPausesAndResumesPersistentIncrementalBacklog(t *testing.T) {
pool := mirrorFixture(t)
ctx := context.Background()
tenant := "fixture-ai-automatic"
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 request struct {
Messages []struct{ Content string } `json:"messages"`
}
_ = json.NewDecoder(r.Body).Decode(&request)
result := `{"question_chinese":"MG400 如何连接?"}`
if len(request.Messages) > 0 && strings.Contains(request.Messages[0].Content, "connection probe") {
result = `{"ok":true}`
}
_ = json.NewEncoder(w).Encode(map[string]any{"model": "fixture", "choices": []any{map[string]any{"message": map[string]string{"content": result}, "finish_reason": "stop"}}})
}))
defer server.Close()
options := qaopsai.Options{KeyFile: keyFile, AllowPrivate: true}
ai := qaopsai.NewStore(pool, tenant, options)
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)
queue := pg.NewStore(pool)
source := &fixtureSource{}
seed := func(id string) {
t.Helper()
entry := fixtureEntry(id, "fixture answer")
var wire map[string]any
_ = json.Unmarshal(entry.Payload, &wire)
wire["question"] = "How do I connect MG400?"
wire["asked_at"] = time.Date(2026, 10, 14, 0, 0, 0, 0, time.UTC).Add(time.Duration(len(source.rows)) * time.Minute).Format(time.RFC3339)
entry.Payload, _ = json.Marshal(wire)
source.rows = append(source.rows, entry)
job := claimMirrorJob(t, master, queue, id)
if err := master.HandleSync(ctx, *job, func(context.Context) (Source, error) { return source, nil }); err != nil {
t.Fatal(err)
}
var masterCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM qa_ops_master_records WHERE tenant_key=$1`, tenant).Scan(&masterCount); err != nil || masterCount != len(source.rows) {
t.Fatal("fixture must admit distinct three-key business events before testing automation", err)
}
if err := queue.Complete(ctx, job.ID, job.LeaseOwner, time.Now()); err != nil {
t.Fatal(err)
}
}
seed("historical-before-enable")
if cfg, err := ai.SetAutomation(ctx, "translation", true, "fixture-admin"); err != nil || !cfg.AutomaticTranslation {
t.Fatal("explicit automation enable failed", err)
}
if n, err := ai.ScanAutomatic(ctx); err != nil || n != 0 {
t.Fatal("enabling automation backfilled initial history", err)
}
seed("new-after-enable")
if n, err := ai.ScanAutomatic(ctx); err != nil || n != 1 {
t.Fatal("new completed source record was not persistently queued", err)
}
if _, err := ai.SetAutomation(ctx, "translation", false, "fixture-admin"); err != nil {
t.Fatal(err)
}
if worked, err := ai.ProcessOne(ctx); err != nil || worked || calls != 1 {
t.Fatal("disabled automation still sent a request", err)
}
var paused int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM qa_ops_ai_work WHERE tenant_key=$1 AND state='paused'`, tenant).Scan(&paused); err != nil || paused != 1 {
t.Fatal("stop did not preserve paused backlog", err)
}
seed("new-while-disabled")
if n, err := ai.ScanAutomatic(ctx); err != nil || n != 0 {
t.Fatal("disabled automation enqueued new work", err)
}
if _, err := ai.SetAutomation(ctx, "translation", true, "fixture-admin"); err != nil {
t.Fatal(err)
}
if n, err := ai.ScanAutomatic(ctx); err != nil || n != 1 {
t.Fatal("resumed increment did not include disclosed disabled-period backlog", err)
}
restarted := qaopsai.NewStore(pool, tenant, options)
for i := 0; i < 2; i++ {
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)
}
if worked, err := restarted.ProcessOne(ctx); err != nil || !worked {
t.Fatal("restart lost persistent backlog", err)
}
}
var completed int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM qa_ops_ai_work WHERE tenant_key=$1 AND state='succeeded'`, tenant).Scan(&completed); err != nil || completed != 2 || calls != 3 {
t.Fatalf("wrong completed work/call count: completed=%d calls=%d error=%v", completed, calls, err)
}
if n, err := restarted.ScanAutomatic(ctx); err != nil || n != 0 {
t.Fatal("automatic jobs duplicated after completion/restart", err)
}
}