merge: candidate squad-commit-authority-2026-09-28 into dev
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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 非编制人重跑)。
|
||||
- 修订与失效:若产品改为「非编制人不得触发生成」,本条应退役并改回触发者身份。
|
||||
@@ -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`。
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user