# WorkUtils API Debug Proxy Startup Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current session. Do not dispatch subagents. **Goal:** Start the API debug proxy with WorkUtils, show its read-only state on the API export page, and remove the per-export proxy opt-in. **Architecture:** A Spring singleton `ApiDebugProxyLifecycle` owns startup state, endpoint publication, failure degradation, and shutdown. `WorkUtilsApplication` starts and stops it around the existing GUI/CLI dispatch, while `ApiDocPlugin` passes the same service to `ApiDocWindow`; export requests no longer carry a proxy boolean. **Tech Stack:** Java 17, Spring Boot, JavaFX 17, JDK `HttpServer`, JUnit 5, Maven Wrapper. ## Global Constraints - WorkUtils must continue when proxy startup fails. - The proxy starts before GUI or CLI dispatch and stops in `finally`. - Keep the random `127.0.0.1` port and `/dubbo/invoke` path. - Remove the API page proxy checkbox; provide no retry/start/stop UI. - Failed proxy startup must not block HTML or Markdown export. - Do not modify or stage the three pre-existing untracked paths on `master`. - Execute single-threaded; do not use subagents. --- ### Task 1: Proxy Lifecycle and Resource Shutdown **Files:** - Create: `src/main/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycle.java` - Modify: `src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java` - Create: `src/test/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycleTest.java` **Interfaces:** - Produces: `ApiDebugProxyLifecycle.start()`, `stop()`, `status()`, and `endpoint()`. - Produces: `Status(State state, String endpoint, String errorMessage)` and `State.NOT_STARTED/RUNNING/FAILED`. - Produces: idempotent `DubboDebugProxy.stop()`. - [ ] **Step 1: Write the failing lifecycle tests** ```java @Test void startsOnceAndStopsOnce() { AtomicInteger starts = new AtomicInteger(); AtomicInteger stops = new AtomicInteger(); ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(() -> { starts.incrementAndGet(); return new ApiDebugProxyLifecycle.ProxyHandle() { public String endpoint() { return "http://127.0.0.1:1234/dubbo/invoke"; } public void stop() { stops.incrementAndGet(); } }; }); lifecycle.start(); lifecycle.start(); lifecycle.stop(); lifecycle.stop(); assertEquals(1, starts.get()); assertEquals(1, stops.get()); assertEquals(ApiDebugProxyLifecycle.State.RUNNING, lifecycle.status().state()); } @Test void recordsFailureWithoutThrowing() { ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle( () -> { throw new IllegalStateException("端口不可用"); }); assertDoesNotThrow(lifecycle::start); assertEquals(ApiDebugProxyLifecycle.State.FAILED, lifecycle.status().state()); assertEquals("", lifecycle.endpoint()); assertTrue(lifecycle.status().errorMessage().contains("端口不可用")); } ``` - [ ] **Step 2: Verify RED** Run: `.\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest' test` Expected: test compilation fails because `ApiDebugProxyLifecycle` does not exist. - [ ] **Step 3: Implement the lifecycle service** Use a public default constructor backed by `DubboDebugProxy.getInstance()` and a package-private constructor accepting `Supplier`. `start()` must be synchronized and catch `RuntimeException`: ```java public synchronized Status start() { if (status.state() != State.NOT_STARTED) return status; try { proxy = proxyFactory.get(); status = new Status(State.RUNNING, Objects.requireNonNull(proxy.endpoint(), "endpoint"), ""); } catch (RuntimeException error) { status = new Status(State.FAILED, "", readableMessage(error)); log.error("API 本地调试代理启动失败", error); } return status; } ``` `stop()` atomically detaches the handle, calls it at most once, and logs rather than rethrows shutdown failures. - [ ] **Step 4: Release proxy resources** Store the fixed pool in an `ExecutorService` field and implement: ```java public synchronized void stop() { if (stopped) return; stopped = true; server.stop(0); executor.shutdownNow(); log.info("DubboDebugProxy 已停止"); } ``` - [ ] **Step 5: Verify GREEN** Run: `.\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest,ApiDocGeneratorProxyTest' test` Expected: PASS. - [ ] **Step 6: Commit** ```powershell git add src/main/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycle.java src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java src/test/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycleTest.java git commit -m "Start and stop the API debug proxy safely" ``` --- ### Task 2: Remove Per-Export Proxy Selection **Files:** - Modify: `src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java` - Modify: `src/main/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCase.java` - Modify: `src/test/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequestTest.java` - Modify: `src/test/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCaseTest.java` **Interfaces:** - Produces: `path(String)`, `fqn(String)`, and `batch(List, boolean, boolean, boolean)`. - Consumes: lifecycle endpoint supplier returning a URL or `""`. - [ ] **Step 1: Write flag-free request tests** ```java ApiDocExportRequest request = ApiDocExportRequest.fqn( " com.example.UserFacade "); assertEquals("com.example.UserFacade", request.input()); assertFalse(Arrays.stream(ApiDocExportRequest.class.getRecordComponents()) .anyMatch(component -> component.getName().equals("debugProxyEnabled"))); ``` Update path and batch tests to omit the final boolean. - [ ] **Step 2: Write unconditional endpoint tests** ```java @Test void alwaysPassesStartupEndpointToExporter() throws Exception { AtomicReference endpoint = new AtomicReference<>(); RunApiDocExportUseCase useCase = new RunApiDocExportUseCase( () -> "http://127.0.0.1:1234/dubbo/invoke", (request, value, context) -> endpoint.set(value)); useCase.execute(ApiDocExportRequest.path("A.java"), OperationContext.none()); assertEquals("http://127.0.0.1:1234/dubbo/invoke", endpoint.get()); } @Test void emptyEndpointStillRunsExporter() throws Exception { AtomicInteger exports = new AtomicInteger(); RunApiDocExportUseCase useCase = new RunApiDocExportUseCase( () -> "", (request, value, context) -> exports.incrementAndGet()); useCase.execute(ApiDocExportRequest.path("A.java"), OperationContext.none()); assertEquals(1, exports.get()); } ``` - [ ] **Step 3: Verify RED** Run: `.\mvnw.cmd -q '-Dtest=ApiDocExportRequestTest,RunApiDocExportUseCaseTest' test` Expected: compilation fails because flag-free factories do not exist. - [ ] **Step 4: Remove the boolean and simplify the use case** Delete the record component and factory parameters. Resolve the endpoint unconditionally: ```java String endpoint = Objects.requireNonNullElse(debugEndpointSupplier.get(), ""); exporter.export(request, endpoint, context); ``` - [ ] **Step 5: Verify GREEN and commit** Run the Step 3 command; expect PASS. Then: ```powershell git add src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java src/main/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCase.java src/test/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequestTest.java src/test/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCaseTest.java git commit -m "Remove per-export proxy selection" ``` --- ### Task 3: Application Startup and API Page Status **Files:** - Modify: `src/main/java/com/example/workutils/WorkUtilsApplication.java` - Modify: `src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java` - Modify: `src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java` - Modify: `src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java` - Modify: `src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java` - Create: `src/test/java/com/example/workutils/biz/apidoc/ApiDebugProxyStatusLabelTest.java` **Interfaces:** - Consumes: lifecycle API from Task 1 and flag-free request factories from Task 2. - Produces: package-private `ApiDocWindow.buildProxyStatusLabel(Status, int)`. - [ ] **Step 1: Write application lifecycle architecture test** Extract `run(String... args)` and assert startup precedes dispatch, `finally` exists, and `apiDebugProxyLifecycle.stop()` is present. - [ ] **Step 2: Write page architecture and JavaFX label tests** Assert `ApiDocWindow` contains `buildProxyStatusLabel` and excludes `debugProxyCb`, `new CheckBox("启用接口调试`, and `DubboDebugProxy.getInstance()`. ```java Label running = ApiDocWindow.buildProxyStatusLabel( new Status(State.RUNNING, "http://127.0.0.1:1234/dubbo/invoke", ""), 13); assertTrue(running.getText().contains("本地代理已启动")); assertEquals(Theme.SUCCESS, running.getTextFill()); Label failed = ApiDocWindow.buildProxyStatusLabel( new Status(State.FAILED, "", "端口不可用"), 13); assertTrue(failed.getText().contains("本地代理启动失败")); assertEquals(Theme.DANGER, failed.getTextFill()); assertTrue(failed.getTooltip().getText().contains("端口不可用")); assertTrue(failed.getAccessibleText().contains("端口不可用")); ``` - [ ] **Step 3: Verify RED** Run: `.\mvnw.cmd -q '-Dtest=MainWindowArchitectureTest,ApiDocWindowArchitectureTest,ApiDebugProxyStatusLabelTest' test` Expected: failures because lifecycle wiring and the label factory are absent. - [ ] **Step 4: Wire application and plugin lifecycle** Inject `ApiDebugProxyLifecycle` into both Spring constructors. Wrap runner dispatch: ```java apiDebugProxyLifecycle.start(); try { if (args.length == 0) showMainWindow(); else processCommand(args); } finally { apiDebugProxyLifecycle.stop(); } ``` Pass the same lifecycle from `ApiDocPlugin` to every `ApiDocWindow` constructor call. - [ ] **Step 5: Replace checkbox with status label** Inject the lifecycle into `ApiDocWindow`, build the use case with `apiDebugProxyLifecycle::endpoint`, add the read-only label to the action row, and remove all checkbox state. Use `Theme.SUCCESS` for `RUNNING`, `Theme.DANGER` otherwise, plus failure Tooltip and accessible text. Before an export with a blank endpoint, log `[WARN] 本地代理不可用,导出文档的接口调试功能已禁用`. Call the flag-free request factories. - [ ] **Step 6: Verify GREEN and commit** Run: ```powershell .\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest,ApiDocExportRequestTest,RunApiDocExportUseCaseTest,ApiDocWindowArchitectureTest,ApiDebugProxyStatusLabelTest,MainWindowArchitectureTest,ApiDocGeneratorProxyTest' test ``` Expected: PASS. Then commit all Task 3 files with message `Start API proxy with WorkUtils`. --- ### Task 4: Full Verification and Master Integration **Files:** Verify all planned files; modify none unless a verification failure exposes a scoped defect. **Interfaces:** Produces a verified local `master` with the owned worktree and feature branch removed. - [ ] **Step 1: Run branch verification** ```powershell .\mvnw.cmd -q test .\mvnw.cmd -q -DskipTests compile git diff --check master...HEAD git status --short --branch ``` Expected: tests, compilation, and diff checks exit 0; the feature worktree is clean. - [ ] **Step 2: Fast-forward local master and verify again** From `D:\workspace\workutils`: ```powershell git merge --ff-only codex/api-proxy-startup .\mvnw.cmd -q test .\mvnw.cmd -q -DskipTests compile git diff --check ``` Expected: every command exits 0; the three pre-existing untracked paths remain untouched. - [ ] **Step 3: Clean the owned worktree and branch** ```powershell git worktree remove D:\workspace\workutils\.worktrees\codex-api-proxy-startup git worktree prune git branch -d codex/api-proxy-startup ```