diff --git a/docs/superpowers/plans/2026-08-03-api-debug-proxy-startup.md b/docs/superpowers/plans/2026-08-03-api-debug-proxy-startup.md new file mode 100644 index 0000000..403b8ed --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-api-debug-proxy-startup.md @@ -0,0 +1,308 @@ +# 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 +```