Files
story/internal/saas/direct_story_integration_test.go
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

114 lines
6.3 KiB
Go

package saas
import (
"context"
"errors"
"strings"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/sharing"
"github.com/google/uuid"
)
func directStoryReviewFixture(t *testing.T) (homeFixture, string, string) {
t.Helper()
f := newHomeFixture(t, "Asia/Shanghai")
f.service.audit = audit.NewRepository()
session := f.direct(t, time.Now().UTC(), time.Minute, "ENDED")
f.exec(t, `UPDATE call_session SET budget_snapshot=budget_snapshot || '{"story_extraction_allowed":true}'::jsonb WHERE id=$1::uuid`, session)
f.exec(t, `INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,'native-user-turn',1,'USER','小时候我和爷爷在河边种了一棵树,爷爷教我每天浇水。',false,now())`, session)
story, public, revision := uuid.NewString(), uuid.NewString(), uuid.NewString()
f.exec(t, `INSERT INTO story_draft(id,public_id,tenant_id,subject_id,call_session_id,call_privacy_epoch,title,generation_version) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',$3::uuid,$4::uuid,0,'和爷爷种树',1)`, story, public, f.subject, session)
f.exec(t, `INSERT INTO story_revision(id,story_id,revision_no,content,author_type,model_ref,prompt_version) VALUES($1::uuid,$2::uuid,1,'小时候和爷爷在河边种树。','model','test','test')`, revision, story)
f.exec(t, `INSERT INTO story_source(story_revision_id,call_session_id,call_utterance_id,call_source_revision,source_start_offset,source_end_offset,source_role) VALUES($1::uuid,$2::uuid,'native-user-turn',1,0,10,'subject')`, revision, session)
f.exec(t, `UPDATE story_draft SET current_revision_id=$2::uuid WHERE id=$1::uuid`, story, revision)
return f, public, session
}
func TestDirectStoryReviewRequiresSourceAccessAndCurrentEvidence(t *testing.T) {
f, public, session := directStoryReviewFixture(t)
ctx := context.Background()
story, err := f.service.GetStory(ctx, f.owner, public)
if err != nil || story.ConversationID != "" || story.CallSessionID != session || len(story.Sources) != 1 {
t.Fatalf("native story=%+v err=%v", story, err)
}
source := story.Sources[0]
if source.TurnID != "native-user-turn" || source.UtteranceID != source.TurnID || source.CallSessionID != session || source.SourceRevision == nil || *source.SourceRevision != 1 {
t.Fatalf("native source=%+v", source)
}
editor := identity.Principal{Issuer: testIssuer, Subject: testEditorSubject}
outsider := identity.Principal{Issuer: testIssuer, Subject: testOutsideSubject}
for _, principal := range []identity.Principal{editor, outsider} {
if _, err := f.service.GetStory(ctx, principal, public); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("ungranted source read error=%v", err)
}
}
f.exec(t, `INSERT INTO memory_access_grant(subject_id,user_id,can_read,can_write,sensitive_read) VALUES($1::uuid,'10000000-0000-0000-0000-000000000102',true,false,false)`, f.subject)
items, err := f.service.ListSubjectStories(ctx, editor, f.public, "")
if err != nil || len(items) != 0 {
t.Fatalf("source access required for native list: items=%+v err=%v", items, err)
}
f.exec(t, `UPDATE memory_access_grant SET sensitive_read=true WHERE subject_id=$1::uuid`, f.subject)
if _, err = f.service.GetStory(ctx, editor, public); err != nil {
t.Fatalf("granted source read: %v", err)
}
if _, err = f.service.ReviseStory(ctx, editor, public, "修改", "核对", "native-story-denied", 1, ""); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("read-only grant allowed editing: %v", err)
}
f.exec(t, `UPDATE call_utterance SET source_revision=2,text_content='更正后的原话' WHERE session_id=$1::uuid`, session)
if _, err = f.service.GetStory(ctx, f.owner, public); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("stale source read: %v", err)
}
if _, _, err = f.service.ConfirmStory(ctx, f.owner, public, "核对", "native-story-stale", 1); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("stale source confirmation: %v", err)
}
items, err = f.service.ListSubjectStories(ctx, f.owner, f.public, "")
if err != nil || len(items) != 0 {
t.Fatalf("stale source list: items=%+v err=%v", items, err)
}
}
type directStorySharingStore struct{}
func (directStorySharingStore) PresignMediaDownload(context.Context, string, time.Duration, string, string, bool) (string, error) {
return "https://objects.invalid/unused", nil
}
func TestDirectStorySharedEditPreservesNativeSourcesAndWithdrawalHidesIt(t *testing.T) {
f, public, session := directStoryReviewFixture(t)
ctx := context.Background()
service, err := sharing.NewService(f.pool, audit.NewRepository(), directStorySharingStore{}, []byte(strings.Repeat("s", 32)), time.Minute)
if err != nil {
t.Fatal(err)
}
var recipient string
if err := f.pool.QueryRow(ctx, `SELECT public_id::text FROM user_account WHERE id='10000000-0000-0000-0000-000000000102'`).Scan(&recipient); err != nil {
t.Fatal(err)
}
grant, _, err := service.Create(ctx, f.owner, uuid.NewString(), "native-sharing-create", sharing.CreateInput{ResourceType: "STORY", ResourceID: public, GranteeUserID: recipient, Permission: "EDIT", Purpose: "核对回忆", ExpiresAt: time.Now().Add(time.Hour), LoginRequired: true})
if err != nil {
t.Fatal(err)
}
editor := identity.Principal{Issuer: testIssuer, Subject: testEditorSubject}
if _, err = service.EditStory(ctx, editor, grant.ID, "小时候我和爷爷一起在河边种树。", "核对原话", "native-sharing-edit", 1); err != nil {
t.Fatal(err)
}
story, err := f.service.GetStory(ctx, f.owner, public)
if err != nil || story.RevisionNo != 2 || len(story.Sources) != 1 || story.Sources[0].CallSessionID != session || story.Sources[0].UtteranceID != "native-user-turn" {
t.Fatalf("shared revision lost evidence: story=%+v err=%v", story, err)
}
if _, err = service.Access(ctx, editor, grant.ID, "native-sharing-read"); err != nil {
t.Fatal(err)
}
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='story_extraction'`, f.subject)
if _, err = service.Access(ctx, editor, grant.ID, "native-sharing-withdrawn-read"); err == nil {
t.Fatal("shared story remained readable after withdrawal")
}
if _, err = service.EditStory(ctx, editor, grant.ID, "撤回后修改", "应拒绝", "native-sharing-withdrawn-edit", 2); err == nil {
t.Fatal("shared story remained editable after withdrawal")
}
}