Commit Graph
67 Commits
Author SHA1 Message Date
Teknium 3ba2b0787e fix(release): Windows x64 MSIX smoke runs on a GitHub-hosted runner and reports why a launch dies
Every Windows x64 MSIX chat smoke since the Blacksmith migration (91fae5f8cb)
failed: first on the GitHub-hosted guard, and since that guard was removed
(272adc7b6f) with Playwright's "Process failed to launch!", on Electron 40 and
44 alike. The same builds' arm64 MSIX, on a GitHub-hosted runner, chats; the
last green x64 smoke (Oct 14) ran on windows-latest-32-core. Move the x64 leg
back there, as f1b290e773 did for the install/update GUI legs.

The failure itself carried no evidence: when the app exits before its debugger
line, Playwright's Electron.launch leaves sibling stderr-watcher rejections
unhandled, Node dies on them, and the smoke's catch never writes its receipt or
Playwright's call log. The driver now logs those rejections (still failing the
run), so the receipt records the app's exit code and stderr, and the Windows
smoke saves Application-log errors from the run on failure.
2026-10-22 18:59:52 -07:00
Teknium 48b5c3283b test(install-e2e): the canonical gateway proves its commit where its cwd is unreadable, and the update window looks for it
Builds on #142306 (Maciej, @c2h5oh). That PR taught the desktop smoke that the
listener Desktop dials can be the canonical gateway named in the home's
gateway_state.json, and proves that gateway's tree by the cwd `gateway ensure`
starts it in. Install-e2e run 39567335733 (main + this branch) hit two cases it
does not reach:

- Windows: CIM exposes neither the cwd nor the environment of another process.
  Every desktop checkpoint that reached the (now startable) detached gateway
  failed with "Source backend listener imports a different source tree
  (cwd=(unreadable), HERMES_PYTHON_SRC_ROOT=(unset) ...)". The gateway now
  vouches for itself there. localBackendProcess keeps the code_sha the gateway
  stamped into its own gateway_state.json (gateway/status.py). assertBackendOrigin
  accepts the app's reported root for it only when that code_sha is the
  installed commit, which both callers already read. A readable cwd is still
  the evidence everywhere else, and the child/ledger rules are unchanged.
- All OSes, HEAD -> NEXT app-driven update legs: update-window-chat.mjs called
  localBackendProcess without a home, so it never looked for the canonical
  gateway ("Expected one Desktop-owned, registered host or canonical gateway
  backend listener on port N; found 0"). It now passes the app's own
  hermesHome and the installed commit.
2026-10-21 03:04:36 -07:00
Teknium a4c13ee015 test(install-e2e): a later step's mock rebinds the port the surviving gateway froze
An install/update journey runs its phases in separate CI steps. Each step's
mock provider dies with its step, but the gateway Desktop started is detached
on purpose and survives into the next step with the provider URL it read at
boot. The next step started a mock on a fresh port and rewrote config.yaml/.env,
so the surviving gateway kept dialing the dead port: every HEAD -> NEXT desktop
leg on macOS and Windows failed with "The mock must receive this checkpoint
prompt after the send" (run 39523017025: agent.log shows the gateway calling
127.0.0.1:61022 while the new mock listened on 53982; macOS 49478 vs 49533).
Linux passed only because its mock happened to survive between steps.

mock-provider.mjs now takes the prior step's URL and rebinds that port when it
is free (any free port otherwise), and both callers (mock_start and
Start-DesktopJourneyMock) pass it. The config stays byte-identical across
steps, and the chat smoke's commit check still proves the gateway runs the
updated code.
2026-10-21 03:04:36 -07:00
Maciej Lisiewski 9a6140877b fix(install-e2e): desktop smoke accepts the canonical gateway Desktop attaches to
Since #106742 Desktop reaches its local backend through `hermes gateway ensure`,
which attaches to (or starts, detached) the canonical `gateway run` daemon. That
listener is reparented away from Electron and has no `serve` spawn-ledger record,
so localBackendProcess found 0 listeners and every Linux +desktop install/update
leg failed after an otherwise healthy boot.

Accept a third listener kind, `host-gateway`: the pid gateway_state.json names in
the home under test or its machine root, running, kind hermes-gateway, same
incarnation (start_time in gateway.status units), and the actual OS listener on
the connected port. The installed-tree origin check is unchanged and gets no
app-report waiver for it; the gateway proves its tree by the install-root cwd
`gateway ensure` starts it in.
2026-10-21 02:43:42 +02:00
Teknium 5c0b7024e0 fix(install-e2e): install and update journeys record first-run onboarding as done
Since #137522 and #137807 a fresh desktop install opens the first-run
onboarding chat. The install/update E2E desktop chat smoke then sent its
checkpoint prompt into that chat instead of a normal one, so the mock
never received it ("The mock must receive this checkpoint prompt after
the send"), and on the Windows GUI installer path the installer-launched
app was mid onboarding turn when the journey closed it, so it did not exit
within 30 s. Seen on v0.22.0 attempt rc.1 in three Windows
desktop-installer@latest (HEAD -> NEXT) legs.

The mock provider writer now sets onboarding.run: false in the root
config, and the Windows GUI install phase seeds the same key before the
installer launches the app. The journeys test install and update, not
onboarding (which has its own desktop e2e spec).
2026-10-13 15:07:03 -07:00
alt-glitch 303b70f75b chore(release): retire HERMES_GUEST_ONBOARDING from bundles, build tooling and the updater
The Microsoft Store variant forced HERMES_GUEST_ONBOARDING=1 into its bundle env,
and commit/channel builds could bake it with --bundle-env. Nothing reads it now.

- scripts/bundles/desktop_inputs.py: the Store bundle env is just the request's.
- bundle_env.py / bundle-env.mjs: the key is no longer allowed for new builds.
- channel-protocol.ts: the key moves to RETIRED_BUNDLE_ENV_KEYS, so preview
  manifests already published with it still decode (the HERMES_SKIP_INTRO
  precedent).
- BUILDING.md example uses HERMES_PREVIEW_FULL_CONNECTORS.
- Build tests use another allowed key as their sample; both validators reject the
  retired key and the decoder drops it.
- msix-virtualization.test.mjs: every generated MSIX manifest (store, release,
  canary, channel) keeps filesystem and registry write virtualisation disabled,
  and the builder config declares unvirtualizedResources. The machine marker and
  the shared %LOCALAPPDATA%\hermes root depend on that.
2026-10-14 00:32:21 +05:30
ethernet a174341a89 fix(install-e2e): chat smoke waits for the app to show the mock model before sending
The v2026.7.1 desktop keeps the composer's model as renderer state and ships
it as the session override on session.create. waitForChatReady only proves the
composer is interactable, so a send that beat the configured model's load went
to the app's default (openrouter/claude-opus-4.6), got a 401, and the mock never
saw the prompt: "The mock must receive this checkpoint prompt after the send".
rc.12-v0.21.7 hit it in 2 of the 5 macOS dmg legs on that baseline.

The smoke now waits (30s cap) for the mock model's name to appear in the page
text. Apps prettify the id ("Mock Model"), so case and separators are folded,
and no selector is assumed. An app that never renders the name proceeds and
the mock-received assertion still decides; the renderer log records it.
2026-10-11 14:54:08 -04:00
ethernet baef00bf4c fix(smoke): check bundled Hermes home before sandbox setup
The smoke driver created .desktop-smoke-home inside the predicted Hermes
home before checking that the home was empty. Bundles with stamped env
that retained the explicit HERMES_HOME failed before Electron launched.

Resolve and admit the predicted home before any sandbox writes. Keep the
existing-profile refusal and provider seeding intact. Extend the seeding
contract for onboarding defaults and forced home clears.

Verified the old failure and fixed launch boundary on native Windows ARM64
with real driver imports and filesystem writes. Full package install/chat
acceptance still needs a fresh hosted smoke run.
2026-10-06 11:04:35 -04:00
Teknium d1a7aa7c14 fix: date a macOS backend's start in UTC so the DST-end hour cannot misdate it
processStartTime read `ps -o lstart` in local time and rebuilt it with a
hand-written month table. In the hour that repeats when DST ends, local
01:30 names two instants and the reconstruction picked the first, putting a
backend started in the second 3600 s off its ledger create_time, so a valid
host backend was rejected. ps now renders lstart with TZ=UTC0 and the value
is parsed as GMT with Date.parse (V8 accepts the exact `ps` format,
including the double-spaced day), which also drops the month table.
2026-10-06 02:53:32 -07:00
Teknium e4f862b7ca fix: grant the app-report waiver only to a listener proven to be the app's child
appVouchesForBackend tested `ownership !== 'host-ledger'`, so a backend
object that never had its ownership established (the field was optional;
untyped .mjs callers can build one) received the ancestry waiver. The
waiver's premise is ancestry, so it must be stated: localBackendProcess now
returns a BackendProcess whose `ownership` is required, and the waiver
requires `=== 'child'`.
2026-10-06 02:53:32 -07:00
Teknium eda0fcaaa0 refactor(desktop-smoke): extract appVouchesForBackend from assertBackendOrigin
Moves the app-ancestry evidence predicate (process evidence unreadable,
not a ledger-attached host backend, app reported the same tree) and its
comment into a named helper. Same expression, same short-circuit order,
evaluated at the same point; behaviour unchanged. Brings
assertBackendOrigin's CC back under its main cap (24 -> 19).
2026-10-06 02:53:32 -07:00
Teknium 1946a105b2 test(desktop-smoke): bind ledger-attached backends to their own evidence
A host-ledger backend is not an Electron descendant, so the app-reported
root no longer stands in for its tree when process evidence is unreadable
(Windows). Ledger records must match the live process's start time
(create_time, 2 s like the product), and the ledger is read at the machine
root the app resolves for the home, where the product writes it.
2026-10-06 02:53:32 -07:00
Teknium 047f8aa46f style: format desktop smoke ownership regression 2026-10-06 02:53:32 -07:00
Teknium 35d0cb86e0 test: recognize shared host backends in desktop update smoke 2026-10-06 02:53:32 -07:00
Siddharth Balyan 5eea87882a Clarify: one question shape and one result shape on every surface (skip, cancel, timeout and undelivered are told apart) (#127760)
* refactor(desktop): split clarify-tool.tsx into a clarify/ folder

Pure moves, no behaviour change. Parsing, the question-card core
(shell, choice rows, question block), the delivery watchdog, and each
pending/settled card get their own file so the core can be reused.

* feat(clarify): one questions[] shape with per-question status and one outcome

The clarify tool now takes only questions=[{question, choices?, multi_select?}].
A wrong shape is a tool error that names the right one, so a model that
sends the old top-level question/choices corrects itself on the next call.

Every result has the same shape on every surface: each response carries
status (answered, skipped, unanswered) with user_response null unless
answered, and the result carries one outcome (submitted, cancelled,
timed_out, undelivered) plus an optional surface-written notice. The
timeout and cancel sentences that used to pose as the user's answer are
gone, and the compressor reads status instead of matching their prefixes.

The callback contract is callback(questions) -> {answers, outcome, notice?}
(None = skipped, missing qid = unanswered). tui_gateway settles a batch the
same way for the last lock, a cancel, an interrupt and the deadline, and
clarify.lock keeps a null answer as a skip. A multi-select answer that is
not a JSON array counts as one typed ("Other") answer. The tool-row preview
reads the first question.

* feat(clarify): every surface asks through the one question card

CLI: the batch panel is the only panel; Enter on an empty field skips a
question, Ctrl+C cancels and keeps the locked answers, the deadline returns
timed_out. -q and -z return undelivered with a no-user notice.

Ink TUI: the single-question prompt is gone; the card supports multi-select
(Space or a digit toggles, Enter locks, typed Other text joins the picks),
an empty submit skips, Esc cancels, and "Batch" names are dropped.

Desktop: the single-question card is deleted and its keyboard handling moved
into the one card (arrows, letters and digits, auto-advance, Enter picks then
confirms). Confirm enables at one answer; blank questions lock null; Skip and
a composer message cancel; the settled card shows No answer for unanswered
questions. The bots room card follows the same contract.

Messaging: one card per question with a "Reply skip to skip" line in every
locale; timeouts and delivery failures map to timed_out and undelivered.

* fix(clarify): cancelled for a released messaging card, labels win over the skip word

A prose reply to a messaging choice card, /new and session end released
the wait with "", which read as timed_out. They now resolve with a
CANCELLED marker and the tool gets outcome cancelled.

A typed reply that matches a choice label ("Skip") now resolves to that
choice; the skip word applies only when no choice matches.

The bots room card sends the picked labels as they are; the tool already
strips the recommendation label. Unused OUTCOMES and a new docstring and
comment are removed.

* fix(tui): re-editing a multi-select answer keeps its picks

The Ink card stored a multi-select answer as display text ("A, B"), so
revisiting the question put the whole string into Other and re-locked it
as one item. The answers map now keeps the raw JSON array, the card
restores the picks on revisit, and only the display lines format it.

Comments that earlier edits reworded are restored to their original
words, minus the phrases the change made wrong. The compute-host clarify
lock accepts None for a skipped question.

* test(clarify): align three checks with the one-answer confirm and the regenerated keys

The desktop Cmd+Enter test now expects a send with one answer (the blank
question locks null), the compressor test expects the single-answer summary
the code gives, and locales/_keys.desktop.json is regenerated for the
removed and added clarify strings.

* test(tui): the one-question card header is singular
2026-09-29 18:33:44 +05:30
liuhao1024 00c8e5ca48 test(desktop): cover the one-entry batch clarify shape end to end
Since the questions[]-only schema (#95907) every single question is a
one-entry batch on both the tool args and the gateway wire, yet no test
exercised that shape (called out in #98645). Lock the behavior down at
both layers:

- unit: a one-entry batch renders the batch card (not a blank/spinner
  single card) both with the wire already parked and when the request
  lands after the tool row, and answers with the qid-keyed lock
- e2e: a SINGLE_BATCH trigger drives the real chain (composer -> gateway
  -> agent -> clarify tool -> clarify.request -> renderer) through mount,
  pick, confirm, and settle for questions.length === 1

The e2e mock's trigger routing also learns to scope its has-tool-result
guard to the answering turn's own question text: the existing any-tool-
result check would false-positive once a second scripted clarify shares
the conversation history.

Adapted to main: the mock server now lives in tests-js/scripts, and a batch
confirm answers with clarify.lock.
2026-09-26 18:00:36 -05:00
hermes-seaeye[bot]andgithub-actions[bot] aae6c2044e fmt(js): npm run fix on merge (#123390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-26 03:39:27 +00:00
ethernet 0c1b34826f fix(install-e2e): settle OLD source desktop before launch 2026-09-24 03:13:54 -04:00
ethernet 156efd4ba2 test(install-e2e): the desktop smoke reads the install commit with the driver's real git
The HEAD -> NEXT Windows legs take every git off PATH so the product must
provision its own; readInstallationCommit spawned bare `git` and died with
spawnSync git ENOENT before the chat checkpoint could judge the product.
2026-09-24 01:05:17 -04:00
ethernet 9a8cdf96bf Merge origin/main into ethie/pm-clean
- tools/browser_tool_install.py: keep pm-clean's frozen old-updater stub; main's
  UTF-8 decode fix touched only the npx prefetch body it replaces.
- tests/hermes_cli/test_update_scoped_reconciliation.py: keep pm-clean's test
  subset (catch-up rides the PM completion owner) and take main's gateway-less
  host evidence (#120740): the updated seed that holds the host at a running
  gateway, and the two gateway-less matrices for the source change that merged
  cleanly into update_cmd_fleet.py.
2026-09-23 20:04:27 -04:00
teknium1 9d6e4e72a4 fix(desktop): a member that fails after pre-tool text is reported, not posted as its reply
Independent-review follow-up for the group room failed-turn fix.

- Group room poll: a retained error newer than the pre-submit snapshot
  (turn start replaces it with a fresh started_at) is this turn's failure
  and wins over transcript text. The core closer writes no failed-turn row
  behind a tool row, so "said X, called a tool, provider 401" left X as the
  newest assistant row and the room posted it, dropped the 401 and re-drove
  the member to the round cap (live repro on the PR head).
- Both pickers end the turn at a failed_turn row instead of scanning past
  it; with the retained error gone (backend restart) the row's notice is
  reported through the failed path instead of a silent pass / dropped
  stranded marker. The stranded harvest treats a retained error as the
  stranded turn's own the same way.
- REST cold-load/paging (/api/sessions/{id}/messages, /messages/around)
  type legacy untyped notice rows like session.resume does, via one
  read-side helper in agent/turn_failure_copy.py.
- Gateway closer: the fresh-session closure test now asserts the row's
  display_kind through a real SessionDB round-trip (red without the
  run_turn.py stamp).
- E2E: mock trigger that says text + calls a tool, then 401s; spec asserts
  the room reports the error and never posts that text.
2026-09-23 16:58:39 -07:00
ethernet de09ca1ab1 test(e2e): seed portable provider env for desktop smoke 2026-09-23 05:27:46 -04:00
ethernet c991b1fb8b test(e2e): stop source settlement at update completion 2026-09-23 01:59:55 -04:00
ethernet e2636cec01 test(e2e): terminate Windows source settling 2026-09-22 22:25:41 -04:00
ethernet b358846033 test(e2e): keep Electron singleton socket path short 2026-09-22 14:23:40 -04:00
ethernet 8b9ea624ef fix(e2e): isolate update window from Chromium state 2026-09-22 13:03:06 -04:00
ethernet 7c4d7b332a fix(e2e): pin Electron update-window route before boot 2026-09-22 12:46:48 -04:00
ethernet e64351c8e7 fix(release): support the pre-stable development graph 2026-09-22 12:19:01 -04:00
ethernet 8ba2b2b558 fix install e2e update-window setup 2026-09-22 12:09:47 -04:00
ethernet feb643ec89 Merge branch 'fix/r6-linux-e2e' into ethie/pm-clean 2026-09-22 09:38:37 -04:00
ethernet aed7ffd821 test(installer): isolate Linux update Electron route 2026-09-22 09:37:10 -04:00
ethernet 29f74ab71c fix(e2e): bridge historical Windows desktop launches
The v2026.6.19 Desktop starts its source backend through the host Python.
Windows cannot expose that process environment or working directory. Carry
the app's verified source root into the existing strict provenance check.

A PM update can leave a locked historical executable beside the current
command-file launcher. Settle the updated runtime through the command file and
use verbatim cmd.exe arguments so paths with spaces remain valid.

Verified with the focused Desktop smoke tests, the tests-js typecheck, syntax
checks, and a native Windows ARM command-file launch probe.
2026-09-22 09:36:53 -04:00
ethernet 0b8df36728 test(install): settle source runtime before desktop smoke
Run deferred source dependency and bundle replacement work under the same
sanitized environment used by Electron before attaching Playwright. This
keeps the expected process boundary outside the smoke session.
2026-09-22 00:27:14 -04:00
ethernet 980a70dd74 test(install-e2e): navigate historical updates and permit unattended quit 2026-09-21 14:49:33 -04:00
ethernet 77987dba8e test(install-e2e): match historical inputs and preserve authored profile state 2026-09-21 14:13:07 -04:00
ethernet f3b1399211 fix: round-5 CI backlog after the 779-commit main merge
Merge fallout (my resolution errors, all caught by CI):
- hermes_cli/backup.py + gateway.py: `theirs` on those hunks re-imported clusters HEAD had
  already moved to backup_restore.py / kept in the facade. backup.py loses the 349-line
  duplicate (main's #110179 fix is ported into backup_restore._import_db_member); the
  systemd service-unit cluster returns to gateway.py (PM's _prepare_service_launcher /
  _pm_managed_node_dirs / _systemd_command have no home in main's extraction) with main's
  utf-8-sig read. gateway_service_unit.py is dropped.
- gateway/run.py: main's plugin-update chore is not profile-scoped (the housekeeping
  ordering test pins the scope/drain sequence).
- pyproject + 30 test files: `import yaml` -> `import hermes_yaml as yaml` (pm-clean has no
  pyyaml); gateway/config._bundled_platform_manifest_name reads through hermes_yaml.
- tests re-seamed onto pm-clean's shape: residency admission (installed_engine),
  supervisor child env (binary is a constructor argument), update import guard
  (update_cmd_deps is gone; our probe already scrubs PYTHONPATH — both #115032 invariants
  pass), shallow-count git responses (stash path asks `status --porcelain -z`); dropped
  tests for retired code (_run_node_bootstrap/_ensure_tui_node, Windows resume demotion).
- tests/tools/test_local_env_blocklist.py: restore the two helpers the suite-reduction
  commit dropped and the blocklist import.

Real fixes:
- pm: classify_uv_failure/ResolutionConflict move beside the uv runner (pm.environment,
  stdlib-only). pm.workspace imports tomllib at module level and cannot load on the 3.10
  bootstrap python that streams uv output in the Docker arm64 image.
- tools/browser_tool.warm_agent_browser_npx_cache: back as a permanent definition — it is on
  the frozen old-updater surface, and the revert-scheduled compat pointer does not count.
- hermes_cli/memory_setup: the dashboard's pip row uses pm.environments.
  running_from_selected_environment for installed vs restart_required.
- scripts/windows-build-deps.ps1: export DISTUTILS_USE_SDK/MSSdk so setuptools trusts the
  primed MSVC environment instead of asking vswhere (`env -i` test runner on win32-arm64
  compiling ruamel-yaml-clib); run_tests.sh forwards them.
- tests/pm/test_windows_build_deps.py: start the protocol test from a parent env without the
  toolchain variables the runner job already exports.
- tests/conftest.py scrubs HERMES_BUNDLED_PLUGINS (Nix-wrapped hermes on the dev host);
  tests/home_io_guard.py treats sys.path site-packages under the real home as the
  interpreter's installation (PM-activated developer shell).
- tests-js: four `curly` lint errors from main's new scripts.
2026-09-21 02:47:48 -04:00
ethernet 925c08ceca fix: CI python-tests backlog — no import-time dependency syncs, CI-shaped test fixtures
Production:
- agent/bedrock_adapter.py, agent/vertex_adapter.py: pm.ensure_import ran at
  module import. In any process that imports these modules without a committed
  PM selection (CI's build_environment test venv, a fresh checkout) that sync
  rebuilt the dependency environment mid-process and replaced sys.path with a
  generation missing the caller's own packages (anthropic, aiohttp vanished).
  The extra is now ensured at first client build / credential request.
- plugins/platforms/matrix/adapter.py: a complete install needs no
  ensure_and_bind round trip; only a partial one syncs.
- tools/browser_tool.py: drop the facade's duplicate warm_agent_browser_npx_cache
  shim; the compat pointer already resolves to browser_tool_install.

Test harness:
- tests/home_io_guard.py: PATH-entry probes (shutil.which) and the running
  interpreter's own installation (stdlib reads, realpath ancestry, fixture
  symlinks into it) are not Hermes state; a patched Path.expanduser must not
  crash the guard. run_tests.sh no longer filters PATH — the guard owns it.
- tests/tui_gateway/conftest.py: import hermes_bootstrap before any file opens
  a MagicMock hermes_constants window (6 files exited the process at boot).
- tests/hermes_cli/conftest.py probe_root: scratch checkouts the import guard
  probes need hermes_bootstrap.py (the launcher imports it).
- tests/pm/_fixtures.py stage_host_python: a copied relocatable python needs
  its stdlib beside it (No module named 'encodings' on CI).
- tests/install/e2e-assets/smoke-env.mjs: dependency-free env shaping so the
  source-build-env probe runs under bare node (main deleted the Playwright
  entry it was imported through).
- adapt main's new tests to branch seams (model_metadata_http, launch
  completion tail, CI toolchain exports uv after python, source_launch
  hermes_cli stub, systemd_notify single marker).
2026-09-20 11:47:06 -04:00
ethernet 9214174e07 fix(ci): lint legs after the main merge
- check_no_tmp_literals: resolve scratch via tempfile/os.tmpdir; the termux
  container mount point is one marked variable per script
- ruff TID251: desktop E2E fixtures may reach PM internals like tests do;
  the pm.runtime_stage ban message no longer names a module that never existed
- auth_codex: build the capped httpx stream subclass on first use so importing
  hermes_cli.auth_codex no longer forces httpx (the lazy proxy in auth_constants
  was defeated by a module-scope base class; broke lanes without httpx)
- desktop-smoke: launchApp is a parameter; the bundle-env test substitutes a
  refusing launcher instead of letting Playwright spawn a dying binary
  (3 unhandled rejections failed the tests-js lane)
2026-09-19 23:25:18 -04:00
ethernet 48f619f4e4 fix(install-e2e): accept the app's own report where the backend environment is unreadable
assertBackendOrigin demands evidence of the tree a module-launched backend
imports from. On Windows there is none to read: the venv launcher hands the
interpreter over as a system python, so argv never names the tree, and the
platform exposes neither the process's cwd nor its environment. Every desktop
leg there died with

    Source backend listener imports a different source tree
      (cwd=(unreadable), HERMES_PYTHON_SRC_ROOT=(unset),
       executable=C:\hostedtoolcache\...\python.exe, ...)

while the backend was in fact the installation's own: its app log says
`[backend] `serve` supported for Hermes at <root> (venv: <root>/venv)` and it
came up and served.

The two facts that matter are already established before the assertion runs:
localBackendProcess only returns a listener that is a descendant of the app
process the driver launched, and the driver asserts the root that app reported
resolving against options.root. So pass that reported root through as evidence
and accept it -- but only when the platform supplied no process evidence at all
(no cwd, no PYTHONPATH, no VIRTUAL_ENV), which keeps the platforms that can read
one exactly as strict as they are today.

Verified: tsc -p tests-js/tsconfig.json --noEmit clean; the new invariant test
covers the Windows shape, its control row (nothing readable and no report is
still a different tree), a report of some other tree, and the case that matters
most -- readable evidence naming another tree is NOT rescued by the app's report.
The only failing test in that file is the pre-existing headless Electron launch.
2026-09-18 12:13:04 -04:00
ethernet 4abc223f90 fix(install-e2e): prove the desktop backend's tree from the environment
The backend-origin assertion accepts a command that names the installation
root, which is the LINUX shape. On macOS `<root>/venv/bin/python` is a symlink
to the framework binary and the app resolves it before spawning, so argv names
that binary and never the root; on Windows the install's venv copy is bypassed
for the toolcache interpreter. Twelve macos legs and their windows twins died
with "Source backend listener imports a different source tree" while the
backend was the installation's own: its log reads
`[backend] serve supported for Hermes at <root> (venv: <root>/venv)`, and it
came up and served on a real port.

The app binds that backend to the tree by ENVIRONMENT -- main.ts puts
`[ACTIVE_HERMES_ROOT, process.env.PYTHONPATH]` on the backend's PYTHONPATH and
VIRTUAL_ENV names its venv, which is how `import hermes_cli` resolves from the
installation. macOS reads a process environment through `ps eww`, so the
assertion now also accepts an installation-root entry in PYTHONPATH, or a
VIRTUAL_ENV whose parent is the root. Windows exposes no equivalent reader, so
a Windows backend stays exactly as strict as it was.

Verified: `vitest run tests-js/scripts/desktop-smoke.test.ts` passes including
the new test (macOS shape accepted; control row with no environment evidence
still a different tree; evidence naming another tree still rejected), and
`tsc -p tests-js/tsconfig.json --noEmit` is clean.
2026-09-18 04:25:06 -04:00
ethernet 626b3a80a0 fix(install-e2e): clear the restored composer draft before a chat checkpoint types
The update window's checkpoint submitted the ROOT checkpoint's prompt: the
backend turn for the window's fresh session carried the earlier checkpoint's
exact text, so the witness could never match the prompt the smoke typed. The
app persists its composer draft across launches, so the window boots with the
previous checkpoint's text already in the composer.

Select-all + Delete it and refuse to type until the composer reads empty, so
the checkpoint proves its own input rather than inheriting a draft.
2026-09-18 01:17:52 -04:00
ethernet d66bd10769 test(install-e2e): prove the desktop composer took the prompt before trusting Enter
The draft-clear poll passes vacuously: the composer reads '' when the editor
never accepted the keystrokes, so a window whose chat is inert looked exactly
like a window that sent and cleared. Assert the echoed prompt first, so the
checkpoint fails fast and says which half broke.

On failure, also write the renderer's own evidence (`desktop-chat-<phase>-renderer.log`:
timeline-free console lines plus thread/composer DOM state) — from the mock's
side a swallowed send and a send that was never made are identical.
2026-09-18 01:17:52 -04:00
ethernet 999a923e80 fix(install-e2e): log what the mock parsed, not just the request line
The desktop chat smoke's witness poll timed out on the app-update leg while
`POST /v1/chat/completions` was logged and answered — a request arriving and a
request being *recorded* look identical in mock.log. Log the recorded prompt
text on the two recognized body shapes, and when neither matches log the parsed
lastUserMessage plus the raw body (truncated). One run then names the shape
mismatch instead of another round of theory.

Extracting the shape check also pins that an unrecognized body records nothing,
rather than a half-parsed witness, and describeForLog tolerates an absent
lastUserMessage (`JSON.stringify(undefined)` is not a string).
2026-09-18 01:17:52 -04:00
ethernet 9dd3a21fa1 fix(install-e2e): log every request the mock receives
The driver collects this server's stdout as mock.log, so a request that never
arrived and a request to an endpoint the mock does not serve were both invisible:
"0 requests" was read as evidence the desktop app never sent anything, when the
server was in fact incapable of recording a request at all.

One line per request (method + path), plus an explicit
"NOT IMPLEMENTED <method> <path> → 404" on the fallthrough. The mock serves only
/v1/models, /v1/chat/completions and the witness endpoint, so an app that POSTs
/v1/responses now says so by name instead of failing silently.

Verified against the real server: GET /v1/models → 200, POST /v1/chat/completions
→ 200, POST /v1/responses → "NOT IMPLEMENTED POST /v1/responses → 404".
2026-09-18 01:17:52 -04:00
ethernet 7ffadfa546 fix(install-e2e): give the submitted draft the same window the other post-send checks get
installer-script+desktop -> hermes-desktop-app-update failed on
"The submitted draft must clear before the idle control proves completion"
(15s predicate), while the failure screenshot shows a healthy, configured app --
Gateway ready, Mock Model selected, and the exchange COMPLETED
("Hello from the mock inference server! The full boot chain is working.") with an
empty composer by capture time.

So the submit was accepted and QUEUED, not rejected: v2026.8.31's composer routes
a submit to its queue while a turn is in flight (its own comment: "busy submit
routes text to the queue instead of a steer"), so the draft legitimately stays
visible past 15s and clears once the queue drains. That is why this leg differs
from its sibling, whose app the capture step builds from HEAD.

The draft-clear poll now uses 90s like the mock-receipt and reply polls beside it;
those still enforce delivery, so a genuinely stuck composer fails there instead.
2026-09-17 22:02:38 -04:00
ethernet 0117a28b44 fix(install-e2e): writeEnvFile rewrites keys in place instead of moving them
desktop->desktop failed with "0 deleted, 1 modified" and MODIFIED .env but no
`variables ...` line; the new line/order summary said "no key differs --
comments/order/blanks only", which is reordering. The writer filtered its managed
keys out and re-appended them at the end, so every call moved a journey's own .env
entries while keeping the same keys and values: a byte-level change with nothing
for a value-level diff to name.

Managed keys are now rewritten where they already stand, with only missing ones
appended, and the prior file's trailing blank lines are dropped BEFORE appending
(trimEnd() runs after them and cannot reach a blank line they follow -- the first
version of this fix left one, caught by asserting the writer's exact bytes).

Verified: writeEnvFile leaves
  OPENAI_BASE_URL=<url>/v1, "# keep me", OTHER_TEST_VALUE=kept, MOCK_API_KEY=..., OPENAI_API_KEY=...
in that order; tests-js/scripts/desktop-smoke.test.ts is 12 passed | 3 skipped.
2026-09-17 20:16:07 -04:00
ethernet 690b8d5b34 fix(install-e2e): writeEnvFile must not delete a provider endpoint it isn't replacing
The run configures the mock provider through the CLI path, which writes
OPENAI_BASE_URL/OPENAI_API_KEY into .env. tests/install/e2e-assets/desktop-smoke.ts
then calls writeEnvFile(home) with NO url -- and that call filtered the portable
pair out of the file while writing only MOCK_API_KEY. So the user-state snapshot
recorded .env without the endpoint, the upgrade's own post-update .env sync added
it back, and the leg failed as "the upgrade changed the user's own state ... 0
deleted, 1 modified" with the pair reported as ADDITIONS (before: 26403 bytes with
MOCK_API_KEY; after: 26473 bytes with the pair).

Only keys this call actually replaces are dropped now: without a mockUrl the
MOCK_API_KEY line is still rewritten and the portable pair is left untouched.
Proven red on base by the new case (writeEnvFile(home) left
'\nMOCK_API_KEY=e2e-mock-key\n', endpoint gone) and green with the fix.
tests-js/scripts/desktop-smoke.test.ts: 11 passed | 3 skipped.
2026-09-17 19:49:01 -04:00
ethernet 2364dc30c0 fix(install-e2e): prove a module-launched backend's tree without its app-owned cwd
The diagnostic from the last run named the mismatch:

  cwd=.../home/.hermes/.desktop-smoke-home  HERMES_PYTHON_SRC_ROOT=(unset)
  expected=.../home/.hermes/hermes-agent
  command=".../hermes-agent/venv/bin/python" "-m" "hermes_cli.main" "serve" ...

The backend was the installation's own venv interpreter; the check inferred the
import root from the process cwd, which the APP owns (the smoke's home), so a
correct backend read as "imports a different source tree".

A captured HERMES_PYTHON_SRC_ROOT stays authoritative (a launcher that bound a
tree still has to match). Without one, accept the launcher cd'ing into the tree
or a command that names it, and only then fail. Unreadable paths count as no
evidence instead of throwing ENOENT. Invariant test covers all three, including
the foreign interpreter that still fails.
2026-09-17 16:45:46 -04:00
ethernet 7d4eab88f1 chore(install-e2e): make the backend-origin mismatch and the composer inspectable
Both failures reached new assertions and reported nothing to inspect. The
source-origin check now names the cwd, HERMES_PYTHON_SRC_ROOT and the command it
saw beside the expected root, and the smoke writes desktop-backend-<phase>.log
BEFORE that assertion instead of after it, so the backend's identity is on disk
when the check fails.
2026-09-17 16:19:54 -04:00
ethernet 9606ba3bd6 fix(install-e2e): make the desktop smoke's composer locator hit-test the editor
waitForChatReady targeted a bare `[data-slot="composer-root"] textarea`, and the
composer renders an aria-hidden, sr-only <textarea> as assistant-ui's binding
primitive beside the real contentEditable editor. That primitive is "editable",
so it passed toBeEditable and then no trial click could ever hit it -- 225
retries ending in "element is outside of the viewport", which is what every
+desktop leg reported once the app stopped booting behind its onboarding
overlay (see the run with model.base_url in config.yaml: the app now resolves
http://127.0.0.1:<port>/v1 and renders the chat UI).

Require the visible editor, keep a textarea fallback only for a real input
(not aria-hidden, not sr-only), and let the failure explain itself: it reports
the composer-root/contenteditable counts and the composer's outerHTML instead of
a bare Playwright timeout.
2026-09-17 16:08:51 -04:00