WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupDigitalHumanTest(t *testing.T) (*DigitalHumanRepository, *gorm.DB, types.DigitalHumanScope, *types.DigitalHuman) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { sqlDB.Close() })
|
||||
require.NoError(t, db.AutoMigrate(&types.CustomAgent{}, &types.DigitalHuman{}, &types.DigitalHumanVersion{}, &types.DigitalHumanBinding{}, &types.DigitalHumanJob{}))
|
||||
require.NoError(t, db.Exec("CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT, deleted_at DATETIME)").Error)
|
||||
require.NoError(t, db.Exec("CREATE TABLE im_channels (id TEXT PRIMARY KEY, agent_id TEXT, tenant_id INTEGER, enabled BOOLEAN, updated_at DATETIME, deleted_at DATETIME)").Error)
|
||||
require.NoError(t, db.Exec("INSERT INTO users (id, username) VALUES (?, ?)", "owner", "Teacher Owner").Error)
|
||||
scope := types.DigitalHumanScope{TenantID: 7, OwnerID: "owner"}
|
||||
human := &types.DigitalHuman{ID: uuid.NewString(), TenantID: scope.TenantID, OwnerID: scope.OwnerID, Name: "Teacher", PrimaryAgentID: uuid.NewString(), LatestVersion: 1, Status: "draft", Revision: 1}
|
||||
repo := NewDigitalHumanRepository(db)
|
||||
require.NoError(t, repo.Create(context.Background(), human, map[string]string{"tone": "measured"}))
|
||||
return repo, db, scope, human
|
||||
}
|
||||
|
||||
func TestDigitalHumanProgressRenewsLeaseAndFencesExpiredOrForeignJobs(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
job := &types.DigitalHumanJob{ID: "job", TenantID: scope.TenantID, OwnerID: scope.OwnerID, DigitalHumanID: human.ID, Status: "running", ExpiresAt: time.Now().Add(time.Minute)}
|
||||
require.NoError(t, repo.StartJob(ctx, scope, human.ID, human.Revision, job))
|
||||
require.NoError(t, repo.JobProgress(ctx, scope, job.ID, 40, "batch 4/10"))
|
||||
stored, err := repo.Job(ctx, scope, human.ID, job.ID)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, stored.ExpiresAt.Sub(time.Now()), 9*time.Minute)
|
||||
foreign := scope
|
||||
foreign.OwnerID = "other-owner"
|
||||
require.ErrorIs(t, repo.JobProgress(ctx, foreign, job.ID, 50, "foreign"), types.ErrDigitalHumanConflict)
|
||||
require.NoError(t, db.Model(&types.DigitalHumanJob{}).Where("id = ?", job.ID).Update("expires_at", time.Now().Add(-time.Minute)).Error)
|
||||
require.ErrorIs(t, repo.JobProgress(ctx, scope, job.ID, 60, "late"), types.ErrDigitalHumanConflict)
|
||||
}
|
||||
|
||||
func TestDigitalHumanCheckpointIsPrivateOwnerScopedAndFenced(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
key := strings.Repeat("a", 64)
|
||||
checkpoint := &types.DigitalHumanDistillCheckpoint{CompletedBatches: 1, TotalBatches: 3, Persona: map[string]string{"tone": "private draft"}}
|
||||
job := &types.DigitalHumanJob{ID: "checkpoint-job", TenantID: scope.TenantID, OwnerID: scope.OwnerID, DigitalHumanID: human.ID, Status: "running", ExpiresAt: time.Now().Add(time.Minute)}
|
||||
require.NoError(t, repo.StartJob(ctx, scope, human.ID, human.Revision, job))
|
||||
require.NoError(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, job.ID, key, checkpoint))
|
||||
foreign := scope
|
||||
foreign.OwnerID = "other"
|
||||
require.Error(t, repo.SaveDistillCheckpoint(ctx, foreign, human.ID, job.ID, key, checkpoint))
|
||||
require.ErrorIs(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, job.ID, strings.Repeat("b", 64), checkpoint), types.ErrDigitalHumanConflict)
|
||||
require.ErrorIs(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, job.ID, key, &types.DigitalHumanDistillCheckpoint{CompletedBatches: 0, TotalBatches: 3}), types.ErrDigitalHumanConflict)
|
||||
require.NoError(t, db.Model(&types.DigitalHumanJob{}).Where("id = ?", job.ID).Update("expires_at", time.Now().Add(-time.Minute)).Error)
|
||||
require.ErrorIs(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, job.ID, key, checkpoint), types.ErrDigitalHumanConflict)
|
||||
require.NoError(t, repo.RecoverExpiredJobs(ctx, scope))
|
||||
found, err := repo.DistillCheckpoint(ctx, scope, human.ID, key)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, checkpoint, found)
|
||||
for _, lookup := range []struct {
|
||||
scope types.DigitalHumanScope
|
||||
human, fingerprint string
|
||||
}{
|
||||
{foreign, human.ID, key},
|
||||
{types.DigitalHumanScope{TenantID: 8, OwnerID: scope.OwnerID}, human.ID, key},
|
||||
{scope, "another-human", key},
|
||||
{scope, human.ID, strings.Repeat("c", 64)},
|
||||
} {
|
||||
found, err := repo.DistillCheckpoint(ctx, lookup.scope, lookup.human, lookup.fingerprint)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, found)
|
||||
}
|
||||
detail, err := repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
newJob := &types.DigitalHumanJob{ID: "new-job", TenantID: scope.TenantID, OwnerID: scope.OwnerID, DigitalHumanID: human.ID, Status: "running", ExpiresAt: time.Now().Add(time.Minute)}
|
||||
require.NoError(t, repo.StartJob(ctx, scope, human.ID, detail.Revision, newJob))
|
||||
// Even a stale worker with a stale local lease may not change old retry state.
|
||||
require.ErrorIs(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, job.ID, key, checkpoint), types.ErrDigitalHumanConflict)
|
||||
require.NoError(t, repo.SaveDistillCheckpoint(ctx, scope, human.ID, newJob.ID, key, checkpoint))
|
||||
}
|
||||
|
||||
func TestDigitalHumanDeletionCommitsChannelsTogether(t *testing.T) {
|
||||
for _, rollback := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "commit", true: "rollback"}[rollback], func(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
for _, row := range []struct {
|
||||
id, agent string
|
||||
tenant uint64
|
||||
}{
|
||||
{"owned", human.PrimaryAgentID, scope.TenantID},
|
||||
{"other-agent", "other-agent", scope.TenantID},
|
||||
{"other-tenant", human.PrimaryAgentID, scope.TenantID + 1},
|
||||
} {
|
||||
require.NoError(t, db.Exec("INSERT INTO im_channels (id, agent_id, tenant_id, enabled) VALUES (?, ?, ?, true)", row.id, row.agent, row.tenant).Error)
|
||||
}
|
||||
if rollback {
|
||||
require.NoError(t, db.Exec("CREATE TRIGGER reject_digital_human_delete BEFORE UPDATE OF deleted_at ON digital_humans BEGIN SELECT RAISE(ABORT, 'simulated write failure'); END").Error)
|
||||
}
|
||||
ids, err := repo.DeleteWithIMChannels(context.Background(), scope, human.ID)
|
||||
var channels int64
|
||||
require.NoError(t, db.Table("im_channels").Where("deleted_at IS NULL AND enabled = true").Count(&channels).Error)
|
||||
var agents int64
|
||||
require.NoError(t, db.Model(&types.CustomAgent{}).Where("id = ?", human.PrimaryAgentID).Count(&agents).Error)
|
||||
if rollback {
|
||||
require.Error(t, err)
|
||||
require.Empty(t, ids)
|
||||
require.Equal(t, int64(3), channels)
|
||||
require.Equal(t, int64(1), agents)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"owned"}, ids)
|
||||
require.Equal(t, int64(2), channels)
|
||||
require.Zero(t, agents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigitalHumanOwnerIsolationAndAdminRead(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
var agent types.CustomAgent
|
||||
require.NoError(t, db.First(&agent, "id = ?", human.PrimaryAgentID).Error)
|
||||
require.Equal(t, human.Name, agent.Name)
|
||||
require.Equal(t, scope.OwnerID, agent.CreatedBy)
|
||||
other := types.DigitalHumanScope{TenantID: scope.TenantID, OwnerID: "other"}
|
||||
_, err := repo.Get(ctx, other, human.ID)
|
||||
require.ErrorIs(t, err, types.ErrDigitalHumanNotFound)
|
||||
other.ReadAll = true
|
||||
detail, err := repo.Get(ctx, other, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, detail.Versions, 1)
|
||||
require.Len(t, detail.Bindings, 1)
|
||||
require.Equal(t, "Teacher Owner", detail.CreatorName)
|
||||
listed, err := repo.List(ctx, other, 0, 100)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listed, 1)
|
||||
require.Equal(t, "Teacher Owner", listed[0].CreatorName)
|
||||
require.ErrorIs(t, repo.Delete(ctx, other, human.ID), types.ErrDigitalHumanNotFound)
|
||||
require.ErrorIs(t, repo.Review(ctx, other, human.ID, 1, true, ""), types.ErrDigitalHumanNotFound)
|
||||
other.TenantID++
|
||||
_, err = repo.Get(ctx, other, human.ID)
|
||||
require.ErrorIs(t, err, types.ErrDigitalHumanNotFound)
|
||||
}
|
||||
|
||||
func TestDigitalHumanReviewBindingsAndDeletionPreserveAgentEdits(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
extra := types.CustomAgent{ID: uuid.NewString(), TenantID: scope.TenantID, CreatedBy: scope.OwnerID, Name: "Content", Config: types.CustomAgentConfig{SystemPrompt: "Original task rules"}}
|
||||
require.NoError(t, db.Create(&extra).Error)
|
||||
require.NoError(t, repo.Bind(ctx, scope, human.ID, extra.ID))
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 1, true, "approved"))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.Contains(t, extra.Config.SystemPrompt, "measured")
|
||||
extra.Config.SystemPrompt = "Revised task rules\n" + extra.Config.SystemPrompt + "\nKeep continuity"
|
||||
require.NoError(t, db.Save(&extra).Error)
|
||||
h, err := repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, repo.Update(ctx, scope, human.ID, h.Revision, h.Name, h.Description, h.Avatar, h.ModelID, map[string]string{"tone": "clear"}))
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 2, true, ""))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.Contains(t, extra.Config.SystemPrompt, "Revised task rules")
|
||||
require.Contains(t, extra.Config.SystemPrompt, "Keep continuity")
|
||||
require.NotContains(t, extra.Config.SystemPrompt, "measured")
|
||||
require.Equal(t, 1, strings.Count(extra.Config.SystemPrompt, ":start -->"))
|
||||
require.NoError(t, repo.Delete(ctx, scope, human.ID))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.NotContains(t, extra.Config.SystemPrompt, "weknora:digital-human:")
|
||||
require.Contains(t, extra.Config.SystemPrompt, "Keep continuity")
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.CustomAgent{}).Where("id = ?", human.PrimaryAgentID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
require.NoError(t, db.Model(&types.DigitalHumanBinding{}).Where("digital_human_id = ?", human.ID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
func TestDigitalHumanAgentCopyKeepsLiveBinding(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.WithValue(context.Background(), types.TenantIDContextKey, scope.TenantID)
|
||||
ctx = context.WithValue(ctx, types.UserIDContextKey, scope.OwnerID)
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 1, true, "approved"))
|
||||
var source types.CustomAgent
|
||||
require.NoError(t, db.First(&source, "id = ?", human.PrimaryAgentID).Error)
|
||||
copy := source
|
||||
copy.ID, copy.Name = uuid.NewString(), "Copied persona"
|
||||
agents := &customAgentRepository{db: db}
|
||||
require.NoError(t, agents.CreateAgentCopy(ctx, source.ID, ©))
|
||||
require.NoError(t, agents.CheckDigitalHumanAgentExecution(ctx, ©))
|
||||
detail, err := repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, detail.Bindings, 2)
|
||||
require.NoError(t, repo.Update(ctx, scope, human.ID, detail.Revision, detail.Name, detail.Description, detail.Avatar, detail.ModelID, map[string]string{"tone": "new voice"}))
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 2, true, "approved"))
|
||||
require.NoError(t, db.First(©, "id = ?", copy.ID).Error)
|
||||
require.Contains(t, copy.Config.SystemPrompt, "new voice")
|
||||
require.NoError(t, agents.CheckDigitalHumanAgentExecution(ctx, ©))
|
||||
staleCopy := source
|
||||
staleCopy.ID = uuid.NewString()
|
||||
require.ErrorIs(t, agents.CreateAgentCopy(ctx, source.ID, &staleCopy), types.ErrDigitalHumanConflict)
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.CustomAgent{}).Where("id = ?", staleCopy.ID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
require.NoError(t, repo.Unbind(ctx, scope, human.ID, copy.ID))
|
||||
require.Error(t, agents.CheckDigitalHumanAgentExecution(ctx, ©), "saved profile must stop executing after unbind")
|
||||
}
|
||||
|
||||
func TestSharedDigitalHumanCanBeMountedWithoutChangingSourceOrOtherUsers(t *testing.T) {
|
||||
repo, db, owner, human := setupDigitalHumanTest(t)
|
||||
ctx := accesspolicy.WithSnapshot(context.Background(), &accesspolicy.Snapshot{Publications: map[string]bool{"digital-humans:" + human.ID: true}})
|
||||
consumer := types.DigitalHumanScope{TenantID: owner.TenantID, OwnerID: "consumer"}
|
||||
for _, agent := range []types.CustomAgent{
|
||||
{ID: "consumer-agent", TenantID: owner.TenantID, CreatedBy: consumer.OwnerID, Name: "Consumer", Config: types.CustomAgentConfig{SystemPrompt: "Consumer task"}},
|
||||
{ID: "other-agent", TenantID: owner.TenantID, CreatedBy: "other", Name: "Other"},
|
||||
{ID: "foreign-agent", TenantID: owner.TenantID + 1, CreatedBy: consumer.OwnerID, Name: "Foreign"},
|
||||
} {
|
||||
require.NoError(t, db.Create(&agent).Error)
|
||||
}
|
||||
require.NoError(t, repo.Review(ctx, owner, human.ID, 1, true, ""))
|
||||
require.NoError(t, repo.Bind(ctx, consumer, human.ID, "consumer-agent"))
|
||||
require.ErrorIs(t, repo.Bind(ctx, consumer, human.ID, "other-agent"), types.ErrDigitalHumanNotFound)
|
||||
require.ErrorIs(t, repo.Bind(ctx, consumer, human.ID, "foreign-agent"), types.ErrDigitalHumanNotFound)
|
||||
require.ErrorIs(t, repo.Delete(ctx, consumer, human.ID), types.ErrDigitalHumanNotFound)
|
||||
detail, err := repo.Get(ctx, consumer, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, detail.Bindings, 1, "recipients only see bindings for their own Agents")
|
||||
require.Equal(t, "consumer-agent", detail.Bindings[0].AgentID)
|
||||
var agent types.CustomAgent
|
||||
require.NoError(t, db.First(&agent, "id = ?", "consumer-agent").Error)
|
||||
require.Contains(t, agent.Config.SystemPrompt, "measured")
|
||||
agentRepo := &customAgentRepository{db: db}
|
||||
consumerContext := context.WithValue(ctx, types.TenantIDContextKey, owner.TenantID)
|
||||
consumerContext = context.WithValue(consumerContext, types.UserIDContextKey, consumer.OwnerID)
|
||||
visible, err := agentRepo.FilterDigitalHumanAgents(consumerContext, []*types.CustomAgent{&agent})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, visible, 1)
|
||||
require.NotNil(t, visible[0].DigitalHuman)
|
||||
require.Equal(t, human.ID, visible[0].DigitalHuman.ID)
|
||||
require.Equal(t, human.Name, visible[0].DigitalHuman.Name)
|
||||
require.Equal(t, 1, visible[0].DigitalHuman.AppliedVersion)
|
||||
require.ErrorContains(t, agentRepo.CheckDigitalHumanAgentCopy(consumerContext, agent.ID, owner.TenantID), "共享数字人")
|
||||
require.NoError(t, agentRepo.CheckDigitalHumanAgentExecution(consumerContext, &agent))
|
||||
savedSnapshot := agent
|
||||
revokedExecution := accesspolicy.WithSnapshot(consumerContext, &accesspolicy.Snapshot{Publications: map[string]bool{"digital-humans:" + human.ID: false}})
|
||||
require.ErrorContains(t, agentRepo.CheckDigitalHumanAgentExecution(revokedExecution, &savedSnapshot), "授权已撤销")
|
||||
foreignExecution := context.WithValue(consumerContext, types.TenantIDContextKey, owner.TenantID+1)
|
||||
require.Error(t, agentRepo.CheckDigitalHumanAgentExecution(foreignExecution, &savedSnapshot))
|
||||
adminContext := context.WithValue(consumerContext, types.TenantRoleContextKey, types.TenantRoleAdmin)
|
||||
require.NoError(t, agentRepo.CheckDigitalHumanAgentCopy(adminContext, agent.ID, owner.TenantID))
|
||||
// Owner publication updates every installed binding, preserving its owner.
|
||||
require.NoError(t, repo.Update(ctx, owner, human.ID, detail.Revision, human.Name, "", "", "", map[string]string{"tone": "revised"}))
|
||||
require.NoError(t, repo.Review(ctx, owner, human.ID, 2, true, ""))
|
||||
require.NoError(t, db.First(&agent, "id = ?", "consumer-agent").Error)
|
||||
require.Contains(t, agent.Config.SystemPrompt, "revised")
|
||||
require.Equal(t, consumer.OwnerID, agent.CreatedBy)
|
||||
// Revocation must still permit removing a persona from one's own Agent.
|
||||
revoked := accesspolicy.WithSnapshot(ctx, &accesspolicy.Snapshot{})
|
||||
require.NoError(t, repo.Unbind(revoked, consumer, human.ID, agent.ID))
|
||||
require.ErrorContains(t, agentRepo.CheckDigitalHumanAgentExecution(consumerContext, &savedSnapshot), "解绑")
|
||||
require.NoError(t, db.First(&agent, "id = ?", "consumer-agent").Error)
|
||||
require.Equal(t, "Consumer task", agent.Config.SystemPrompt)
|
||||
require.ErrorIs(t, repo.Bind(revoked, consumer, human.ID, agent.ID), types.ErrDigitalHumanNotFound)
|
||||
require.NoError(t, repo.Bind(ctx, consumer, human.ID, agent.ID))
|
||||
require.NoError(t, repo.Delete(ctx, owner, human.ID))
|
||||
require.NoError(t, db.First(&agent, "id = ?", "consumer-agent").Error)
|
||||
require.Equal(t, "Consumer task", agent.Config.SystemPrompt)
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.DigitalHumanBinding{}).Where("digital_human_id = ?", human.ID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
func TestDigitalHumanJobRecoveryFencesOldWorker(t *testing.T) {
|
||||
repo, _, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
job := &types.DigitalHumanJob{ID: uuid.NewString(), TenantID: scope.TenantID, OwnerID: scope.OwnerID, DigitalHumanID: human.ID, Status: "running", ExpiresAt: time.Now().Add(-time.Minute)}
|
||||
require.NoError(t, repo.StartJob(ctx, scope, human.ID, human.Revision, job))
|
||||
require.ErrorIs(t, repo.StartJob(ctx, scope, human.ID, human.Revision, job), types.ErrDigitalHumanConflict)
|
||||
require.NoError(t, repo.RecoverExpiredJobs(ctx, scope))
|
||||
h, err := repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, h.ProcessingJobID)
|
||||
stored, err := repo.Job(ctx, scope, human.ID, job.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "failed", stored.Status)
|
||||
require.ErrorIs(t, repo.FinishJob(ctx, scope, human.ID, job.ID, &types.DigitalHumanVersion{Persona: map[string]string{"tone": "stale"}}, ""), types.ErrDigitalHumanConflict)
|
||||
job2 := &types.DigitalHumanJob{ID: uuid.NewString(), TenantID: scope.TenantID, OwnerID: scope.OwnerID, DigitalHumanID: human.ID, Status: "running", ExpiresAt: time.Now().Add(time.Minute)}
|
||||
require.NoError(t, repo.StartJob(ctx, scope, human.ID, h.Revision, job2))
|
||||
require.NoError(t, repo.FinishJob(ctx, scope, human.ID, job2.ID, &types.DigitalHumanVersion{Persona: map[string]string{"tone": "fresh"}}, ""))
|
||||
h, err = repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, h.LatestVersion)
|
||||
require.Equal(t, "fresh", h.Versions[0].Persona["tone"])
|
||||
}
|
||||
|
||||
func TestImportedDigitalHumanKeepsStyleUntilNewVersionReviewed(t *testing.T) {
|
||||
repo, db, scope, human := setupDigitalHumanTest(t)
|
||||
ctx := context.Background()
|
||||
persona := map[string]string{"persona_name": "Legacy teacher", "tone": "direct", "taboo": "no jargon", "vocab": "hello", "identity": "teacher", "interaction": "ask questions"}
|
||||
require.NoError(t, db.Model(&types.DigitalHumanVersion{}).Where("digital_human_id = ?", human.ID).
|
||||
Updates(&types.DigitalHumanVersion{Status: "imported", Persona: persona}).Error)
|
||||
require.NoError(t, db.Model(human).Updates(map[string]any{"status": "active", "active_version": 1}).Error)
|
||||
extra := types.CustomAgent{ID: uuid.NewString(), TenantID: scope.TenantID, CreatedBy: scope.OwnerID,
|
||||
Name: "Imported style consumer", Config: types.CustomAgentConfig{SystemPrompt: "Keep current task rules"}}
|
||||
require.NoError(t, db.Create(&extra).Error)
|
||||
require.NoError(t, repo.Bind(ctx, scope, human.ID, extra.ID))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.Contains(t, extra.Config.SystemPrompt, "旧配置迁入数字人(未重新蒸馏)")
|
||||
require.NotContains(t, extra.Config.SystemPrompt, "已审核数字人")
|
||||
for _, value := range persona {
|
||||
require.Contains(t, extra.Config.SystemPrompt, value)
|
||||
}
|
||||
require.ErrorIs(t, repo.Review(ctx, scope, human.ID, 1, true, ""), types.ErrDigitalHumanConflict)
|
||||
detail, err := repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, detail.Versions[0].ReviewedAt)
|
||||
require.NoError(t, repo.Update(ctx, scope, human.ID, detail.Revision, human.Name, "", "", "", map[string]string{"tone": "new tone"}))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.Contains(t, extra.Config.SystemPrompt, "tone:direct")
|
||||
require.NotContains(t, extra.Config.SystemPrompt, "new tone")
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 2, false, "revise"))
|
||||
detail, err = repo.Get(ctx, scope, human.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, detail.ActiveVersion)
|
||||
require.Equal(t, "active", detail.Status)
|
||||
require.NoError(t, repo.Update(ctx, scope, human.ID, detail.Revision, human.Name, "", "", "", map[string]string{"tone": "reviewed replacement"}))
|
||||
require.NoError(t, repo.Review(ctx, scope, human.ID, 3, true, ""))
|
||||
require.NoError(t, db.First(&extra, "id = ?", extra.ID).Error)
|
||||
require.Contains(t, extra.Config.SystemPrompt, "已审核数字人")
|
||||
require.Contains(t, extra.Config.SystemPrompt, "Keep current task rules")
|
||||
require.Contains(t, extra.Config.SystemPrompt, "reviewed replacement")
|
||||
require.NotContains(t, extra.Config.SystemPrompt, "tone:direct")
|
||||
require.Equal(t, 1, strings.Count(extra.Config.SystemPrompt, ":start -->"))
|
||||
}
|
||||
Reference in New Issue
Block a user