Commit Graph

33 Commits

Author SHA1 Message Date
mcfn b3884e386b test(network): add phase-a coverage 2026-04-14 16:19:46 +08:00
Junliang Feng 7c166e2b73 fix(tests): allow clippy::too_many_arguments on select diagnostics helper
fix(tests): allow too_many_arguments on assert_select_not_found_diagnostics

clippy::too_many_arguments fires on the 8-parameter test helper.
Adding the targeted allow attribute is the right fix for a test-only
helper that doesn't warrant a struct refactor.
2026-04-14 15:09:34 +08:00
mcfn 0c0f85617b test(select): add diagnostics failure coverage 2026-04-14 14:31:31 +08:00
mcfn 4723cfed44 test(eval): add regression for async trailing line comment
Covers the case where an expression like `await Promise.resolve(1) // note`
previously caused a SyntaxError because the trailing comment commented out
the generated closing tokens of the async wrapper.
2026-04-14 14:24:16 +08:00
mcfn e16219cfd7 test(cli): add eval improvements e2e coverage 2026-04-13 23:13:12 +08:00
mcfn 58fbaf5551 fix(test): log powershell stderr on non-zero exit in chrome_pids_for_profile
Address Codex review: non-zero exit from Get-CimInstance is logged but
stdout is still parsed (some Windows Server versions exit 1 on empty
result sets). Panic is reserved for spawn failure (Err path).
2026-04-12 19:50:44 +08:00
mcfn 638d8c2b94 fix(windows): ignore 2 flaky Windows tests + improve orphan error message
Job Object approach is blocked by CI runner's own Job Object nesting
restrictions (GitHub Actions places all processes in a parent Job).
Per team decision, ignore these 2 tests on Windows as follow-up work.

Changes:
- close_kills_chrome_process: #[cfg_attr(windows, ignore)]
- lifecycle_daemon_sigkill_orphan_recovered: #[cfg_attr(windows, ignore)]
- start.rs: if orphan Chrome still holds user-data-dir lock after kill
  attempts, return CHROME_ORPHAN_STILL_RUNNING with actionable message
  "Kill it manually: taskkill /F /IM chrome.exe" instead of a generic
  CDP_CONNECTION_FAILED timeout

Expected: 528 passed, 0 failed, 8 ignored
2026-04-12 19:05:15 +08:00
mcfn 95223f5915 fix(windows): kill Chrome process tree to prevent helper lingering
On Windows, child.kill() / taskkill /F /PID only terminates the main
Chrome process, leaving renderer/GPU/utility helper processes alive.
These helpers continue holding the user-data-dir lock, causing:

- close_kills_chrome_process: PowerShell finds helper PIDs after close
- lifecycle_daemon_sigkill_orphan_recovered: new Chrome hangs on launch
  because the orphan's helpers still hold the profile directory lock

Fix:
1. chrome_reaper.rs: add taskkill /F /T /PID before child.kill() on
   Windows to kill the entire process tree before the main-process kill.
2. start.rs orphan detection: change /F /PID to /F /T /PID and extend
   wait to 1000ms so helpers are fully gone before launching new Chrome.
3. browser_lifecycle.rs: replace fixed 500ms sleep with a 5s poll loop
   in close_kills_chrome_process to handle WMI lag after process tree kill.
2026-04-12 12:57:18 +08:00
mcfn 94eea61490 fix(windows): resolve daemon restart timeout and Chrome PID detection
- Remove daemon.port after taskkill in restart_daemon_windows() so the
  TCP-based is_pid_alive() check returns false immediately instead of
  timing out for 5 seconds.
- Add daemon.lock to cleanup_stale_files_windows().
- Fix PowerShell -like pattern escaping in find_chrome_pids_for_dir():
  backslash is literal in single-quoted strings, so doubling it caused
  the pattern to never match Windows paths.
2026-04-12 12:29:04 +08:00
mcfn d0cb9a9bb3 fix(windows): resolve remaining 10 Windows e2e test failures
- Replace deprecated `wmic` with PowerShell `Get-CimInstance` in
  `find_chrome_pids_for_dir()` — wmic removed from newer Windows Server
  images (fixes 3 tests)
- Use separate `daemon.lock` for exclusive singleton lock instead of
  locking `daemon.pid` — Windows mandatory byte-range locks prevent
  other processes from reading locked files (fixes 6 tests)
- Trim snapshot content before `starts_with("- ")` check to handle
  potential CRLF/BOM prefix on Windows (fixes 1 test)
2026-04-12 11:47:44 +08:00
mcfn 5c4c5039d1 style(e2e): apply rustfmt to Windows compatibility fixes
Auto-format browser_lifecycle.rs and cloud_mode.rs to pass CI lint check.
2026-04-12 11:23:01 +08:00
mcfn 288af816dd test(e2e): fix 54 Windows e2e test failures for cross-platform compatibility
- cloud_mode.rs: find_chrome_executable() gains #[cfg(windows)] branch
  with PROGRAMFILES/PROGRAMFILES(X86)/LOCALAPPDATA Chrome paths and
  uses `where` instead of `which` to locate Chrome on Windows

- browser_lifecycle.rs: replace Unix-only process management with
  cross-platform helpers
  - Add pid_is_alive() using tasklist on Windows, kill -0 on Unix
  - Add force_kill_pid() using taskkill /F on Windows, kill -9 on Unix
  - Add kill_chrome_for_dir() using taskkill /IM chrome.exe on Windows, pkill on Unix
  - find_chrome_pids_for_dir() gains #[cfg(windows)] wmic branch
  - daemon_shutdown_kills_all_chrome: #[cfg_attr(windows, ignore)]
    (SIGTERM-based graceful shutdown is Unix-only)
  - lifecycle_daemon_sighup_closes_chrome: #[cfg_attr(windows, ignore)]
    (SIGHUP does not exist on Windows)

- interaction.rs: fix upload error-path tests and cursor_position tests
  - Replace hardcoded /tmp/example.txt with std::env::temp_dir() so
    Path::is_absolute() returns true on Windows (fixes INVALID_ARGUMENT
    errors masking the expected SESSION_NOT_FOUND / TAB_NOT_FOUND)
  - cursor_position_json, cursor_position_text, cursor_position_after_click_json:
    #[cfg_attr(windows, ignore)] — mouse-move coordinate tracking is
    unreliable on Windows headless Chrome

Fixes the 54 pre-existing Windows e2e failures discovered after ACT-897.
2026-04-12 11:18:46 +08:00
Junliang Feng 11e71d9d03 feat(daemon): add Windows TCP localhost IPC support (ACT-897) (#508)
* test(daemon): add Windows daemon red coverage (ACT-897)

Add tests and CI configuration for Windows daemon TCP transport:
- config.rs: test_actionbook_home_uses_userprofile_when_home_unset — cross-platform
  red test; fails until USERPROFILE fallback is implemented
- server.rs: port_path() stub (path-only) + #[cfg(all(test,windows))] unit tests for
  is_daemon_running, send_sigterm, is_pid_alive Windows behaviour
- tests/e2e/windows_daemon.rs: 3 e2e tests ignored on non-Windows, run on Windows CI
- .github/workflows/e2e-windows.yml: windows-latest CI job for unit + e2e tests

* feat(daemon): add Windows TCP localhost IPC support (ACT-897)

Unix behaviour is unchanged (UnixListener/UnixStream, flock).
Windows now uses TCP localhost instead of the missing Unix socket:

server.rs:
- run_daemon(): bind TcpListener on 127.0.0.1:0, write ephemeral port to
  daemon.port; use fs2::FileExt for cross-platform exclusive file locking
- is_daemon_running(): TCP connect-probe against daemon.port (Windows)
- send_sigterm(): taskkill /F /PID (Windows)
- is_pid_alive(): delegates to is_daemon_running() TCP probe (Windows)
- handle_connection_inner(): generic AsyncRead+AsyncWrite handler shared by
  both platforms; Unix/Windows handle_connection() call it with the
  appropriate stream type
- port_path(): daemon.port path (used both platforms for path derivation)

client.rs:
- DaemonClient (Windows): TcpStream-based connect/send_action
- auto_start_daemon_windows(): spawns daemon with DETACHED_PROCESS |
  CREATE_NEW_PROCESS_GROUP flags so it survives parent exit
- wait/restart/cleanup helpers mirroring the Unix equivalents

config.rs:
- actionbook_home(): fall back to USERPROFILE when HOME is not set
  (standard Windows home directory convention)

Cargo.toml:
- Add fs2 = "0.4" for cross-platform LockFileEx file locking

* style: apply cargo fmt to windows_daemon tests

* fix: serialize USERPROFILE test with config test_lock to prevent env var race

The test_actionbook_home_uses_userprofile_when_home_unset test manipulates
ACTIONBOOK_HOME and HOME env vars without acquiring the shared test_lock,
causing intermittent failures in other config tests that run concurrently.

* fix: gate Unix-only kill() FFI behind #[cfg(unix)] for Windows build

- start.rs: wrap orphan Chrome kill(pid, 0)/kill(pid, 9) in #[cfg(unix)],
  add #[cfg(windows)] equivalent using taskkill
- chrome_reaper.rs: gate Duration/Instant imports behind #[cfg(unix)]
  since they're only used in the Unix SIGTERM graceful shutdown path

* fix: make drop_session_entry test cross-platform for Windows CI

The test used Unix-only `sleep` and `kill -0` commands. Use `ping` for
a long-lived process on Windows and `tasklist` for liveness checks.

* ci: limit Windows e2e to lifecycle, tab management, and daemon tests

Run only windows_daemon, browser_lifecycle, and tab_management test
modules on Windows CI to avoid timeouts from tests not yet adapted
for Windows. Run sequentially with --test-threads=1.

* ci: narrow Windows e2e to windows_daemon + tab_management

browser_lifecycle tests hang on Windows (close_kills_chrome_process
stuck for 30min). These tests exercise daemon lifecycle edge cases
not yet adapted for Windows. Narrow scope to windows_daemon and
tab_management which cover core IPC and browser operations.

* ci: narrow Windows e2e to windows_daemon tests only

tab_management tests require full Chrome browser interaction and hang
on Windows CI. Only run our Windows-specific daemon IPC tests which
validate the TCP localhost approach without Chrome dependencies.

* fix: avoid pipe-inheritance hang in e2e harness on Windows

On Windows, assert_cmd pipes stdout/stderr. When the CLI spawns a
detached daemon, the daemon inherits those pipe handles via
CreateProcess(bInheritHandles=TRUE). Even after the CLI exits, the
daemon still holds write handles, blocking the test's read_to_end()
forever.

Fix: redirect stdout/stderr to temp files on Windows instead of pipes.
Also make SoloEnv::drop() cross-platform (taskkill on Windows).

* fix: add wait-timeout dev-dependency for Windows e2e harness

The Windows-specific timeout code in the e2e harness uses
wait_timeout::ChildExt directly. It was previously only available
transitively via assert_cmd, which isn't sufficient on Windows.

* fix: add Windows Chrome discovery and fix e2e test subcommand

- find_chrome(): Add Windows paths (Program Files, LOCALAPPDATA) and
  use `where` instead of `which` for PATH lookup on Windows
- Fix session_status_works_over_tcp test: use 'status' subcommand
  (not 'session-status' which doesn't exist)

* fix: use test env's ACTIONBOOK_HOME for daemon.port path assertion

The port file test was checking the global ~/.actionbook/daemon.port
but the test daemon writes to SoloEnv's isolated temp directory.
2026-04-12 10:06:24 +08:00
Junliang Feng f119e54565 feat(browser): add network requests commands (ACT-882)
Add `browser network requests` and `browser network request <id>` commands.

- Per-tab ring buffer (500 cap) stores TrackedRequest with URL, method,
  resource type, status, headers (normalized HashMap), post data, timestamps
- CDP event handlers for Network.requestWillBeSent / Network.responseReceived
- Filters: --filter (URL substring), --type, --method, --status (exact/class/range)
- Response body fetched on-demand via Network.getResponseBody
- Chrome internal URLs filtered from tracking
- 9 unit tests + 5 e2e tests

Closes ACT-882
2026-04-11 01:09:53 +08:00
Junliang Feng 479828d1ea fix(browser): await Promise in eval command via awaitPromise CDP flag
## Summary
- Add `awaitPromise: true` to the `Runtime.evaluate` CDP call in the `eval` command
- Without this flag, evaluating a Promise expression returns the Promise object itself, which serializes to `{}` (no enumerable properties)
- With `awaitPromise: true`, CDP waits for the Promise to resolve and returns the actual value — matching Playwright's behavior
- Add e2e regression test covering `Promise.resolve(42)` and delayed Promise resolution

Cherry-picked from #490 (closed — had stale click.rs/output.rs conflicts with main).

## Test plan
- [x] `eval_json_promise` e2e: `Promise.resolve(42)` returns 42
- [x] `eval_json_promise` e2e: delayed Promise via setTimeout resolves correctly
- [x] `eval_json_number` e2e: sync expressions still work
- [x] CI lint + unit tests green
2026-04-10 17:40:34 +08:00
Junliang Feng 673d21dc81 fix(browser): fix wait-navigation timeout for fast/delayed redirects (ACT-873)
## Summary
- Fix `browser wait navigation` timing out when navigation completes before
  the wait command starts (fast-redirect scenario, ACT-873).
- Add CDP `Page.frameNavigated` event subscription alongside polling for
  reliable in-watch navigation detection.
- Use registry `prev_url` as baseline to detect navigation that occurred
  between commands.
- Add time-based URL stability (300ms) to distinguish final destinations
  from intermediate redirect pages.
- Add 5 unit tests for NavigationDetector and 3 e2e tests covering
  fast-redirect, delayed-redirect, and no-navigation timeout scenarios.

Closes ACT-873
2026-04-10 17:15:22 +08:00
mcfn 6da55f0d94 Merge branch 'main' into feat/multi-click
Resolve version conflicts (take main's 1.1.0), apply cargo fmt.
2026-04-09 13:38:32 +08:00
4bmis f8c3be406b Fix e2e test helper import 2026-04-07 17:10:49 +08:00
4bmis b3498b9625 Support multi-URL browser new-tab 2026-04-07 17:03:12 +08:00
Junliang Feng 90accb0213 test(browser): add batch snapshot e2e coverage
* feat(browser): add batch-snapshot command

Implements browser batch-snapshot (tasks #14, #15):
- Cmd struct with --session, --tabs (1+), and all shared snapshot flags
- execute() loops over tabs, reuses snapshot logic per tab, collects
  results with partial success (SESSION_NOT_FOUND bubbles as Fatal,
  all other per-tab errors become inline error entries)
- Registers in mod.rs, action.rs, cli.rs (BrowserCommands), router.rs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(browser): add batch snapshot e2e coverage

---------

Co-authored-by: cli-roger-test <cli-roger-test@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 16:12:04 +08:00
mcfn 0352417e90 Merge branch 'main' of github.com:actionbook/actionbook into feat/multi-click
# Conflicts:
#	packages/cli-darwin-arm64/package.json
#	packages/cli/Cargo.lock
#	packages/cli/Cargo.toml
#	packages/cli/package.json
2026-04-03 15:18:15 +08:00
mcfn 7bc8644ce7 [packages/cli]feat: add batch-new-tab command for opening multiple tabs at once
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:14:09 +08:00
Junliang Feng 0711c870e7 feat(browser): save snapshot as Playwright-style YAML (#477)
* test(browser): add render_yaml unit tests

* test(browser): extend render_yaml edge coverage

* feat(browser): save snapshot as Playwright-style YAML

Add render_yaml() to snapshot_transform.rs that converts a flat AXNode
list into a nested Playwright-style YAML DSL string. Format rules:
- Container nodes (children or URL): `- role "name" [ref=eN]:`
- Leaf with ref: `- role "name" [ref=eN]`
- Leaf with value (no ref): `- role "name": value`
- Leaf with name only (no ref): `- role: name`
- URL rendered as child: `- /url: <url>`
- [cursor=pointer] added when cursor_info has cursor:pointer hint

Switch build_output() to call render_yaml() instead of render_content(),
and change snapshot file extension from .txt to .yaml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(browser): escape name and value in render_yaml leaf nodes

- Use escaped_name (not node.name) in the inline leaf branch
- Escape node.value with the same rules to prevent injection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(browser): render value inline even when ref is present in render_yaml

Textbox/combobox nodes are interactive and typically have both a ref and
a value. Previously, the ref branch dropped the value entirely. Now the
value is appended inline: `- textbox "Label" [ref=eN]: value`.

Also moves escaped_value computation to the top of the loop so it can be
shared across both the ref and no-ref branches.

Adds unit test for the ref+value path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(browser): update snapshot e2e for yaml artifacts

- assert data.path ends with .yaml
- snapshot_content() validates .yaml extension before reading
- rename assert_content_has_refs to assert_content_is_yaml_snapshot
- add assertions: no legacy `url=` format, at least one YAML container line

Co-Authored-By: cli-roger-test <cli-roger-test@local>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in render_yaml tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: rustfmt formatting and update link URL test for YAML format

Apply rustfmt-required formatting:
- Expand format!() call with 3+ args to multi-line
- Expand assert!(cond, msg) to multi-line
- Expand assert_eq! with long string to multi-line

Update test_build_output_renders_link_urls_from_ax_properties:
- Old format: `url=https://...` inline
- New YAML format: `- link "Docs" [ref=e1]:` + child `- /url: https://...`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix final rustfmt issue in link URL test assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: replace map_or(false) with is_some_and per clippy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: cli-roger-test <cli-roger-test@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:10:56 +08:00
mcfn.feng 6a2b6010fd [packages/cli]fix: increase Chrome startup timeout to 30s and relax snapshot assertion
- Increase DevTools URL discovery timeout from 15s to 30s for CI environments
- Relax snap_selector_flag_limits_subtree assertion to allow ±2 node tolerance
- Increase E2E test workflow timeout to 20 minutes
2026-04-02 23:01:55 +08:00
mcfn.feng c2a2a43de5 [packages/cli]fix: select fixture uses DOM APIs to avoid TrustedHTML error
Chrome 146+ may enforce Trusted Types on certain pages, causing
innerHTML assignments to fail. Replace innerHTML with createElement
+ appendChild in install_select_fixture.
2026-04-02 18:13:32 +08:00
mcfn.feng 5cee89cde8 [packages/cli]fix: type/press key events not reaching correct tab + contenteditable fix
Three issues fixed:

1. Input.dispatchKeyEvent routes to Chrome's active tab, ignoring sessionId.
   - type: add Target.activateTarget before key dispatch loop
   - press: add Target.activateTarget before key dispatch

2. type command used minimal key definitions (just key+text) which didn't
   trigger native text insertion on contenteditable elements.
   - Reuse key_definition() from press module for full CDP key definitions
     (code, windowsVirtualKeyCode, nativeVirtualKeyCode, unmodifiedText)

3. Contenteditable e2e test fixture had element at top:500px, whose center
   (y=531) exceeded headless viewport height (513), causing click to miss.
   - Changed to top:200px

Closes ACT-822
2026-04-02 18:02:23 +08:00
Junliang Feng ead3cbe740 fix(cli): support contenteditable elements in type/fill/focus (#461) 2026-04-02 17:44:26 +08:00
AlexZ a2024fbcb1 [packages/cli]chore: fix formatting in contenteditable test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:41:29 +08:00
AlexZ 7ab8a7c576 [packages/cli]fix: support contenteditable elements in type/fill/focus commands
CDP DOM.focus fails on contenteditable divs (e.g., Lark/Notion/Slate editors)
with "Element is not focusable" (-32000). This change adds a JS .focus()
fallback and click-to-place-cursor for rich text editors.

Changes:
- Add TabContext::focus_element() with JS .focus() fallback when DOM.focus fails
- type command: detect contenteditable and click element center to establish
  cursor position (required by Slate/ProseMirror internal cursor management)
- fill/focus commands: use focus_element() instead of raw DOM.focus
- Add E2E tests for type and focus on contenteditable elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:39:34 +08:00
mcfn.feng 0758c9ef62 [packages/cli]feat: add --session get-or-create and --tab alias for browser commands
browser start --session sid: get-or-create semantics — reuses existing
session if found, creates with that ID if not. Works for both local and
cloud modes. Conflicts with --set-session-id (which always force-creates).

browser new-tab --tab tid: visible alias for --set-tab-id — creates tab
with custom ID, returns TAB_ID_CONFLICT on duplicate.
2026-04-02 16:21:56 +08:00
Junliang Feng d2f9ab93e0 fix: send mouseMoved before mousePressed in dispatch_click (#220)
* [packages/cli]fix: send mouseMoved before mousePressed in dispatch_click (#220)

SPA frameworks (React Router, Vue Router, Next.js) require a prior
mouseMoved event to establish pointer hover state before the click
event synthesizes correctly. Without it, the router's click listener
never fires and the URL remains unchanged (url_changed: false).

Add a single mouseMoved dispatch at (x, y) with button=none before
the existing mousePressed/mouseReleased loop — matches Playwright's
standard click implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [packages/cli]test: fix click_spa_navigation — use hash for pushState in about:blank (#220)

history.pushState with a path-only URL (/spa-destination) throws a
SecurityError in about:blank (null origin). Use a hash fragment
(#spa-destination) instead, which is always allowed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 13:15:06 +08:00
Junliang Feng 66e74cb139 test: add click_spa_navigation E2E test (#220)
[packages/cli]test: add click_spa_navigation E2E test (#220)

Injects a SPA-style fixture (history.pushState on click with
preventDefault) into about:blank and verifies that browser click
triggers the router handler and returns url_changed=true.

Tests the mouseMoved fix — will fail until dispatch_click sends
mouseMoved before mousePressed in click.rs.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 13:08:40 +08:00
mcfn.feng 58545129de [root]refactor: remove actionbook-rs, promote cli-v2 to packages/cli
- Delete packages/actionbook-rs/ (legacy 0.x CLI)
- Delete old packages/cli/ (JS npm wrapper for 0.x)
- Rename packages/cli-v2/ → packages/cli/ (git mv)
- Merge npm wrapper files (bin/actionbook.js, scripts/postinstall.js) into new cli/
- Update package.json: name → @actionbookdev/cli, private → false, add npm distribution fields
- Update sync-versions.js: point Cargo.toml/Cargo.lock sync at packages/cli/
- Update release.yml: build from packages/cli/ instead of packages/actionbook-rs/
- Update test.yml: paths and working-directory from cli-v2 → cli
- Update CLAUDE.md, CONTRIBUTING.md, docs, issue templates
- Delete stale changeset (fix-screenshot-timeout.md)
- Keep 6 platform binary packages unchanged
2026-04-02 00:46:12 +08:00