ba2f117265
Add a shared structured query builder (knowledge_query.go) and switch all generation paths to it, so retrieval queries carry task intent (type, brand, region, keywords, key points) instead of a raw prompt. ResolveContext now rewrites the query into multiple sub-questions via the URL-markdown model, embeds and searches each, and merges/dedupes candidates before rerank; falls back to parsed fallback questions when rewrite is unavailable or returns too few. Resolve logs gain query list, count, and rewrite stage/model for observability. - knowledge_query.go/_test.go: BuildKnowledgeQuery + intent normalization - knowledge_service.go: per-query embed/search loop, query rewrite, logs - template_prompt/template_service/prompt_generate/article_imitation/ kol_generation_worker: adopt the shared query builder - generation_observability: richer task error logging
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestGenerationTaskLogFieldsUsesStableStructuredKeys(t *testing.T) {
|
|
fields := GenerationTaskLogFields(GenerationTaskLogContext{
|
|
TaskID: 11,
|
|
TenantID: 22,
|
|
OperatorID: 33,
|
|
ArticleID: 44,
|
|
ReservationID: 55,
|
|
TaskType: "template",
|
|
Stage: "llm_generate",
|
|
AttemptCount: 2,
|
|
LeaseOwner: "worker-1",
|
|
StatusBefore: "running",
|
|
StatusAfter: "failed",
|
|
Result: GenerationTaskResultFailure,
|
|
AnomalyType: "article_failed_task_running",
|
|
Severity: "critical",
|
|
})
|
|
|
|
values := map[string]any{}
|
|
for _, field := range fields {
|
|
values[field.Key] = field.Interface
|
|
if field.String != "" {
|
|
values[field.Key] = field.String
|
|
}
|
|
if field.Integer != 0 {
|
|
values[field.Key] = field.Integer
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, "article_generation", values["event_domain"])
|
|
assert.Equal(t, int64(11), values["task_id"])
|
|
assert.Equal(t, int64(22), values["tenant_id"])
|
|
assert.Equal(t, int64(44), values["article_id"])
|
|
assert.Equal(t, "template", values["task_type"])
|
|
assert.Equal(t, "llm_generate", values["stage"])
|
|
assert.Equal(t, "failed", values["status_after"])
|
|
assert.Equal(t, "critical", values["severity"])
|
|
}
|
|
|
|
func TestGenerationTaskMetricsRecordsEventsAndPrometheus(t *testing.T) {
|
|
resetGenerationTaskMetricsForTest()
|
|
t.Cleanup(resetGenerationTaskMetricsForTest)
|
|
|
|
RecordGenerationTaskEvent(GenerationTaskEventTransition, GenerationTaskLogContext{
|
|
TaskType: "template",
|
|
Stage: "worker_claim",
|
|
StatusAfter: GenerationTaskStatusRunning,
|
|
Result: GenerationTaskResultSuccess,
|
|
})
|
|
RecordGenerationTaskEvent(GenerationTaskEventCleanupFailure, GenerationTaskLogContext{
|
|
TaskType: "template",
|
|
Stage: "refund_reservation",
|
|
Result: GenerationTaskResultFailure,
|
|
Severity: "critical",
|
|
})
|
|
RecordGenerationTaskEvent(GenerationTaskEventStateAnomaly, GenerationTaskLogContext{
|
|
TaskType: "template",
|
|
Stage: "state_check",
|
|
AnomalyType: "completed_task_missing_version",
|
|
Severity: "critical",
|
|
})
|
|
|
|
snapshot := GenerationTaskMetricsSnapshotValue()
|
|
assert.Equal(t, int64(1), snapshot.TransitionTotal)
|
|
assert.Equal(t, int64(1), snapshot.CleanupFailureTotal)
|
|
assert.Equal(t, int64(1), snapshot.StateAnomalyTotal)
|
|
assert.Equal(t, "state_anomaly", snapshot.LastEvent)
|
|
assert.Equal(t, "completed_task_missing_version", snapshot.LastAnomalyType)
|
|
assert.NotNil(t, snapshot.LastEventAt)
|
|
|
|
prometheus := GenerationTaskMetricsPrometheus()
|
|
assert.Contains(t, prometheus, "article_generation_task_transitions_total 1")
|
|
assert.Contains(t, prometheus, "article_generation_task_cleanup_failures_total 1")
|
|
assert.Contains(t, prometheus, "article_generation_task_state_anomalies_total 1")
|
|
lastEventLine := findPrometheusMetricLine(prometheus, "article_generation_task_last_event_info")
|
|
assert.Contains(t, lastEventLine, `event="state_anomaly"`)
|
|
assert.Contains(t, lastEventLine, `task_type="template"`)
|
|
assert.Contains(t, lastEventLine, `stage="state_check"`)
|
|
assert.Contains(t, lastEventLine, `result=""`)
|
|
assert.Contains(t, lastEventLine, `anomaly_type="completed_task_missing_version"`)
|
|
assert.Contains(t, lastEventLine, `severity="critical"`)
|
|
assert.True(t, strings.HasSuffix(lastEventLine, " 1"), lastEventLine)
|
|
}
|
|
|
|
func TestFormatGenerationFailureKeepsAppErrorBriefForUI(t *testing.T) {
|
|
err := response.ErrServiceUnavailable(50353, "embedding_failed", "request siliconflow: timeout")
|
|
|
|
got := formatGenerationFailure("knowledge_resolve", err)
|
|
|
|
assert.Equal(t, "知识库检索失败 [knowledge_resolve]: embedding_failed", got)
|
|
}
|
|
|
|
func TestGenerationTaskErrorFieldsIncludesAppErrorDetailForLogs(t *testing.T) {
|
|
err := response.ErrServiceUnavailable(50353, "embedding_failed", "request siliconflow: timeout")
|
|
|
|
fields := GenerationTaskErrorFields(err)
|
|
values := map[string]any{}
|
|
for _, field := range fields {
|
|
switch {
|
|
case field.String != "":
|
|
values[field.Key] = field.String
|
|
case field.Integer != 0:
|
|
values[field.Key] = int(field.Integer)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, "embedding_failed", values["error_message"])
|
|
assert.Equal(t, "request siliconflow: timeout", values["error_detail"])
|
|
assert.Equal(t, 50353, values["error_code"])
|
|
assert.Equal(t, 503, values["http_status"])
|
|
}
|
|
|
|
func findPrometheusMetricLine(output, metricName string) string {
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if strings.HasPrefix(line, metricName+"{") || strings.HasPrefix(line, metricName+" ") {
|
|
return line
|
|
}
|
|
}
|
|
return ""
|
|
}
|