Plan API proxy startup implementation
This commit is contained in:
@@ -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<ProxyHandle>`. `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<String>, 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<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:
|
||||
|
||||
```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
|
||||
```
|
||||
Reference in New Issue
Block a user