Files
workutils/docs/superpowers/plans/2026-08-03-api-debug-proxy-startup.md
T
2026-08-03 10:46:56 +08:00

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.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

@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), and batch(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