Persist candidate scope scrub state across restarts
Why: A crash after dropping candidate raw prompt columns but before SQLite compaction left physical prompt bytes without a visible reason to retry, and storage backup could accept that file. Evidence: RED post-commit interruption and pending-backup tests; full Go suite, targeted race, isolated PostgreSQL package tests, vet, and build passed locally before commit. Impact: Commit a durable pending marker with v35, retry compaction and WAL truncation until completion, conservatively re-scrub earlier v35 candidates, and reject backup or migration while cleanup is pending or unverified. Refs: Approved RDM stage6 W6 source-recheck design adjustment r1; W6 independent review crash-window finding.
This commit is contained in:
@@ -430,6 +430,21 @@ func refuseCandidateRawContextScope(db *sql.DB) error {
|
||||
if columns > 0 {
|
||||
return fmt.Errorf("candidate context scope text columns remain; isolate and scrub source before backup or migration")
|
||||
}
|
||||
var version int
|
||||
if sqliteHasTable(db, "schema_migrations") {
|
||||
if err := db.QueryRow(`SELECT COALESCE(MAX(version),0) FROM schema_migrations`).Scan(&version); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if version >= 34 {
|
||||
if !sqliteHasTable(db, "context_scope_scrub_state") {
|
||||
return fmt.Errorf("candidate context physical scrub marker missing; isolate and scrub source before backup or migration")
|
||||
}
|
||||
var pending int
|
||||
if err := db.QueryRow(`SELECT pending FROM context_scope_scrub_state WHERE id=1`).Scan(&pending); err != nil || pending != 0 {
|
||||
return fmt.Errorf("candidate context physical scrub pending or unverified; isolate and scrub source before backup or migration")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,8 @@ func TestCopyOrderCoversAllFixtureTables(t *testing.T) {
|
||||
for _, t := range copyOrder {
|
||||
known[t] = true
|
||||
}
|
||||
known["schema_migrations"] = true // 迁移器管理,不随迁
|
||||
known["schema_migrations"] = true // 迁移器管理,不随迁
|
||||
known["context_scope_scrub_state"] = true // SQLite 候选原文物理清理状态,不随迁
|
||||
// 运行态表不随迁:执行许可是进程瞬态;执行租约与跨实例命令属当前
|
||||
// 实例的调度现场(接管语义在 U5 内由 lease/fencing 处理,不复制历史)。
|
||||
for _, rt := range []string{"admission_leases", "conversation_executions", "run_commands"} {
|
||||
@@ -248,3 +249,58 @@ func TestBackupAndMigrateRejectCandidateRawContextScope(t *testing.T) {
|
||||
t.Fatal("backup retained candidate raw scope body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupAndMigrateRejectPendingOrMissingContextScrubMarker(t *testing.T) {
|
||||
path := buildFixture(t)
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, query := range []string{
|
||||
`CREATE TABLE IF NOT EXISTS context_scope_scrub_state(id INTEGER PRIMARY KEY,pending INTEGER NOT NULL)`,
|
||||
`INSERT OR REPLACE INTO context_scope_scrub_state(id,pending) VALUES(1,1)`,
|
||||
} {
|
||||
if _, err := db.Exec(query); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRejected := func(name string) {
|
||||
t.Helper()
|
||||
out := filepath.Join(t.TempDir(), name+".db")
|
||||
if err := cmdBackup([]string{"--source", path, "--out", out}); err == nil {
|
||||
t.Fatalf("backup accepted %s context scrub state", name)
|
||||
}
|
||||
if _, err := os.Stat(out); !os.IsNotExist(err) {
|
||||
t.Fatalf("rejected %s backup created output: %v", name, err)
|
||||
}
|
||||
if err := cmdMigrate([]string{"--source", path, "--target", "postgres://invalid", "--migration-id", name}); err == nil {
|
||||
t.Fatalf("migration accepted %s context scrub state", name)
|
||||
}
|
||||
}
|
||||
assertRejected("pending")
|
||||
db, err = sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(`DROP TABLE context_scope_scrub_state`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRejected("missing")
|
||||
db, err = sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(`DELETE FROM schema_migrations WHERE version=35`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRejected("v34-missing")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user