Files
Agent-System-Backend/internal/store/system_identity_test.go
weichenandCursor 69ea7dcc14 Open identity commands on an existing database only
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>
2026-10-12 14:04:41 +00:00

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, ",")
}