chore: commit workspace updates

This commit is contained in:
ddCat
2026-09-14 01:11:56 +08:00
parent 01a2d171f5
commit 9727656b2b
491 changed files with 12826 additions and 72249 deletions

View File

@@ -0,0 +1,79 @@
package database
import (
"fmt"
"testing"
"infinite-canvas/backend/internal/model"
)
func TestMigrateSchemaV14BackfillsRecoveryAcrossBatches(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:agent-recovery-migration?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.CloudAgentExecution{}); err != nil {
t.Fatal(err)
}
for i := 0; i < 205; i++ {
status := []string{"running", "waiting_approval", "failed", "cancelled", "completed"}[i%5]
run := model.CloudAgentExecution{ID: fmt.Sprintf("run-%03d", i), UserID: "user", Status: status, Revision: 7, StateJSON: `{"request":{"canvasId":"canvas"},"activeTaskId":"child","mediaTaskId":"media"}`}
if err := db.Create(&run).Error; err != nil {
t.Fatal(err)
}
}
if err := db.Transaction(migrateSchemaV14); err != nil {
t.Fatal(err)
}
var runs []model.CloudAgentExecution
if err := db.Find(&runs).Error; err != nil {
t.Fatal(err)
}
for _, run := range runs {
if run.Status == "completed" {
continue
}
if run.CanvasID != "canvas" || run.ActiveTaskID != "child" || run.MediaTaskID != "media" || run.Revision != 7 {
t.Fatalf("missing recovery fields: %+v", run)
}
if run.CleanupPending != (run.Status == "failed" || run.Status == "cancelled") {
t.Fatalf("incorrect cleanup flag: %+v", run)
}
}
}
func TestMigrateSchemaV14KeepsCorruptExecutionRecoverable(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:agent-corrupt-migration?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.CloudAgentExecution{}); err != nil {
t.Fatal(err)
}
for _, run := range []model.CloudAgentExecution{
{ID: "a-valid", Status: "cancelled", StateJSON: `{"activeTaskId":"child"}`},
{ID: "b-corrupt", Status: "failed", StateJSON: `{broken`},
} {
if err := db.Create(&run).Error; err != nil {
t.Fatal(err)
}
}
err = db.Transaction(migrateSchemaV14)
if err != nil {
t.Fatal(err)
}
var valid model.CloudAgentExecution
if err := db.First(&valid, "id = ?", "a-valid").Error; err != nil {
t.Fatal(err)
}
if !valid.CleanupPending || valid.ActiveTaskID != "child" {
t.Fatalf("valid cancelled execution is not recoverable: %+v", valid)
}
var corrupt model.CloudAgentExecution
if err := db.First(&corrupt, "id = ?", "b-corrupt").Error; err != nil {
t.Fatal(err)
}
if !corrupt.CleanupPending || corrupt.ActiveTaskID != "b-corrupt" || corrupt.FailureMessage == "" {
t.Fatalf("corrupt execution was orphaned: %+v", corrupt)
}
}

View File

@@ -1,6 +1,7 @@
package database
import (
"encoding/json"
"errors"
"fmt"
"time"
@@ -10,7 +11,7 @@ import (
"gorm.io/gorm"
)
const CurrentSchemaVersion int64 = 10
const CurrentSchemaVersion int64 = 14
const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830"
const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830"
@@ -57,6 +58,59 @@ var schemaMigrations = []migration{
{version: 8, name: "logical_model_active_code", checksum: logicalModelActiveCodeChecksum, apply: migrateSchemaV8},
{version: 9, name: "channel_presentation", checksum: "sha256:channel-presentation-v9-20260908", apply: migrateChannelPresentation},
{version: 10, name: "creation_runtime", checksum: creationRuntimeChecksum, apply: migrateSchemaV10},
{version: 11, name: "cloud_agent_runtime", checksum: "sha256:cloud-agent-runtime-v11-20260912", apply: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.CloudAgentExecution{}) }},
{version: 12, name: "agent_token_charge_limit", checksum: "sha256:agent-token-charge-limit-v12-20260913", apply: migrateSchemaV12},
{version: 13, name: "cloud_agent_canvas_mutation", checksum: "sha256:cloud-agent-canvas-mutation-v13-20260913", apply: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.CloudAgentCanvasMutation{})
}},
{version: 14, name: "cloud_agent_recovery_control", checksum: "sha256:cloud-agent-recovery-control-v14", apply: migrateSchemaV14},
}
func migrateSchemaV14(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.CloudAgentExecution{}); err != nil {
return err
}
// Keep cancellation recoverable for executions admitted before this schema.
// Bound memory while retaining the migration transaction's all-or-nothing semantics.
after := ""
for {
var runs []model.CloudAgentExecution
if err := tx.Where("id > ? AND status <> ?", after, "completed").Order("id ASC").Limit(100).Find(&runs).Error; err != nil {
return err
}
if len(runs) == 0 {
return nil
}
for _, run := range runs {
var state struct {
Request struct {
CanvasID string `json:"canvasId"`
} `json:"request"`
ActiveTaskID string `json:"activeTaskId"`
MediaTaskID string `json:"mediaTaskId"`
}
// The root task ID is always a safe cancellation anchor. If an old
// transcript is damaged, retain a durable warning and cancel that
// root task during recovery instead of blocking the whole deployment.
updates := map[string]any{"active_task_id": run.ID}
if err := json.Unmarshal([]byte(run.StateJSON), &state); err != nil {
updates["failure_message"] = "旧 Agent 运行记录损坏,已保留根任务并进入安全收尾;请核对任务中心"
} else {
updates["canvas_id"] = state.Request.CanvasID
if state.ActiveTaskID != "" {
updates["active_task_id"] = state.ActiveTaskID
}
updates["media_task_id"] = state.MediaTaskID
}
if run.Status == "cancelled" || run.Status == "failed" {
updates["cleanup_pending"] = true
}
if err := tx.Model(&model.CloudAgentExecution{}).Where("id = ?", run.ID).Updates(updates).Error; err != nil {
return err
}
after = run.ID
}
}
}
func migrateChannelPresentation(tx *gorm.DB) error {
@@ -223,6 +277,20 @@ func migrateSchemaV10(tx *gorm.DB) error {
return nil
}
// migrateSchemaV12 为 Agent 的 Token 计费增加最终扣费上限;旧账单保持 0,继续沿用既有按 usage 结算语义。
func migrateSchemaV12(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.BillingOrder{}) {
return nil
}
if tx.Migrator().HasColumn(&model.BillingOrder{}, "ChargeLimitMicrocredits") {
return nil
}
if err := tx.Migrator().AddColumn(&model.BillingOrder{}, "ChargeLimitMicrocredits"); err != nil {
return fmt.Errorf("增加 Agent Token 扣费上限列:%w", err)
}
return nil
}
func MigrateSchema(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if tx.Dialector.Name() == "postgres" {

View File

@@ -62,6 +62,22 @@ func TestMigrateSchemaV8AllowsReusingArchivedLogicalModelCode(t *testing.T) {
}
}
func TestMigrateSchemaV12AddsAgentTokenChargeLimit(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-agent-token-charge-limit?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := db.Exec(`CREATE TABLE billing_orders (id text PRIMARY KEY)`).Error; err != nil {
t.Fatal(err)
}
if err := migrateSchemaV12(db); err != nil {
t.Fatal(err)
}
if !db.Migrator().HasColumn(&model.BillingOrder{}, "ChargeLimitMicrocredits") {
t.Fatal("migration v12 did not add Agent token charge limit")
}
}
func TestMigrateSchemaRejectsChecksumMismatch(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-checksum?mode=memory&cache=shared"})
if err != nil {
@@ -257,3 +273,21 @@ func TestRequireSchemaVersionRejectsUninitializedDatabase(t *testing.T) {
t.Fatalf("expected missing migration error, got %v", err)
}
}
func TestMigrateSchemaV13AddsCloudAgentCanvasMutation(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-cloud-agent-canvas-mutation?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := MigrateSchema(db); err != nil {
t.Fatal(err)
}
if !db.Migrator().HasTable(&model.CloudAgentCanvasMutation{}) {
t.Fatal("migration v13 did not create cloud agent canvas mutation table")
}
for _, field := range []string{"RunID", "BeforeSnapshotHash", "AfterSnapshotHash", "BeforeJSON", "HasSubmittedTask", "Status"} {
if !db.Migrator().HasColumn(&model.CloudAgentCanvasMutation{}, field) {
t.Fatalf("migration v13 did not add %s", field)
}
}
}

View File

@@ -16,6 +16,8 @@ import (
// Models 是应用持久化表的唯一清单,服务启动和跨数据库迁移必须共用它。
func Models() []any {
return []any{
&model.CloudAgentExecution{},
&model.CloudAgentCanvasMutation{},
&model.User{},
&model.AuthSession{},
&model.UserIdentity{},
@@ -90,13 +92,8 @@ func Models() []any {
&model.CreationRun{},
&model.CreationSubmission{},
&model.TaskTextDelta{},
&model.Session{},
&model.Message{},
&model.TaskLog{},
&model.SessionFile{},
&model.Result{},
&model.ComfyBridge{},
&model.ComfyBridgeRequest{},
}
}

View File

@@ -60,3 +60,12 @@ func TestPostgresAssetIDMigrationsCoverEveryAssetIDColumn(t *testing.T) {
}
}
}
func TestModelsIncludeCloudAgentCanvasMutation(t *testing.T) {
for _, item := range Models() {
if _, ok := item.(*model.CloudAgentCanvasMutation); ok {
return
}
}
t.Fatal("Models must include CloudAgentCanvasMutation")
}