Skip to content

test#33

Merged
myluke merged 27 commits into
EasyMetaAu:mainfrom
jackwener:main
Jul 5, 2026
Merged

test#33
myluke merged 27 commits into
EasyMetaAu:mainfrom
jackwener:main

Conversation

@myluke

@myluke myluke commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Related issue:

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Checklist

  • I ran the checks relevant to this PR
  • I updated tests or docs if needed
  • I included output or screenshots when useful

Documentation (if adding/modifying an adapter)

  • Added doc page under docs/adapters/ (if new adapter)
  • Updated docs/adapters/index.md table (if new adapter)
  • Updated sidebar in docs/.vitepress/config.mts (if new adapter)
  • Updated README.md / README.zh-CN.md when command discoverability changed
  • Used positional args for the command's primary subject unless a named flag is clearly better
  • Normalized expected adapter failures to CliError subclasses instead of raw Error

Screenshots / Output

jackwener and others added 27 commits July 1, 2026 00:59
…rs (#1979)

Two unrelated try/catch blocks were eating errors with no observable
signal, both reachable from production paths:

1) `src/pipeline/template.ts:215` `sanitizeContext` (the JSON round-trip
   that severs prototype chains before handing pipeline context to the VM
   sandbox) caught any `JSON.stringify` failure and returned `{}`. The
   most common cause is a BigInt anywhere in `data` / `args` / `item` /
   `root` (e.g. GraphQL 64-bit IDs). After collapse, every template
   expression referencing that branch resolved to `undefined`, producing
   silent column-drops downstream with no warning.

   Fix:
   - Add a JSON.stringify replacer that coerces BigInt to string, so the
     common BigInt-in-context case survives the sandbox copy.
   - For everything else (circular references, Symbol, etc.), the
     fallback is still `{}` but now log.warn so the failure shows up in
     `~/.opencli` logs and doctor output instead of silently producing
     blank rows.

2) `src/daemon.ts:445` the WS message handler from the extension caught
   `JSON.parse` failures and ran the `// Ignore malformed messages`
   comment. A malformed message presents downstream as a generic command
   timeout (`pending` never resolves), so the actual protocol drift /
   version skew between daemon and extension never surfaced in the log.

   Fix: log.warn the parse error plus the first 200 chars of the offending
   frame so the root cause is visible during triage.

Both changes are observability-only: no successful path changes behavior;
only previously-silent failure paths get a log line, plus BigInt now
serializes to a string instead of nuking its containing branch.

Tests:
- `src/pipeline/template.test.ts`: two new cases covering the BigInt
  preservation path (forces the VM sandbox via `String(args.id)`, not the
  resolvePath fast path) and the circular-ref no-crash invariant.
- daemon WS handler change is log-only; existing daemon tests cover the
  message dispatch path.
* Add Mercury reimbursement helpers

* fix(mercury): fail closed reimbursement drafts

* fix(mercury): harden reimbursement draft safety checks

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
…1980)

reconcileTargetLeaseRegistry computed each lease's remaining lifetime
(stored.idleDeadlineAt - now) but used it only to decide expire-vs-keep;
the keep branch called resetWindowIdleTimer(leaseKey), which always
schedules a fresh FULL idle timeout, discarding the remaining time.

Under MV3 service-worker churn (the SW is evicted/restarted routinely),
a lease's idle deadline was refreshed to the full timeout on every
restart, so an owned adapter tab/placeholder that should auto-release
could linger far past its idle timeout — effectively indefinitely.

Add an optional remainingMs override to resetWindowIdleTimer and pass the
computed remaining from reconcile, clamped to [0, timeout]. Adds a
regression test (5s-remaining lease must schedule a ~5s alarm, not 30s);
reverse-validated.
…ach (#1978)

A forced detach inside ensureAttached's re-attach loop fires
chrome.debugger.onDetach, whose handler deletes the tab's armed
networkCaptures state; the detach also disables the CDP Network domain,
and re-attach only re-issued Runtime.enable. So any non-navigate command
that triggered a re-attach (a stale-attach health-check failure during SPA
navigation, or third-party debugger interference) left
network-capture-read returning [] even though requests fired — the
recorded "0 captures" symptom.

Snapshot the capture before the re-attach and, on success, re-enable the
Network domain and restore the accumulated state (restored last so it
wins over the onDetach handler's delete). Adds a regression test that
fails without the restore.
* feat(gemini): add model and thinking selection

Co-authored-by: multica-agent <github@multica.ai>

* fix(gemini): harden model selection contracts

---------

Co-authored-by: coder-SOTA-hm <coder-SOTA-hm@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: jackwener <jakevingoo@gmail.com>
Three independent CDP-layer correctness fixes:

1. Redirect wiped the captured POST body. On an HTTP 30x, CDP re-fires
   Network.requestWillBeSent with the SAME requestId (the prior hop in
   `redirectResponse`) for the redirect target — usually a GET with no
   postData. The handler overwrote the entry's request-body fields
   unconditionally, destroying the original POST body. Now the body is
   only populated on the initial send (guarded on `!redirectResponse`).

2. responseReceived created orphan entries. If readNetworkCapture()
   drained the entries (clearing requestToIndex) while a request was in
   flight, the later Network.responseReceived ran getOrCreate and made a
   new half-entry with a defaulted method ('GET') and no request data.
   Now it is lookup-only, mirroring loadingFinished.

3. evaluateInFrame had no retry on a stale cached context. A navigated/
   reloaded frame invalidates its cached execution-context id, but the
   executionContextDestroyed event may not be processed yet, so
   Runtime.evaluate rejects with "Cannot find context with specified id".
   Now that rejection drops the stale id and falls through to the
   frame-target path (mirrors evaluate()'s re-resolution); genuine page
   errors still propagate.

Tests: redirect-body preservation, orphan-entry prevention, and
stale-context fallback — all reverse-validated. cdp suite 15/15, tsc
clean, extension/dist rebuilt.
* Fix ChatGPT intelligence level selection

* Document ChatGPT model adapter usage in browser skill

* fix(chatgpt): verify model config selection

* fix(chatgpt): classify model preference api drift

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
* Add ChatGPT Deep Research result extraction

* fix(chatgpt): bind deep research results to requested conversation

* fix(chatgpt): preserve deep research payload failures

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
…DP timeouts (#2067)

* fix(browser): end-to-end command deadlines, safe transport retries, CDP timeouts

Three connectivity/stability fixes that share one root cause: the timeout
and retry contracts between CLI, daemon, and extension were disconnected.

1. Plumb one command deadline through all three layers. The client HTTP
   request was hardcoded to 30s while the daemon default was 120s and no
   caller ever set body.timeout — every command slower than 30s died with
   an opaque client-side AbortError while still running in the browser.
   Now the transport computes an effective timeout (user --timeout via
   setDaemonCommandTimeoutSeconds, or timeoutMs + margin for extension-side
   waits like wait-download), sends it as body.timeout, and aborts the HTTP
   request only after the daemon's structured 408 should have arrived.
   The daemon timer now rejects with the command_result_unknown contract
   instead of a bare Error the client cannot classify.

2. Stop replaying possibly-dispatched commands on fetch TypeError. Any
   `TypeError: fetch failed` used to trigger ensure + resend with a fresh
   id, bypassing the daemon's duplicate-id guard — a daemon crash mid-click
   could double-submit a form. Only pre-connect failures (ECONNREFUSED and
   friends, checked via err.cause) are retried now; post-connect drops
   surface as command_result_unknown per the existing contract.

3. Give chrome.debugger commands a real deadline. The extension's CDP
   calls had none (sendCommandInFrameTarget declared _timeoutMs and never
   used it), so a page-blocking native dialog (alert/confirm/beforeunload)
   hung Runtime.evaluate forever and wedged every later command on the tab.
   All sendCommand calls now race a timer; exec/cdp commands derive their
   deadline from the transport's body.timeout, undercut by 5s so the more
   specific extension error beats the daemon's generic timer.

* fix(browser): swallow post-timeout CDP rejections; short deadline for doctor probe

Two issues found in self-review of the deadline work:

- sendDebuggerCommand raced the command promise against a timer but left
  the losing command promise unobserved — if it rejected later (debugger
  detach on tab close long after the timeout fired) it surfaced as an
  unhandled rejection in the service worker. Swallow it on a side branch.

- doctor's checkConnectivity probe inherited the default 120s transport
  deadline, so a daemon that accepts requests but never answers made
  doctor hang for 2 minutes before reporting FAIL. A health probe wants
  the opposite: shrink the per-command deadline to the probe budget (8s)
  and restore it afterwards.

* fix(extension): honor derived CDP deadline in evaluateInFrame warm-up; pin deadline tests

From adversarial review of the deadline work:

- evaluateInFrame's Runtime.enable warm-up on the frame-target fallback
  path dropped the caller's derived deadline and fell back to the 60s
  default — a blocked iframe could burn the whole daemon budget in the
  warm-up alone, so the daemon's generic 408 always beat the extension's
  specific error on the cross-frame path.

- Two untested links in the deadline chain are now pinned by regression
  tests: the client HTTP abort fires exactly at timeout*1000 + 10s (not
  before the daemon's structured 408 can arrive), and handleExec derives
  115s from a 120s transport timeout (10s floor for tiny timeouts).
…s, absolute deadlines (#2070)

* refactor(transport): exactly-once command transport — journal, waiters, absolute deadlines

Rebuilds the CLI→daemon→extension command transport around one principle:
exactly-once = at-least-once retry + an idempotent executor. This replaces
the accumulated per-layer compensation (three client retry flags, cause-code
walking, duplicate-id 409s, an inner extension retry loop, a phased
reconnect state machine) with three small primitives:

1. Command journal (extension/src/journal.ts, chrome.storage.session).
   Every command id executes exactly once: duplicates attach to the
   in-flight promise, completed ids replay the recorded result, and ids
   whose worker died mid-execution report `command_lost` honestly.
   storage.session survives service-worker restarts and clears on browser
   exit — precisely the lifetime a retry cares about.

2. Stable ids + daemon waiters. Transport retries keep the SAME id; the
   daemon attaches duplicate ids to the pending command instead of 409ing.
   With the executor idempotent, every transport failure becomes safely
   retryable (gated on extension >= 1.0.22; legacy extensions keep the old
   conservative pre-connect-only retry). Semantic retries (attach_failed /
   tab_gone — failures BEFORE any page code ran) are the only place a new
   id is minted, once.

3. Absolute deadline (`deadlineAt`, epoch ms) instead of per-hop durations.
   Same machine, one clock: every layer computes remaining = deadlineAt -
   now, so daemon queueing and service-worker wake latency no longer
   silently shrink the innermost budget or invert the layering.

Error classification now happens once, at the failure site: the extension
tags results with machine-readable codes (attach_failed, tab_gone,
target_navigated, detached_mid_command, cdp_timeout) and errors.ts prefers
codes over the legacy message-pattern tables (kept only for old
extensions). detached_mid_command / cdp_timeout are now correctly
non-retryable — they die mid-execution, so a blind re-run could
double-apply a write.

Deletions and stability fixes riding the same contract:
- extension evaluate()'s inner retry loop (the client owns semantic
  retries now); the fast/slow reconnect window + notifyDaemonReachable
  rescheduling (plain exponential backoff with jitter, reset on success);
  the three parallel session-override Maps (one record per lease).
- WS application-level keepalive ({type:'ping'} every 20s): Chrome 116+
  only extends the service worker's lifetime on WS *activity*, so an idle
  socket lived on a knife-edge between the 30s idle kill and the 30s
  keepalive alarm.
- idle-lease release is deferred while a command is executing on the
  lease (refcount) — a 30s idle timer can no longer tear the tab down
  mid-command; completion re-arms the timer.
- daemon shutdown flushes structured 503s to waiting clients before
  exiting instead of process.exit() killing the queued responses.
- results are delivered on the freshest open socket after a reconnect
  instead of being dropped when the executing socket was superseded.

* fix(transport): gate daemon_shutting_down resend on journal capability; bound ensure by deadline

From adversarial review of the transport-v2 work:

- daemon_shutting_down was resent with the same id regardless of the
  extension's journal capability. The daemon fires it for DISPATCHED
  commands too, so on a pre-journal extension the resend re-executes a
  write. The daemon now returns the pre-dispatch contract for commands
  that never reached the extension (safe to resend anywhere) and
  daemon_shutting_down only for dispatched ones; the client resends
  those only when the extension journals ids, else surfaces
  command_result_unknown.

- ensureBridge's connect wait was a fixed 45s regardless of the
  command's remaining budget — repeated daemon failures could stretch a
  30s --timeout command past two minutes. The wait is now clamped to
  the remaining deadline.
…layer (#2017)

The "Create a file like clis/<site>/<command>.ts" example predates #928, which
converted the entire adapter layer from TypeScript to JavaScript. The repo now
ships 0 .ts and 1259 .js built-in adapters, so a contributor following the doc
creates a file in the wrong format. Update the example to JavaScript (keeping a
pointer to the still-supported TypeScript path), and fix the adapter test
command, which pointed at src/ rather than the adapter's own clis/ test file.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(hltv): add HLTV adapters

* fix(hltv): harden row identity contracts

---------

Co-authored-by: Adong <jhdong8855@gmail.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
#2074)

OpenCLIApp injects OPENCLI_DAEMON_PORT=19825 into the environment of every
CLI it manages. The CLI hard-rejected the variable regardless of its value,
so fresh OpenCLIApp installs failed on every command — including --version
and doctor — with EX_CONFIG, and the daemon never started (#2068, #2072).

A value equal to the default port carries no configuration at all; only a
NON-default value is a genuine misconfiguration worth failing on. All three
rejection points (main.ts entry, daemon startup, transport assert) now share
isIgnorableDaemonPortEnv().

Also fixes the README multi-profile example that was missing the required
browser <session> positional (#1893).
…t on signal death (#2075)

npm-installed CLIs on Windows are .cmd shims: `where` finds them (so the
installed-check passes), but Node refuses to spawn them directly since the
CVE-2024-27980 hardening — spawnSync fails with EINVAL/ENOENT and every
CLI-hub passthrough to an npm-installed tool breaks (#1958). On that
specific failure the passthrough now retries through the shell with each
token quoted for cmd.exe.

Also: a child killed by a signal left status null and opencli exited 0,
reporting success to the calling shell/agent; signal death now maps to a
non-zero exit code.
…2073)

* fix(browser): stale default profile must not veto live connections

A persisted default profile (browser-profiles.json defaultContextId) has a
lifetime that routinely exceeds the extension instance it names — reinstalling
the extension or resetting Chrome regenerates the contextId. Since #1235 that
stale preference was folded together with --profile/OPENCLI_PROFILE into one
hard contextId on every command, so the daemon refused to serve it
(profile_disconnected) even when exactly one live profile was connected,
breaking the documented promise "with only one connected profile, OpenCLI
uses it automatically" — and making doctor hang waiting for a dead profile.

First-principles fix: distinguish REQUIREMENT from PREFERENCE end to end and
let the component that knows live state arbitrate.

- profile.ts resolves a ProfileSelection tagged 'explicit' (--profile arg,
  OPENCLI_PROFILE env — fail loud when offline) or 'preferred' (config
  default); profileRouteParams() maps it to the wire fields.
- New wire field preferredContextId (both protocol copies); contextId keeps
  its strict semantics. Old daemons ignore the new field, which degrades to
  the no-contextId single-profile auto-use — exactly the documented behavior.
- The daemon arbitrates via a pure, tested resolveProfileRoute(): requested →
  strict; preferred → use when connected, fall back to the only connected
  profile when not (logged once per stale id), ask with a stale-default hint
  when multiple are connected.
- bridge/ensure only pin readiness to a profile for explicit requirements —
  a stale preference no longer makes connect()/doctor wait for a dead
  profile.
- doctor surfaces the stale default with the fallback status and the
  recovery command (opencli profile use).

* fix(cli): key saved-tab scope by the selected profile in getPageScope

From adversarial review: getBrowserPage computed the target scope from the
profile SELECTION (explicit or preferred), but getPageScope read only the
explicit Page.contextId — so with a config-default profile the remembered
tab was saved under "<session>" and looked up under "<contextId>:<session>",
silently forgetting the selection on every command. Both sites now key the
scope by the selected profile.
Use stable Chrome for Testing for headed E2E and keep e2e child-process timeouts below Vitest's framework timeout.
…2047) (#2055)

* fix(weibo): resolve uid before the full auth probe to avoid HTTP 400 (#2047)

`auth status --site weibo --full` failed with `HTTP 400 from /ajax/profile/info`
even on a logged-in session: verifyWeiboIdentity fetched the bare
/ajax/profile/info, which the current Weibo web app rejects without a uid.
`weibo me` already works because it resolves the current uid first.

Mirror that path: call getSelfUid(page) (which throws AuthRequiredError when no
logged-in uid resolves), then probe /ajax/profile/info?uid=<uid>. Extract the
probe into buildWeiboIdentityProbe(uid) and add clis/weibo/auth.test.js.

* fix(weibo): unwrap auth identity probes

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
Pin headed E2E macOS coverage to macos-15 while macos-latest migrates to macOS 26.
… article (#2001) (#2026)

* fix(twitter): match localized delete menu and poll for late-hydrating article (#2001)

twitter delete failed on a Simplified-Chinese X detail page: (1) the More
caret was matched by aria-label === 'More', which X localizes (zh-Hans 更多),
and (2) findTargetArticle() ran before the article's self-referential
/status/<id> link hydrated on slow networks. Inside buildDeleteScript:

- Prefer the language-agnostic [data-testid="caret"] (scoped to the matched
  article), falling back to a multilingual /^(More|更多)/ aria-label match.
- Poll findTargetArticle() for ~5s (20 x 250ms) before giving up.
- Broaden the Delete menu item to Delete/删除 and exclude the Lists item in
  both languages (List/列表).

* fix(twitter): harden delete menu result handling

* fix(twitter): scope delete menu items to opened menu

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
Keep Linux AX smoke release-blocking while allowing hosted macOS to skip when command-line unpacked extension startup is unavailable.
Bump @jackwener/opencli to 1.8.6.
…rt contract E2E (#2081)

The AX real-Chrome smoke's contract is "the extension bridge works in a
real Chrome", not "a window appears" — Linux already admitted that by
faking a display with xvfb. Hosted macOS runners fail headed Chrome at
the OS level (child processes lose the Mach port rendezvous with the
browser process because CI jobs run outside a regular Aqua session), and
PR #2079 papered over that by skipping the platform. Running the smoke
with --headless=new removes the display/GUI-session dependency entirely:
new headless is a full browser (MV3 service worker, chrome.debugger,
--load-extension), verified locally against the same Chrome for Testing
build CI uses. The smoke is release-blocking on every OS again; headed
mode stays available locally via OPENCLI_E2E_HEADED=1.

New daemon-transport contract E2E: the real dist/src/daemon.js process
with a scripted fake extension, pinning the cross-layer contracts end to
end — duplicate ids attach to the pending command without re-dispatch,
deadlines produce a structured 408 command_result_unknown, extension
death after dispatch yields command_result_unknown, a stale
preferredContextId falls back to the only connected profile while an
explicit contextId fails loud, and graceful shutdown flushes structured
daemon_shutting_down 503s with exit code 0. No browser required; runs in
the fixed-port project on every OS.
#2082)

#2081 tried to make the real-browser AX smoke run everywhere via
--headless=new. That fixed headed macOS's Mach-port crash but exposed a
second environment property: hosted runners don't reliably start the MV3
extension service worker in headless, so main went red on macOS anyway.
Chasing headless reliability across runner images is the wrong axis.

First principles: gate each check on the environment that can run it
deterministically, and make sure every OS has a real blocking gate.

- Real-browser extension smoke (AX tree + cross-frame CDP): Linux under
  xvfb is the one hosted environment where a real Chrome reliably starts
  an MV3 extension. It runs there, headed, release-blocking. It is not
  scheduled on macOS/Windows because neither can run it deterministically
  (headed macOS crashes on Mach port rendezvous outside an Aqua session;
  headless connects no SW).
- Daemon transport contracts: no browser, deterministic, so they run
  blocking on all three OSes including Windows — macOS/Windows now have a
  real gate over the exact layer our recent bugs lived in (#2067/#2070/
  #2073), not a skipped test that proves nothing.
- Windows joins the matrix for the first time (transport gate); the
  setup-chrome action is skipped there since it hangs on the MSI path and
  Windows needs no browser.

Local Chrome launch stays headed by default; OPENCLI_E2E_HEADLESS=1 opts
into headless for display-less local runs.
@myluke myluke merged commit 3e2ea59 into EasyMetaAu:main Jul 5, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants