Recognize “刚刚上传” as @latest_upload and summarize the project’s newest CAS attachment (with clear parse pending/failed/no-asset replies) instead of searching only controlled workflow docs.
185 lines
6.3 KiB
Go
185 lines
6.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/coordinator"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/projectinstance"
|
|
"chat-agent-system/internal/storage"
|
|
"chat-agent-system/internal/workflowscope"
|
|
)
|
|
|
|
func TestSelectReadableWorkflowDocumentMatchesInitialRequirementsAlias(t *testing.T) {
|
|
docs := []docgov.WorkflowDocumentSummary{
|
|
{DocID: "doc_proj143_intake", TaskKey: "intake", Title: "用户初始需求收集表"},
|
|
{DocID: "doc_proj143_research", TaskKey: "research", Title: "技术与市场调研报告"},
|
|
}
|
|
doc, err := selectReadableWorkflowDocument(docs, "用户初始需求表", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("select: %v", err)
|
|
}
|
|
if doc.DocID != "doc_proj143_intake" {
|
|
t.Fatalf("doc=%s", doc.DocID)
|
|
}
|
|
}
|
|
|
|
func TestSelectReadableWorkflowDocumentDoesNotGuessWhenTargetMissing(t *testing.T) {
|
|
docs := []docgov.WorkflowDocumentSummary{
|
|
{DocID: "doc_a", TaskKey: "intake", Title: "用户初始需求收集表"},
|
|
{DocID: "doc_b", TaskKey: "research", Title: "调研报告"},
|
|
}
|
|
if _, err := selectReadableWorkflowDocument(docs, "", "", nil); err == nil {
|
|
t.Fatal("expected target-required error")
|
|
}
|
|
}
|
|
|
|
func TestSelectReadableWorkflowDocumentScopesToActiveWorkflow(t *testing.T) {
|
|
docs := []docgov.WorkflowDocumentSummary{
|
|
{DocID: "doc_proj1_research", TaskKey: "bd_research", Title: "调研报告"},
|
|
{DocID: "doc_proj1_proposal", TaskKey: "rd_proposal", Title: "项目建议书"},
|
|
}
|
|
scope := map[string]bool{"bd_research": true, "research": true}
|
|
|
|
doc, err := selectReadableWorkflowDocument(docs, "调研报告", "main", scope)
|
|
if err != nil {
|
|
t.Fatalf("in-scope select: %v", err)
|
|
}
|
|
if doc.TaskKey != "bd_research" {
|
|
t.Fatalf("want bd_research, got %s", doc.TaskKey)
|
|
}
|
|
|
|
_, err = selectReadableWorkflowDocument(docs, "项目建议书", "main", scope)
|
|
if err == nil {
|
|
t.Fatal("expected wrong-workflow error")
|
|
}
|
|
var wrong *coordinator.ErrWrongWorkflow
|
|
if !errors.As(err, &wrong) {
|
|
t.Fatalf("want ErrWrongWorkflow, got %T %v", err, err)
|
|
}
|
|
if !strings.Contains(wrong.Message, "切换") {
|
|
t.Fatalf("guidance should mention 切换, got %q", wrong.Message)
|
|
}
|
|
}
|
|
|
|
func TestSelectReadableWorkflowDocumentNoScopeKeepsLegacy(t *testing.T) {
|
|
docs := []docgov.WorkflowDocumentSummary{
|
|
{DocID: "doc_proj1_proposal", TaskKey: "rd_proposal", Title: "项目建议书"},
|
|
}
|
|
doc, err := selectReadableWorkflowDocument(docs, "项目建议书", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("legacy passthrough: %v", err)
|
|
}
|
|
if doc.TaskKey != "rd_proposal" {
|
|
t.Fatalf("want rd_proposal, got %s", doc.TaskKey)
|
|
}
|
|
}
|
|
|
|
func TestSelectReadableWeightedAllowsExactCrossRead(t *testing.T) {
|
|
workflowscope.SetModeForTest(workflowscope.ModeWeighted)
|
|
t.Cleanup(workflowscope.ResetModeForTest)
|
|
|
|
docs := []docgov.WorkflowDocumentSummary{
|
|
{DocID: "doc_proj1_research", TaskKey: "bd_research", Title: "调研报告"},
|
|
{DocID: "doc_proj1_proposal", TaskKey: "rd_proposal", Title: "项目建议书"},
|
|
}
|
|
scope := map[string]bool{"bd_research": true}
|
|
doc, err := selectReadableWorkflowDocument(docs, "项目建议书", "main", scope)
|
|
if err != nil {
|
|
t.Fatalf("weighted cross read: %v", err)
|
|
}
|
|
if doc.TaskKey != "rd_proposal" {
|
|
t.Fatalf("want rd_proposal, got %s", doc.TaskKey)
|
|
}
|
|
|
|
inScope, err := selectReadableWorkflowDocument(docs, "调研报告", "main", scope)
|
|
if err != nil || inScope.TaskKey != "bd_research" {
|
|
t.Fatalf("in-scope still wins: %v %v", inScope, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossWorkflowTaskVisible(t *testing.T) {
|
|
open := &projectinstance.StageAccess{
|
|
Restricted: true,
|
|
ViewableTaskKeys: map[string]bool{"bd_research": true},
|
|
}
|
|
scope := map[string]bool{"bd_research": true, "bd_hidden": true}
|
|
if !crossWorkflowTaskVisible(open, scope, "rd_proposal") {
|
|
t.Fatal("foreign key should be readable when the caller can view the active instance")
|
|
}
|
|
if crossWorkflowTaskVisible(open, scope, "bd_hidden") {
|
|
t.Fatal("in-scope but not viewable must be denied")
|
|
}
|
|
closed := &projectinstance.StageAccess{Restricted: true, ViewableTaskKeys: map[string]bool{}}
|
|
if crossWorkflowTaskVisible(closed, scope, "rd_proposal") {
|
|
t.Fatal("closed instance must deny cross read")
|
|
}
|
|
if !crossWorkflowTaskVisible(nil, scope, "rd_proposal") {
|
|
t.Fatal("unrestricted access must allow")
|
|
}
|
|
}
|
|
|
|
func TestPickLatestFileReference(t *testing.T) {
|
|
older := &storage.FileReference{ID: 1, FileName: "a.txt", CreatedAt: time.Unix(100, 0)}
|
|
newer := &storage.FileReference{ID: 2, FileName: "b.txt", CreatedAt: time.Unix(200, 0)}
|
|
got := pickLatestFileReference([]*storage.FileReference{older, newer, nil})
|
|
if got == nil || got.ID != 2 {
|
|
t.Fatalf("want id=2, got %+v", got)
|
|
}
|
|
sameTimeHigherID := &storage.FileReference{ID: 9, FileName: "c.txt", CreatedAt: time.Unix(200, 0)}
|
|
got = pickLatestFileReference([]*storage.FileReference{newer, sameTimeHigherID})
|
|
if got == nil || got.ID != 9 {
|
|
t.Fatalf("same created_at should prefer higher id, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveLatestUploadDocument_TextAsset(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
if srv.cas == nil {
|
|
t.Fatal("expected CAS")
|
|
}
|
|
projectID := int64(42)
|
|
userID := int64(7)
|
|
_, err := srv.cas.StoreFile(context.Background(), projectID, userID, "shockwave.txt", "material",
|
|
[]byte("冲击波治疗仪核心适应症:软组织损伤与疼痛管理。\n关键约束:治疗强度需医师确认。"), "text/plain")
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
|
|
doc, err := srv.resolveLatestUploadDocument(context.Background(), projectID)
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if doc == nil || !strings.Contains(doc.Markdown, "冲击波治疗仪") {
|
|
t.Fatalf("unexpected doc: %+v", doc)
|
|
}
|
|
if doc.Title != "shockwave" {
|
|
t.Fatalf("title=%q", doc.Title)
|
|
}
|
|
if doc.LifecycleState != "uploaded" {
|
|
t.Fatalf("lifecycle=%q", doc.LifecycleState)
|
|
}
|
|
}
|
|
|
|
func TestResolveLatestUploadDocument_NoAsset(t *testing.T) {
|
|
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
_, err := srv.resolveLatestUploadDocument(context.Background(), 99)
|
|
if err == nil {
|
|
t.Fatal("expected ErrNoUploadAsset")
|
|
}
|
|
var noAsset *coordinator.ErrNoUploadAsset
|
|
if !errors.As(err, &noAsset) {
|
|
t.Fatalf("want ErrNoUploadAsset, got %T %v", err, err)
|
|
}
|
|
}
|