12 KiB
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.1port and/dubbo/invokepath. - 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(), andendpoint(). -
Produces:
Status(State state, String endpoint, String errorMessage)andState.NOT_STARTED/RUNNING/FAILED. -
Produces: idempotent
DubboDebugProxy.stop(). -
Step 1: Write the failing lifecycle tests
@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<ProxyHandle>. start() must be synchronized and catch RuntimeException:
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:
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
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), andbatch(List<String>, boolean, boolean, boolean). -
Consumes: lifecycle endpoint supplier returning a URL or
"". -
Step 1: Write flag-free request tests
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
@Test
void alwaysPassesStartupEndpointToExporter() throws Exception {
AtomicReference<String> 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:
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:
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().
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:
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:
.\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
.\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:
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
git worktree remove D:\workspace\workutils\.worktrees\codex-api-proxy-startup
git worktree prune
git branch -d codex/api-proxy-startup