merge: candidate squad-commit-authority-2026-09-28 into dev

This commit is contained in:
weichen
2026-09-28 01:25:28 +00:00
8 changed files with 309 additions and 2 deletions
+2 -1
View File
@@ -2,10 +2,11 @@
维护规则:[问题记录与开发经验闭环](../ops/2026-09-23-issue-learning-workflow.md)。每个问题一个稳定 ID;主记录是事实与状态来源,本表为可检索投影。新发现及时登记,修复后保留记录;发布 blocker 仍见 [gap-matrix](../remediation/gap-matrix.md)。
更新:2026-09-28。test 注册 INTERNAL:标准角色 role-guest 缺失导致 FK 23503,已补种并重启 test(goose 77,resolved)。重复用户名 400 在新代码单测通过;当前 rc 二进制仍返回 INTERNAL,随下次含本提交的 test 制品生效。管理台重置密码入口被 F4-A 误删(端点仍在),已恢复 UI(resolved)。2026-09-27。上传链路:登记并修复上传文档跨端不可访问(占位模板退化 + 元数据/正文不落服务端,fixing 待 dev 集成)。2026-09-26。docgov 批准链:登记并修复 review_snapshot_id 装载缺失与 uow 行锁自死锁(resolved);memDocs 持久化失败缓存投毒(investigating)。历史 resolved 不表示本日产品回归通过。系统 code review P0/P1 批次修复落账(5 resolved + 1 deferred + 1 open,主记录见 [整改文档](2026-09-26-system-code-review-fixes.md),来源 [审计](2026-09-26-system-code-review.md))。
更新:2026-09-28。非编制人触发小队账本提交被 `uow: permission denied` 拒绝(fixing,待 dev 重跑)。test 注册 INTERNAL:标准角色 role-guest 缺失导致 FK 23503,已补种并重启 test(goose 77,resolved)。重复用户名 400 在新代码单测通过;当前 rc 二进制仍返回 INTERNAL,随下次含本提交的 test 制品生效。管理台重置密码入口被 F4-A 误删(端点仍在),已恢复 UI(resolved)。2026-09-27。上传链路:登记并修复上传文档跨端不可访问(占位模板退化 + 元数据/正文不落服务端,fixing 待 dev 集成)。2026-09-26。docgov 批准链:登记并修复 review_snapshot_id 装载缺失与 uow 行锁自死锁(resolved);memDocs 持久化失败缓存投毒(investigating)。历史 resolved 不表示本日产品回归通过。系统 code review P0/P1 批次修复落账(5 resolved + 1 deferred + 1 open,主记录见 [整改文档](2026-09-26-system-code-review-fixes.md),来源 [审计](2026-09-26-system-code-review.md))。
| issue_id | 模块 / 检索词 | 问题 | 严重度 | issue_state | owner | 证据日期 / 范围 | 唯一主记录 | 经验 |
|---|---|---|---|---|---|---|---|---|
| ISSUE-20260928-squad-commit-authority | bizdev / docflow / dutyAuthorizer / uow permission denied / OnBehalfOf / bd_intake | 非编制人触发受控小队任务时,账本提交用触发者身份,被编制人职责门拒绝,正文无法落盘 | high | fixing | Cursor | 2026-09-28;dev task_runs user 74 ≠ author 4(doc_proj6022_bd_intake);单测已绿,dev 重跑未做 | [主记录](../ops/short-term/2026-09-28-squad-commit-authority.md#issue-20260928-squad-commit-authority) | [LESSON-20260928-squad-commit-as-author](../ops/lessons/2026-09-28-squad-commit-as-author.md) |
| ISSUE-20260928-register-internal | auth / register / role-guest / system_roles / 23503 / 23505 / INTERNAL | test 缺 role-guest 时注册 FK 失败被脱敏为 INTERNAL;重复用户名唯一冲突同样 500 | high | resolved | Cursor | 2026-09-28;test:8081 补种并重启后注册 200(goose 77,req_regfix_after_restart);重复用户名 400 由单测覆盖,rc 二进制仍为 INTERNAL | [主记录](../ops/short-term/2026-09-28-auth-register-internal.md#issue-20260928-register-internal) | [LESSON-20260928-standard-role-reseed](../ops/lessons/2026-09-28-standard-role-reseed.md) |
| ISSUE-20260928-admin-reset-ui-removed | web / UsersSection / 重置密码 / F4-A / adminResetPassword | F4-A 在 F4-B 端点已合入的基线上删除管理台重置密码按钮,管理员无法重置口令 | high | resolved | Cursor | 2026-09-28;dev API :8080 + worktree Vite :3110 真实 UI 探针 PASS(临时口令长度 16、强制改密仅一次、旧口令 401) | [主记录](../ops/short-term/2026-09-28-restore-admin-password-reset.md#issue-20260928-admin-reset-ui-removed) | [LESSON-20260928-ui-dead-control](../ops/lessons/2026-09-28-ui-dead-control.md) |
| ISSUE-20260927-upload-doc-cross-device | freedoc / upload / freeform / fileMeta / 占位模板 / 跨端 | 上传文档源文件元数据与正文不落服务端:换浏览器/清存储/共享他人查看时退化为注册占位模板,文本类连二进制都未存(伴生 <512B 显示 "0 KB") | high | resolved | pi | 2026-09-27;dev `86efba2c` 合入并在 dev 环境验证:FS-UPLOAD-01/02 真实浏览器 PASS(跨上下文可见真实正文),DB 核验元数据+asset 落库;存量无元数据旧文档不可恢复(重新上传即可) | [主记录](../ops/short-term/2026-09-27-upload-doc-access.md) | [LESSON-20260927-server-facts-and-iso-db-recipe](../ops/lessons/2026-09-27-server-facts-and-iso-db-recipe.md) |
+1
View File
@@ -18,6 +18,7 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| Cursor | squad-commit-authority-2026-09-28 | fix/squad-commit-authority-2026-09-28 | .worktrees/squad-commit-authority-2026-09-28 | bizdev+docflow | in_progress |
| Cursor | auth-register-internal-2026-09-28 | fix/auth-register-internal-2026-09-28 | .worktrees/auth-register-internal-2026-09-28 | auth-register | done |
| Cursor | restore-admin-password-reset-2026-09-28 | feature/restore-admin-password-reset-2026-09-28 | .worktrees/restore-admin-password-reset-2026-09-28 | auth | done |
| pi | test-single-binary-2026-09-27 | feature/test-single-binary-2026-09-27 | .worktrees/test-single-binary-2026-09-27 | env:dev+env:test+git:dev+git:test | done |
@@ -0,0 +1,20 @@
---
lesson_id: LESSON-20260928-squad-commit-as-author
lesson_state: candidate
owner: Cursor
reviewer: Cursor(自审,待 dev 重跑后升 validated)
last_reviewed: 2026-09-28
reviewed_revision: 774df0599813fb8d7115f95ce255384542d6a0f9
source_issues: [ISSUE-20260928-squad-commit-authority]
keywords: [bizdev, docflow, dutyAuthorizer, AutoCommitUserID, OnBehalfOf, uow permission denied, 非编制人]
---
# 小队账本自动提交要用编制人身份
- 触发条件:受控文档小队任务成功生成正文,但落盘报 `uow: permission denied` 或「版本账本/变更报告落盘失败」。
- 机制与来源:`dutyAuthorizer` 对 `ActionCommit` 要求 principal 等于 `document_records.author_id`。触发者常常是其他项目成员。见 ISSUE-20260928-squad-commit-authority。
- 开发检查:自动提交的 `AutoCommitUserID` 用文档编制人;实际触发用户写入 `OnBehalfOf`。不要为了小队路径放宽编制人校验。触发者或编制人为 0 时不要伪造代理身份。权限失败文案要写明「非编制人」,同时保留 `uow.ErrPermissionDenied` 哨兵。
- 验证方式:非编制人触发应成功且 `OnBehalfOf` 为触发者;编制人触发不设置 `OnBehalfOf`;非编制人直接 `ActionCommit` 仍拒绝。关联 `internal/bizdev/squad_commit_actor_test.go` 与 `TestDutyAuthorizerAllowsDocumentAuthorCommit`。dev 原例重跑完成前本条保持 candidate。
- 适用边界:只覆盖小队/agent 自动提交。人工编辑、批准、驳回仍按各自职责人校验。高风险候选仍要编制人确认。
- 防护落实:上述单测已落地。原例验收是 manual(dev 非编制人重跑)。
- 修订与失效:若产品改为「非编制人不得触发生成」,本条应退役并改回触发者身份。
+1
View File
@@ -4,6 +4,7 @@
| lesson_id | 触发条件 / 关键词 | 注意事项 | lesson_state | 主记录 / 最近复核 |
|---|---|---|---|---|
| LESSON-20260928-squad-commit-as-author | 小队落盘、uow permission denied、AutoCommitUserID、OnBehalfOf、非编制人 | 自动提交身份用文档编制人,触发者只写入 OnBehalfOf;不要放宽 ActionCommit | candidate | [编制人代理提交](2026-09-28-squad-commit-as-author.md);2026-09-28 |
| LESSON-20260928-standard-role-reseed | 注册、role-guest、system_roles、is_system、23503、23505、INTERNAL | 硬编码标准角色须幂等补种并标 is_system;用户名冲突返回领域 400,不要包进 insert user | validated | [标准角色可自愈](2026-09-28-standard-role-reseed.md);2026-09-28 |
| LESSON-20260928-ui-dead-control | 死按钮、端点未开通、UsersSection、重置密码、merge-base | 删除「未开通」控件前用当前基线核对路由已存在;注释不能代替 merge-base | validated | [删控件前核对端点](2026-09-28-ui-dead-control.md);2026-09-28 |
| LESSON-20260927-server-facts-and-iso-db-recipe | 上传持久化、fileMeta、占位模板、config.Load 默认 DSN、TEST_DATABASE_URL、隔离库、RefuseSharedEnvDSN、goose 顺序 | 服务端事实源优先(本地仅缓存);worktree 测试必须显式钉隔离库并避开 5433/5434/5435 子串端口;隔离库配方=扩展→goose→init SQL,重组件用例先做基线四格对照 | validated | [服务端事实源与隔离库配方](2026-09-27-server-facts-and-iso-db-recipe.md) |
@@ -0,0 +1,74 @@
---
title: "修复非编制人触发小队账本提交被拒"
task_id: squad-commit-authority-2026-09-28
status: in_progress
created: 2026-09-28
updated: 2026-09-28
owner: Cursor
task_type: fix
policy_revision: "AGENTS.md + agent-worktree-workflow @ 2026-09-21"
branch: fix/squad-commit-authority-2026-09-28
worktree: .worktrees/squad-commit-authority-2026-09-28
base_dev_sha: "774df0599813fb8d7115f95ce255384542d6a0f9"
write_scope:
- internal
- .doc
mutex: bizdev+docflow
review_state: pending
integration_state: pending
deployment: pending
cleanup: pending
remote_sync: not_requested
---
# 修复非编制人触发小队账本提交被拒
## 用户目标与授权
用户生成《用户初始需求收集表》时小队任务失败,诊断为 `TASK_FAILED` / `uow: permission denied`。授权:以编制人身份自动提交账本,并用 `OnBehalfOf` 记录实际触发用户;先用 dev 库核实身份后再改代码。
## 检索
- 检索词:`uow: permission denied`、`ErrPermissionDenied`、`文档版本账本/变更报告落盘失败`、`bd_intake`、非编制。
- 命中 [ISSUE-20260924-non-author-400-duty](../../audit/ISSUES.md) 与 [LESSON-20260924-deny-only-submit](../lessons/2026-09-24-deny-only-submit.md):成员可以跑草稿,但下游门必须一起放行。本问题是账本 `ActionCommit` 仍按触发者比对编制人,属于该教训的下游缺口,不是同一 HTTP 400。
- 命中 [ISSUE-20260926-docgov-uow-deadlock](../../audit/ISSUES.md):同为 uow 失败,机制是行锁自死锁,不适用。
- catalog overlap(mutex `bizdev+docflow`)hard/soft 为空。登记时 `integrate-restore-admin-password-reset-2026-09-28` 与其 close 任务仍 `active` 且 scope 为 `.`,队列已 completed、提交已是 dev 祖先,已 `task close --status done` 以释放仓库级写范围,未删除其 worktree。
## ISSUE-20260928-squad-commit-authority
issue_id: ISSUE-20260928-squad-commit-authority
issue_state: fixing
severity: high
owner: Cursor
task_ids: [squad-commit-authority-2026-09-28]
created: 2026-09-28
occurred_at: 2026-09-28
updated: 2026-09-28
recurrence_count: 0
evidence_basis: current
lesson_ids: [LESSON-20260928-squad-commit-as-author]
### 事实与证据
预期:项目成员触发受控文档生成后,小队正文能写入版本账本。实际:任务 `run_1790556318606671340`、`run_1790556736148378090`(project 6022,task_key `intake`,user_id 74)失败,`error_message=uow: permission denied`。界面文案为「《用户初始需求收集表》未成功落盘」加上裸 `uow: permission denied`。
dev 库只读核实(2026-09-28,`agent_system_dev`):`document_records.doc_proj6022_bd_intake` 的 `author_id=4`(admin,项目 owner,`document_duty=author`);触发者 74 是成员 `ceshi`,`project_role=viewer`。项目创建者是 user 4。
### 根因与同类范围
已证实:`finalizeSquadDeliverable` 把 `AutoCommitUserID` 设为触发者 `req.UserID`。生产路径经 `docflow.Commands.MaterializeAgent` 用该 id 做 `ActionCommit`。`dutyAuthorizer` 在编制人与触发者都非 0 且不相等时返回 `uow.ErrPermissionDenied`。小队正文与质量门禁已通过,失败只发生在账本提交。
同类:API 直连已把该哨兵映射为 403。缺口只在小队自动提交。编制人自己触发、高风险 `needs_confirmation`、批准/驳回职责检查应保持不变。
### 修复思路与过程
选择以文档编制人作为自动提交身份,触发者写入已有 `OnBehalfOf`。不放宽 `dutyAuthorizer`,避免 viewer 直接获得编制人编辑权。高风险候选仍待编制人确认。
| 日期 | 动作 | 结果 | 下一步 |
|---|---|---|---|
| 2026-09-28 | dev 库核对 author 与 task_runs.user_id | 4 ≠ 74,两次失败均为 permission denied | 按编制人代理提交实现 |
| 2026-09-28 | `squadCommitActor` + 回归 | 内存单测通过;dev 真实重跑未做 | 合入 dev 后用非编制人重跑 |
### 验收与关闭
尚未关闭。原例待 dev 重跑;变体与正常路径见 `TestExecuteTask_NonAuthorTriggerMaterializesAsAuthor`、`TestExecuteTask_AuthorTriggerDoesNotSetOnBehalfOf`、`TestExecuteTask_NonAuthorPassesDutyAuthorizer`、`TestDutyAuthorizerAllowsDocumentAuthorCommit`。
+14
View File
@@ -145,6 +145,20 @@ func TestIdempotentHumanEditSameDigest(t *testing.T) {
}
}
func TestDutyAuthorizerAllowsDocumentAuthorCommit(t *testing.T) {
rec := &docgov.DocumentRecord{DocID: "doc_flow_author_commit", AuthorID: 4, ApproverID: 9}
auth := DutyAuthorizer()
if err := auth.Authorize(context.Background(), Principal{UserID: 4}, rec, ActionCommit); err != nil {
t.Fatalf("document author must pass agent commit: %v", err)
}
if err := auth.Authorize(context.Background(), Principal{UserID: 74}, rec, ActionCommit); err != uow.ErrPermissionDenied {
t.Fatalf("non-author commit must be denied, got %v", err)
}
if err := auth.Authorize(context.Background(), Principal{UserID: 4}, rec, ActionEdit); err != nil {
t.Fatalf("document author must still pass edit: %v", err)
}
}
func TestReauthDeniesWrongAuthor(t *testing.T) {
ctx := context.Background()
gov := docgov.NewGovernanceEngine(nil)
+19 -1
View File
@@ -4,10 +4,12 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/projectworkflow"
@@ -27,6 +29,17 @@ func autoRepairForControlledSquad(reqAutoRepair bool, docID string, docRec *docg
return reqAutoRepair
}
// squadCommitActor 让受控账本提交通过编制人职责门。
// 触发者不是编制人时,以文档编制人作为 AutoCommitUserID,并把触发者写入 OnBehalfOf。
// 触发者或编制人缺失时保持原触发者,避免把 0 写成代理身份。
func squadCommitActor(triggerUserID int64, docRec *docgov.DocumentRecord) (int64, *int64) {
if docRec == nil || docRec.AuthorID <= 0 || triggerUserID <= 0 || docRec.AuthorID == triggerUserID {
return triggerUserID, nil
}
onBehalf := triggerUserID
return docRec.AuthorID, &onBehalf
}
func qualityFailedErrorReason(report *squad.QualityReport) string {
if report == nil {
return "质量门禁未通过"
@@ -87,15 +100,20 @@ func (s *Service) finalizeSquadDeliverable(
// Squad may have already written candidate body to workDir/output.md — pending must restore ledger head.
if s.docRev != nil && docRec != nil && strings.TrimSpace(docRec.DocID) != "" {
actor := docrevision.ActorSnapshot{SquadName: "bizdev-squad", Model: res.BackendUsed}
commitUserID, onBehalfOf := squadCommitActor(req.UserID, docRec)
mat, matErr := s.materializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{
DocID: docID, RunID: fmt.Sprintf("bizdev-%d-%s", time.Now().UnixNano(), req.TaskKey),
ProposalID: appliedProposalID, OutputMarkdown: res.MarkdownBody,
ProjectID: req.ProjectID, WorkDir: res.WorkDir,
ActorKind: docrevision.ActorAgentSquad, ActorSnapshot: actor,
AutoCommitUserID: req.UserID,
OnBehalfOf: onBehalfOf,
AutoCommitUserID: commitUserID,
})
if matErr != nil {
reason := fmt.Sprintf("文档版本账本/变更报告落盘失败: %v", matErr)
if errors.Is(matErr, uow.ErrPermissionDenied) {
reason = fmt.Sprintf("触发用户非文档编制人,账本提交被拒: %v", matErr)
}
return &TaskRunResult{
TaskKey: res.TaskKey,
ArtifactTitle: res.ArtifactTitle,
+178
View File
@@ -0,0 +1,178 @@
package bizdev
import (
"context"
"errors"
"strings"
"testing"
"time"
"chat-agent-system/internal/app/docflow"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/config"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
)
type recordingMaterializer struct {
inner AgentOutputMaterializer
got docrevision.AgentMaterializeRequest
err error
}
func (r *recordingMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
r.got = req
if r.err != nil {
return nil, r.err
}
if r.inner == nil {
return nil, errors.New("recording materializer has no inner")
}
return r.inner.MaterializeAgentOutput(ctx, req)
}
func TestSquadCommitActor_UsesAuthorWhenTriggerDiffers(t *testing.T) {
author, behalf := squadCommitActor(74, &docgov.DocumentRecord{AuthorID: 4})
if author != 4 {
t.Fatalf("commit actor want 4 got %d", author)
}
if behalf == nil || *behalf != 74 {
t.Fatalf("on behalf want 74 got %v", behalf)
}
same, behalfSame := squadCommitActor(4, &docgov.DocumentRecord{AuthorID: 4})
if same != 4 || behalfSame != nil {
t.Fatalf("author trigger must not proxy: actor=%d behalf=%v", same, behalfSame)
}
missing, behalfMissing := squadCommitActor(74, &docgov.DocumentRecord{})
if missing != 74 || behalfMissing != nil {
t.Fatalf("unset author must keep trigger: actor=%d behalf=%v", missing, behalfMissing)
}
}
func TestExecuteTask_NonAuthorTriggerMaterializesAsAuthor(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6022)
authorID = int64(4)
triggerID = int64(74)
docID = "doc_proj6022_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", authorID, "admin"); err != nil {
t.Fatal(err)
}
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
svc.SetAgentMaterializer(rec)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "测试项目",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
})
if err != nil {
t.Fatalf("ExecuteTask: %v reason=%v", err, res)
}
if res == nil || !res.Success {
t.Fatalf("non-author trigger must succeed, got %#v", res)
}
if rec.got.AutoCommitUserID != authorID {
t.Fatalf("AutoCommitUserID want %d got %d", authorID, rec.got.AutoCommitUserID)
}
if rec.got.OnBehalfOf == nil || *rec.got.OnBehalfOf != triggerID {
t.Fatalf("OnBehalfOf want %d got %v", triggerID, rec.got.OnBehalfOf)
}
}
func TestExecuteTask_AuthorTriggerDoesNotSetOnBehalfOf(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6023)
authorID = int64(4)
docID = "doc_proj6023_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
svc.SetAgentMaterializer(rec)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: authorID,
})
if err != nil || res == nil || !res.Success {
t.Fatalf("author trigger failed: err=%v res=%#v", err, res)
}
if rec.got.AutoCommitUserID != authorID || rec.got.OnBehalfOf != nil {
t.Fatalf("author path changed: user=%d behalf=%v", rec.got.AutoCommitUserID, rec.got.OnBehalfOf)
}
}
func TestExecuteTask_NonAuthorPassesDutyAuthorizer(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6024)
authorID = int64(4)
triggerID = int64(74)
docID = "doc_proj6024_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
svc.SetAgentMaterializer(&docflow.Commands{Gov: svc.docGov, Rev: svc.docRev, Auth: docflow.DutyAuthorizer()})
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
})
if err != nil || res == nil || !res.Success {
t.Fatalf("duty authorizer must accept author-proxy commit: err=%v reason=%v", err, errorReason(res))
}
if strings.Contains(errorReason(res), "permission denied") {
t.Fatalf("permission denied leaked: %s", res.ErrorReason)
}
}
func TestExecuteTask_PermissionDeniedNamesNonAuthor(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const docID = "doc_perm_denied"
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, 77, "research", "调研", "V0.5", "", 4, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
svc.SetAgentMaterializer(&recordingMaterializer{err: uow.ErrPermissionDenied})
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "research", ProjectID: 77, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: 74,
})
if err == nil || res == nil || res.Success {
t.Fatalf("want visible permission failure, err=%v res=%#v", err, res)
}
if !strings.Contains(res.ErrorReason, "触发用户非文档编制人") {
t.Fatalf("reason must name non-author denial, got %q", res.ErrorReason)
}
if !errors.Is(err, uow.ErrPermissionDenied) {
t.Fatalf("sentinel must stay errors.Is compatible, got %v", err)
}
}
type revMaterializer struct {
rev *docrevision.Service
}
func (r revMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
return r.rev.MaterializeAgentOutput(ctx, req)
}
func errorReason(res *TaskRunResult) string {
if res == nil {
return ""
}
return res.ErrorReason
}