diff --git a/services/core/tests/integration/provider_configuration_migration_test.go b/services/core/tests/integration/provider_configuration_migration_test.go index 1e9d06f5c..506115fed 100644 --- a/services/core/tests/integration/provider_configuration_migration_test.go +++ b/services/core/tests/integration/provider_configuration_migration_test.go @@ -2,58 +2,60 @@ package integration import ( "bytes" - "database/sql" "encoding/json" - "os" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/google/uuid" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" ) func TestProviderConfigurationMigrationPreservesCiphertextAndRetainedOwnership(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 1 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { + db, migration := runtimeNamesMigrationSchema(t) + ctx := t.Context() + if _, err := migration.UpTo(ctx, 90); err != nil { t.Fatal(err) } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migration, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) + installation := uuid.NewString() + cipher := pgtest.CredentialKey(t) + secret := []byte("migration-provider-key") + before, err := cipher.SealSandboxDeployment(secret, installation, 2) if err != nil { t.Fatal(err) } - if _, err = migration.DownTo(t.Context(), 90); err != nil { - t.Fatal(err) - } - // A valid old record can have only part of its observation populated. - if _, err = db.ExecContext(t.Context(), "UPDATE runtime_deployment SET e2b_template_cpus=2"); err != nil { - t.Fatal(err) + exec := func(query string, args ...any) { + t.Helper() + if _, err := db.ExecContext(ctx, query, args...); err != nil { + t.Fatal(err) + } } - var before, after []byte - var generation int64 - if err = db.QueryRowContext(t.Context(), "SELECT e2b_credential,generation FROM runtime_deployment").Scan(&before, &generation); err != nil { + // The current deployment and retained generation exist in the old schema. + // A partial observation is valid and must retain its null fields. + exec(`UPDATE runtime_deployment SET installation_id=$1,backend_fingerprint=repeat('a',64),provider_kind='e2b',mode='direct',web_managed=true,generation=2, + e2b_template='next-template',e2b_credential=$2,e2b_api_url='https://api.e2b.app',e2b_domain='e2b.app',e2b_template_cpus=2`, installation, before) + exec(`INSERT INTO runtime_deployment_generations(generation,provider_kind,specification,e2b_template,e2b_api_url,e2b_domain) + VALUES(1,'e2b','{}','old-template','https://api.e2b.app','e2b.app')`) + session, tenant, environment, device, allocation := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() + exec(`INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES($1,$2,'codex','old','old','{}')`, session, tenant) + exec(`INSERT INTO environments(id,session_id) VALUES($1,$2)`, environment, session) + exec(`INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES($1,$2,'old',repeat('b',64))`, device, tenant) + exec(`INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key,deployment_generation) VALUES($1,$2,$3,$4,1)`, allocation, environment, device, installation) + var allocationBefore, allocationAfter string + if err := db.QueryRowContext(ctx, `SELECT to_jsonb(a)::text FROM runtime_allocations a WHERE id=$1`, allocation).Scan(&allocationBefore); err != nil { t.Fatal(err) } - if _, err = migration.Up(t.Context()); err != nil { + if _, err := migration.UpTo(ctx, 91); err != nil { t.Fatal(err) } - if err = db.QueryRowContext(t.Context(), "SELECT provider_credential FROM runtime_deployment").Scan(&after); err != nil || !bytes.Equal(before, after) { + var after, metadataJSON []byte + var restoredInstallation, template string + var generation uint64 + if err := db.QueryRowContext(ctx, `SELECT provider_credential,installation_id::text,generation,provider_config->>'template',provider_metadata FROM runtime_deployment`).Scan(&after, &restoredInstallation, &generation, &template, &metadataJSON); err != nil || !bytes.Equal(before, after) { t.Fatal("ciphertext rewritten", err) } - restored, err := deploymentService(t, s).Setup(t.Context()) - if err != nil || restored.Generation != uint64(generation) || restored.Configuration.(*e2b.DeploymentConfiguration).APIKey != input.Configuration.(*e2b.DeploymentConfiguration).APIKey { + restored, err := cipher.OpenSandboxDeployment(after, restoredInstallation, generation) + if err != nil || !bytes.Equal(restored, secret) || restoredInstallation != installation || generation != 2 || template != "next-template" { t.Fatal("ownership or credential changed", err) } - public, err := deploymentService(t, s).View(t.Context()) - if err != nil { - t.Fatal(err) - } var metadata struct { TemplateBuild struct { Resources struct { @@ -62,22 +64,25 @@ func TestProviderConfigurationMigrationPreservesCiphertextAndRetainedOwnership(t } `json:"resources"` } `json:"template_build"` } - if err = json.Unmarshal(public.Metadata, &metadata); err != nil || metadata.TemplateBuild.Resources.CPUs == nil || *metadata.TemplateBuild.Resources.CPUs != 2 || metadata.TemplateBuild.Resources.Memory != nil { - t.Fatal("partial metadata lost", string(public.Metadata), err) + if err := json.Unmarshal(metadataJSON, &metadata); err != nil || metadata.TemplateBuild.Resources.CPUs == nil || *metadata.TemplateBuild.Resources.CPUs != 2 || metadata.TemplateBuild.Resources.Memory != nil { + t.Fatal("partial metadata lost", string(metadataJSON), err) } var retained int - if err = db.QueryRowContext(t.Context(), "SELECT count(*) FROM runtime_deployment_generations WHERE generation=1 AND provider_config->>'template'<>$1", input.Configuration.(*e2b.DeploymentConfiguration).Template).Scan(&retained); err != nil || retained != 1 { + if err := db.QueryRowContext(ctx, `SELECT count(*) FROM runtime_deployment_generations WHERE generation=1 AND provider_config->>'template'='old-template'`).Scan(&retained); err != nil || retained != 1 { t.Fatal("retained ownership lost", err) } - if _, err = db.ExecContext(t.Context(), "UPDATE runtime_deployment_generations SET provider_config='{}'"); err == nil { + if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment_generations SET provider_config='{}'`); err == nil { t.Fatal("migration removed immutability") } - if _, err = migration.DownTo(t.Context(), 90); err != nil { + if _, err := migration.DownTo(ctx, 90); err != nil { t.Fatal(err) } var cpu int var memory *int - if err = db.QueryRowContext(t.Context(), "SELECT e2b_credential,e2b_template_cpus,e2b_template_memory_mib FROM runtime_deployment").Scan(&after, &cpu, &memory); err != nil || !bytes.Equal(before, after) || cpu != 2 || memory != nil { + if err := db.QueryRowContext(ctx, `SELECT e2b_credential,e2b_template_cpus,e2b_template_memory_mib FROM runtime_deployment`).Scan(&after, &cpu, &memory); err != nil || !bytes.Equal(before, after) || cpu != 2 || memory != nil { t.Fatal("downgrade lost valid data", err) } + if err := db.QueryRowContext(ctx, `SELECT to_jsonb(a)::text FROM runtime_allocations a WHERE id=$1`, allocation).Scan(&allocationAfter); err != nil || allocationAfter != allocationBefore { + t.Fatal("migration changed retained allocation", err) + } } diff --git a/services/core/tests/integration/provider_registration_migration_test.go b/services/core/tests/integration/provider_registration_migration_test.go index bbc4cfb1e..bc852049e 100644 --- a/services/core/tests/integration/provider_registration_migration_test.go +++ b/services/core/tests/integration/provider_registration_migration_test.go @@ -1,51 +1,45 @@ package integration import ( - "database/sql" - "os" "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/google/uuid" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" ) func TestProviderRegistrationDowngradePreservesCustomEndpoints(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).APIURL, input.Configuration.(*e2b.DeploymentConfiguration).Domain = "https://api.example.test", "example.test" - input.ExpectedGeneration = view.Generation - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { + db, migrations := runtimeNamesMigrationSchema(t) + ctx := t.Context() + if _, err := migrations.UpTo(ctx, 89); err != nil { t.Fatal(err) } - tenant, session = managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 2 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { + installation := uuid.NewString() + credential, err := pgtest.CredentialKey(t).SealSandboxDeployment([]byte("migration-provider-key"), installation, 3) + if err != nil { t.Fatal(err) } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migrations, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { + const customEndpoint = "https://api.example.test" + if _, err := db.ExecContext(ctx, `UPDATE runtime_deployment SET installation_id=$1,backend_fingerprint=repeat('a',64),provider_kind='e2b',mode='direct',web_managed=true,generation=3, + e2b_template='next-template',e2b_credential=$2,e2b_api_url=$3,e2b_domain='example.test'`, installation, credential, customEndpoint); err != nil { + t.Fatal(err) + } + if _, err := db.ExecContext(ctx, `INSERT INTO runtime_deployment_generations(generation,provider_kind,specification,e2b_template,e2b_api_url,e2b_domain) VALUES + (1,'e2b','{}','old-template','https://api.e2b.app','e2b.app'),(2,'e2b','{}','old-template',$1,'example.test')`, customEndpoint); err != nil { t.Fatal(err) } if _, err := migrations.DownTo(t.Context(), 88); err != nil { t.Fatal(err) } - for generation, want := range map[int]string{1: "", 2: input.Configuration.(*e2b.DeploymentConfiguration).APIURL} { + for generation, want := range map[int]string{1: "", 2: customEndpoint} { var endpoint string if err := db.QueryRowContext(t.Context(), "SELECT e2b_api_url FROM runtime_deployment_generations WHERE generation=$1", generation).Scan(&endpoint); err != nil || endpoint != want { t.Fatal("downgrade changed endpoint identity", generation, endpoint, err) } } var endpoint string - if err := db.QueryRowContext(t.Context(), "SELECT e2b_api_url FROM runtime_deployment").Scan(&endpoint); err != nil || endpoint != input.Configuration.(*e2b.DeploymentConfiguration).APIURL { + if err := db.QueryRowContext(t.Context(), "SELECT e2b_api_url FROM runtime_deployment").Scan(&endpoint); err != nil || endpoint != customEndpoint { t.Fatal("downgrade changed current endpoint", endpoint, err) } if _, err := db.ExecContext(t.Context(), "UPDATE runtime_deployment_generations SET e2b_domain='changed.test' WHERE generation=1"); err == nil || !strings.Contains(err.Error(), "Retained sandbox specifications are immutable") { diff --git a/services/core/tests/integration/sandbox_generations_test.go b/services/core/tests/integration/sandbox_generations_test.go index af3147a55..e429e6d9a 100644 --- a/services/core/tests/integration/sandbox_generations_test.go +++ b/services/core/tests/integration/sandbox_generations_test.go @@ -1,15 +1,13 @@ package integration import ( - "database/sql" - "os" + "strings" "testing" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" @@ -209,40 +207,43 @@ func TestGenerationMigrationBackfillsAndRejectsLossyDowngrade(t *testing.T) { } func TestGenerationDowngradeRefusesOldAllocation(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "e2b") - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - owner := archiveAllocation(t, w, tenant, session, view.InstallationID) - input.Configuration.(*e2b.DeploymentConfiguration).Template = "next:" + uuid.NewString() - input.ExpectedGeneration = 1 - if _, err := deploymentExecution(t, w).Update(SandboxResetTestContext(t.Context()), view.InstallationID, input); err != nil { + db, migrations := runtimeNamesMigrationSchema(t) + ctx := t.Context() + if _, err := migrations.UpTo(ctx, 81); err != nil { t.Fatal(err) } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - migrations, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) + installation := uuid.NewString() + credential, err := pgtest.CredentialKey(t).SealSandboxDeployment([]byte("migration-provider-key"), installation, 2) if err != nil { t.Fatal(err) } - if _, err = migrations.DownTo(t.Context(), 80); err == nil { - t.Fatal("downgrade erased old owned allocation") - } - // Earlier down migrations can commit before the generation guard vetoes - // downgrade. Restore the current schema before invoking current Store code. - if _, err = migrations.Up(t.Context()); err != nil { - t.Fatal(err) - } - if _, err = deploymentExecution(t, w).RequestCleanup(t.Context(), owner); err != nil { - t.Fatal(err) + exec := func(query string, args ...any) { + t.Helper() + if _, err := db.ExecContext(ctx, query, args...); err != nil { + t.Fatal(err) + } } - if _, err = deploymentExecution(t, w).SettleCreation(t.Context(), owner); err != nil { - t.Fatal(err) + exec(`UPDATE runtime_deployment SET installation_id=$1,backend_fingerprint=repeat('a',64),provider_kind='e2b',mode='direct',web_managed=true,generation=2,e2b_template='next-template',e2b_credential=$2`, installation, credential) + exec(`INSERT INTO runtime_deployment_generations(generation,provider_kind,specification,e2b_template) VALUES(1,'e2b','{}','old-template')`) + session, tenant, environment, device, allocation := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() + exec(`INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES($1,$2,'codex','old','old','{}')`, session, tenant) + exec(`INSERT INTO environments(id,session_id) VALUES($1,$2)`, environment, session) + exec(`INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES($1,$2,'old',repeat('b',64))`, device, tenant) + exec(`INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key,deployment_generation) VALUES($1,$2,$3,$4,1)`, allocation, environment, device, installation) + if _, err := migrations.DownTo(ctx, 80); err == nil || !strings.Contains(err.Error(), "retained ownership or node serving pins require generation routing") { + t.Fatal("downgrade erased old owned allocation", err) } - if _, err = deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { + exec(`UPDATE runtime_allocations SET state='released',create_settled=true,released_at=clock_timestamp() WHERE id=$1`, allocation) + var before, after string + if err := db.QueryRowContext(ctx, `SELECT (to_jsonb(a)-'deployment_generation')::text FROM runtime_allocations a WHERE id=$1`, allocation).Scan(&before); err != nil { t.Fatal(err) } - if _, err = migrations.DownTo(t.Context(), 80); err != nil { + if _, err := migrations.DownTo(ctx, 80); err != nil { t.Fatal("released history prevented safe downgrade", err) } + if err := db.QueryRowContext(ctx, `SELECT to_jsonb(a)::text FROM runtime_allocations a WHERE id=$1`, allocation).Scan(&after); err != nil || after != before { + t.Fatal("downgrade changed released allocation history", err) + } } func TestGenerationUpdateSerializesWithAllocationAdmission(t *testing.T) {