diff --git a/docs/plans/2026-04-16-supply-api-runtime-layered-surfaces-plan.md b/docs/plans/2026-04-16-supply-api-runtime-layered-surfaces-plan.md new file mode 100644 index 00000000..fb1a12d8 --- /dev/null +++ b/docs/plans/2026-04-16-supply-api-runtime-layered-surfaces-plan.md @@ -0,0 +1,143 @@ +# Supply API Runtime Layered Surfaces Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 把 `Runtime` 自身继续降噪为“外部资源句柄 + 业务启动视图”两层结构,进一步压低 `Runtime` 顶层字段密度。 + +**Architecture:** 保留 `BuildRuntime`、`Runtime.BuildServer`、`Runtime.StartBackgroundWorkers` 等对外接口不变,但把 `Runtime` 顶层的 `db/redis/api/auth/tuning/serverConfig/env/logger` 等字段收进更有语义的聚合体。具体做法是新增 `runtimeExternalResources` 和 `runtimeStartupViews`,其中启动视图再分为 HTTP 与 background 两支;现有 `runtimeHTTPView`/`runtimeBackgroundView` builder 改为从分层后的 `Runtime` 聚合中派生。 + +**Tech Stack:** Go, Go test + +--- + +### Task 1: 为 Runtime 引入分层聚合 + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Write the failing test** + +```go +func TestBuildRuntime_GroupsResourcesAndStartupViews(t *testing.T) { + runtime, err := buildRuntimeWithFactory(...) + if err != nil { + t.Fatalf("expected runtime build to succeed: %v", err) + } + if runtime.startupViews.http.env != "dev" { + t.Fatal("expected http startup view env") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildRuntime_GroupsResourcesAndStartupViews' -v` +Expected: FAIL,因为新聚合字段尚不存在 + +**Step 3: Write minimal implementation** + +```go +type runtimeExternalResources struct { ... } +type runtimeStartupViews struct { ... } +type runtimeHTTPStartupView struct { ... } +type runtimeBackgroundStartupView struct { ... } +type Runtime struct { resources runtimeExternalResources; startupViews runtimeStartupViews } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildRuntime_GroupsResourcesAndStartupViews' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): layer runtime resources and startup views" +``` + +### Task 2: 改造现有 HTTP/background view builder 读取分层 Runtime + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/background.go` +- Modify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Write the failing test** + +```go +func TestBuildRuntimeHTTPView_MapsLayeredRuntimeFields(t *testing.T) { + view, err := buildRuntimeHTTPView(runtime) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if view.supplyAPI == nil { + t.Fatal("expected supply api") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'Test(BuildRuntimeHTTPView_MapsHTTPFields|BuildRuntimeBackgroundView_MapsBackgroundFields|AdaptRuntimeToBuildServerOptions_MapsRuntimeFields)' -v` +Expected: FAIL,旧 builder 仍读取旧顶层字段 + +**Step 3: Write minimal implementation** + +```go +func resolveRuntimeHealthChecks(runtime *Runtime) runtimeHealthChecks { ... } // 改为读取 runtime.resources +func buildRuntimeHTTPView(runtime *Runtime) (runtimeHTTPView, error) { ... } // 改为读取 runtime.startupViews.http +func buildRuntimeBackgroundView(runtime *Runtime) (runtimeBackgroundView, error) { ... } // 改为读取 runtime.startupViews.background + runtime.resources +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'Test(BuildRuntimeHTTPView_MapsHTTPFields|BuildRuntimeBackgroundView_MapsBackgroundFields|AdaptRuntimeToBuildServerOptions_MapsRuntimeFields)' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/runtime.go supply-api/internal/app/background.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): route runtime views through layered surfaces" +``` + +### Task 3: 回归验证与收尾 + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/background.go` +- Verify: `supply-api/internal/app/runtime_test.go` + +**Step 1: Run focused tests** + +Run: `cd "supply-api" && go test ./internal/app -run 'Test(BuildRuntime_GroupsResourcesAndStartupViews|BuildRuntimeHTTPView_.*|BuildRuntimeBackgroundView_.*|AdaptRuntime(ToBuildServerOptions|HTTPViewToBuildServerOptions)_.*|Runtime_StartBackgroundWorkers_.*|BuildRuntime_.*)' -v` +Expected: PASS + +**Step 2: Run package regression** + +Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi` +Expected: PASS + +**Step 3: Run e2e tests** + +Run: `cd "supply-api" && go test -tags=e2e ./e2e` +Expected: PASS + +**Step 4: Run repo exit verification** + +Run: `bash "scripts/ci/repo_integrity_check.sh"` +Expected: PASS + +**Step 5: Check formatting** + +Run: `git diff --check` +Expected: no output + +**Step 6: Commit** + +```bash +git add docs/plans/2026-04-16-supply-api-runtime-layered-surfaces-plan.md supply-api/internal/app/runtime.go supply-api/internal/app/background.go supply-api/internal/app/runtime_test.go +git commit -m "refactor(supply-api): reduce runtime aggregation density" +``` diff --git a/supply-api/internal/app/background.go b/supply-api/internal/app/background.go index e5332f84..f9bb7d95 100644 --- a/supply-api/internal/app/background.go +++ b/supply-api/internal/app/background.go @@ -79,17 +79,17 @@ func buildRuntimeBackgroundView(runtime *Runtime) (runtimeBackgroundView, error) if runtime == nil { return runtimeBackgroundView{}, errors.New("runtime is required") } - if runtime.logger == nil { + if runtime.startupViews.background.logger == nil { return runtimeBackgroundView{}, errors.New("runtime logger is required") } view := runtimeBackgroundView{ - env: runtime.env, - logger: runtime.logger, - tuning: runtime.tuning, - db: runtime.db, - redisCache: runtime.redisCache, - revocationSubscriber: runtime.revocationSubscriber, + env: runtime.startupViews.background.env, + logger: runtime.startupViews.background.logger, + tuning: runtime.startupViews.background.tuning, + db: runtime.resources.db, + redisCache: runtime.resources.redisCache, + revocationSubscriber: runtime.startupViews.background.revocationSubscriber, } if view.tuning.outboxStreamName == "" { view.tuning = defaultRuntimeTuning() diff --git a/supply-api/internal/app/runtime.go b/supply-api/internal/app/runtime.go index 87fc3d23..dfb2bad9 100644 --- a/supply-api/internal/app/runtime.go +++ b/supply-api/internal/app/runtime.go @@ -41,24 +41,41 @@ type runtimeTuning struct { // Runtime 聚合 HTTP 启动和后台任务启动所需的运行时依赖。 type Runtime struct { - env string - logger logging.Logger - now func() time.Time - tuning runtimeTuning - serverConfig config.ServerConfig - db *repository.DB - redisCache *cache.RedisCache - supplyAPI *httpapi.SupplyAPI - alertAPI *httpapi.AlertAPI - authMiddleware *middleware.AuthMiddleware - rateLimitConfig *middleware.RateLimitConfig - revocationSubscriber revocationSubscriber + resources runtimeExternalResources + startupViews runtimeStartupViews } type revocationSubscriber interface { StartRevocationSubscriber(ctx context.Context) error } +type runtimeExternalResources struct { + db *repository.DB + redisCache *cache.RedisCache +} + +type runtimeHTTPStartupView struct { + env string + logger logging.Logger + serverConfig config.ServerConfig + supplyAPI *httpapi.SupplyAPI + alertAPI *httpapi.AlertAPI + authMiddleware *middleware.AuthMiddleware + rateLimitConfig *middleware.RateLimitConfig +} + +type runtimeBackgroundStartupView struct { + env string + logger logging.Logger + tuning runtimeTuning + revocationSubscriber revocationSubscriber +} + +type runtimeStartupViews struct { + http runtimeHTTPStartupView + background runtimeBackgroundStartupView +} + type runtimeFactory struct { newDB func(ctx context.Context, cfg config.DatabaseConfig) (*repository.DB, error) newRedisCache func(cfg config.RedisConfig) (*cache.RedisCache, error) @@ -173,18 +190,27 @@ func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runt } return &Runtime{ - env: env, - logger: opts.Logger, - now: now, - tuning: tuning, - serverConfig: normalizeServerConfig(opts.Config.Server), - db: db, - redisCache: redisCache, - supplyAPI: apiBundle.supplyAPI, - alertAPI: apiBundle.alertAPI, - authMiddleware: securityBundle.authMiddleware, - rateLimitConfig: apiBundle.rateLimitConfig, - revocationSubscriber: securityBundle.revocationSubscriber, + resources: runtimeExternalResources{ + db: db, + redisCache: redisCache, + }, + startupViews: runtimeStartupViews{ + http: runtimeHTTPStartupView{ + env: env, + logger: opts.Logger, + serverConfig: normalizeServerConfig(opts.Config.Server), + supplyAPI: apiBundle.supplyAPI, + alertAPI: apiBundle.alertAPI, + authMiddleware: securityBundle.authMiddleware, + rateLimitConfig: apiBundle.rateLimitConfig, + }, + background: runtimeBackgroundStartupView{ + env: env, + logger: opts.Logger, + tuning: tuning, + revocationSubscriber: securityBundle.revocationSubscriber, + }, + }, }, nil } @@ -361,11 +387,11 @@ func resolveRuntimeHealthChecks(runtime *Runtime) runtimeHealthChecks { if runtime == nil { return checks } - if runtime.db != nil { - checks.DBHealthCheck = runtime.db.HealthCheck + if runtime.resources.db != nil { + checks.DBHealthCheck = runtime.resources.db.HealthCheck } - if runtime.redisCache != nil { - checks.RedisHealthCheck = runtime.redisCache.HealthCheck + if runtime.resources.redisCache != nil { + checks.RedisHealthCheck = runtime.resources.redisCache.HealthCheck } return checks } @@ -376,13 +402,13 @@ func buildRuntimeHTTPView(runtime *Runtime) (runtimeHTTPView, error) { } return runtimeHTTPView{ - env: runtime.env, - logger: runtime.logger, - serverConfig: runtime.serverConfig, - supplyAPI: runtime.supplyAPI, - alertAPI: runtime.alertAPI, - authMiddleware: runtime.authMiddleware, - rateLimitConfig: runtime.rateLimitConfig, + env: runtime.startupViews.http.env, + logger: runtime.startupViews.http.logger, + serverConfig: runtime.startupViews.http.serverConfig, + supplyAPI: runtime.startupViews.http.supplyAPI, + alertAPI: runtime.startupViews.http.alertAPI, + authMiddleware: runtime.startupViews.http.authMiddleware, + rateLimitConfig: runtime.startupViews.http.rateLimitConfig, healthChecks: resolveRuntimeHealthChecks(runtime), }, nil } @@ -414,11 +440,11 @@ func (r *Runtime) Close() { if r == nil { return } - if r.redisCache != nil { - _ = r.redisCache.Close() + if r.resources.redisCache != nil { + _ = r.resources.redisCache.Close() } - if r.db != nil { - r.db.Close() + if r.resources.db != nil { + r.resources.db.Close() } } @@ -427,7 +453,7 @@ func (r *Runtime) ShutdownTimeout() time.Duration { if r == nil { return 0 } - return r.serverConfig.ShutdownTimeout + return r.startupViews.http.serverConfig.ShutdownTimeout } func ResolveEnv(env string) (string, error) { diff --git a/supply-api/internal/app/runtime_test.go b/supply-api/internal/app/runtime_test.go index d5d1afc1..036ae69c 100644 --- a/supply-api/internal/app/runtime_test.go +++ b/supply-api/internal/app/runtime_test.go @@ -243,19 +243,19 @@ func TestBuildRuntime_DevFallsBackToInMemoryDependencies(t *testing.T) { if runtime == nil { t.Fatal("expected runtime") } - if runtime.db != nil { + if runtime.resources.db != nil { t.Fatal("expected nil db after dev fallback") } - if runtime.redisCache != nil { + if runtime.resources.redisCache != nil { t.Fatal("expected nil redis cache after dev fallback") } - if runtime.supplyAPI == nil || runtime.alertAPI == nil { + if runtime.startupViews.http.supplyAPI == nil || runtime.startupViews.http.alertAPI == nil { t.Fatal("expected apis to be initialized") } - if runtime.authMiddleware == nil { + if runtime.startupViews.http.authMiddleware == nil { t.Fatal("expected auth middleware to be initialized") } - if runtime.rateLimitConfig == nil { + if runtime.startupViews.http.rateLimitConfig == nil { t.Fatal("expected rate limit config to be initialized") } } @@ -283,8 +283,8 @@ func TestBuildRuntime_NormalizesServerConfigDefaults(t *testing.T) { if err != nil { t.Fatalf("expected runtime build to succeed, got %v", err) } - if runtime.serverConfig.Addr != ":18082" { - t.Fatalf("unexpected addr: %s", runtime.serverConfig.Addr) + if runtime.startupViews.http.serverConfig.Addr != ":18082" { + t.Fatalf("unexpected addr: %s", runtime.startupViews.http.serverConfig.Addr) } if runtime.ShutdownTimeout() != 5*time.Second { t.Fatalf("unexpected shutdown timeout: %s", runtime.ShutdownTimeout()) @@ -311,20 +311,20 @@ func TestBuildRuntime_SeedsDefaultTuning(t *testing.T) { if err != nil { t.Fatalf("expected runtime build to succeed, got %v", err) } - if runtime.tuning.outboxStreamName != "supply:outbox:stream" { - t.Fatalf("unexpected outbox stream: %s", runtime.tuning.outboxStreamName) + if runtime.startupViews.background.tuning.outboxStreamName != "supply:outbox:stream" { + t.Fatalf("unexpected outbox stream: %s", runtime.startupViews.background.tuning.outboxStreamName) } - if runtime.tuning.outboxConsumerGroup != "outbox-processor" { - t.Fatalf("unexpected outbox group: %s", runtime.tuning.outboxConsumerGroup) + if runtime.startupViews.background.tuning.outboxConsumerGroup != "outbox-processor" { + t.Fatalf("unexpected outbox group: %s", runtime.startupViews.background.tuning.outboxConsumerGroup) } - if runtime.tuning.idempotencyTTL != 24*time.Hour { - t.Fatalf("unexpected idempotency ttl: %s", runtime.tuning.idempotencyTTL) + if runtime.startupViews.background.tuning.idempotencyTTL != 24*time.Hour { + t.Fatalf("unexpected idempotency ttl: %s", runtime.startupViews.background.tuning.idempotencyTTL) } - if runtime.tuning.partitionMaintenanceInterval != time.Hour { - t.Fatalf("unexpected partition maintenance interval: %s", runtime.tuning.partitionMaintenanceInterval) + if runtime.startupViews.background.tuning.partitionMaintenanceInterval != time.Hour { + t.Fatalf("unexpected partition maintenance interval: %s", runtime.startupViews.background.tuning.partitionMaintenanceInterval) } - if runtime.tuning.compensationCheckInterval != 5*time.Minute { - t.Fatalf("unexpected compensation interval: %s", runtime.tuning.compensationCheckInterval) + if runtime.startupViews.background.tuning.compensationCheckInterval != 5*time.Minute { + t.Fatalf("unexpected compensation interval: %s", runtime.startupViews.background.tuning.compensationCheckInterval) } } @@ -355,6 +355,49 @@ func TestBuildRuntime_DevFallbackLogsWarnings(t *testing.T) { } } +func TestBuildRuntime_GroupsResourcesAndStartupViews(t *testing.T) { + runtime, err := buildRuntimeWithFactory(RuntimeOptions{ + Env: "dev", + Config: testRuntimeConfig(), + Logger: testLogger{}, + InitContext: context.Background(), + Now: func() time.Time { + return time.Unix(1712800000, 0).UTC() + }, + }, runtimeFactory{ + newDB: func(context.Context, config.DatabaseConfig) (*repository.DB, error) { + return nil, errors.New("db down") + }, + newRedisCache: func(config.RedisConfig) (*cache.RedisCache, error) { + return nil, errors.New("redis down") + }, + }) + if err != nil { + t.Fatalf("expected runtime build to succeed, got %v", err) + } + if runtime.startupViews.http.env != "dev" { + t.Fatalf("unexpected http env: %s", runtime.startupViews.http.env) + } + if runtime.startupViews.background.env != "dev" { + t.Fatalf("unexpected background env: %s", runtime.startupViews.background.env) + } + if runtime.resources.db != nil { + t.Fatal("expected nil db resource after dev fallback") + } + if runtime.resources.redisCache != nil { + t.Fatal("expected nil redis resource after dev fallback") + } + if runtime.startupViews.http.supplyAPI == nil || runtime.startupViews.http.alertAPI == nil { + t.Fatal("expected http startup view apis to be initialized") + } + if runtime.startupViews.http.authMiddleware == nil { + t.Fatal("expected http startup view auth middleware") + } + if runtime.startupViews.background.tuning.outboxStreamName != "supply:outbox:stream" { + t.Fatalf("unexpected background outbox stream: %s", runtime.startupViews.background.tuning.outboxStreamName) + } +} + func TestResolveRuntimeHealthChecks_OmitsUnavailableDependencies(t *testing.T) { checks := resolveRuntimeHealthChecks(&Runtime{}) if checks.DBHealthCheck != nil { @@ -367,8 +410,10 @@ func TestResolveRuntimeHealthChecks_OmitsUnavailableDependencies(t *testing.T) { func TestResolveRuntimeHealthChecks_ExposesAvailableDependencies(t *testing.T) { checks := resolveRuntimeHealthChecks(&Runtime{ - db: &repository.DB{}, - redisCache: &cache.RedisCache{}, + resources: runtimeExternalResources{ + db: &repository.DB{}, + redisCache: &cache.RedisCache{}, + }, }) if checks.DBHealthCheck == nil { t.Fatal("expected db health check") @@ -394,15 +439,21 @@ func TestBuildRuntimeHTTPView_MapsHTTPFields(t *testing.T) { rateLimitConfig := &middleware.RateLimitConfig{Enabled: true} view, err := buildRuntimeHTTPView(&Runtime{ - env: "staging", - logger: testLogger{}, - serverConfig: config.ServerConfig{Addr: ":19090"}, - supplyAPI: supplyAPI, - alertAPI: alertAPI, - authMiddleware: authMiddleware, - rateLimitConfig: rateLimitConfig, - db: &repository.DB{}, - redisCache: &cache.RedisCache{}, + resources: runtimeExternalResources{ + db: &repository.DB{}, + redisCache: &cache.RedisCache{}, + }, + startupViews: runtimeStartupViews{ + http: runtimeHTTPStartupView{ + env: "staging", + logger: testLogger{}, + serverConfig: config.ServerConfig{Addr: ":19090"}, + supplyAPI: supplyAPI, + alertAPI: alertAPI, + authMiddleware: authMiddleware, + rateLimitConfig: rateLimitConfig, + }, + }, }) if err != nil { t.Fatalf("expected view build to succeed, got %v", err) @@ -449,15 +500,21 @@ func TestAdaptRuntimeToBuildServerOptions_MapsRuntimeFields(t *testing.T) { rateLimitConfig := &middleware.RateLimitConfig{Enabled: true} opts, err := adaptRuntimeToBuildServerOptions(&Runtime{ - env: "staging", - logger: testLogger{}, - serverConfig: config.ServerConfig{Addr: ":19090"}, - supplyAPI: supplyAPI, - alertAPI: alertAPI, - authMiddleware: authMiddleware, - rateLimitConfig: rateLimitConfig, - db: &repository.DB{}, - redisCache: &cache.RedisCache{}, + resources: runtimeExternalResources{ + db: &repository.DB{}, + redisCache: &cache.RedisCache{}, + }, + startupViews: runtimeStartupViews{ + http: runtimeHTTPStartupView{ + env: "staging", + logger: testLogger{}, + serverConfig: config.ServerConfig{Addr: ":19090"}, + supplyAPI: supplyAPI, + alertAPI: alertAPI, + authMiddleware: authMiddleware, + rateLimitConfig: rateLimitConfig, + }, + }, }) if err != nil { t.Fatalf("expected adapter to succeed, got %v", err) @@ -544,12 +601,18 @@ func TestBuildRuntimeBackgroundView_MapsBackgroundFields(t *testing.T) { subscriber := stubRevocationSubscriber{} view, err := buildRuntimeBackgroundView(&Runtime{ - env: "prod", - logger: testLogger{}, - tuning: defaultRuntimeTuning(), - db: &repository.DB{}, - redisCache: &cache.RedisCache{}, - revocationSubscriber: subscriber, + resources: runtimeExternalResources{ + db: &repository.DB{}, + redisCache: &cache.RedisCache{}, + }, + startupViews: runtimeStartupViews{ + background: runtimeBackgroundStartupView{ + env: "prod", + logger: testLogger{}, + tuning: defaultRuntimeTuning(), + revocationSubscriber: subscriber, + }, + }, }) if err != nil { t.Fatalf("expected background view build to succeed, got %v", err) @@ -578,8 +641,12 @@ func TestRuntime_StartBackgroundWorkers_WithoutDatabaseIsNoop(t *testing.T) { var outboxRepoCalled bool err := startBackgroundWorkersWithFactory(context.Background(), context.Background(), &Runtime{ - env: "dev", - logger: testLogger{}, + startupViews: runtimeStartupViews{ + background: runtimeBackgroundStartupView{ + env: "dev", + logger: testLogger{}, + }, + }, }, backgroundFactory{ newOutboxRepository: func(*repository.DB) outboxRepository { outboxRepoCalled = true @@ -596,9 +663,15 @@ func TestRuntime_StartBackgroundWorkers_WithoutDatabaseIsNoop(t *testing.T) { func TestRuntime_StartBackgroundWorkers_ProdRequiresOutboxBroker(t *testing.T) { err := startBackgroundWorkersWithFactory(context.Background(), context.Background(), &Runtime{ - env: "prod", - logger: testLogger{}, - db: &repository.DB{}, + resources: runtimeExternalResources{ + db: &repository.DB{}, + }, + startupViews: runtimeStartupViews{ + background: runtimeBackgroundStartupView{ + env: "prod", + logger: testLogger{}, + }, + }, }, backgroundFactory{ newOutboxRepository: func(*repository.DB) outboxRepository { return stubOutboxRepository{} @@ -641,10 +714,16 @@ func TestRuntime_StartBackgroundWorkers_UsesDefaultCompensationInterval(t *testi var gotInterval time.Duration err := startBackgroundWorkersWithFactory(context.Background(), context.Background(), &Runtime{ - env: "dev", - logger: testLogger{}, - db: &repository.DB{}, - tuning: defaultRuntimeTuning(), + resources: runtimeExternalResources{ + db: &repository.DB{}, + }, + startupViews: runtimeStartupViews{ + background: runtimeBackgroundStartupView{ + env: "dev", + logger: testLogger{}, + tuning: defaultRuntimeTuning(), + }, + }, }, backgroundFactory{ newOutboxRepository: func(*repository.DB) outboxRepository { return stubOutboxRepository{} @@ -720,10 +799,16 @@ func TestRuntime_StartBackgroundWorkers_DevMissingOutboxBrokerLogsWarning(t *tes logger := &captureLogger{} err := startBackgroundWorkersWithFactory(context.Background(), context.Background(), &Runtime{ - env: "dev", - logger: logger, - db: &repository.DB{}, - tuning: defaultRuntimeTuning(), + resources: runtimeExternalResources{ + db: &repository.DB{}, + }, + startupViews: runtimeStartupViews{ + background: runtimeBackgroundStartupView{ + env: "dev", + logger: logger, + tuning: defaultRuntimeTuning(), + }, + }, }, backgroundFactory{ newOutboxRepository: func(*repository.DB) outboxRepository { return stubOutboxRepository{}