diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/ConfigWiringSmokeTest.java b/backend-java/src/test/java/com/nanri/aiimage/config/ConfigWiringSmokeTest.java new file mode 100644 index 00000000..b6b58b18 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/config/ConfigWiringSmokeTest.java @@ -0,0 +1,122 @@ +package com.nanri.aiimage.config; + +import com.nanri.aiimage.metrics.TaskObservabilityMetrics; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 启动冒烟(收尾验证前置):离线 Spring 装配切片。 + * + * 在真实 Spring 容器里装配模块 10/11 新增的配置组件,验证:@ConfigurationProperties 绑定默认值、 + * @Component 自动注入、可选 MeterRegistry(ObjectProvider) 路径、@ConfigurationProperties 注册 + * 均正常——上线前先排除"bean 装配/属性绑定"类启动崩溃。 + * + * 注意:不加载 DataSource/Redis/MinIO 等(会连生产),全量真实启动需在非生产环境执行。 + */ +class ConfigWiringSmokeTest { + + @Configuration + @EnableConfigurationProperties({HttpClientProperties.class, ZiniaoProperties.class, + SimilarAsinProperties.class}) + @Import({LlmHttpConfigResolver.class, BrandCheckHttpConfigResolver.class, + ZiniaoHttpConfigResolver.class, ImageDownloadHttpConfigResolver.class, + IdempotentRetryGuard.class}) + static class Slice { + } + + private static AnnotationConfigApplicationContext context() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Slice.class); + ctx.refresh(); + return ctx; + } + + @Test + void configPropertiesBeansBindable() { + try (AnnotationConfigApplicationContext ctx = context()) { + HttpClientProperties http = ctx.getBean(HttpClientProperties.class); + assertNotNull(http); + assertEquals(10_000L, http.effectiveConnectTimeoutMillis(), "命名空间默认 connect 10s 应绑定"); + assertEquals(3, http.effectiveMaxRetries()); + assertNotNull(ctx.getBean(ZiniaoProperties.class)); + assertNotNull(ctx.getBean(SimilarAsinProperties.class)); + } + } + + @Test + void resolversAreSpringBeansAndInjectable() { + try (AnnotationConfigApplicationContext ctx = context()) { + assertNotNull(ctx.getBean(LlmHttpConfigResolver.class)); + assertNotNull(ctx.getBean(BrandCheckHttpConfigResolver.class)); + assertNotNull(ctx.getBean(ZiniaoHttpConfigResolver.class)); + assertNotNull(ctx.getBean(ImageDownloadHttpConfigResolver.class)); + } + } + + @Test + void resolversReadBoundConfig() { + try (AnnotationConfigApplicationContext ctx = context()) { + ZiniaoHttpConfigResolver ziniao = ctx.getBean(ZiniaoHttpConfigResolver.class); + // 模块未显式配置 read 秒数(默认 0) → 回退命名空间 60s + assertEquals(60_000L, ziniao.readTimeoutMillis()); + assertEquals(10_000L, ziniao.connectTimeoutMillis()); + ImageDownloadHttpConfigResolver image = ctx.getBean(ImageDownloadHttpConfigResolver.class); + // SimilarAsinProperties.imageDownloadTimeoutSeconds 默认 5s → 模块优先 + assertEquals(5_000L, image.readTimeoutMillis()); + } + } + + @Test + void retryGuardWiresWithOptionalRegistry() { + try (AnnotationConfigApplicationContext ctx = context()) { + IdempotentRetryGuard guard = ctx.getBean(IdempotentRetryGuard.class); + assertNotNull(guard); + assertTrue(guard.isIdempotent("GET", false), "GET 应视为幂等"); + assertEquals(500L, guard.backoffMillis(1), "守卫读取命名空间默认基础退避 500ms"); + } + } + + @Test + void observabilityRecorderIsPlainInstantiable() { + TaskObservabilityMetrics metrics = new TaskObservabilityMetrics("server-110", null); + assertEquals("server-110", metrics.instanceId()); + // registry=null 时不抛错(无监控栈降级) + metrics.taskCreated("similarasin"); + metrics.fileJobState("similarasin", "success"); + } + + @Test + void containerClosesCleanly() { + AnnotationConfigApplicationContext ctx = context(); + ctx.close(); + assertTrue(true); + } + + @Test + void sliceRegistersExactlyExpectedConfigBeans() { + try (AnnotationConfigApplicationContext ctx = context()) { + String[] names = ctx.getBeanDefinitionNames(); + assertTrue(names.length >= 8, "装配 bean 数量应充足"); + assertNotNull(ctx.getBean(HttpClientProperties.class)); + assertNotNull(ctx.getBean(IdempotentRetryGuard.class)); + } + } + + @Test + void defaultPropsMatchSpecBaseline() { + try (AnnotationConfigApplicationContext ctx = context()) { + HttpClientProperties http = ctx.getBean(HttpClientProperties.class); + assertSame(http, ctx.getBean(HttpClientProperties.class), "命名空间属性 bean 单例"); + assertEquals(90_000L, http.effectiveCallTimeoutMillis()); + assertEquals(500L, http.getBaseRetryDelayMillis()); + } + } +}