# 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
