重构插件异步状态与业务用例
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# Plugin UI Refactor Phase 1 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Introduce one testable asynchronous operation model and migrate Scanner and MyBatis SQL Export to consistent loading, cancellation, progress, error recovery, and history UI behavior.
|
||||
|
||||
**Architecture:** Add JavaFX-neutral operation state in `system/ui/async`, with a JavaFX adapter that binds buttons and status controls. Plugin windows submit immutable requests to operation controllers; services publish structured progress. Existing business services remain intact in this phase.
|
||||
|
||||
**Tech Stack:** Java 17, JavaFX 17, Spring Boot 2.7, JUnit 5, Maven.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve all existing command-line plugin behavior.
|
||||
- Do not add raw `new Thread` calls.
|
||||
- Do not put Git, SQL, HTTP, process, or file traversal logic in Window classes.
|
||||
- Use enum state instead of boolean combinations.
|
||||
- Give unrelated user actions independent operation state.
|
||||
- Keep production files below 350 lines where practical; do not increase existing Window line counts.
|
||||
- Do not modify the user's existing checkout; work only in the isolated worktree.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Operation State Machine
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationState.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationSnapshot.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationStateMachine.java`
|
||||
- Test: `src/test/java/com/example/workutils/system/ui/async/OperationStateMachineTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `OperationState`, immutable `OperationSnapshot`, and `OperationStateMachine`.
|
||||
- `OperationStateMachine.start(String)`, `progress(String,double)`, `requestCancel()`, `succeed(String)`, `fail(String)`, and `reset()`.
|
||||
- Invalid transitions throw `IllegalStateException`.
|
||||
|
||||
- [ ] **Step 1: Write failing state-transition tests**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void runningOperationCanProgressAndSucceed() {
|
||||
OperationStateMachine machine = new OperationStateMachine();
|
||||
machine.start("扫描中");
|
||||
machine.progress("项目 1/3", 1.0 / 3.0);
|
||||
machine.succeed("扫描完成");
|
||||
|
||||
assertEquals(OperationState.SUCCEEDED, machine.snapshot().state());
|
||||
assertEquals("扫描完成", machine.snapshot().message());
|
||||
}
|
||||
|
||||
@Test
|
||||
void idleOperationCannotBeCancelled() {
|
||||
OperationStateMachine machine = new OperationStateMachine();
|
||||
assertThrows(IllegalStateException.class, machine::requestCancel);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=OperationStateMachineTest test`
|
||||
|
||||
Expected: compilation failure because the production types do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal state machine**
|
||||
|
||||
Implement explicit legal transitions:
|
||||
|
||||
```text
|
||||
IDLE -> RUNNING
|
||||
RUNNING -> CANCELLING | SUCCEEDED | FAILED
|
||||
CANCELLING -> SUCCEEDED | FAILED
|
||||
SUCCEEDED | FAILED -> IDLE
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=OperationStateMachineTest test`
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
### Task 2: Asynchronous Operation Controller
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationTask.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationContext.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/AsyncOperationController.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/support/AppExecutors.java`
|
||||
- Test: `src/test/java/com/example/workutils/system/ui/async/AsyncOperationControllerTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `OperationStateMachine`.
|
||||
- Produces: one controller with `start(OperationTask<T>, Consumer<T>)`, `cancel()`, `snapshotProperty()`, and `runningProperty()`.
|
||||
- `OperationContext` supplies `report(message, progress)` and `isCancellationRequested()`.
|
||||
|
||||
- [ ] **Step 1: Write failing success, failure, and cancellation tests**
|
||||
|
||||
Use a single-thread test executor and `CountDownLatch`. Assert observable controller state, not mock calls.
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=AsyncOperationControllerTest test`
|
||||
|
||||
Expected: compilation failure because the controller does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal controller**
|
||||
|
||||
Use the injected `ExecutorService`; marshal observable property changes through an injected `Consumer<Runnable>` so tests can use `Runnable::run` and production can use `Platform::runLater`.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=AsyncOperationControllerTest test`
|
||||
|
||||
Expected: success, failure, and cancellation tests pass.
|
||||
|
||||
### Task 3: Loading and Status Components
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/LoadingButtonBinder.java`
|
||||
- Create: `src/main/java/com/example/workutils/system/ui/async/OperationStatusPane.java`
|
||||
- Modify: `src/main/resources/css/light-theme.css`
|
||||
- Test: `src/test/java/com/example/workutils/system/ui/async/LoadingButtonTextTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `AsyncOperationController`.
|
||||
- Produces: consistent primary-button loading text, conditional cancel button, progress indicator, status message, and error/success style classes.
|
||||
|
||||
- [ ] **Step 1: Write a failing pure text-mapping test**
|
||||
|
||||
Test `LoadingButtonBinder.textFor(snapshot, idleText)` with literal expectations:
|
||||
|
||||
```java
|
||||
assertEquals("扫描中…", textFor(running("扫描中"), "开始扫描"));
|
||||
assertEquals("正在停止…", textFor(cancelling(), "开始扫描"));
|
||||
assertEquals("开始扫描", textFor(idle(), "开始扫描"));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=LoadingButtonTextTest test`
|
||||
|
||||
- [ ] **Step 3: Implement mapper, binder, and status pane**
|
||||
|
||||
Use a real JavaFX `ProgressIndicator` only in the binder. Keep text selection pure and independently testable.
|
||||
|
||||
- [ ] **Step 4: Run tests and compile**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=LoadingButtonTextTest test`
|
||||
|
||||
Run: `.\mvnw.cmd -q -DskipTests compile`
|
||||
|
||||
### Task 4: Scanner Migration
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/com/example/workutils/biz/scanner/application/ScanRequest.java`
|
||||
- Create: `src/main/java/com/example/workutils/biz/scanner/application/ScanProgress.java`
|
||||
- Create: `src/main/java/com/example/workutils/biz/scanner/application/RunScanUseCase.java`
|
||||
- Modify: `src/main/java/com/example/workutils/biz/scanner/ScannerWindow.java`
|
||||
- Test: `src/test/java/com/example/workutils/biz/scanner/application/ScanRequestTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ScannerService`, `AsyncOperationController`.
|
||||
- Produces: immutable validated `ScanRequest` and structured `ScanProgress`.
|
||||
|
||||
- [ ] **Step 1: Write failing ScanRequest validation tests**
|
||||
|
||||
Test blank keyword rejection, invalid regular expression rejection, and trimmed keyword preservation.
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=ScanRequestTest test`
|
||||
|
||||
- [ ] **Step 3: Implement request and use case**
|
||||
|
||||
The use case adapts existing `ScannerService` callbacks/results without changing scan behavior.
|
||||
|
||||
- [ ] **Step 4: Migrate ScannerWindow**
|
||||
|
||||
Replace raw thread creation and manual `scanBtn.setDisable` with `AsyncOperationController`. Add `OperationStatusPane`; display the cancel button only while running. Keep history behavior unchanged.
|
||||
|
||||
- [ ] **Step 5: Run focused and full tests**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=ScanRequestTest test`
|
||||
|
||||
Run: `.\mvnw.cmd -q test`
|
||||
|
||||
### Task 5: MyBatis SQL Export Migration
|
||||
|
||||
**Files:**
|
||||
- Create: `src/main/java/com/example/workutils/biz/mybatisexport/application/SqlExportRequest.java`
|
||||
- Create: `src/main/java/com/example/workutils/biz/mybatisexport/application/SqlExportProgress.java`
|
||||
- Create: `src/main/java/com/example/workutils/biz/mybatisexport/application/RunSqlExportUseCase.java`
|
||||
- Modify: `src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java`
|
||||
- Test: `src/test/java/com/example/workutils/biz/mybatisexport/application/SqlExportRequestTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `MyBatisExportService`, `AsyncOperationController`.
|
||||
- Produces: immutable validated request and structured progress.
|
||||
|
||||
- [ ] **Step 1: Write failing filter validation tests**
|
||||
|
||||
Test that ALL requires no value, TABLE requires a nonblank table, and STATEMENT requires a nonblank statement id.
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=SqlExportRequestTest test`
|
||||
|
||||
- [ ] **Step 3: Implement request and use case**
|
||||
|
||||
Adapt current export service without changing CSV format or history persistence.
|
||||
|
||||
- [ ] **Step 4: Migrate MyBatisExportWindow**
|
||||
|
||||
Replace raw thread and manual button toggles with the shared controller. Show progress, current project, and conditional cancel action.
|
||||
|
||||
- [ ] **Step 5: Run focused and full tests**
|
||||
|
||||
Run: `.\mvnw.cmd -q -Dtest=SqlExportRequestTest test`
|
||||
|
||||
Run: `.\mvnw.cmd -q test`
|
||||
|
||||
### Task 6: Phase Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/audit/plugin-ui-code-audit.md` only if implementation differs from the approved design.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: all previous tasks.
|
||||
- Produces: a compiling, tested first phase ready for UI inspection.
|
||||
|
||||
- [ ] **Step 1: Run all tests**
|
||||
|
||||
Run: `.\mvnw.cmd -q test`
|
||||
|
||||
- [ ] **Step 2: Compile production code**
|
||||
|
||||
Run: `.\mvnw.cmd -q -DskipTests compile`
|
||||
|
||||
- [ ] **Step 3: Inspect changes**
|
||||
|
||||
Run: `git diff --check`
|
||||
|
||||
Run: `git status --short`
|
||||
|
||||
- [ ] **Step 4: Launch and visually verify**
|
||||
|
||||
Verify Scanner and MyBatis SQL Export in IDLE, RUNNING, CANCELLING, FAILED, and SUCCEEDED states without triggering destructive operations.
|
||||
Reference in New Issue
Block a user