Files
omnigent-ai--omnigent/tests/e2e/omnigent/test_run_harness_without_agent_e2e.py
T
Michael Gardner 6f0257dbc7 feat(kiro): add native CLI harness (#899)
* feat: add Kiro native CLI harness

Signed-off-by: Michael Gardner <gardnmi@gmail.com>

* fix(kiro): avoid ambient env in tmux attach

Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com>

* fix: restore uv.lock pypi.org sources (drop accidental databricks-proxy re-lock)

A local `uv run` during the merge re-locked uv.lock against this machine's
Databricks-internal pypi proxy, flipping every package source URL. Kiro changes
no dependencies and pyproject.toml is unchanged vs main, so restore main's
uv.lock verbatim (pypi.org sources). Only registry URLs differed — no version
or hash changes.

Co-authored-by: Isaac

* test(e2e-ui): add native-kiro render-parity suite (E2E UI Required gate)

The E2E UI Required gate flagged that #899 changes the agent-picker/session UI
(adds Kiro) without a tests/e2e_ui/** test. Add test_native_kiro_render_parity.py
mirroring the cursor/goose siblings — composer-IN parity, a TUI-originated turn
surfacing OUT, and no duplicate rendering — plus the native_kiro_session fixture.
Skip-gated on kiro-cli + tmux, so it skips in CI (no Kiro account provisioned)
exactly like the goose/cursor suites, and runs for real where Kiro is signed in.

Verified: collects + skips cleanly (kiro-cli absent); ruff clean.

Co-authored-by: Isaac

* fix: restore ap-web/package-lock.json npmjs.org sources (drop databricks npm-proxy)

Same root cause as the uv.lock fix: an npm command during round-1 merge re-resolved
one dependency (yaml-1.10.3) against this machine's Databricks-internal npm proxy
(npm-proxy.cloud.databricks.com), which CI (pinned to registry.npmjs.org) can't reach
-> 'npm ci' ETIMEDOUT. ap-web/package.json is unchanged vs main and Kiro adds no npm
dependency, so restore main's package-lock.json verbatim (clean npmjs.org sources).

Co-authored-by: Isaac

* test(e2e): exclude kiro-native from the live-harness matrix coverage check

test_run_harness_live_matrix_covers_registered_coding_harnesses asserts every
registered coding harness is either in the live no-AGENT e2e matrix or explicitly
excluded. kiro-native is a terminal-first TUI launched via `omni kiro` (tmux pane
+ bridge dir), not `omnigent run --harness kiro-native`, so — like goose-native /
qwen-native / cursor-native — it can't run in this matrix. Add it to the exclusion
set with the matching rationale; its coverage is the kiro-native bridge/executor/
forwarder unit tests + the test_native_kiro_render_parity e2e_ui suite.

Co-authored-by: Isaac

* test(ap-web): set isNativeWrapper in /compact composer menu tests

#1139 gated "/compact" behind isNativeWrapper (hidden for non-native
harnesses), but the three slash-menu-UX tests that assert "/compact"
tops/appears in the suggestions still rendered a non-native composer,
so they now fail on main (and on every PR that merges main).

Render those three with isNativeWrapper:true so "/compact" is offered,
restoring the built-in ordering the tests pin. Test-only; no behavior
change. Fixes the inherited ChatPage.composer.test.tsx red on this PR.

Co-authored-by: Isaac

* test(kiro): cover kiro_native launcher helpers (raise coverage 43%→70%)

The kiro-native launcher (omnigent/kiro_native.py) was the largest
coverage gap on this PR: its CLI/daemon orchestration is only exercised
by the live render-parity e2e, which skips in CI when kiro-cli is
absent. Add focused unit tests (with a fake httpx client) for the
unit-testable surface: executable resolution, launch-argv assembly,
terminal-payload decoding, tmux attach gating, startup-progress
forwarding, preflight, resume-id resolution, and the create/fetch/
ensure/find/wait session helpers (success + error branches).

Lifts kiro_native.py from 43% to 70%; remaining misses are the
daemon-driven async orchestration covered by runner/e2e paths.

Co-authored-by: Isaac

* test(kiro): rename test env var to avoid exfil-scan false positive

The CI exfil scanner flags any added file containing a secret-named
source (regex `[A-Z0-9]+_SECRET\b`) together with a network sink. The
tmux-allowlist test used `OMNIGENT_SECRET` purely as a non-allowlisted
sample var, which matched the secret regex and — combined with the
fake httpx client's .post()/.get() in the same file — tripped the
"secret-named source + network sink" block. Rename it to a neutral
`OMNIGENT_UNLISTED_VAR`; the test's intent (filtering non-allowlisted
keys) is unchanged.

Co-authored-by: Isaac

---------

Signed-off-by: Michael Gardner <gardnmi@gmail.com>
Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-06-25 00:55:25 +00:00

232 lines
10 KiB
Python

"""Live REPL e2e for ``omnigent run --harness`` without AGENT.
Migrated to use the mock LLM server. This test drives the user-facing
launcher shape::
omnigent run --harness <harness> -p <prompt>
under a real pseudo-TTY against the mock LLM server. It waits for the
REPL banner, lets the ``-p`` startup hook submit a real user turn, and
asserts the mock model returns the expected marker. No real Databricks
credentials are required.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from omnigent.runtime.harnesses import _HARNESS_MODULES
from omnigent.spec._omnigent_compat import OMNIGENT_HARNESSES
from tests.e2e._harness_probes import (
HARNESS_IDS,
HARNESS_PROBES,
HarnessProbe,
skip_if_harness_cli_missing,
)
from tests.e2e.omnigent._pexpect_harness import (
clean_exit,
spawn_omnigent_run,
strip_ansi,
)
from tests.e2e.omnigent.conftest import configure_mock_llm
_PROMPT_TEMPLATE = (
"Reply with exactly the identifier between <answer> tags, but omit the tags: "
"<answer>{marker}</answer>. Do not include any other text."
)
_SPAWN_TIMEOUT = 120.0
# Kept UNDER the e2e ``--timeout=180`` cap so a stalled turn fails cleanly here
# (a pexpect TIMEOUT with a captured buffer) instead of tripping the pytest cap
# and crashing the xdist worker with no diagnostic.
_COMPLETION_TIMEOUT = 150.0
_EXIT_TIMEOUT = 20.0
@pytest.mark.parametrize("probe", HARNESS_PROBES, ids=HARNESS_IDS)
def test_run_harness_without_agent_live_repl_round_trip(
probe: HarnessProbe,
omnigent_python: Path,
omnigent_repo_root: Path,
mock_credentials_env: dict[str, str],
mock_llm_server_url: str,
tmp_path: Path,
) -> None:
"""``omnigent run --harness`` boots and answers via each wrapped harness.
Uses the mock LLM server for deterministic responses. The no-AGENT
launcher should behave like a first-class agent: it should render the
selected harness banner, auto-submit the provided ``-p`` prompt,
stream a mock reply, and exit cleanly. A missing marker means either
the launch path did not reach the model or the response was garbled
before the REPL rendered it.
:param probe: Harness probe with model and marker.
:param omnigent_python: Interpreter with omnigent installed.
:param omnigent_repo_root: Working directory for the subprocess.
:param mock_credentials_env: Mock-LLM env vars.
:param mock_llm_server_url: Mock server URL.
:param tmp_path: Per-test temp directory.
"""
skip_if_harness_cli_missing(probe.harness)
model = f"mock-harness-no-agent-{probe.harness}"
marker = f"{probe.marker}_RUN_HARNESS_WITHOUT_AGENT"
prompt = _PROMPT_TEMPLATE.format(marker=marker)
# claude-code issues a warmup/title call before the turn that consumes one
# queued response, so the turn call needs another; queue a few markers.
responses = [{"text": marker}] * (4 if probe.harness == "claude-sdk" else 1)
configure_mock_llm(
mock_llm_server_url,
responses,
key=model,
)
# claude-sdk speaks the Anthropic wire, not OPENAI_*. Point it at the mock
# and pass a static gateway token via ANTHROPIC_AUTH_TOKEN (Authorization:
# Bearer) -- the docs-sanctioned custom-gateway auth. ANTHROPIC_API_KEY
# (x-api-key) would trigger claude-code's external-key validation, which
# the mock cannot satisfy ("Invalid API key").
env = dict(mock_credentials_env)
if probe.harness == "claude-sdk":
env["ANTHROPIC_BASE_URL"] = mock_llm_server_url
env["ANTHROPIC_AUTH_TOKEN"] = "mock-key"
child = spawn_omnigent_run(
omnigent_python=omnigent_python,
yaml_path=None,
model=model,
harness=probe.harness,
env=env,
cwd=omnigent_repo_root,
timeout=_SPAWN_TIMEOUT,
initial_prompt=prompt,
)
try:
# Headless one-shot ``-p``: the launcher boots, auto-submits the
# prompt, and prints the accumulated reply. It does NOT render the
# interactive ``◆`` assistant-turn glyph (the streaming contract
# changed in #783), so sync on the marker text the model returns —
# that landing in stdout is the load-bearing proof the no-AGENT
# launcher reached the model and rendered the reply.
child.expect(marker, timeout=_COMPLETION_TIMEOUT)
output = strip_ansi(child.before or "") + marker
finally:
# Drive the exit. The one-shot process does not always terminate
# promptly under CI load (shutdown/teardown lag — parked tasks,
# session-log write), so clean_exit sends ``/quit`` and force-kills as
# a fallback rather than blocking on EOF. Teardown cleanliness is not
# asserted (it is a known CI-load flake; see clean_exit's docstring).
clean_exit(child, timeout=_EXIT_TIMEOUT)
assert marker in output, (
f"[{probe.harness}] marker {marker!r} missing from REPL output; "
f"output tail:\n{output[-4000:]}"
)
def test_run_harness_live_matrix_covers_registered_coding_harnesses() -> None:
"""The live no-AGENT e2e matrix tracks REPL-launchable harnesses.
``OMNIGENT_HARNESSES`` also contains ``open-responses`` for the
legacy in-process executor path, but that harness is not currently
registered in the server-backed REPL harness registry. This test
makes the distinction explicit: when a coding harness is added to
``_HARNESS_MODULES``, this file must gain a live round-trip row
for it.
``claude-native``, ``codex-native``, ``pi-native``, and
``opencode-native`` are excluded because their inner executors require
bridge directories plus runner-managed terminal panes to inject keys
into — both set up by their native launchers, not by
``omnigent run --harness <native>``. (``opencode-native`` is a
terminal-takeover ``native-server`` harness, the same shape as the
other natives.) Running them through this matrix would hang or crash.
Their e2e coverage is via native launcher smoke tests (tracked
separately as native-launcher PTY/REPL smoke tests).
``cursor`` is excluded because this matrix authenticates through
the Databricks gateway/profile, while cursor-agent talks only to
Cursor's own backend and rejects gateway model ids.
``antigravity`` is excluded for the same reason as ``cursor``: it is
Gemini-native and its SDK launches a native binary needing a modern
glibc.
``copilot`` is excluded for the same reason as ``cursor`` / ``antigravity``:
the GitHub Copilot SDK authenticates with a GitHub token and talks only to
GitHub's Copilot backend (no Databricks gateway path), so ``_build_copilot_spawn_env``
emits none of the shared ``HARNESS_<H>_GATEWAY`` / profile probe vars this
matrix drives. Its live round-trip is covered by the gated
``tests/e2e/test_polly_copilot_e2e.py`` and the ``copilot-sdk-e2e-dev`` skill.
``cursor-native`` is excluded for the union of both reasons above.
``qwen`` is excluded because it does not follow the shared
``HARNESS_<HARNESS>_GATEWAY``/``DATABRICKS_PROFILE`` probe wiring that
this matrix (and ``test_harness_wrap_e2e.py``) drive harnesses with: its
wrap routes through ``HARNESS_QWEN_GATEWAY_BASE_URL`` /
``HARNESS_QWEN_GATEWAY_AUTH_COMMAND`` instead. Its live round-trip is
covered by the dedicated ``test_per_harness_qwen.py`` suite.
``goose`` (headless ACP) is excluded for the same reason as ``qwen``: it
authenticates from Goose's own config (``goose configure``), not the shared
gateway/profile probe wiring, so ``_build_goose_spawn_env`` emits no
``HARNESS_GOOSE_GATEWAY*`` vars for this matrix to drive. Its live round-trip
is covered by the dedicated ``test_goose_acp_e2e.py`` suite.
``goose-native`` is excluded for the same reason as ``claude-native`` /
``cursor-native``: it is a terminal-first TUI launched via ``omni goose``
(tmux pane + bridge dir), not ``omnigent run --harness goose-native``.
``antigravity-native`` is excluded for the union of both reasons above: it
is a terminal-first TUI launched via ``omnigent antigravity`` (runner-owned
agy tmux pane + bridge dir), not ``omnigent run --harness antigravity-native``,
AND it is Gemini-native (agy authenticates via Google OAuth, not the shared
Databricks gateway/profile probe wiring this matrix drives).
``qwen-native`` is excluded for the same reason as ``goose-native`` /
``cursor-native``: it is a terminal-first TUI launched via ``omni qwen``
(tmux pane + bridge dir, driving qwen's ``--input-file`` / ``--json-file``),
not ``omnigent run --harness qwen-native``. Its coverage is the dedicated
qwen-native bridge/executor/forwarder unit tests.
``kiro-native`` is excluded for the same reason as ``goose-native`` /
``qwen-native`` / ``cursor-native``: it is a terminal-first TUI launched via
``omni kiro`` (tmux pane + bridge dir), not ``omnigent run --harness
kiro-native``. Its coverage is the dedicated kiro-native bridge/executor/
forwarder unit tests plus the ``test_native_kiro_render_parity`` e2e_ui suite.
``hermes`` is excluded because it requires the ``hermes`` CLI binary
(installed separately via Nous Research's install script) and authenticates
through its own provider config, not the shared gateway/profile probe
wiring this matrix drives.
``hermes-native`` is excluded for the union of both reasons: it is a
terminal-first TUI launched via ``omni hermes`` (tmux pane + bridge dir), not
``omnigent run --harness hermes-native``, AND it wraps the ``hermes`` CLI
binary. Its coverage is the dedicated hermes-native bridge/executor/forwarder/
approval-mirror unit tests.
"""
expected_live_harnesses = set(OMNIGENT_HARNESSES).intersection(_HARNESS_MODULES) - {
"claude-native",
"codex-native",
"pi-native",
"opencode-native",
"cursor",
"cursor-native",
"antigravity",
"antigravity-native",
"copilot",
"qwen",
"qwen-native",
"goose",
"goose-native",
"kiro-native",
"hermes",
"hermes-native",
}
assert {probe.harness for probe in HARNESS_PROBES} == expected_live_harnesses