merge: fix/workflow-seed-bootstrap-2026-09-20
This commit is contained in:
+4
-3
@@ -5,14 +5,15 @@
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **2026-09-20 模块解耦规范与审计 done(仅文档)**:耦合审计、开发规范、批次 A–G 计划已写;不改业务代码,不改 `production_ready`。见 [审计](../audit/2026-09-20-module-coupling-audit.md) · [规范](../system-design/2026-09-20-module-development-standards.md) · [批次](../remediation/2026-09-20-module-decoupling-batch-plan.md)。
|
||||
2. **2026-09-20 P0 系统体检热修 done**:迁移 00058、三项 IDOR、panic/race/XSS、凭证轮换;见 [报告](../test-reports/2026-09-20-p0-hotfixes-test-report.md) · [审计](../audit/2026-09-20-system-triage-audit.md)。
|
||||
1. **2026-09-20 rdev 种子热修 in_progress**:dev 库 rdev 停在 `retired`;test 库因 `publishedBy=1` 外键失败未发布 rdev。见 [short-term](./short-term/2026-09-20-rdev-seed-hotfix.md)。
|
||||
2. **2026-09-20 模块解耦规范与审计 done(仅文档)**:耦合审计、开发规范、批次 A–G 计划已写;不改业务代码,不改 `production_ready`。见 [审计](../audit/2026-09-20-module-coupling-audit.md) · [规范](../system-design/2026-09-20-module-development-standards.md) · [批次](../remediation/2026-09-20-module-decoupling-batch-plan.md)。
|
||||
3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读;SWBOOT-10 保持 fail-closed stub;D7 待合规签字;**P0 期间不改 `production_ready`**;模块解耦批次 A–G 未单独认领前保持 `planned`。
|
||||
|
||||
## 并行认领
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|-------|------|--------|----------|-------|--------|
|
||||
| Cursor | 2026-09-20-rdev-seed-hotfix | fix/workflow-seed-bootstrap-2026-09-20 | - | workflow-def | in_progress |
|
||||
| Cursor-Auto | 2026-09-20-module-decoupling-spec-audit | docs/module-decoupling-spec-2026-09-20 | - | module-decoupling-spec | done |
|
||||
| Cursor-Auto | 2026-09-20-p0-closeout | docs/p0-closeout-2026-09-20 | - | docs | done |
|
||||
| Cursor-Auto | 2026-09-20-p0-hotfixes(Wave 0 文档) | docs/p0-triage-2026-09-20 | - | docs | done |
|
||||
@@ -166,7 +167,7 @@
|
||||
|
||||
| holder | phase | since | scope | queue |
|
||||
|--------|-------|-------|-------|-------|
|
||||
| - | idle | 2026-09-20 | 模块解耦文档已合入 `133ceb45`;`go vet` 通过。`go test -short` 仅两个 docker 拉镜像用例因镜像源 EOF 失败,与文档 diff 无关 | - |
|
||||
| Cursor | merging | 2026-09-20 | fix/workflow-seed-bootstrap-2026-09-20:rdev 种子 publishedBy 回落与 retired 自愈 | - |
|
||||
|
||||
**phase**: `idle` | `merging` | `smoke` | `e2e`
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "修复工作流种子:publishedBy 外键与 retired 不自愈"
|
||||
status: in_progress
|
||||
created: 2026-09-20
|
||||
updated: 2026-09-20
|
||||
owner: Cursor
|
||||
branch: fix/workflow-seed-bootstrap-2026-09-20
|
||||
worktree: "-"
|
||||
mutex: workflow-def
|
||||
---
|
||||
|
||||
# 修复工作流种子:publishedBy 外键与 retired 不自愈
|
||||
|
||||
## 目标
|
||||
|
||||
dev 与 test 启动后,正式工作流目录包含「研发设计控制流程」(`rdev`),且数据库 `status=active`、已发布版本外键有效。
|
||||
|
||||
## 范围
|
||||
|
||||
- `BootstrapSeeds`:`publishedBy` 用户不存在时回落到库内最小 id;无用户则跳过并记日志
|
||||
- `UpsertPublishedSeed` / `RetireNonOfficialDefinitions`:白名单 key 幂等更新时把 `status` 恢复为 `active`
|
||||
- `bizdev` 启动 seed 失败改为 `slog.Warn`,不再静默丢弃
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改 rdev manifest / 60 份模板内容
|
||||
- 不改 `production_ready`
|
||||
- 不启用 FAV / 语义摘要 REPLACE
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] 从 dev 切 `fix/workflow-seed-bootstrap-2026-09-20` 并登记认领
|
||||
2. [ ] 修复 seed actor 回落与 retired 自愈
|
||||
3. [ ] `go vet` / `go test -short` / PG 集成
|
||||
4. [ ] 合入 dev 后 rebuild;晋升 test(`rc-v1.0.0-beta.7`)并 rebuild
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] dev/test catalog 均含 rdev,DB `status=active`
|
||||
- [ ] test 库 `workflow_versions` 无 `created_by` 外键错误
|
||||
- [ ] `go vet` / `go test -short` 通过
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Checklist: -
|
||||
- Handoff: -
|
||||
- Test report: `.doc/test-reports/2026-09-20-rdev-seed-hotfix-test-report.md`
|
||||
|
||||
## 证据 / 备注
|
||||
|
||||
- test PG `2026-09-20 09:49:51`:`workflow_versions_created_by_fkey`(`publishedBy=1`,test 用户最小 id=4)
|
||||
- dev 库 rdev v3 `status=retired`,`project_selectable=true`(元数据更新不恢复 status)
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -125,7 +126,9 @@ func NewService(cfg *config.Config) (*Service, error) {
|
||||
fallbackCatalog := projectworkflow.NewDefaultCatalog()
|
||||
wfStore := workflowdef.NewStore(pool)
|
||||
if pool != nil && workflowdef.TablesReady(context.Background(), pool) {
|
||||
_ = workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1)
|
||||
if seedErr := workflowdef.BootstrapSeeds(context.Background(), pool, casStore, "templates/docs", 1); seedErr != nil {
|
||||
slog.Warn("workflow seed bootstrap failed", "error", seedErr)
|
||||
}
|
||||
}
|
||||
catalog := workflowdef.NewCatalog(context.Background(), wfStore, fallbackCatalog)
|
||||
pwSvc := projectworkflow.NewService(pwStore, catalog)
|
||||
|
||||
@@ -2,10 +2,13 @@ package workflowdef
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"chat-agent-system/internal/storage"
|
||||
@@ -48,11 +51,49 @@ var officialSeedMeta = map[string]struct {
|
||||
},
|
||||
}
|
||||
|
||||
// resolveSeedActor 返回可作为 workflow_versions.created_by 外键的用户 id。
|
||||
// 请求的 publishedBy 不存在时回落到库内最小 id;没有任何用户时返回 0。
|
||||
func resolveSeedActor(ctx context.Context, pool *pgxpool.Pool, publishedBy int64) (int64, error) {
|
||||
if pool == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if publishedBy > 0 {
|
||||
var exists bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE id = $1)`, publishedBy).Scan(&exists); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if exists {
|
||||
return publishedBy, nil
|
||||
}
|
||||
}
|
||||
var id int64
|
||||
err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY id ASC LIMIT 1`).Scan(&id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// BootstrapSeeds 幂等导入模板与正式 v2 工作流;非正式定义标记为不可选。
|
||||
func BootstrapSeeds(ctx context.Context, pool *pgxpool.Pool, cas *storage.CAS, templatesDir string, publishedBy int64) error {
|
||||
if pool == nil || !TablesReady(ctx, pool) {
|
||||
return nil
|
||||
}
|
||||
actor, err := resolveSeedActor(ctx, pool, publishedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if actor == 0 {
|
||||
slog.Warn("workflow seed skipped: no users available for published_by")
|
||||
return nil
|
||||
}
|
||||
if actor != publishedBy {
|
||||
slog.Warn("workflow seed published_by fallback", "requested", publishedBy, "actor", actor)
|
||||
}
|
||||
publishedBy = actor
|
||||
store := NewStore(pool)
|
||||
svc := NewService(pool, cas)
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package workflowdef
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"chat-agent-system/internal/db"
|
||||
)
|
||||
|
||||
func openSeedPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
dsn := db.SkipIfNoTestDatabaseURL(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
t.Skip(err)
|
||||
}
|
||||
if !TablesReady(ctx, pool) {
|
||||
pool.Close()
|
||||
t.Skip("workflowdef tables not migrated")
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
func TestResolveSeedActor_FallsBackWhenUserMissing(t *testing.T) {
|
||||
pool := openSeedPool(t)
|
||||
defer pool.Close()
|
||||
ctx := context.Background()
|
||||
|
||||
var minID int64
|
||||
if err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY id ASC LIMIT 1`).Scan(&minID); err != nil {
|
||||
t.Skip("no users to fall back to")
|
||||
}
|
||||
|
||||
got, err := resolveSeedActor(ctx, pool, minID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != minID {
|
||||
t.Fatalf("existing user: got %d want %d", got, minID)
|
||||
}
|
||||
|
||||
const missing int64 = 9_000_000_001
|
||||
got, err = resolveSeedActor(ctx, pool, missing)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != minID {
|
||||
t.Fatalf("missing user fallback: got %d want %d", got, minID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapSeeds_RestoresRetiredOfficialStatus(t *testing.T) {
|
||||
pool := openSeedPool(t)
|
||||
defer pool.Close()
|
||||
ctx := context.Background()
|
||||
|
||||
var exists bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM workflow_definitions WHERE def_key = 'rdev')`).Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Skip("rdev definition not present")
|
||||
}
|
||||
|
||||
if _, err := pool.Exec(ctx, `UPDATE workflow_definitions SET status = 'retired' WHERE def_key = 'rdev'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := BootstrapSeeds(ctx, pool, nil, t.TempDir(), 9_000_000_001); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var status string
|
||||
if err := pool.QueryRow(ctx, `SELECT status FROM workflow_definitions WHERE def_key = 'rdev'`).Scan(&status); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != "active" {
|
||||
t.Fatalf("rdev status = %q, want active", status)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ func (s *Store) RetireNonOfficialDefinitions(ctx context.Context) error {
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
UPDATE workflow_definitions
|
||||
SET project_selectable = true,
|
||||
status = 'active',
|
||||
catalog_order = CASE def_key
|
||||
WHEN 'main' THEN 10
|
||||
WHEN 'product_manager' THEN 20
|
||||
|
||||
@@ -401,7 +401,7 @@ func (s *Store) UpsertPublishedSeed(ctx context.Context, in CreateDefinitionInpu
|
||||
if latestHash == defHash {
|
||||
_, _ = tx.Exec(ctx, `
|
||||
UPDATE workflow_definitions SET name = $2, description = $3, project_selectable = true,
|
||||
catalog_order = $4, updated_at = now()
|
||||
catalog_order = $4, status = 'active', updated_at = now()
|
||||
WHERE id = $1
|
||||
`, defID, in.Name, in.Description, OfficialCatalogOrder(in.Key))
|
||||
return tx.Commit(ctx)
|
||||
@@ -410,7 +410,7 @@ func (s *Store) UpsertPublishedSeed(ctx context.Context, in CreateDefinitionInpu
|
||||
}
|
||||
_, err = tx.Exec(ctx, `
|
||||
UPDATE workflow_definitions SET name = $2, description = $3, product_domain = $4,
|
||||
project_selectable = true, catalog_order = $5, updated_at = now()
|
||||
project_selectable = true, catalog_order = $5, status = 'active', updated_at = now()
|
||||
WHERE id = $1
|
||||
`, defID, in.Name, in.Description, in.ProductDomain, OfficialCatalogOrder(in.Key))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user