Skip to main content

max / makenotwork

Decompose AppState Phase 4: route test harness through AppState::build The integration harness and load runner both inlined the full AppState struct literal, duplicating the field assembly that main.rs already does via AppState::build. Route both through AppState::build(AppStateParts{...}) so the derived in-memory state (start timestamps, empty cache maps, concurrency semaphores sized from constants) has a single source of truth; each test path now supplies only the externally-wired dependencies. Semaphores in tests move from ad-hoc 4/8 to the production constants (immaterial to tests). Minimal-slice handler testing is already enabled by the FromRef design: a DB-only handler test builds a PgPool (the slice) directly, and any slice projects from a harness AppState via <Slice as FromRef<AppState>>::from_ref. No behavior change; scan 25 + synckit 74 + load (compile) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-14 13:53 UTC
Commit: 8b97cb2e1caf4552b6755ff40a4c9d9f2381a808
Parent: 0ea5f0e
2 files changed, +42 insertions, -67 deletions
@@ -17,10 +17,9 @@ use docengine::DocLoader;
17 17 use makenotwork::email::{EmailClient, EmailConfig};
18 18 use makenotwork::payments::{PaymentProvider, StripeClient};
19 19 use makenotwork::scanning::ScanPipeline;
20 - use makenotwork::{build_app, AppCaches, AppLimiters, AppState, AppStorage};
20 + use makenotwork::{build_app, AppState, AppStateParts, AppStorage};
21 21 use sqlx::PgPool;
22 22 use std::sync::Arc;
23 - use std::time::Instant;
24 23 use tower_sessions::cookie::time::Duration as CookieDuration;
25 24 use tower_sessions::cookie::SameSite;
26 25 use tower_sessions::{Expiry, SessionManagerLayer};
@@ -375,9 +374,27 @@ impl TestHarness {
375 374 // no bucket isolation) so the cross-bucket image promote resolves.
376 375 let public_s3 = s3.clone();
377 376
378 - let state = AppState {
377 + // Route through the same `AppState::build` constructor production uses
378 + // (main.rs), so the derived in-memory state — start timestamps, the empty
379 + // cache maps, the concurrency semaphores — has a single source of truth.
380 + // The harness only supplies the externally-wired dependencies.
381 + let state = AppState::build(AppStateParts {
379 382 db: pool.clone(),
380 383 config,
384 + storage: AppStorage {
385 + s3,
386 + synckit_s3,
387 + public_s3,
388 + },
389 + stripe: opts.stripe_client,
390 + email,
391 + docs: Arc::new(DocLoader::load(std::path::Path::new("."), &docengine::DocLoaderConfig {
392 + sections: vec![],
393 + link_prefix: "/docs".to_string(),
394 + unpublished_pattern: None,
395 + examples_path: None,
396 + pre_process: None,
397 + })),
381 398 tier_prices: {
382 399 // Install the process-global TierPrices so handler code paths
383 400 // that call CreatorTier::{price_cents,max_file_bytes,
@@ -390,47 +407,18 @@ impl TestHarness {
390 407 pricing_comparison: makenotwork::pricing_comparison::PricingComparison::load(
391 408 "docs/business/assumptions.toml",
392 409 ),
393 - stripe: opts.stripe_client,
394 - email,
395 - docs: Arc::new(DocLoader::load(std::path::Path::new("."), &docengine::DocLoaderConfig {
396 - sections: vec![],
397 - link_prefix: "/docs".to_string(),
398 - unpublished_pattern: None,
399 - examples_path: None,
400 - pre_process: None,
401 - })),
402 410 scanner: opts.scanner,
403 411 webauthn,
404 412 syntax: None,
405 - started_at: chrono::Utc::now(),
406 - start_instant: Instant::now(),
407 413 mt_client: opts.mt_base_url.zip(opts.internal_shared_secret).map(
408 414 |(url, secret)| makenotwork::mt_client::MtClient::new(url, secret),
409 415 ),
410 416 wam: None,
411 - restart_at: Arc::new(std::sync::atomic::AtomicI64::new(0)),
417 + domain_cache: Arc::new(dashmap::DashMap::new()),
412 418 metrics_handle: None,
413 419 page_view_tx: makenotwork::db::page_views::spawn_batcher(pool.clone()),
414 420 bg: makenotwork::background::spawn_pool_detached(),
415 - storage: AppStorage {
416 - s3,
417 - synckit_s3,
418 - public_s3,
419 - },
420 - caches: AppCaches {
421 - session_cache: Arc::new(dashmap::DashMap::new()),
422 - domain_cache: Arc::new(dashmap::DashMap::new()),
423 - sync_notify: Arc::new(dashmap::DashMap::new()),
424 - sse_connections: Arc::new(dashmap::DashMap::new()),
425 - },
426 - limiters: AppLimiters {
427 - scan_semaphore: Arc::new(tokio::sync::Semaphore::new(4)),
428 - caddy_ask_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
429 - git_smart_http_semaphore: Arc::new(tokio::sync::Semaphore::new(
430 - makenotwork::constants::GIT_SMART_HTTP_MAX_CONCURRENT,
431 - )),
432 - },
433 - };
421 + });
434 422
435 423 // Capture scan deps before `build_app` consumes `state`.
436 424 let scan_deps = match (state.scanner.clone(), state.storage.s3.clone()) {
@@ -14,7 +14,7 @@ use tower_sessions_sqlx_store::PostgresStore;
14 14 use makenotwork::config::{BuildConfig, Config, CreatorTierPricing, EmailWebhookConfig, IntegrationsConfig};
15 15 use docengine::DocLoader;
16 16 use makenotwork::email::{EmailClient, EmailConfig};
17 - use makenotwork::{build_app, AppCaches, AppLimiters, AppState, AppStorage};
17 + use makenotwork::{build_app, AppState, AppStateParts, AppStorage};
18 18
19 19 use crate::harness::client::TestClient;
20 20 use crate::harness::db::TestDb;
@@ -111,19 +111,16 @@ pub async fn run(config: LoadConfig) {
111 111 .expect("Webauthn"),
112 112 );
113 113
114 - let state = AppState {
114 + // Route through the shared `AppState::build` constructor (same as main.rs
115 + // and the integration harness) — derived in-memory state has one source.
116 + let state = AppState::build(AppStateParts {
115 117 db: pool.clone(),
116 118 config: app_config,
117 - tier_prices: {
118 - // Install the process-global TierPrices so CreatorTier accessors
119 - // work under the load harness (they read the global). Idempotent.
120 - makenotwork::tier_prices::TierPrices::install_test_default();
121 - makenotwork::tier_prices::TierPrices::global().clone()
119 + storage: AppStorage {
120 + s3: None,
121 + synckit_s3: None,
122 + public_s3: None,
122 123 },
123 - runway_config: makenotwork::tier_prices::RunwayConfig::default(),
124 - pricing_comparison: makenotwork::pricing_comparison::PricingComparison::load(
125 - "docs/business/assumptions.toml",
126 - ),
127 124 stripe: None,
128 125 email,
129 126 docs: Arc::new(DocLoader::load(std::path::Path::new("."), &docengine::DocLoaderConfig {
@@ -133,36 +130,26 @@ pub async fn run(config: LoadConfig) {
133 130 examples_path: None,
134 131 pre_process: None,
135 132 })),
133 + tier_prices: {
134 + // Install the process-global TierPrices so CreatorTier accessors
135 + // work under the load harness (they read the global). Idempotent.
136 + makenotwork::tier_prices::TierPrices::install_test_default();
137 + makenotwork::tier_prices::TierPrices::global().clone()
138 + },
139 + runway_config: makenotwork::tier_prices::RunwayConfig::default(),
140 + pricing_comparison: makenotwork::pricing_comparison::PricingComparison::load(
141 + "docs/business/assumptions.toml",
142 + ),
136 143 scanner: None,
137 144 webauthn,
138 145 syntax: None,
139 - started_at: chrono::Utc::now(),
140 - start_instant: Instant::now(),
141 146 mt_client: None,
142 147 wam: None,
143 - restart_at: Arc::new(std::sync::atomic::AtomicI64::new(0)),
148 + domain_cache: Arc::new(dashmap::DashMap::new()),
144 149 metrics_handle: None,
145 150 page_view_tx: makenotwork::db::page_views::spawn_batcher(pool.clone()),
146 151 bg: makenotwork::background::spawn_pool_detached(),
147 - storage: AppStorage {
148 - s3: None,
149 - synckit_s3: None,
150 - public_s3: None,
151 - },
152 - caches: AppCaches {
153 - session_cache: Arc::new(dashmap::DashMap::new()),
154 - domain_cache: Arc::new(dashmap::DashMap::new()),
155 - sync_notify: Arc::new(dashmap::DashMap::new()),
156 - sse_connections: Arc::new(dashmap::DashMap::new()),
157 - },
158 - limiters: AppLimiters {
159 - scan_semaphore: Arc::new(tokio::sync::Semaphore::new(4)),
160 - caddy_ask_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
161 - git_smart_http_semaphore: Arc::new(tokio::sync::Semaphore::new(
162 - makenotwork::constants::GIT_SMART_HTTP_MAX_CONCURRENT,
163 - )),
164 - },
165 - };
152 + });
166 153
167 154 let app = build_app(state, session_layer);
168 155