From 5c26a86331da49ac3041d62eecfecd9af53daf50 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 14:11:07 +0000 Subject: [PATCH] Publish schema target and compatibility floor Why: a rollback can only be safe when the candidate says which schema it will migrate to and whether older binaries can still read that schema. A new migration that forgets to register that fact must fail the test. Evidence: go test ./internal/store/ ./pkg/identity/ -run 'TestMigrationCompat|TestCapabilities' passed. Impact: version --json and identity capabilities include schema.target and schema.compatFloor. Every migration version is listed in migrationBreaksOldReaders. Current versions do not break old readers, so the floor stays 0. Refs: recovery plan r3.1 D2 Co-authored-by: Cursor --- internal/store/migrate_compat.go | 49 +++++++++++++++++++++++++++ internal/store/migrate_compat_test.go | 20 +++++++++++ pkg/identity/identity.go | 2 ++ pkg/identity/identity_test.go | 3 ++ 4 files changed, 74 insertions(+) create mode 100644 internal/store/migrate_compat.go create mode 100644 internal/store/migrate_compat_test.go diff --git a/internal/store/migrate_compat.go b/internal/store/migrate_compat.go new file mode 100644 index 0000000..cd8e6fb --- /dev/null +++ b/internal/store/migrate_compat.go @@ -0,0 +1,49 @@ +package store + +// migrationBreaksOldReaders 登记每个迁移版本落地后,schema.target 更低的旧制品 +// 还能不能读这个库。新增迁移漏登记时 TestMigrationCompatRegistered 失败。 +// true 会抬高 SchemaCompatFloor:候选若要把库迁过该版本,而旧制品的 target +// 低于该下限,切换必须在停旧服务前拒绝,除非走已确认的 forward-only 迁移。 +var migrationBreaksOldReaders = map[int]bool{ + 1: false, + 2: false, + 3: false, + 4: false, + 5: false, + 6: false, + 7: false, + 8: false, + 9: false, + 10: false, + 11: false, + 12: false, + 13: false, + 14: false, + 15: false, + 16: false, + 17: false, + 18: false, + 19: false, + 20: false, + 21: false, + 22: false, + 23: false, + 24: false, + 25: false, + 26: false, + 27: false, +} + +// SchemaTarget 是本二进制会迁移到的 schema 版本。 +func SchemaTarget() int { return latestMigrationVersion() } + +// SchemaCompatFloor 是旧制品仍能读取的最高破坏性版本。0 表示当前全部迁移都不破坏旧读。 +func SchemaCompatFloor() int { + floor := 0 + for version, breaks := range migrationBreaksOldReaders { + if breaks && version > floor { + floor = version + } + } + return floor +} diff --git a/internal/store/migrate_compat_test.go b/internal/store/migrate_compat_test.go new file mode 100644 index 0000000..6c46cf7 --- /dev/null +++ b/internal/store/migrate_compat_test.go @@ -0,0 +1,20 @@ +package store + +import "testing" + +func TestMigrationCompatRegistered(t *testing.T) { + if len(migrationBreaksOldReaders) != len(migrations) { + t.Fatalf("兼容登记 %d 条,迁移 %d 条", len(migrationBreaksOldReaders), len(migrations)) + } + for _, m := range migrations { + if _, ok := migrationBreaksOldReaders[m.version]; !ok { + t.Fatalf("迁移 v%d %s 未登记是否破坏旧制品读取", m.version, m.name) + } + } + if SchemaTarget() != latestMigrationVersion() { + t.Fatalf("target=%d latest=%d", SchemaTarget(), latestMigrationVersion()) + } + if SchemaCompatFloor() < 0 || SchemaCompatFloor() > SchemaTarget() { + t.Fatalf("compatFloor=%d target=%d", SchemaCompatFloor(), SchemaTarget()) + } +} diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index fcd6e82..76a4e92 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -14,6 +14,7 @@ import ( "strings" "agent-system-backend/internal/app" + "agent-system-backend/internal/store" ) // Usage 无参/未知子命令时打印到 stderr 的用法行。探测契约:同时含 "identity" @@ -65,6 +66,7 @@ func NewDocument(product, version string) Document { Version: version, Identity: IdentityCaps{Ops: []string{"ensure", "verify"}, Open: "existing-no-migrate", DSN: "env"}, CLI: CLICaps{UnknownSubcommand: "reject"}, + Schema: &SchemaCaps{Target: store.SchemaTarget(), CompatFloor: store.SchemaCompatFloor()}, } } diff --git a/pkg/identity/identity_test.go b/pkg/identity/identity_test.go index 1b1ae0c..f58aff3 100644 --- a/pkg/identity/identity_test.go +++ b/pkg/identity/identity_test.go @@ -61,6 +61,9 @@ func TestCapabilities(t *testing.T) { if doc.Protocol != 1 || doc.Identity.Open != "existing-no-migrate" || doc.CLI.UnknownSubcommand != "reject" { t.Fatalf("能力文档异常:%+v", doc) } + if doc.Schema == nil || doc.Schema.Target != store.SchemaTarget() || doc.Schema.CompatFloor != store.SchemaCompatFloor() { + t.Fatalf("schema 能力异常:%+v", doc.Schema) + } } func migratedData(t *testing.T) (string, int) {