Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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") {
Expand Down
55 changes: 28 additions & 27 deletions services/core/tests/integration/sandbox_generations_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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) {
Expand Down