refactor(schema): 迁移 043 schema 收敛——42→19 张表,统一迁移 runner

- 迁移 043 一次到位:runtime_instance/environment_binding 并入 browser_env(账号即环境)、
  credential_reference 并入 social_account、work_source/metric_plan 并入 creator_work、
  sync 租约并入 creator_collection_checkpoint(kind='sync')、audit_event 瘦列、
  creator_settings 去转写列、TRUNCATE 账号域(竞品采集数据无损保留)
- 统一迁移 runner:environment.Open 串起 001 账号基座 + hub 链 + 038 creator 版本回填
  (v+1000) + creator 1017-1042 + 043;account/creator Open 不再迁移
- 三域 store SQL 全量适配:CreateBoundEnv 就绪门禁、SetRuntimeCleanupPending fence
  语义(未知代 MissingRuntimeID 允许登记并释放活跃实例)、metric_plan 列并入 work、
  checkpoint sync 租约、deletion 链路重写
- api 适配:envView 去 runtime_instance_id、audit filter 去任务列、browser_env 单表查询
- 测试:迁移 043 形态探针、迁移不可变映射新路径、legacy 迁移重放子测试重写、
  fence/出口生命周期/单来源 upsert 语义覆盖;CREATORHUB_POSTGRES_TEST_URL 全绿,
  environment 70%/creator 67.5% 覆盖率,tsc 通过
This commit is contained in:
2026-09-28 19:48:55 +08:00
parent 4b8cd264cc
commit 0879d9fd25
58 changed files with 1110 additions and 1067 deletions
+113 -117
View File
@@ -80,7 +80,7 @@ func TestResumeBlockReasonIsStable(t *testing.T) {
"direct exit": {account, hub.EnvironmentContext{}, true, "account_conflict"},
"unhealthy exit": {account, hub.EnvironmentContext{Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "unhealthy"}}, true, "network_exit_unhealthy"},
"cleanup pending": {account, hub.EnvironmentContext{Exit: healthy.Exit, RuntimeCleanupPending: true}, true, "runtime_stop_pending"},
"runtime active": {account, hub.EnvironmentContext{Exit: healthy.Exit, RuntimeInstanceID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, true, "runtime_active"},
"runtime active": {account, hub.EnvironmentContext{Exit: healthy.Exit, RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, true, "runtime_active"},
} {
t.Run(name, func(t *testing.T) {
if got := resumeBlockReason(test.account, test.env, test.found); got != test.want {
@@ -293,7 +293,7 @@ func (s *memoryStore) CreateBoundEnv(ctx context.Context, env hub.Env, accountID
s.mu.Lock()
defer s.mu.Unlock()
bound := hub.EnvironmentContext{Env: env, AccountID: accountID, AccountStatus: "active", AuthorizationStatus: "authorized",
BindingID: accountID, BindingVersion: 1, Exit: s.exits[exitID]}
BindingVersion: 1, Exit: s.exits[exitID]}
s.bindings[env.Alias] = bound
return bound, true, nil
}
@@ -311,7 +311,7 @@ func (s *memoryStore) GetEnvironmentContext(_ context.Context, alias string) (hu
return hub.EnvironmentContext{}, hub.ErrNotFound
}
return hub.EnvironmentContext{Env: env, AccountID: alias, AccountStatus: "active", AuthorizationStatus: "authorized",
BindingID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}, nil
BindingVersion: 1, Exit: s.exits["exit-1"]}, nil
}
func (s *memoryStore) GetEnvironmentContextForAccount(ctx context.Context, accountID string) (hub.EnvironmentContext, error) {
s.mu.Lock()
@@ -341,14 +341,14 @@ func (s *memoryStore) RebindEnvironment(_ context.Context, alias, exitID, runtim
defer s.mu.Unlock()
bound, ok := s.bindings[alias]
if !ok {
bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
}
if bound.BindingVersion != expectedBindingVersion || bound.RuntimeCleanupPending {
return hub.EnvironmentContext{}, hub.ErrConflict
}
bound.RuntimeInstanceID, bound.RuntimeID, bound.RuntimeNetworkID = "", "", ""
bound.RuntimeID, bound.RuntimeNetworkID = "", ""
if runtimeID != "" {
bound.RuntimeInstanceID, bound.RuntimeID = "runtime-instance", runtimeID
bound.RuntimeID = runtimeID
if len(networkIDs) == 1 {
bound.RuntimeNetworkID = networkIDs[0]
}
@@ -365,7 +365,7 @@ func (s *memoryStore) ActivateRuntime(_ context.Context, alias, runtimeID string
}
bound, ok := s.bindings[alias]
if !ok {
bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
}
if bound.AccountStatus == "" {
bound.AccountStatus, bound.AuthorizationStatus = "active", "authorized"
@@ -374,7 +374,7 @@ func (s *memoryStore) ActivateRuntime(_ context.Context, alias, runtimeID string
bound.BindingVersion != bindingVersion || bound.Exit.ID != exitID || bound.RuntimeCleanupPending {
return hub.EnvironmentContext{}, hub.ErrConflict
}
bound.RuntimeInstanceID, bound.RuntimeID = "runtime-instance", runtimeID
bound.RuntimeID = runtimeID
bound.RuntimeNetworkID = networkIDs[0]
s.bindings[alias] = bound
return bound, nil
@@ -388,16 +388,16 @@ func (s *memoryStore) ReleaseRuntime(_ context.Context, environment hub.Environm
bound, ok := s.bindings[environment.Alias]
if !ok {
bound = hub.EnvironmentContext{Env: s.envs[environment.Alias], AccountID: environment.Alias,
BindingID: environment.Alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
BindingVersion: 1, Exit: s.exits["exit-1"]}
}
if environment.RuntimeInstanceID == "" {
if environment.RuntimeID == "" {
return nil
}
if bound.BindingID != environment.BindingID || bound.BindingVersion != environment.BindingVersion ||
bound.RuntimeInstanceID != environment.RuntimeInstanceID {
if bound.Alias != environment.Alias || bound.BindingVersion != environment.BindingVersion ||
bound.RuntimeID != environment.RuntimeID {
return hub.ErrConflict
}
bound.RuntimeInstanceID, bound.RuntimeID, bound.RuntimeNetworkID = "", "", ""
bound.RuntimeID, bound.RuntimeNetworkID = "", ""
s.bindings[environment.Alias] = bound
return nil
}
@@ -416,31 +416,30 @@ func (s *memoryStore) SetRuntimeCleanupPending(_ context.Context, environment hu
bound, ok := s.bindings[environment.Alias]
if !ok {
bound = hub.EnvironmentContext{Env: s.envs[environment.Alias], AccountID: environment.Alias,
BindingID: environment.Alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
BindingVersion: 1, Exit: s.exits["exit-1"]}
}
if bound.BindingID != environment.BindingID || bound.BindingVersion != environment.BindingVersion {
if bound.Alias != environment.Alias || bound.BindingVersion != environment.BindingVersion {
return hub.ErrConflict
}
if bound.RuntimeCleanupPending {
if bound.RuntimeCleanupBindingVersion != environment.RuntimeCleanupBindingVersion ||
bound.RuntimeCleanupInstanceID != environment.RuntimeCleanupInstanceID ||
bound.RuntimeCleanupRuntimeID != environment.RuntimeCleanupRuntimeID ||
bound.RuntimeCleanupNetworkID != environment.RuntimeCleanupNetworkID {
return hub.ErrConflict
}
} else if pending && bound.RuntimeInstanceID != environment.RuntimeCleanupInstanceID {
} else if pending && bound.RuntimeID != "" && bound.RuntimeID != environment.RuntimeCleanupRuntimeID &&
environment.RuntimeCleanupRuntimeID != hub.MissingRuntimeID {
return hub.ErrConflict
}
bound.RuntimeCleanupPending = pending
if pending {
bound.RuntimeCleanupBindingVersion = environment.RuntimeCleanupBindingVersion
bound.RuntimeCleanupInstanceID = environment.RuntimeCleanupInstanceID
bound.RuntimeCleanupRuntimeID = environment.RuntimeCleanupRuntimeID
bound.RuntimeCleanupNetworkID = environment.RuntimeCleanupNetworkID
bound.RuntimeInstanceID, bound.RuntimeID, bound.RuntimeNetworkID = "", "", ""
bound.RuntimeID, bound.RuntimeNetworkID = "", ""
} else {
bound.RuntimeCleanupBindingVersion = 0
bound.RuntimeCleanupInstanceID, bound.RuntimeCleanupRuntimeID, bound.RuntimeCleanupNetworkID = "", "", ""
bound.RuntimeCleanupRuntimeID, bound.RuntimeCleanupNetworkID = "", ""
}
s.bindings[environment.Alias] = bound
return s.cleanupPendingErr
@@ -964,7 +963,7 @@ func TestExitFailuresStopCreateBeforeGateway(t *testing.T) {
t.Fatalf("exit failure must stop before any gateway start/create call: %#v", gateway.recorded())
}
bound, err := store.GetEnvironmentContext(context.Background(), "account-a")
if err != nil || bound.RuntimeInstanceID != "" || store.exits["exit-1"].HealthStatus != "unhealthy" {
if err != nil || bound.RuntimeID != "" || store.exits["exit-1"].HealthStatus != "unhealthy" {
t.Fatalf("failed create must retain only the stable inactive binding: %#v err=%v", bound, err)
}
if len(store.actions) != 2 || store.actions[1].Outcome != "failed" || store.actions[1].ReasonCode != test.reason {
@@ -1029,8 +1028,8 @@ func TestReconcileGatewayCreateDoesNotReuseOldNetworkGeneration(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
AuthorizationStatus: "authorized", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeInstanceID: "runtime-instance-c1", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
store.bindings[environment.Alias] = environment
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", NetworkID: "native-99999999999999999999999999999999", ProxyReady: true,
@@ -1064,17 +1063,17 @@ func TestValidCreatedRuntimeGeneration(t *testing.T) {
{name: "active invalid network", created: runtimeStatus{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", NetworkID: "network n1"}, running: true},
{name: "active replacement network", created: runtimeStatus{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", NetworkID: "native-88888888888888888888888888888888"}, running: true},
{name: "active existing matching generation", created: runtimeStatus{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", NetworkID: "native-99999999999999999999999999999999"}, running: true, want: true,
environment: hub.EnvironmentContext{RuntimeInstanceID: "runtime-instance-c1", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}},
environment: hub.EnvironmentContext{RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}},
{name: "active existing successor container", created: runtimeStatus{ID: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", NetworkID: "native-99999999999999999999999999999999"}, running: true,
environment: hub.EnvironmentContext{RuntimeInstanceID: "runtime-instance-c1", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}},
environment: hub.EnvironmentContext{RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}},
{name: "active legacy lease without network", created: runtimeStatus{ID: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", NetworkID: "native-88888888888888888888888888888888"}, running: true,
environment: hub.EnvironmentContext{RuntimeInstanceID: "runtime-instance-c1", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}},
environment: hub.EnvironmentContext{RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}},
{name: "stopped empty network", created: runtimeStatus{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, want: true},
{name: "stopped invalid network", created: runtimeStatus{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", NetworkID: "network n1"}},
} {
t.Run(test.name, func(t *testing.T) {
current := environment
if test.environment.RuntimeInstanceID != "" {
if test.environment.RuntimeID != "" {
current = test.environment
}
if got := validCreatedRuntime(test.created, current, test.running); got != test.want {
@@ -1107,9 +1106,7 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) {
AccountID: "account-a",
AccountStatus: "active",
AuthorizationStatus: "authorized",
BindingID: "account-a",
BindingVersion: 1,
RuntimeInstanceID: "runtime-instance-c1",
RuntimeID: candidateID,
RuntimeNetworkID: candidateNetwork,
Exit: store.exits["exit-1"],
@@ -1121,7 +1118,7 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) {
if err != nil {
t.Fatalf("activation race was reported as failure: %v", err)
}
if got.RuntimeID != candidateID || got.RuntimeNetworkID != candidateNetwork || got.RuntimeInstanceID == "" {
if got.RuntimeID != candidateID || got.RuntimeNetworkID != candidateNetwork || got.RuntimeID == "" {
t.Fatalf("heartbeat winner was not returned: %#v", got)
}
}
@@ -1130,8 +1127,8 @@ func TestLegacyActiveRuntimeWithoutNetworkGenerationDoesNotTouchSuccessor(t *tes
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
AuthorizationStatus: "authorized", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeInstanceID: "runtime-instance-c1", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}
store.bindings[environment.Alias] = environment
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", NetworkID: "native-88888888888888888888888888888888", ProxyReady: true,
@@ -1378,7 +1375,7 @@ func TestListRestoresProxyAfterGatewayRestartBeforeHeartbeat(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "店铺一号", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 3, Exit: store.exits["exit-1"],
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 3, Exit: store.exits["exit-1"],
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Alias: "account-a", State: "running", BindingVersion: 3, NetworkExitID: "exit-1", NetworkID: "native-cccccccccccccccccccccccccccccccc", ProxyReady: false,
@@ -1404,7 +1401,7 @@ func TestListRestoresProxyAfterGatewayRestartBeforeHeartbeat(t *testing.T) {
func TestGatewayRestartRebuildsWhenOriginalProxyPortCannotBeRestored(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "店铺一号", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 2, Exit: store.exits["exit-1"]}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 2, Exit: store.exits["exit-1"]}
gateway := &fakeGateway{token: "unit-test-gateway-token", failProxy: true, runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", BindingVersion: 2, NetworkExitID: "exit-1", NetworkID: "native-66666666666666666666666666666666",
}}}
@@ -1613,7 +1610,7 @@ func installCleanupTransitionFailure(t *testing.T, ctx context.Context, db *sql.
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `
CREATE TRIGGER fail_cleanup_transition BEFORE UPDATE OF runtime_cleanup_pending ON environment_binding
CREATE TRIGGER fail_cleanup_transition BEFORE UPDATE OF runtime_cleanup_pending ON browser_env
FOR EACH ROW EXECUTE FUNCTION fail_cleanup_transition()`); err != nil {
t.Fatal(err)
}
@@ -1621,7 +1618,7 @@ func installCleanupTransitionFailure(t *testing.T, ctx context.Context, db *sql.
func dropCleanupTransitionFailure(t *testing.T, ctx context.Context, db *sql.DB) {
t.Helper()
if _, err := db.ExecContext(ctx, `DROP TRIGGER fail_cleanup_transition ON environment_binding`); err != nil {
if _, err := db.ExecContext(ctx, `DROP TRIGGER fail_cleanup_transition ON browser_env`); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `DROP FUNCTION fail_cleanup_transition()`); err != nil {
@@ -1675,8 +1672,8 @@ func TestPostgresCleanupPendingPersistsAndReconciles(t *testing.T) {
after.Exit.ID != fixture.bound.Exit.ID || after.RuntimeID != "" {
t.Fatalf("PostgreSQL did not persist cleanup pending atomically: %#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM environment_binding WHERE browser_env_alias = 'account-a' AND runtime_cleanup_pending`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_cleanup_pending`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
if err := reconcileRuntimeLeases(ctx, fixture.store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil }); err != nil {
t.Fatalf("reconcile did not confirm cleanup: %v", err)
@@ -1713,8 +1710,8 @@ func TestRestoreAndDiscardPreserveGenerationWhenCleanupMarkFails(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 2,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 2,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
}
store.cleanupPendingErr = errors.New("cleanup state unavailable")
store.cleanupPendingErrAfterMutation = !test.commitKnown
@@ -1768,8 +1765,8 @@ func TestRestoreAndDiscardPreserveGenerationWhenCleanupMarkFails(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
store.cleanupPendingErr = errors.New("cleanup state unavailable")
store.cleanupPendingErrAfterMutation = !test.commitKnown
@@ -1824,8 +1821,8 @@ func TestExistingCreateStopsOnGatewayUnknownAndRetriesReuse(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", disconnectList: gatewayReconcileAttempts, runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", ProxyReady: true, BindingVersion: 1, NetworkExitID: "exit-1", NetworkID: "native-66666666666666666666666666666666",
@@ -1874,8 +1871,8 @@ func TestRemoveGatewayRuntimePreservesKnownNetworkGeneration(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
AuthorizationStatus: "authorized", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeInstanceID: "runtime-instance", RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
store.bindings["account-a"] = environment
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Alias: "account-a",
State: "running", BindingVersion: 1, NetworkExitID: "exit-1", NetworkID: test.observedNetwork, ProxyReady: true}}}
@@ -1901,8 +1898,8 @@ func TestGatewayLookupFailurePreservesLease(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
environment := hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
store.bindings[environment.Alias] = environment
store.gatewayFn = func(string) (hub.Gateway, error) { return hub.Gateway{}, errors.New("gateway lookup unavailable") }
@@ -1952,7 +1949,7 @@ func TestPostgresCleanupPendingTransactionRollbacks(t *testing.T) {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" || len(fixture.gateway.runtimes) != 1 {
t.Fatalf("PostgreSQL rollback did not preserve the active generation: after=%#v runtimes=%#v err=%v", after, fixture.gateway.runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
dropCleanupTransitionFailure(t, ctx, fixture.db)
fixture.gateway.cleanupPending = 2
@@ -1964,7 +1961,7 @@ func TestPostgresCleanupPendingTransactionRollbacks(t *testing.T) {
if err != nil || !after.RuntimeCleanupPending || after.RuntimeID != "" || len(fixture.gateway.runtimes) != 0 {
t.Fatalf("202 retry was not durable: after=%#v runtimes=%#v err=%v", after, fixture.gateway.runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
})
t.Run("clear rollback remains pending and lifecycle retry recovers", func(t *testing.T) {
@@ -1994,7 +1991,7 @@ func TestPostgresCleanupPendingTransactionRollbacks(t *testing.T) {
if err != nil || !after.RuntimeCleanupPending || after.RuntimeID != "" || len(fixture.gateway.runtimes) != 0 {
t.Fatalf("clear rollback was not durably pending: after=%#v runtimes=%#v err=%v", after, fixture.gateway.runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
dropCleanupTransitionFailure(t, ctx, fixture.db)
setFixtureAccountStatus(t, fixture.databaseURL, "active")
@@ -2024,9 +2021,9 @@ func TestPostgresLegacyCleanupPendingRequiresManualReconcile(t *testing.T) {
ctx := context.Background()
fixture := newPostgresRebindFixture(t, databaseURL)
if _, err := fixture.db.ExecContext(ctx, `
UPDATE environment_binding SET runtime_cleanup_pending = true,
runtime_cleanup_binding_version = NULL, runtime_cleanup_instance_id = NULL, runtime_cleanup_runtime_id = NULL
WHERE id = $1`, fixture.bound.BindingID); err != nil {
UPDATE browser_env SET runtime_cleanup_pending = true,
runtime_cleanup_binding_version = NULL, runtime_cleanup_runtime_id = NULL
WHERE alias = $1`, fixture.bound.Alias); err != nil {
t.Fatalf("write legacy pending state: %v", err)
}
environment, err := fixture.store.GetEnvironmentContext(ctx, fixture.bound.Alias)
@@ -2068,7 +2065,7 @@ func TestPostgresCleanupMissingContainerReleasesKnownLease(t *testing.T) {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "" {
t.Fatalf("known PostgreSQL lease did not converge: %#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
requests := fixture.gateway.recorded()
if len(requests) == 0 || requests[len(requests)-1].method != http.MethodDelete ||
requests[len(requests)-1].body["runtime_id"] != "known-container" {
@@ -2117,12 +2114,12 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
if err != nil || !after.RuntimeCleanupPending || after.RuntimeID != "" {
t.Fatalf("restore commit-unknown lost pending state: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
} else {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" {
t.Fatalf("restore rollback changed the old generation: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
}
if len(fixture.gateway.runtimes) != 1 {
t.Fatalf("restore cleanup failure touched the container: runtimes=%#v requests=%#v", fixture.gateway.runtimes, fixture.gateway.recorded())
@@ -2162,7 +2159,7 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID == "" || len(runtimes) != 1 || !runtimeMatchesBinding(runtimes[0], after) {
t.Fatalf("restore retry ended inconsistently: after=%#v runtimes=%#v err=%v", after, runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
})
t.Run("create restore "+test.name, func(t *testing.T) {
@@ -2196,12 +2193,12 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
if err != nil || !after.RuntimeCleanupPending || after.RuntimeID != "" {
t.Fatalf("create restore commit-unknown lost pending state: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
} else {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" {
t.Fatalf("create restore rollback changed the old generation: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
}
if len(fixture.gateway.runtimes) != 1 {
t.Fatalf("create restore cleanup failure touched the container: runtimes=%#v requests=%#v", fixture.gateway.runtimes, fixture.gateway.recorded())
@@ -2237,7 +2234,7 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID == "" || len(runtimes) != 1 || !runtimeMatchesBinding(runtimes[0], after) {
t.Fatalf("create restore retry ended inconsistently: after=%#v runtimes=%#v err=%v", after, runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
})
t.Run("discard "+test.name, func(t *testing.T) {
@@ -2270,12 +2267,12 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
if err != nil || !after.RuntimeCleanupPending || after.RuntimeID != "" {
t.Fatalf("discard commit-unknown lost pending state: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
} else {
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" {
t.Fatalf("discard rollback changed the old generation: after=%#v err=%v", after, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
}
if len(fixture.gateway.runtimes) != 1 {
t.Fatalf("discard cleanup failure touched the container: runtimes=%#v requests=%#v", fixture.gateway.runtimes, fixture.gateway.recorded())
@@ -2310,7 +2307,7 @@ func TestPostgresCleanupCallChainsPreserveGeneration(t *testing.T) {
t.Fatalf("discard retry retained a running container: %#v", fixture.gateway.runtimes)
}
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 0)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 0)
})
}
}
@@ -2352,7 +2349,7 @@ func TestPostgresGatewayUnknownDoesNotAffectListAndStillBlocksCreate(t *testing.
if err != nil || after.RuntimeCleanupPending || after.RuntimeID != "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" || len(fixture.gateway.runtimes) != 1 {
t.Fatalf("browser list changed the generation: after=%#v runtimes=%#v err=%v", after, fixture.gateway.runtimes, err)
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, 1)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, 1)
requests := fixture.gateway.recorded()
if len(requests) != 1 || requests[0].method != http.MethodGet || requests[0].path != "/v1/browsers" {
t.Fatalf("browser list did not perform the expected live gateway status read: %#v", requests)
@@ -2495,7 +2492,7 @@ func TestPostgresStrictGatewayValidationBlocksLifecycleUntilRetry(t *testing.T)
if test.active {
activeCount = 1
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, activeCount)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, activeCount)
if test.action != "" {
var outcome, reason string
if err := fixture.db.QueryRowContext(ctx, `
@@ -2531,7 +2528,7 @@ func TestPostgresStrictGatewayValidationBlocksLifecycleUntilRetry(t *testing.T)
if test.action == "rebind" {
activeAfter = 0
}
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM runtime_instance WHERE binding_id = 'account-a' AND released_at IS NULL`, activeAfter)
assertControlPlaneDatabaseCount(t, fixture.db, `SELECT count(*) FROM browser_env WHERE alias = 'account-a' AND runtime_id IS NOT NULL`, activeAfter)
if test.action != "" {
var outcome, reason string
if err := fixture.db.QueryRowContext(ctx, `
@@ -2648,19 +2645,18 @@ func TestPostgresReconcileFinishedAuditUsesActivatedRuntime(t *testing.T) {
t.Fatalf("reconcile failed: %v", err)
}
after, err := fixture.store.GetEnvironmentContext(context.Background(), "account-a")
if err != nil || after.RuntimeInstanceID == "" || after.RuntimeID != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
if err != nil || after.RuntimeID == "" || after.RuntimeID != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
t.Fatalf("reconcile did not activate replacement runtime: %#v err=%v", after, err)
}
var auditedRuntime string
var auditedBinding int64
if err := fixture.db.QueryRow(`
SELECT runtime_instance_id, binding_version FROM audit_event
SELECT binding_version FROM audit_event
WHERE event_type = 'environment_action_finished' AND action = 'reconcile'
ORDER BY id DESC LIMIT 1`).Scan(&auditedRuntime, &auditedBinding); err != nil {
ORDER BY id DESC LIMIT 1`).Scan(&auditedBinding); err != nil {
t.Fatal(err)
}
if auditedRuntime != after.RuntimeInstanceID || auditedBinding != after.BindingVersion {
t.Fatalf("finished audit retained stale runtime: runtime=%q binding=%d current=%#v", auditedRuntime, auditedBinding, after)
if auditedBinding != after.BindingVersion {
t.Fatalf("finished audit retained stale binding: binding=%d current=%#v", auditedBinding, after)
}
}
@@ -2710,7 +2706,7 @@ func TestPostgresNonRunnableReconcileAuditsRuntimeRelease(t *testing.T) {
t.Fatalf("reconcile error=%v, releaseFailure=%v", err, releaseFailure)
}
after, err := fixture.store.GetEnvironmentContext(ctx, "account-a")
if err != nil || (after.RuntimeInstanceID != "") == !releaseFailure {
if err != nil || (after.RuntimeID != "") == !releaseFailure {
t.Fatalf("runtime lease after reconcile: %#v err=%v", after, err)
}
@@ -2771,7 +2767,7 @@ func TestStartRejectsPausedOrRevokedAccountBeforeGatewayCall(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: test.status,
AuthorizationStatus: test.authorization, BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
AuthorizationStatus: test.authorization, BindingVersion: 1, Exit: store.exits["exit-1"],
}
gateway := &fakeGateway{token: "unit-test-gateway-token"}
app := newTestApp(t, store, gateway)
@@ -2794,8 +2790,8 @@ func TestReconcileStopsRuntimeForPausedOrRevokedAccount(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: authorization,
BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeInstanceID: "old-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
BindingVersion: 1, Exit: store.exits["exit-1"],
RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", ProxyReady: true, BindingVersion: 1, NetworkExitID: "exit-1",
@@ -2831,7 +2827,7 @@ func TestActivationConflictCleanupIsGenerationSafe(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
environment := hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: "authorized",
BindingID: "account-a", BindingVersion: 2, Exit: store.exits["exit-1"],
BindingVersion: 2, Exit: store.exits["exit-1"],
}
store.bindings[environment.Alias] = environment
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
@@ -2866,7 +2862,7 @@ func TestExplicitStopRejectsStaleBindingBeforeGatewaySideEffect(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
BindingID: "account-a", BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeInstanceID: "new-instance", RuntimeID: "new-container",
BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "new-container",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "new-container", Alias: "account-a", State: "running", ProxyReady: true, BindingVersion: 2, NetworkExitID: "exit-1",
@@ -2875,7 +2871,7 @@ func TestExplicitStopRejectsStaleBindingBeforeGatewaySideEffect(t *testing.T) {
defer server.Close()
store.gateways["gw-1"] = hub.Gateway{Name: "gw-1", Endpoint: server.URL, Token: gateway.token}
stale := store.bindings["account-a"]
stale.BindingVersion, stale.RuntimeInstanceID, stale.RuntimeID = 1, "old-instance", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
stale.BindingVersion, stale.RuntimeID, stale.RuntimeID = 1, "old-instance", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
if err := stopEnvironmentRuntime(context.Background(), store, stale); !errors.Is(err, hub.ErrConflict) {
t.Fatalf("stale explicit stop was not rejected: %v", err)
@@ -2891,7 +2887,7 @@ func TestReconcileFinishedAuditUsesRebuiltRuntime(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
BindingID: "account-a", BindingVersion: 2, Exit: store.exits["exit-2"], RuntimeInstanceID: "old-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
BindingVersion: 2, Exit: store.exits["exit-2"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", ProxyReady: true, BindingVersion: 2, NetworkExitID: "exit-1", NetworkID: "native-66666666666666666666666666666666",
@@ -2901,8 +2897,10 @@ func TestReconcileFinishedAuditUsesRebuiltRuntime(t *testing.T) {
if err := reconcileRuntimeLeases(context.Background(), store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil }); err != nil {
t.Fatalf("reconcile failed: %v", err)
}
// 真库审计已无 runtime_instance_id,改断言内存 binding 的重建 runtime 代际(fake 网关固定创建 ID)
if len(store.actions) != 2 || store.actions[1].Outcome != "succeeded" ||
store.actions[1].RuntimeInstanceID != "runtime-instance" || store.actions[1].BindingVersion != 2 || store.actions[1].NetworkExitID != "exit-2" {
store.bindings["account-a"].RuntimeID != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ||
store.actions[1].BindingVersion != 2 || store.actions[1].NetworkExitID != "exit-2" {
t.Fatalf("finished audit used stale runtime context: %#v", store.actions)
}
}
@@ -2912,7 +2910,7 @@ func TestReconcileContextRefreshFailureClearsAllAuditCorrelation(t *testing.T) {
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
BindingID: "account-a", BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeInstanceID: "old-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", ProxyReady: true, BindingVersion: 2, NetworkExitID: "stale-exit", NetworkID: "native-66666666666666666666666666666666",
@@ -2926,7 +2924,7 @@ func TestReconcileContextRefreshFailureClearsAllAuditCorrelation(t *testing.T) {
t.Fatal("context refresh failure unexpectedly succeeded")
}
finished := store.actions[len(store.actions)-1]
if finished.RuntimeInstanceID != "" || finished.BindingVersion != 0 || finished.NetworkExitID != "" {
if finished.BindingVersion != 0 || finished.NetworkExitID != "" {
t.Fatalf("failed context refresh retained partial stale correlation: %#v", finished)
}
}
@@ -2945,7 +2943,7 @@ func TestStoppedReconcileReportsRuntimeReleaseFailure(t *testing.T) {
defer gatewayServer.Close()
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
store.releaseErr = errors.New("database unavailable")
store.gateways["gw-1"] = hub.Gateway{Name: "gw-1", Endpoint: gatewayServer.URL, Token: "unit-test-gateway-token"}
app := fiber.New()
@@ -2964,8 +2962,8 @@ func TestReconcileAuditsRuntimeReleaseFailure(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
store.releaseErr = errors.New("database unavailable")
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
@@ -2987,8 +2985,8 @@ func TestRebindRebuildsRunningContainerWithLatestBinding(t *testing.T) {
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy.example", Port: 8080, HealthStatus: "healthy", Version: 1}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
@@ -3016,8 +3014,8 @@ func TestSameExitRebindStillRebuildsRunningContainer(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "expired-runtime", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
@@ -3045,8 +3043,8 @@ func TestRuntimeReuseRechecksHealthAndDiscardsFailedExit(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
@@ -3071,8 +3069,8 @@ func TestReconcileDeleteFailureReleasesLeaseAndAuditsUnknown(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 2,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "stale-container", RuntimeNetworkID: "native-66666666666666666666666666666666",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 2,
Exit: store.exits["exit-1"], RuntimeID: "stale-container", RuntimeNetworkID: "native-66666666666666666666666666666666",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", failDelete: 1, runtimes: []runtimeStatus{{
ID: "stale-container", Alias: "account-a", State: "running", BindingVersion: 2, NetworkExitID: "stale-exit", NetworkID: "native-66666666666666666666666666666666", ProxyReady: true,
@@ -3098,8 +3096,8 @@ func TestDisableExitImmediatelyDiscardsRuntime(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
@@ -3129,7 +3127,7 @@ func TestDisableExitWaitsForInFlightBrowserLifecycle(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"],
}
gatewayServer := httptest.NewServer(gateway.handler(t))
defer gatewayServer.Close()
@@ -3206,8 +3204,8 @@ func TestDisableExitPropagatesUnknownGatewayReadAndPreservesLease(t *testing.T)
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
store.gateways["gw-1"] = hub.Gateway{Name: "gw-1", Endpoint: gatewayServer.URL, Token: "unit-test-gateway-token"}
app := fiber.New()
@@ -3233,8 +3231,8 @@ func TestDirectBindingIsListable(t *testing.T) {
Seed: 1, ProxyServer: "http://legacy:secret@proxy.example:8080", DisableNonProxiedUDP: true,
}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
RuntimeInstanceID: "4444444444444444444444444444444444444444444444444444444444444444", RuntimeID: "direct-container", RuntimeNetworkID: "direct-network",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
RuntimeID: "direct-container", RuntimeNetworkID: "direct-network",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "direct-container", Alias: "account-a", State: "running", BindingVersion: 1, NetworkID: "direct-network", ProxyReady: true,
@@ -3258,13 +3256,13 @@ func TestRebindDeleteFailureKeepsOriginalBinding(t *testing.T) {
targetID string
}{
{name: "running binding", binding: hub.EnvironmentContext{
AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: hub.NetworkExit{ID: "exit-1", Protocol: "socks5", Host: "127.0.0.1", Port: 1080, HealthStatus: "healthy"},
RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
AccountID: "account-a", BindingVersion: 1,
Exit: hub.NetworkExit{ID: "exit-1", Protocol: "socks5", Host: "127.0.0.1", Port: 1080, HealthStatus: "healthy"},
RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}, targetID: "exit-2"},
{name: "legacy NULL binding", binding: hub.EnvironmentContext{
AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
RuntimeInstanceID: "5555555555555555555555555555555555555555555555555555555555555555", RuntimeID: "legacy-container",
AccountID: "account-a", BindingVersion: 1,
RuntimeID: "legacy-container",
}, targetID: "exit-1"},
} {
t.Run(test.name, func(t *testing.T) {
@@ -3312,8 +3310,8 @@ func TestRebindNetworkCleanupPendingBlocksUntilConfirmed(t *testing.T) {
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy.example", Port: 8080, HealthStatus: "healthy", Version: 1}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", cleanupPending: test.cleanupPending,
disconnectDelete: test.disconnectDelete, disconnectListAfterDelete: test.disconnectListAfterDelete,
@@ -3362,7 +3360,7 @@ func TestCleanupPendingBlocksEveryLifecyclePath(t *testing.T) {
store := newMemoryStore()
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
RuntimeCleanupPending: true, RuntimeCleanupBindingVersion: 1,
RuntimeCleanupRuntimeID: missingRuntimeID, RuntimeCleanupNetworkID: "native-66666666666666666666666666666666", Exit: store.exits["exit-1"],
}
@@ -3391,8 +3389,7 @@ func TestRebindPreparesRunningRuntimeBeforeDelete(t *testing.T) {
store := newMemoryStore()
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy.example", Port: 8080, HealthStatus: "healthy", Version: 1}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a",
BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
}}}
@@ -3411,8 +3408,7 @@ func TestRebindCandidateUnknownCreateLeavesGenerationPending(t *testing.T) {
store := newMemoryStore()
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy.example", Port: 8080, HealthStatus: "healthy", Version: 1}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a",
BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}
gateway := &fakeGateway{token: "unit-test-gateway-token", failCreate: 1, runtimes: []runtimeStatus{{
ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1", ProxyReady: true,
}}}
@@ -3450,8 +3446,8 @@ func TestExistingEnvironmentCleanupNeverReturnsReusedSuccess(t *testing.T) {
}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "店铺一号", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 2024, Platform: "windows", Timezone: "Asia/Shanghai"}}
store.bindings["account-a"] = hub.EnvironmentContext{
Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeInstanceID: "runtime-instance", RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1,
Exit: store.exits["exit-1"], RuntimeID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
ID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Alias: "account-a", State: "running", BindingVersion: 1, NetworkExitID: "exit-1",
@@ -3479,7 +3475,7 @@ func TestStartRebuildsStoppedContainerAfterRebind(t *testing.T) {
store := newMemoryStore()
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy-2.example", Port: 8080, HealthStatus: "healthy", Version: 1}
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"]}
store.bindings["account-a"] = hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", BindingVersion: 1, Exit: store.exits["exit-1"]}
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", Alias: "account-a", State: "exited", BindingVersion: 1}}}
app := newTestApp(t, store, gateway)