feat: publish immutable workflow definitions with typed SDK

This commit is contained in:
2026-09-18 00:28:00 +08:00
parent a664ec9370
commit 520e98d9d1
33 changed files with 2603 additions and 11 deletions

View File

@@ -14,6 +14,7 @@ import (
// BootstrapInput 仅供持有数据库管理权限的初始化命令使用,不暴露为公共 HTTP。
type BootstrapInput struct {
WorkflowAccess bool // WorkflowAccess 显式授予流程定义读取和发布权限,不含实例执行权限。
RecoveryAccess bool // RecoveryAccess 显式授予任务核查、人工重试及操作审计读取权限。
EventAccess bool // EventAccess 显式增加结果订阅查询、管理和消费权限。
Application string // Application 新应用名称,与 ExistingApplicationID 二选一。
@@ -76,6 +77,9 @@ func Bootstrap(ctx context.Context, db *sql.DB, in BootstrapInput, deliver func(
if in.WorkerAccess {
permissions.Actions = append(permissions.Actions, "workers:connect", "workers:consume")
}
if in.WorkflowAccess {
permissions.Actions = append(permissions.Actions, "workflows:definitions:read", "workflows:definitions:publish")
}
if in.ScheduleAccess {
permissions.Actions = append(permissions.Actions, "schedules:read", "schedules:manage")
}

View File

@@ -0,0 +1,258 @@
package mysql
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
driver "github.com/go-sql-driver/mysql"
)
// WorkflowDefinitionStore 只管理定义与不可变版本,关联校验、幂等和审计同事务,不创建业务任务。
type WorkflowDefinitionStore struct{ *TaskStore }
func NewWorkflowDefinitionStore(db *sql.DB) *WorkflowDefinitionStore {
return &WorkflowDefinitionStore{NewTaskStore(db)}
}
var _ definition.Store = (*WorkflowDefinitionStore)(nil)
func readWorkflowDefinition(ctx context.Context, tx *sql.Tx, ns, id core.ID, lock bool) (definition.Summary, error) {
var out definition.Summary
q := `SELECT id,namespace_id,name,description,revision,created_at,updated_at FROM weave_definitions WHERE namespace_id=? AND id=?`
if lock {
q += ` FOR UPDATE`
}
err := tx.QueryRowContext(ctx, q, ns, id).Scan(&out.ID, &out.NamespaceID, &out.Name, &out.Description, &out.Revision, &out.CreatedAt, &out.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
err = fault.NotFound
}
return out, err
}
func workflowAnalysis(c definition.Checked) definition.Analysis {
queues := append([]core.ID{}, c.Queues...)
return definition.Analysis{Steps: c.Steps, MaxConcurrency: c.MaxConcurrency, MaxTaskAttempts: c.MaxTaskAttempts, Queues: queues}
}
func readWorkflowVersion(ctx context.Context, tx *sql.Tx, ns, id core.ID, version string) (definition.VersionView, error) {
var out definition.VersionView
var raw []byte
var schema uint32
var actor string
var digest core.Digest
err := tx.QueryRowContext(ctx, `SELECT id,namespace_id,definition_id,version,schema_version,specification,content_digest,published_by,created_at FROM weave_definition_versions WHERE namespace_id=? AND definition_id=? AND version=?`, ns, id, version).Scan(&out.ID, &out.NamespaceID, &out.DefinitionID, &out.Version, &schema, &raw, &digest, &actor, &out.PublishedAt)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.NotFound
}
if err != nil {
return out, err
}
out.PublishedBy = core.ID(actor)
if out.PublishedBy.Validate() != nil {
return out, errors.New("invalid stored workflow publisher")
}
if len(raw) > definition.MaxSpecificationBytes || json.Unmarshal(raw, &out.Specification) != nil || out.Specification.SchemaVersion != schema {
return out, errors.New("invalid stored workflow specification")
}
checked, err := definition.Check(out.Specification)
if err != nil {
return out, err
}
// 同时检查原文,不能让JSON解码忽略的未知字段或大小写别名绕过完整性验证。
canonical, err := jsonvalue.Canonical(raw)
if err != nil {
return out, err
}
rawDigest := core.Digest(sha256.Sum256(append([]byte("orbit.weave.spec.v1\x00"), canonical...)))
if digest != checked.Digest || digest != rawDigest {
return out, errors.New("workflow version digest mismatch")
}
out.ContentDigest = hex.EncodeToString(digest[:])
out.Analysis = workflowAnalysis(checked)
return out, nil
}
func authorizeWorkflowQueues(ctx context.Context, tx *sql.Tx, ns core.ID, p auth.Permissions, queues []core.ID) error {
for _, id := range queues {
if !p.AllowsQueue(id) {
return fault.NotFound
}
var found core.ID
err := tx.QueryRowContext(ctx, `SELECT id FROM pulse_queues WHERE namespace_id=? AND id=? FOR SHARE`, ns, id).Scan(&found)
if errors.Is(err, sql.ErrNoRows) {
return fault.NotFound
}
if err != nil {
return err
}
}
return nil
}
func (s *WorkflowDefinitionStore) Create(ctx context.Context, t auth.Token, key string, in definition.CreateInput) (definition.CreateResult, error) {
var out definition.CreateResult
err := s.transaction(ctx, t, definition.PublishAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := lockApplicationResources(ctx, tx, t.NamespaceID, false); err != nil {
return err
}
id, err := core.NewID()
if err != nil {
return err
}
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "workflow.definition.create", "", key, "workflow_definition", id, in)
if err != nil {
return err
}
if cached != nil {
if err := json.Unmarshal(cached, &out); err != nil {
return err
}
out.Deduplicated = true
return nil
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_definitions WHERE namespace_id=?`, t.NamespaceID).Scan(&count); err != nil {
return err
}
if count >= definition.MaxDefinitions {
return fault.New("WORKFLOW_DEFINITION_LIMIT", "namespace workflow definition limit reached")
}
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO weave_definitions(namespace_id,id,name,description,created_at,updated_at) VALUES (?,?,?,?,?,?)`, t.NamespaceID, id, in.Name, in.Description, now, now)
var duplicate *driver.MySQLError
if errors.As(err, &duplicate) && duplicate.Number == 1062 {
return fault.Conflict
}
if err != nil {
return err
}
out.Definition, err = readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false)
if err != nil {
return err
}
if err := appendAudit(ctx, tx, t, identity, id, "workflow_definition", "workflow.definition.create", "create workflow definition", nil, out.Definition); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
func (s *WorkflowDefinitionStore) Get(ctx context.Context, t auth.Token, id core.ID) (definition.Summary, error) {
var out definition.Summary
err := s.transaction(ctx, t, definition.ReadAction, func(tx *sql.Tx, _ auth.Permissions) error {
var err error
out, err = readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false)
return err
})
return out, err
}
func (s *WorkflowDefinitionStore) Publish(ctx context.Context, t auth.Token, id core.ID, key string, in definition.PublishInput) (definition.PublishResult, error) {
var out definition.PublishResult
checked, err := definition.Check(in.Specification)
if err != nil {
return out, fault.Invalid(err.Error())
}
err = s.transaction(ctx, t, definition.PublishAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := lockApplicationResources(ctx, tx, t.NamespaceID, false); err != nil {
return err
}
// 摘要身份冻结整个命令(含原因),同版本去重只比较specification内容摘要。
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "workflow.version.publish", string(id), key, "workflow_definition", id, in)
if err != nil {
return err
}
before, err := readWorkflowDefinition(ctx, tx, t.NamespaceID, id, true)
if err != nil {
return err
}
if err := authorizeWorkflowQueues(ctx, tx, t.NamespaceID, p, checked.Queues); err != nil {
return err
}
stored, err := readWorkflowVersion(ctx, tx, t.NamespaceID, id, in.Version)
if err != nil && !errors.Is(err, fault.NotFound) {
return err
}
if cached != nil {
if err != nil {
return errors.New("published workflow version missing")
}
if err := json.Unmarshal(cached, &out); err != nil {
return err
}
if out.Version.ID != stored.ID || stored.ContentDigest != hex.EncodeToString(checked.Digest[:]) {
return errors.New("workflow publication receipt mismatch")
}
out.Version = stored
out.Deduplicated = true
return nil
}
if err == nil {
if stored.ContentDigest != hex.EncodeToString(checked.Digest[:]) {
return fault.Conflict
}
out = definition.PublishResult{Version: stored, Deduplicated: true}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_definition_versions WHERE namespace_id=? AND definition_id=?`, t.NamespaceID, id).Scan(&count); err != nil {
return err
}
if count >= definition.MaxVersions {
return fault.New("WORKFLOW_VERSION_LIMIT", "workflow definition version limit reached")
}
versionID, err := core.NewID()
if err != nil {
return err
}
raw, err := jsonwire.Marshal(in.Specification)
if err != nil {
return err
}
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO weave_definition_versions(namespace_id,id,definition_id,version,schema_version,specification,content_digest,published_by,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)`, t.NamespaceID, versionID, id, in.Version, in.Specification.SchemaVersion, string(raw), checked.Digest, string(t.CredentialID), now, now)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `UPDATE weave_definitions SET revision=revision+1,updated_at=? WHERE namespace_id=? AND id=?`, now, t.NamespaceID, id); err != nil {
return err
}
out.Version, err = readWorkflowVersion(ctx, tx, t.NamespaceID, id, in.Version)
if err != nil {
return err
}
// 审计记录固定版本、摘要和预算,不复制潜在敏感的业务常量。
audit := map[string]any{"version_id": versionID, "version": in.Version, "content_digest": out.Version.ContentDigest, "analysis": out.Version.Analysis, "revision": before.Revision + 1}
if err := appendAudit(ctx, tx, t, identity, id, "workflow_definition", "workflow.version.publish", in.Reason, map[string]any{"revision": before.Revision}, audit); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
func (s *WorkflowDefinitionStore) Version(ctx context.Context, t auth.Token, id core.ID, version string) (definition.VersionView, error) {
var out definition.VersionView
err := s.transaction(ctx, t, definition.ReadAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false); err != nil {
return err
}
var err error
out, err = readWorkflowVersion(ctx, tx, t.NamespaceID, id, version)
if err != nil {
return err
}
return authorizeWorkflowQueues(ctx, tx, t.NamespaceID, p, out.Analysis.Queues)
})
return out, err
}

View File

@@ -0,0 +1,295 @@
package mysql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"reflect"
"sync"
"testing"
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
)
type workflowFixture struct {
t *testing.T
ctx context.Context
db *sql.DB
resource BootstrapResult
token auth.Token
api *definition.Service
}
func newWorkflowFixture(t *testing.T, ctx context.Context, db *sql.DB, access bool) *workflowFixture {
t.Helper()
f := &workflowFixture{t: t, ctx: ctx, db: db, api: definition.New(NewWorkflowDefinitionStore(db))}
if err := Bootstrap(ctx, db, BootstrapInput{Application: "workflow-" + string(executionID(t)), Namespace: "test", Environment: "test", Queue: "work", WorkflowAccess: access}, func(v BootstrapResult) error { f.resource = v; return nil }); err != nil {
t.Fatal(err)
}
var err error
f.token, err = auth.Parse(f.resource.Token)
if err != nil {
t.Fatal(err)
}
return f
}
func (f *workflowFixture) create(key string) definition.CreateResult {
f.t.Helper()
v, err := f.api.Create(f.ctx, f.token, key, definition.CreateInput{Name: "flow-" + key, Description: "通用作业处理"})
if err != nil {
f.t.Fatal(err)
}
return v
}
func (f *workflowFixture) count(q string, args ...any) int {
f.t.Helper()
var n int
if err := f.db.QueryRowContext(f.ctx, q, args...).Scan(&n); err != nil {
f.t.Fatal(err)
}
return n
}
func (f *workflowFixture) exec(q string, args ...any) {
f.t.Helper()
if _, err := f.db.ExecContext(f.ctx, q, args...); err != nil {
f.t.Fatal(err)
}
}
func workflowTestSpec(queue core.ID) definition.Specification {
typ := definition.Type{Kind: "OBJECT", Fields: map[string]definition.Type{"n": {Kind: "NUMBER"}}}
task := definition.Task{QueueID: queue, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", InputType: typ, Input: definition.Value{Kind: "LITERAL", Literal: json.RawMessage(`{"n":9007199254740993}`)}, ResultType: typ, TimeoutMS: 5000, Retry: pulse.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 3000}, RecoverySafety: pulse.RecoveryReconcileOnly}
return definition.Specification{SchemaVersion: 1, InputType: typ, TimeoutMS: 30000, FailurePolicy: "REVIEW", Body: definition.Block{Steps: []definition.Step{{Key: "prepare", Kind: "TASK", Task: &task}}, OutputType: typ, Output: definition.Value{Kind: "REFERENCE", Reference: &definition.Reference{Source: "prepare"}}}}
}
func TestIntegrationWorkflowDefinitions(t *testing.T) {
dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN")
if dsn == "" {
t.Skip("requires disposable MySQL")
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
db, err := Open(ctx, dsn, 24, 8)
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := Migrate(ctx, db); err != nil {
t.Fatal(err)
}
t.Run("concurrency_precision_and_immutable_versions", func(t *testing.T) {
f := newWorkflowFixture(t, ctx, db, true)
key := string(executionID(t))
created := f.create(key)
id := created.Definition.ID
dupe, err := f.api.Create(ctx, f.token, key, definition.CreateInput{Name: "flow-" + key, Description: "通用作业处理"})
if err != nil || !dupe.Deduplicated || dupe.Definition.ID != id {
t.Fatal("create replay", err)
}
in := definition.PublishInput{Version: "v1", Specification: workflowTestSpec(f.resource.QueueID), Reason: "release fixed specification"}
type result struct {
out definition.PublishResult
err error
}
ch := make(chan result, 12)
var wg sync.WaitGroup
// 原键竞争和不同键的同版本竞争都必须只提交一次发布。
for i := 0; i < 12; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
k := fmt.Sprintf("release-%d", i/2)
out, err := f.api.Publish(ctx, f.token, id, k, in)
ch <- result{out, err}
}(i)
}
wg.Wait()
close(ch)
fresh := 0
var original definition.VersionView
for r := range ch {
if r.err != nil {
t.Fatal(r.err)
}
if original.ID == "" {
original = r.out.Version
}
if !reflect.DeepEqual(original, r.out.Version) {
t.Fatal("version changed across replies")
}
if !r.out.Deduplicated {
fresh++
}
}
if fresh != 1 {
t.Fatalf("new publications=%d", fresh)
}
in.Specification.Body.Steps[0].Task.Input.Literal = json.RawMessage(`{"n":9007199254740993.0}`)
equal, err := f.api.Publish(ctx, f.token, id, "release-0", in)
if err != nil || !equal.Deduplicated || !reflect.DeepEqual(equal.Version, original) {
t.Fatal("canonical replay", err)
}
in.Specification.Body.Steps[0].Task.Input.Literal = json.RawMessage(`{"n":9007199254740992}`)
_, err = f.api.Publish(ctx, f.token, id, "release-0", in)
expectWorkerCode(t, err, "IDEMPOTENCY_CONFLICT")
_, err = f.api.Publish(ctx, f.token, id, "changed-content", in)
expectWorkerCode(t, err, "STATE_CONFLICT")
in.Version = "v2"
v2, err := f.api.Publish(ctx, f.token, id, "release-v2", in)
if err != nil || v2.Version.ContentDigest == original.ContentDigest {
t.Fatal("publish v2", err)
}
v1, err := f.api.Version(ctx, f.token, id, "v1")
if err != nil || !reflect.DeepEqual(v1, original) {
t.Fatal("old version changed", err)
}
metadata, err := f.api.Get(ctx, f.token, id)
if err != nil || metadata.Revision != 3 {
t.Fatal("metadata revision", err)
}
if n := f.count(`SELECT COUNT(*) FROM orbit_audit_events WHERE namespace_id=? AND resource_id=? AND action='workflow.version.publish'`, f.resource.NamespaceID, id); n != 2 {
t.Fatalf("audit=%d", n)
}
if n := f.count(`SELECT COUNT(*) FROM pulse_tasks WHERE namespace_id=?`, f.resource.NamespaceID); n != 0 {
t.Fatal("publish created tasks")
}
if _, err := db.ExecContext(ctx, `UPDATE weave_definition_versions SET version='modified' WHERE namespace_id=? AND id=?`, f.resource.NamespaceID, original.ID); err == nil {
t.Fatal("immutable update allowed")
}
})
t.Run("authorization_and_associations_without_foreign_keys", func(t *testing.T) {
f := newWorkflowFixture(t, ctx, db, true)
g := newWorkflowFixture(t, ctx, db, false)
id := f.create(string(executionID(t))).Definition.ID
_, err := g.api.Create(ctx, g.token, string(executionID(t)), definition.CreateInput{Name: "forbidden"})
expectWorkerCode(t, err, "PERMISSION_DENIED")
in := definition.PublishInput{Version: "v1", Reason: "authorization regression", Specification: workflowTestSpec(f.resource.QueueID)}
accepted, err := f.api.Publish(ctx, f.token, id, "pub", in)
if err != nil {
t.Fatal(err)
}
permissions := func(actions []string, queues []string) {
raw, _ := json.Marshal(auth.Permissions{Actions: actions, Queues: queues})
f.exec(`UPDATE orbit_credentials SET permissions=? WHERE namespace_id=? AND id=?`, string(raw), f.resource.NamespaceID, f.resource.CredentialID)
}
permissions([]string{definition.ReadAction, definition.PublishAction}, []string{string(g.resource.QueueID)})
if _, err := f.api.Get(ctx, f.token, id); err != nil {
t.Fatal("metadata scope", err)
}
_, err = f.api.Version(ctx, f.token, id, "v1")
expectWorkerCode(t, err, "NOT_FOUND")
_, err = f.api.Publish(ctx, f.token, id, "pub", in)
expectWorkerCode(t, err, "NOT_FOUND")
in.Version = "v2"
in.Specification.Body.Steps[0].Task.QueueID = g.resource.QueueID
_, err = f.api.Publish(ctx, f.token, id, "foreign-queue", in)
expectWorkerCode(t, err, "NOT_FOUND")
permissions([]string{definition.ReadAction, definition.PublishAction}, []string{"*"})
in.Specification.Body.Steps[0].Task.QueueID = executionID(t)
_, err = f.api.Publish(ctx, f.token, id, "absent-queue", in)
expectWorkerCode(t, err, "NOT_FOUND")
_, err = f.api.Publish(ctx, f.token, executionID(t), "absent-parent", in)
expectWorkerCode(t, err, "NOT_FOUND")
raw, _ := json.Marshal(auth.Permissions{Actions: []string{definition.ReadAction, definition.PublishAction}, Queues: []string{"*"}})
g.exec(`UPDATE orbit_credentials SET permissions=? WHERE namespace_id=? AND id=?`, string(raw), g.resource.NamespaceID, g.resource.CredentialID)
_, err = g.api.Version(ctx, g.token, id, "v1")
expectWorkerCode(t, err, "NOT_FOUND")
// 队列被人工删除后原版本不可用,不能依赖数据库外键兜底。
f.exec(`DELETE FROM pulse_queues WHERE namespace_id=? AND id=?`, f.resource.NamespaceID, f.resource.QueueID)
_, err = f.api.Version(ctx, f.token, id, "v1")
expectWorkerCode(t, err, "NOT_FOUND")
if n := f.count(`SELECT COUNT(*) FROM weave_definition_versions WHERE namespace_id=? AND definition_id=?`, f.resource.NamespaceID, id); n != 1 {
t.Fatal("rejected publication persisted")
}
if n := f.count(`SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA=DATABASE()`); n != 0 {
t.Fatal("foreign keys exist")
}
if accepted.Version.ID == "" {
t.Fatal("missing publication")
}
})
t.Run("audit_failure_rolls_back_entire_publication", func(t *testing.T) {
f := newWorkflowFixture(t, ctx, db, true)
id := f.create(string(executionID(t))).Definition.ID
in := definition.PublishInput{Version: "v1", Reason: "atomic publication", Specification: workflowTestSpec(f.resource.QueueID)}
f.exec(`CREATE TRIGGER orbit_test_workflow_audit BEFORE INSERT ON orbit_audit_events FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='publication audit failure'`)
_, err := f.api.Publish(ctx, f.token, id, "atomic", in)
f.exec(`DROP TRIGGER orbit_test_workflow_audit`)
if err == nil {
t.Fatal("audit failure accepted")
}
for _, q := range []string{`SELECT COUNT(*) FROM weave_definition_versions WHERE namespace_id=?`, `SELECT COUNT(*) FROM orbit_idempotency_keys WHERE namespace_id=? AND operation='workflow.version.publish'`} {
if f.count(q, f.resource.NamespaceID) != 0 {
t.Fatal("partial publication committed")
}
}
metadata, err := f.api.Get(ctx, f.token, id)
if err != nil || metadata.Revision != 1 {
t.Fatal("revision not rolled back", err)
}
out, err := f.api.Publish(ctx, f.token, id, "atomic", in)
if err != nil || out.Deduplicated {
t.Fatal("retry rolled back publication", err)
}
})
t.Run("damaged_or_orphaned_versions_fail_closed", func(t *testing.T) {
f := newWorkflowFixture(t, ctx, db, true)
id := f.create(string(executionID(t))).Definition.ID
in := definition.PublishInput{Version: "v1", Reason: "stored integrity", Specification: workflowTestSpec(f.resource.QueueID)}
out, err := f.api.Publish(ctx, f.token, id, "original", in)
if err != nil {
t.Fatal(err)
}
// 只在隔离库新增损坏记录,不停用不可变触发器;不依赖外键阻止这些写入。
f.exec(`INSERT INTO weave_definition_versions(namespace_id,id,definition_id,version,schema_version,specification,content_digest,published_by) SELECT namespace_id,?,definition_id,'damaged',schema_version,specification,?,published_by FROM weave_definition_versions WHERE namespace_id=? AND id=?`, executionID(t), core.Digest{}, f.resource.NamespaceID, out.Version.ID)
_, err = f.api.Version(ctx, f.token, id, "damaged")
if err == nil {
t.Fatal("damaged digest accepted")
}
f.exec(`DELETE FROM weave_definitions WHERE namespace_id=? AND id=?`, f.resource.NamespaceID, id)
_, err = f.api.Version(ctx, f.token, id, "v1")
expectWorkerCode(t, err, "NOT_FOUND")
_, err = f.api.Publish(ctx, f.token, id, "original", in)
expectWorkerCode(t, err, "NOT_FOUND")
})
t.Run("bounded_metadata_and_receipts_at_capacity", func(t *testing.T) {
f := newWorkflowFixture(t, ctx, db, true)
first := f.create("first")
id := first.Definition.ID
// 使用真实API填满元数据限额;重复回执必须仍可读取。
for i := 1; i < definition.MaxDefinitions; i++ {
f.create(fmt.Sprintf("cap-%d", i))
}
_, err := f.api.Create(ctx, f.token, "overflow", definition.CreateInput{Name: "overflow"})
expectWorkerCode(t, err, "WORKFLOW_DEFINITION_LIMIT")
out, err := f.api.Create(ctx, f.token, "first", definition.CreateInput{Name: "flow-first", Description: "通用作业处理"})
if err != nil || !out.Deduplicated {
t.Fatal("create replay at limit", err)
}
in := definition.PublishInput{Reason: "bounded version history", Specification: workflowTestSpec(f.resource.QueueID)}
for i := 0; i < definition.MaxVersions; i++ {
in.Version = fmt.Sprintf("v%d", i)
if _, err := f.api.Publish(ctx, f.token, id, in.Version, in); err != nil {
t.Fatal(err)
}
}
in.Version = "overflow"
_, err = f.api.Publish(ctx, f.token, id, "overflow", in)
expectWorkerCode(t, err, "WORKFLOW_VERSION_LIMIT")
in.Version = "v0"
same, err := f.api.Publish(ctx, f.token, id, "v0", in)
if err != nil || !same.Deduplicated {
t.Fatal("original version replay at limit", err)
}
same, err = f.api.Publish(ctx, f.token, id, "new-key-same-version", in)
if err != nil || !same.Deduplicated {
t.Fatal("same version replay at limit", err)
}
})
}