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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user