Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
363 lines
12 KiB
Markdown
363 lines
12 KiB
Markdown
# WS Browser Welcome Frame Compatibility 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:** Make the ws service path tolerate the real sgBrowser welcome banner (`Welcome! You are client #...`) without weakening general ws protocol validation or changing pipe behavior.
|
|
|
|
**Architecture:** Keep the shared `WsBrowserBackend` strict and implement the compatibility shim only in `ServiceBrowserWsClient`, which is already the real-browser adapter for the ws service path. Add one positive red test for the known welcome frame and one negative red test proving non-matching first text frames still fail as protocol errors, then make the minimal stateful change in `src/service/server.rs` and verify ws + pipe regressions.
|
|
|
|
**Tech Stack:** Rust 2021, tungstenite websocket client/server, existing `WsBrowserBackend`, existing `ServiceBrowserWsClient`, existing Rust unit/integration test suite.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
### Existing files to modify
|
|
|
|
- Modify: `src/service/server.rs`
|
|
- Add the one-time per-connection welcome-skip state to `ServiceBrowserWsClient`
|
|
- Add the minimal helper(s) for detecting and discarding the first known welcome frame
|
|
- Add focused service-adapter unit tests in the existing `#[cfg(test)]` module
|
|
- Reuse: `src/browser/ws_backend.rs`
|
|
- Do not change protocol parsing rules; only verify behavior remains strict for all non-service callers
|
|
- Reuse: `tests/service_task_flow_test.rs`
|
|
- Re-run to confirm the ws service path still reaches the browser websocket after the service-side shim
|
|
- Reuse: `tests/browser_ws_backend_test.rs`
|
|
- Re-run to prove the shared backend semantics remain unchanged
|
|
|
|
### Files deliberately not changed
|
|
|
|
- `src/browser/ws_backend.rs`
|
|
- `src/browser/ws_protocol.rs`
|
|
- `src/agent/task_runner.rs`
|
|
- `src/compat/runtime.rs`
|
|
- `src/compat/orchestration.rs`
|
|
- `src/compat/workflow_executor.rs`
|
|
- `src/lib.rs`
|
|
|
|
The design explicitly keeps the welcome-banner workaround out of the shared backend and out of the pipe path.
|
|
|
|
---
|
|
|
|
## Task 1: Reproduce the real welcome-frame failure with focused unit tests
|
|
|
|
**Files:**
|
|
- Modify: `src/service/server.rs`
|
|
|
|
- [ ] **Step 1: Add the positive failing test for the known welcome frame**
|
|
|
|
In the existing `#[cfg(test)] mod tests` inside `src/service/server.rs`, add one focused test next to the current ws adapter tests.
|
|
|
|
Test shape:
|
|
|
|
```rust
|
|
#[test]
|
|
fn future_server_side_ws_native_adapter_skips_initial_known_welcome_frame() {
|
|
// fake server sends:
|
|
// 1. "Welcome! You are client #1"
|
|
// 2. "0"
|
|
// backend.invoke(Action::Navigate, ...) should succeed
|
|
}
|
|
```
|
|
|
|
Required assertions:
|
|
- the fake websocket server accepts one connection
|
|
- it sends the welcome banner first, then the numeric success status
|
|
- `WsBrowserBackend.invoke(Action::Navigate, ...)` returns `Ok(CommandOutput { success: true, .. })`
|
|
|
|
- [ ] **Step 2: Run only the positive new test and watch it fail**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture
|
|
```
|
|
|
|
Expected: FAIL with a protocol error containing `invalid browser status frame: Welcome! You are client #1`.
|
|
|
|
- [ ] **Step 3: Add the negative failing test for arbitrary first text**
|
|
|
|
In the same `#[cfg(test)]` module, add one negative test proving we do **not** silently skip arbitrary first text frames.
|
|
|
|
Test shape:
|
|
|
|
```rust
|
|
#[test]
|
|
fn future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame() {
|
|
// fake server sends:
|
|
// 1. "Hello from server"
|
|
// assert invoke(...) fails as PipeError::Protocol(...)
|
|
}
|
|
```
|
|
|
|
Required assertions:
|
|
- the first frame is a non-matching text frame such as `Hello from server`
|
|
- `invoke(...)` fails
|
|
- the failure remains a protocol error rather than success or timeout
|
|
|
|
- [ ] **Step 4: Run only the negative new test and verify the current behavior is already strict**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture
|
|
```
|
|
|
|
Expected: PASS, proving the current implementation already treats arbitrary first text as a protocol error. Keep that assertion in place before any production change.
|
|
|
|
- [ ] **Step 5: Confirm the TDD gate before implementation**
|
|
|
|
Do not implement production code before both tests exist and the positive test has failed on current behavior.
|
|
|
|
---
|
|
|
|
## Task 2: Add the minimal per-connection welcome-skip state in the service adapter
|
|
|
|
**Files:**
|
|
- Modify: `src/service/server.rs`
|
|
|
|
- [ ] **Step 1: Add one-time per-connection state to `ServiceBrowserWsClient`**
|
|
|
|
Extend `ServiceBrowserWsClient` with one extra state field that tracks whether the initial welcome candidate has already been consumed for the current websocket connection.
|
|
|
|
Allowed shape:
|
|
|
|
```rust
|
|
struct ServiceBrowserWsClient {
|
|
browser_ws_url: String,
|
|
browser_socket: Mutex<Option<WebSocket<MaybeTlsStream<TcpStream>>>>,
|
|
initial_text_frame_checked: Mutex<bool>,
|
|
}
|
|
```
|
|
|
|
or an equally small equivalent.
|
|
|
|
Rules:
|
|
- state is per connection, not per request
|
|
- state must survive multiple `invoke(...)` calls while reusing the same socket
|
|
- do not add broader protocol state machines
|
|
|
|
- [ ] **Step 2: Add a narrow welcome-frame matcher**
|
|
|
|
In `src/service/server.rs`, add one small helper that recognizes only the known banner prefix:
|
|
|
|
```rust
|
|
fn is_known_welcome_frame(frame: &str) -> bool {
|
|
frame.starts_with("Welcome! You are client #")
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
- no regex needed
|
|
- no generic “ignore arbitrary text” logic
|
|
- keep the matcher local to `src/service/server.rs`
|
|
|
|
- [ ] **Step 3: Update `recv_text_timeout(...)` to skip at most one initial known banner**
|
|
|
|
Modify `impl WsClient for ServiceBrowserWsClient` so that the first text frame received after connection establishment is handled like this:
|
|
|
|
1. read the next text frame
|
|
2. if the initial-frame state is still false:
|
|
- mark the first-frame check as consumed
|
|
- if the frame matches `is_known_welcome_frame(...)`, read the next frame and return that next frame instead
|
|
3. otherwise, return the frame unchanged
|
|
|
|
Rules:
|
|
- skip only once per connection
|
|
- do not loop indefinitely over multiple text frames
|
|
- do not swallow unknown first text frames
|
|
- do not change timeout / close / reset / connect-failure behavior
|
|
|
|
- [ ] **Step 4: Reset the one-time state when a fresh socket is created**
|
|
|
|
When `with_socket(...)` establishes a brand-new websocket connection, ensure the one-time banner-check state is reset so a new connection can tolerate its own first welcome frame.
|
|
|
|
- [ ] **Step 5: Add one reconnect regression in the service adapter tests**
|
|
|
|
Add one focused test proving the welcome skip resets on a fresh connection after socket close/reset.
|
|
|
|
Test shape:
|
|
|
|
```rust
|
|
#[test]
|
|
fn future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect() {
|
|
// first connection closes after use
|
|
// second fresh connection sends the same welcome banner again
|
|
// both invocations succeed
|
|
}
|
|
```
|
|
|
|
Required assertion:
|
|
- the one-time skip is per connection, not global for the client instance
|
|
|
|
- [ ] **Step 6: Run the positive new test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 7: Run the negative new test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture
|
|
```
|
|
|
|
Expected: PASS, proving unknown first text is still treated as a protocol error.
|
|
|
|
- [ ] **Step 8: Run the reconnect regression**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect -- --nocapture
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 9: Run the full service adapter unit group**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test service::server::tests -- --nocapture
|
|
```
|
|
|
|
Expected: PASS, including the existing tests for:
|
|
- status `0` success
|
|
- connect failure => `PipeError::Protocol("browser websocket connect failed: ...")`
|
|
- disconnect/reset => `PipeError::PipeClosed`
|
|
- callback timeout => `PipeError::Timeout`
|
|
- new known-welcome success path
|
|
- new unknown-first-frame strictness path
|
|
- new reconnect reset behavior
|
|
|
|
---
|
|
|
|
## Task 3: Verify the shared backend stayed strict and the ws service path still works
|
|
|
|
**Files:**
|
|
- Reuse: `tests/browser_ws_backend_test.rs`
|
|
- Reuse: `tests/service_task_flow_test.rs`
|
|
- Reuse: `src/browser/ws_backend.rs`
|
|
|
|
- [ ] **Step 1: Re-run the shared ws backend tests unchanged**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test --test browser_ws_backend_test -- --nocapture
|
|
```
|
|
|
|
Expected: PASS. This proves `WsBrowserBackend` semantics remain unchanged for its existing deterministic callers.
|
|
|
|
- [ ] **Step 2: Re-run the service task-flow regression**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test --test service_task_flow_test -- --nocapture
|
|
```
|
|
|
|
Expected: PASS, including the auth-regression test that proves the ws service path reaches the browser websocket and no longer emits `invalid hmac seed: session key must not be empty`.
|
|
|
|
- [ ] **Step 3: Re-run the ws-focused mixed verification**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
---
|
|
|
|
## Task 4: Re-run the real manual smoke that originally failed
|
|
|
|
**Files:**
|
|
- Reuse only: no code changes unless a fresh reproducer proves another bug
|
|
|
|
- [ ] **Step 1: Confirm real browser websocket reachability**
|
|
|
|
Run a reachability check for `ws://127.0.0.1:12345` (or the configured `browserWsUrl`) before starting smoke.
|
|
|
|
Expected: reachable.
|
|
|
|
- [ ] **Step 2: Start the real ws service**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cargo run --bin sg_claw -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
|
|
```
|
|
|
|
Expected: the service prints:
|
|
- `sg_claw ready: ...`
|
|
- the resolved `service_ws_listen_addr`
|
|
- the configured `browser_ws_url`
|
|
|
|
- [ ] **Step 3: Re-run the original failing manual smoke**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
printf '打开知乎热榜并读取页面主区域文本\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
|
|
```
|
|
|
|
Expected:
|
|
- no `invalid browser status frame: Welcome! You are client #1`
|
|
- browser actions proceed past the first status frame
|
|
- if the browser later fails for another reason, capture that new reason exactly
|
|
|
|
- [ ] **Step 4: Re-run the old Zhihu export task smoke**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
printf '读取知乎热榜数据,并导出 excel 文件\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
|
|
```
|
|
|
|
Expected:
|
|
- no `invalid browser status frame: Welcome! You are client #1`
|
|
- the task reaches the real browser action path beyond connection banner handling
|
|
|
|
- [ ] **Step 5: Stop and debug if a new real-browser issue appears**
|
|
|
|
If smoke now fails for a different reason, do not piggyback a second fix into this slice without:
|
|
- capturing the exact new output
|
|
- writing a new focused spec/plan if the issue is materially different
|
|
|
|
---
|
|
|
|
## Verification Checklist
|
|
|
|
### Service adapter unit tests
|
|
|
|
```bash
|
|
cargo test service::server::tests -- --nocapture
|
|
```
|
|
|
|
Expected: all service-side ws adapter tests pass, including the new welcome-frame positive/negative cases and reconnect reset case.
|
|
|
|
### Shared ws backend + ws service regressions
|
|
|
|
```bash
|
|
cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
### Real smoke verification
|
|
|
|
- `browserWsUrl` reachable
|
|
- `sg_claw` starts with real config
|
|
- `sg_claw_client` no longer fails on `Welcome! You are client #...`
|
|
- Zhihu minimal read task gets past the first status frame
|
|
- Zhihu export task gets past the first status frame
|