Commit Graph

60 Commits

Author SHA1 Message Date
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]
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
ethernet
5265ebed78 fix(install-e2e): put the mock endpoint in config.yaml where the desktop reads it
The smoke writer fed the endpoint to `model.provider: custom` through .env
OPENAI_BASE_URL, which runtime_provider's bare-`custom` trust path never reads
on v2026.8.31 -- that tree's own comment: "OPENAI_BASE_URL env var is no longer
consulted -- config.yaml is the single source of truth for endpoint URLs". So
the app's readiness check (setup.runtime_check) resolved `custom` to no endpoint
and booted behind its onboarding overlay ("No usable credentials found for
custom. setup.status reports configured credentials, but runtime resolution
still failed"), which is why every installer-script+desktop leg failed at its
OLD checkpoint and why the desktop smoke's trial click never landed.

Reproduced against the real ladder before changing anything: with the writer's
config, resolve_runtime_provider(requested='custom') raises
"provider 'custom' resolved without credentials (no endpoint or API key
configured)"; adding
model.base_url makes it resolve to the mock URL (api_key no-key-required, the
loopback bypass). A custom_providers entry alone is not enough, so the endpoint
now goes where both vintages read it. The named/custom:<name> entry and the .env
pair stay for the consumers that use them.
2026-09-17 15:46:38 -04:00
ethernet
f58d34cf5e fix(install-e2e): pin the mock provider's endpoint in config.yaml
The desktop smoke configured a provider as `model.provider: custom` plus
OPENAI_BASE_URL/OPENAI_API_KEY in .env, with deliberately no provider entry in
config.yaml. Every `installer-script+desktop` leg then died at its OLD
checkpoint: the v2026.8.31 app boots behind the onboarding overlay ("No usable
credentials found for custom. setup.status reports configured credentials, but
runtime resolution still failed"), so the smoke's trial click on the composer
never landed (225 retries against the covering div).

That vintage resolves a bare `custom` only from config.yaml
`custom_providers:` (name + base_url + key_env, with a bare-"custom" fallback to
the first valid entry); the env pair stays for the trees that resolve the
endpoint from the environment. Write both, replacing the entry this writer owns
rather than stacking duplicates.
2026-09-17 14:36:43 -04:00
ethernet
b4a294fff9 Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
2026-09-17 13:52:05 -04:00
teknium1
f879fead28 test(desktop): live Bot Chat spec for message_agent friendly-name targets
Mock inference gains a one-shot `E2E_CALL(<tool>)[<json>]` script so a spec can make
the REAL tool run from a real Bot Chat; the new spec creates a sender bot, seeds a
`writer` profile titled "Scribe", sends a DM to "Scribe" and asserts the rendered
tool result says "Message dispatched to @writer" (base: "No teammate named
'Scribe'"), plus the sender's state.db tool row.
2026-09-16 22:25:33 -07:00
teknium1
b466a87bd6 fix(bot-mode): group rooms wake on @all, ignore prose stop words, report dead turns, approve on click
Three group-room turn-loop defects, each live-reproduced in the real
Electron app against a real gateway with mock inference:

- Hold classifier (group-rounds.ts): a stop/halt/pause token holds a member
  only within two words of an @mention, so "@impl go, das ist halt ein Test"
  is delivered instead of setting a sticky hold (#103893); addressing the
  whole room (@all/@everyone) without a stop word releases every hold, so a
  Stopped room wakes on "@all <task>" without the literal "resume" (#97740).
- Retained failure (group-turns.ts): the gateway keeps a failed turn under
  session.resume.inflight as {status:'error'}; the room read that as live
  work and slid the deadline to the 20-minute cap while the member looked
  busy. groupSessionBusy/retainedGroupTurnError classify it as finished;
  the foreground poll throws it into the failed-turn path (activity +
  roster badge) and the harvester consumes the stranded marker (#92760,
  diagnosis from #95103 by @hrnbld).
- Approval card (group-chat-parts.tsx): approval choices submit on click;
  the clip-prone footer Respond button is gone for approvals (#91706).
  Room message code blocks wrap instead of overflowing (#91857 by
  @piskooooo).

Tests: invariant vitest cases in group-rounds/group-turns/group-chat-parts
(all red on base); three Electron specs under apps/desktop/e2e using two
new mock-inference triggers (gated rm -rf for a real approval prompt, a
non-retryable 401 for a retained failure). Docs: bot-mode.md § Groups.

Co-authored-by: hrnbld <hrnbld@users.noreply.github.com>
Co-authored-by: piskooooo <piskooooo@users.noreply.github.com>
2026-09-16 21:55:50 -07:00
teknium1
89f277020c fix(desktop): bot-workspace tiles drop the whole ambient composer selection; live Electron proof
Gate reasoning_effort and fast behind the same switch as model/provider in
desktopSessionCreateParams (includeComposerSelection): a Bot-workspace tile
targets a different profile without switching the window's composer, so all
four fields of an unrelated session's pick would otherwise ride into its
session.create. Omitting them lets the bot profile's configured defaults apply.

The vitest added for this PR left the hoisted requestGatewayForAgent mock
with a recorded call (restoreAllMocks only restores spies), which failed the
next test's not-called assertion in CI ("keeps an unlisted named local
legacy-profile tile owned by its bare profile"). The test now resets that mock
and the composer atoms it set, and asserts the two new omitted fields.

New Playwright spec bot-tile-ignores-ambient-composer-model.spec.ts drives the
real app: open a bot's canonical chat, pick a second model from the composer
menu (the mock provider now lists extraModels; receivedModels records the
model of every completion request), Ctrl+T a side chat, send a turn, and
assert the inference request carried the profile default. Fails on main's
index.ts (request carried the ambient pick), passes on head.
2026-09-16 16:44:13 -07:00
ethernet
74ca008260 fix(e2e): no named 'mock' provider anywhere
The second writer (desktop e2e fixtures' dead-backend setup) hand-rolled
the same mock-named provider block, and the first still emitted
providers.mock. Both now go through the one writer and express an external
endpoint the portable way: provider 'custom' + OPENAI_BASE_URL +
OPENAI_API_KEY. grep for a mock provider in the harness now returns
nothing.

The desktop fixture keeps its dead endpoint (port 1) -- only the spelling
changed.
2026-09-16 19:32:46 -04:00