Why: identity ensure/verify went through the normal store open, which creates a database and runs migrations. A deploy gate that calls the candidate before stopping the old service could migrate production ahead of the switch, and a missing database was turned into an empty one. Evidence: go test ./pkg/identity/ ./internal/store/ ./cmd/agent-system/ -run 'TestCapabilities|TestEnsure|TestVerify|TestDSN|TestUsage|TestSystemIdentity|TestOpenIdentity|TestUnknownSubcommand' passed in this worktree. Impact: identity CLI refuses a missing file or a missing agent_system_identity table, does not migrate, reads DSN from the environment or a 0600 file, and version --json plus identity capabilities emit one capability document. Unknown top-level commands exit 2. Refs: recovery plan r3.1 D1; Dev feature/svc-d1-identity-flow Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSystemIdentityEnsureVerifySQLite(t *testing.T) {
|
|
st, err := OpenSQLite(filepath.Join(t.TempDir(), "id.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
if err := st.VerifySystemIdentity("svc-a-aaaaaa", "sqlite"); !errors.Is(err, ErrSystemIdentityMissing) {
|
|
t.Fatalf("empty verify: want missing, got %v", err)
|
|
}
|
|
if err := st.EnsureSystemIdentity("svc-a-aaaaaa", "sqlite"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.EnsureSystemIdentity("svc-a-aaaaaa", "sqlite"); err != nil {
|
|
t.Fatalf("idempotent ensure: %v", err)
|
|
}
|
|
if err := st.VerifySystemIdentity("svc-a-aaaaaa", "sqlite"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.EnsureSystemIdentity("svc-b-bbbbbb", "sqlite"); !errors.Is(err, ErrSystemIdentityMismatch) {
|
|
t.Fatalf("wrong id ensure: want mismatch, got %v", err)
|
|
}
|
|
if err := st.VerifySystemIdentity("svc-a-aaaaaa", "pg:other"); !errors.Is(err, ErrSystemIdentityMismatch) {
|
|
t.Fatalf("wrong dbref verify: want mismatch, got %v", err)
|
|
}
|
|
row, err := st.GetSystemIdentity()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.InstanceID != "svc-a-aaaaaa" || row.DBRef != "sqlite" {
|
|
t.Fatalf("row=%+v", row)
|
|
}
|
|
if row.SchemaVersion != latestMigrationVersion() {
|
|
t.Fatalf("schema_version=%d want %d", row.SchemaVersion, latestMigrationVersion())
|
|
}
|
|
}
|
|
|
|
func TestSystemIdentityMigrationCreatesTable(t *testing.T) {
|
|
st, err := OpenSQLite(filepath.Join(t.TempDir(), "mig.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
versions, err := st.MigrationStatus()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, v := range versions {
|
|
if v == 27 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("migration v27 not applied: %v", versions)
|
|
}
|
|
var name string
|
|
if err := st.DB().QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name='agent_system_identity'`).Scan(&name); err != nil || name != "agent_system_identity" {
|
|
t.Fatalf("table missing: name=%q err=%v", name, err)
|
|
}
|
|
cols := identityColumnNames(t, st.DB())
|
|
if cols != IdentityColumns {
|
|
t.Fatalf("身份表列已偏离冻结契约:%s", cols)
|
|
}
|
|
}
|
|
|
|
func TestOpenIdentityReadsFutureSchemaWithoutMigrating(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "agent-system.db")
|
|
raw, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stmts := []string{
|
|
`CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`,
|
|
`INSERT INTO schema_migrations(version, name, applied_at) VALUES (99, 'future', 't')`,
|
|
`CREATE TABLE agent_system_identity (
|
|
instance_id TEXT NOT NULL PRIMARY KEY,
|
|
dbref TEXT NOT NULL,
|
|
schema_version INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL)`,
|
|
`INSERT INTO agent_system_identity(instance_id, dbref, schema_version, created_at) VALUES ('svc-future-aaaa', 'sqlite', 99, 't')`,
|
|
}
|
|
for _, stmt := range stmts {
|
|
if _, err := raw.Exec(stmt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
raw.Close()
|
|
st, err := OpenIdentity("", dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.VerifySystemIdentity("svc-future-aaaa", "sqlite"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ver, err := st.SchemaVersion()
|
|
if err != nil || ver != 99 {
|
|
t.Fatalf("schema=%d err=%v,身份打开不得改写迁移版本", ver, err)
|
|
}
|
|
if cols := identityColumnNames(t, st.DB()); cols != IdentityColumns {
|
|
t.Fatalf("列=%s", cols)
|
|
}
|
|
}
|
|
|
|
func TestOpenIdentityMissingFileOrTable(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, err := OpenIdentity("", filepath.Join(dir, "missing")); !errors.Is(err, ErrIdentityDBMissing) {
|
|
t.Fatalf("缺文件:%v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "missing")); !os.IsNotExist(err) {
|
|
t.Fatal("缺库时创建了目录")
|
|
}
|
|
path := filepath.Join(dir, "agent-system.db")
|
|
raw, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := raw.Exec(`CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw.Close()
|
|
parent := filepath.Dir(path)
|
|
if _, err := OpenIdentity("", parent); !errors.Is(err, ErrIdentityTableMissing) {
|
|
t.Fatalf("缺表:%v", err)
|
|
}
|
|
}
|
|
|
|
func identityColumnNames(t *testing.T, db *sql.DB) string {
|
|
t.Helper()
|
|
rows, err := db.Query(`SELECT name FROM pragma_table_info('agent_system_identity') ORDER BY cid`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
var names []string
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|