bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
659 lines
19 KiB
Python
659 lines
19 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from mirage.resource.ram import RAMResource
|
|
from mirage.types import DEFAULT_SESSION_ID, MountMode
|
|
from mirage.workspace import Workspace
|
|
|
|
|
|
@pytest.fixture
|
|
def ws():
|
|
mem = RAMResource()
|
|
asyncio.run(mem.write("/hello.txt", data=b"hello world\n"))
|
|
asyncio.run(mem.write("/numbers.txt", data=b"3\n1\n2\n1\n3\n"))
|
|
asyncio.run(
|
|
mem.write(
|
|
"/log.txt",
|
|
data=b"INFO start\nERROR fail\nINFO ok\nERROR bad\nINFO done\n"))
|
|
asyncio.run(mem.mkdir("/subdir"))
|
|
asyncio.run(mem.write("/subdir/a.txt", data=b"aaa\n"))
|
|
asyncio.run(mem.write("/subdir/b.txt", data=b"bbb\n"))
|
|
asyncio.run(mem.write("/config.json", data=b'{"key": "value"}\n'))
|
|
lines = "\n".join(f"row {i}" for i in range(5000)) + "\n"
|
|
asyncio.run(mem.write("/big.txt", data=lines.encode()))
|
|
ws = Workspace(
|
|
{"/data": (mem, MountMode.WRITE)},
|
|
mode=MountMode.WRITE,
|
|
)
|
|
ws.get_session(DEFAULT_SESSION_ID).cwd = "/"
|
|
return ws
|
|
|
|
|
|
# --- Pipes ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pipe_grep_sort_uniq(ws):
|
|
io = await ws.execute("cat /data/numbers.txt | sort | uniq")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert lines == ["1", "2", "3"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pipe_grep_wc(ws):
|
|
io = await ws.execute("grep ERROR /data/log.txt | wc -l")
|
|
assert (await io.stdout_str()).strip() == "2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pipe_head_stops_early(ws):
|
|
io = await ws.execute("cat /data/big.txt | head -n 3")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert len(lines) == 3
|
|
assert lines[0] == "row 0"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pipe_tail(ws):
|
|
io = await ws.execute("cat /data/log.txt | tail -n 2")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert len(lines) == 2
|
|
assert lines[-1] == "INFO done"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_triple_pipe(ws):
|
|
io = await ws.execute("cat /data/log.txt | grep INFO | head -n 2")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert len(lines) == 2
|
|
assert all("INFO" in line for line in lines)
|
|
|
|
|
|
# --- Control flow ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_and_success(ws):
|
|
io = await ws.execute("cat /data/hello.txt && echo done")
|
|
assert b"done" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_and_failure_short_circuits(ws):
|
|
io = await ws.execute(
|
|
"grep NONEXISTENT /data/hello.txt && echo should_not_appear")
|
|
assert b"should_not_appear" not in (io.stdout or b"")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_or_fallback(ws):
|
|
io = await ws.execute("grep NONEXISTENT /data/hello.txt || echo fallback")
|
|
assert b"fallback" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_semicolon_runs_both(ws):
|
|
io = await ws.execute("echo first ; echo second")
|
|
assert b"second" in io.stdout
|
|
|
|
|
|
# --- Redirects ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_redirect_stdout_to_file(ws):
|
|
await ws.execute("echo written > /data/out.txt")
|
|
io = await ws.execute("cat /data/out.txt")
|
|
assert b"written" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_redirect_append(ws):
|
|
await ws.execute("echo line1 > /data/append.txt")
|
|
await ws.execute("echo line2 >> /data/append.txt")
|
|
io = await ws.execute("cat /data/append.txt")
|
|
out = await io.stdout_str()
|
|
assert "line1" in out
|
|
assert "line2" in out
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_redirect_on_or_chain(ws):
|
|
io = await ws.execute("grep hello /data/hello.txt > /data/out.txt || "
|
|
"echo fallback > /data/out.txt; "
|
|
"cat /data/out.txt")
|
|
assert "hello" in (await io.stdout_str())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_redirect_on_and_chain(ws):
|
|
io = await ws.execute("echo first > /data/chain.txt && "
|
|
"echo second >> /data/chain.txt; "
|
|
"cat /data/chain.txt")
|
|
assert "first" in (await io.stdout_str())
|
|
assert "second" in (await io.stdout_str())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_redirect_stdin(ws):
|
|
io = await ws.execute("grep world < /data/hello.txt")
|
|
assert b"world" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_heredoc(ws):
|
|
io = await ws.execute("cat << EOF\nhello heredoc\nEOF")
|
|
assert b"hello heredoc" in io.stdout
|
|
|
|
|
|
# --- Subshell isolation ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subshell_cd_isolated(ws):
|
|
await ws.execute("cd /data")
|
|
await ws.execute("(cd /data/subdir)")
|
|
assert ws.get_session(DEFAULT_SESSION_ID).cwd == "/data"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subshell_export_isolated(ws):
|
|
await ws.execute("(export LEAK=yes)")
|
|
assert "LEAK" not in ws.get_session(DEFAULT_SESSION_ID).env
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subshell_inherits_parent_env(ws):
|
|
await ws.execute("export INHERITED=true")
|
|
io = await ws.execute("(printenv INHERITED)")
|
|
assert b"true" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nested_subshell(ws):
|
|
await ws.execute("((export DEEP=yes))")
|
|
assert "DEEP" not in ws.get_session(DEFAULT_SESSION_ID).env
|
|
|
|
|
|
# --- Background jobs ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_basic(ws):
|
|
await ws.execute("cat /data/hello.txt &")
|
|
io = await ws.execute("wait %1")
|
|
assert b"hello" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_isolation_env(ws):
|
|
await ws.execute("export BG_VAR=leaked &")
|
|
await ws.execute("wait %1")
|
|
assert "BG_VAR" not in ws.get_session(DEFAULT_SESSION_ID).env
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_isolation_cwd(ws):
|
|
await ws.execute("cd /data &")
|
|
await ws.execute("wait %1")
|
|
assert ws.get_session(DEFAULT_SESSION_ID).cwd == "/"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_sees_parent_env(ws):
|
|
await ws.execute("export VISIBLE=yes")
|
|
await ws.execute("printenv VISIBLE &")
|
|
io = await ws.execute("wait %1")
|
|
assert b"yes" in io.stdout
|
|
|
|
|
|
# --- Session: cd + env ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cd_then_relative_cat(ws):
|
|
await ws.execute("cd /data/subdir")
|
|
io = await ws.execute("cat a.txt")
|
|
assert b"aaa" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cd_nested_relative(ws):
|
|
await ws.execute("cd /data")
|
|
await ws.execute("cd subdir")
|
|
io = await ws.execute("cat b.txt")
|
|
assert b"bbb" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_then_variable_expansion(ws):
|
|
await ws.execute("export PATTERN=ERROR")
|
|
io = await ws.execute("grep $PATTERN /data/log.txt | wc -l")
|
|
assert (await io.stdout_str()).strip() == "2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_unset_cycle(ws):
|
|
await ws.execute("export TMP=val")
|
|
assert ws.get_session(DEFAULT_SESSION_ID).env["TMP"] == "val"
|
|
await ws.execute("unset TMP")
|
|
assert "TMP" not in ws.get_session(DEFAULT_SESSION_ID).env
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_printenv_shows_all(ws):
|
|
await ws.execute("export A=1")
|
|
await ws.execute("export B=2")
|
|
io = await ws.execute("printenv")
|
|
out = await io.stdout_str()
|
|
assert "A=1" in out
|
|
assert "B=2" in out
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_printenv_single_key(ws):
|
|
await ws.execute("export SECRET=abc")
|
|
io = await ws.execute("printenv SECRET")
|
|
assert (await io.stdout_str()).strip() == "abc"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_printenv_missing_key(ws):
|
|
io = await ws.execute("printenv NOSUCH")
|
|
assert io.exit_code == 1
|
|
|
|
|
|
# --- Multi-session isolation ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_sessions_isolated_cwd(ws):
|
|
sa = ws.create_session("s-a")
|
|
sa.cwd = "/"
|
|
sb = ws.create_session("s-b")
|
|
sb.cwd = "/"
|
|
await ws.execute("cd /data", session_id="s-a")
|
|
await ws.execute("cd /data/subdir", session_id="s-b")
|
|
assert sa.cwd == "/data"
|
|
assert sb.cwd == "/data/subdir"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_sessions_isolated_env(ws):
|
|
ws.create_session("s-a")
|
|
ws.create_session("s-b")
|
|
await ws.execute("export X=from_a", session_id="s-a")
|
|
await ws.execute("export X=from_b", session_id="s-b")
|
|
assert ws.get_session("s-a").env["X"] == "from_a"
|
|
assert ws.get_session("s-b").env["X"] == "from_b"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_session_env_not_visible_cross_session(ws):
|
|
ws.create_session("s-a")
|
|
ws.create_session("s-b")
|
|
await ws.execute("export PRIVATE=yes", session_id="s-a")
|
|
io = await ws.execute("printenv PRIVATE", session_id="s-b")
|
|
assert io.exit_code == 1
|
|
|
|
|
|
# --- For loops ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_for_loop_basic(ws):
|
|
io = await ws.execute(
|
|
"for f in /data/subdir/a.txt /data/subdir/b.txt; do cat $f; done")
|
|
out = await io.stdout_str()
|
|
assert "aaa" in out
|
|
assert "bbb" in out
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_for_loop_variable_restored(ws):
|
|
await ws.execute("export i=original")
|
|
await ws.execute("for i in 1 2 3; do echo $i; done")
|
|
assert ws.get_session(DEFAULT_SESSION_ID).env["i"] == "original"
|
|
|
|
|
|
# --- If/else ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_if_true_branch(ws):
|
|
io = await ws.execute(
|
|
"if grep -q world /data/hello.txt; then echo found; else echo nope; fi"
|
|
)
|
|
assert b"found" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_if_false_branch(ws):
|
|
io = await ws.execute(
|
|
"if grep -q NOPE /data/hello.txt; then echo found; else echo nope; fi")
|
|
assert b"nope" in io.stdout
|
|
|
|
|
|
# --- Complex combined ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cd_export_grep_pipe(ws):
|
|
await ws.execute("cd /data")
|
|
await ws.execute("export TERM=ERROR")
|
|
io = await ws.execute("grep $TERM log.txt | wc -l")
|
|
assert (await io.stdout_str()).strip() == "2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subshell_with_redirect(ws):
|
|
await ws.execute("(echo from_subshell) > /data/sub_out.txt")
|
|
io = await ws.execute("cat /data/sub_out.txt")
|
|
assert b"from_subshell" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pipe_into_redirect(ws):
|
|
await ws.execute("grep ERROR /data/log.txt | sort > /data/errors.txt")
|
|
io = await ws.execute("cat /data/errors.txt")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert len(lines) == 2
|
|
assert lines[0] == "ERROR bad"
|
|
assert lines[1] == "ERROR fail"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_background_with_pipe(ws):
|
|
await ws.execute("cat /data/numbers.txt | sort | uniq &")
|
|
io = await ws.execute("wait %1")
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert sorted(lines) == ["1", "2", "3"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_history_tracks_session_id(ws):
|
|
ws.create_session("s-hist")
|
|
ws.get_session("s-hist").cwd = "/"
|
|
await ws.execute("echo tracked", session_id="s-hist")
|
|
records = [e for e in await ws.history() if e["session"] == "s-hist"]
|
|
assert len(records) == 1
|
|
assert records[0]["command"] == "echo tracked"
|
|
|
|
|
|
# --- grep exit codes ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_no_match_exit_code(ws):
|
|
io = await ws.execute("grep NONEXISTENT /data/hello.txt")
|
|
assert io.exit_code == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_match_exit_code(ws):
|
|
io = await ws.execute("grep hello /data/hello.txt")
|
|
assert io.exit_code == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_q_match(ws):
|
|
io = await ws.execute("grep -q world /data/hello.txt")
|
|
assert io.exit_code == 0
|
|
assert not (await io.stdout_str()).strip()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_q_no_match(ws):
|
|
io = await ws.execute("grep -q NONEXISTENT /data/hello.txt")
|
|
assert io.exit_code == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_and_short_circuit(ws):
|
|
io = await ws.execute(
|
|
"grep NONEXISTENT /data/hello.txt && echo should_not_appear")
|
|
assert b"should_not_appear" not in (io.stdout or b"")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_or_fallback(ws):
|
|
io = await ws.execute("grep NONEXISTENT /data/hello.txt || echo fallback")
|
|
assert b"fallback" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_if_condition(ws):
|
|
io = await ws.execute(
|
|
"if grep -q world /data/hello.txt; then echo found; else echo nope; fi"
|
|
)
|
|
assert b"found" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_if_no_match(ws):
|
|
io = await ws.execute(
|
|
"if grep -q NOPE /data/hello.txt; then echo found; else echo nope; fi")
|
|
assert b"nope" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_pipe_no_match_last_stage_wins(ws):
|
|
io = await ws.execute("grep NONEXISTENT /data/hello.txt | sort")
|
|
assert io.exit_code == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_pipe_match(ws):
|
|
io = await ws.execute("grep ERROR /data/log.txt | sort")
|
|
assert io.exit_code == 0
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert len(lines) == 2
|
|
assert lines[0] == "ERROR bad"
|
|
assert lines[1] == "ERROR fail"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_no_match_then_or_chain(ws):
|
|
io = await ws.execute(
|
|
"grep NOPE /data/hello.txt || grep ERROR /data/log.txt | head -n 1")
|
|
assert b"ERROR" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_count_no_match(ws):
|
|
io = await ws.execute("grep -c NONEXISTENT /data/hello.txt")
|
|
assert (await io.stdout_str()).strip() == "0"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_count_match(ws):
|
|
io = await ws.execute("grep -c ERROR /data/log.txt")
|
|
assert io.exit_code == 0
|
|
assert (await io.stdout_str()).strip() == "2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_invert_match(ws):
|
|
io = await ws.execute("grep -v ERROR /data/log.txt")
|
|
assert io.exit_code == 0
|
|
lines = (await io.stdout_str()).strip().split("\n")
|
|
assert all("ERROR" not in line for line in lines)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_invert_no_output(ws):
|
|
io = await ws.execute("echo hello | grep -v hello")
|
|
assert not (await io.stdout_str()).strip()
|
|
|
|
|
|
# --- rg exit codes ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_no_match_exit_code(ws):
|
|
io = await ws.execute("rg NONEXISTENT /data/hello.txt")
|
|
assert io.exit_code == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_match_exit_code(ws):
|
|
io = await ws.execute("rg hello /data/hello.txt")
|
|
assert io.exit_code == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_no_match_and_chain(ws):
|
|
io = await ws.execute("rg NONEXISTENT /data/hello.txt && echo found")
|
|
assert b"found" not in (io.stdout or b"")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_no_match_or_chain(ws):
|
|
io = await ws.execute("rg NONEXISTENT /data/hello.txt || echo fallback")
|
|
assert b"fallback" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_pipe_no_match(ws):
|
|
io = await ws.execute("rg NONEXISTENT /data/hello.txt | wc -l")
|
|
assert io.exit_code == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_match_pipe_head(ws):
|
|
io = await ws.execute("rg INFO /data/log.txt | head -n 1")
|
|
assert io.exit_code == 0
|
|
assert b"INFO" in io.stdout
|
|
|
|
|
|
# --- diff exit codes ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_identical_files(ws):
|
|
await ws.execute("echo same > /data/diff_a.txt")
|
|
await ws.execute("echo same > /data/diff_b.txt")
|
|
io = await ws.execute("diff /data/diff_a.txt /data/diff_b.txt")
|
|
assert io.exit_code == 0
|
|
assert not (await io.stdout_str()).strip()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_different_files(ws):
|
|
await ws.execute("echo aaa > /data/diff_a.txt")
|
|
await ws.execute("echo bbb > /data/diff_b.txt")
|
|
io = await ws.execute("diff /data/diff_a.txt /data/diff_b.txt")
|
|
assert io.exit_code == 1
|
|
assert (await io.stdout_str()).strip()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_and_chain(ws):
|
|
await ws.execute("echo same > /data/diff_a.txt")
|
|
await ws.execute("echo same > /data/diff_b.txt")
|
|
io = await ws.execute(
|
|
"diff /data/diff_a.txt /data/diff_b.txt && echo identical")
|
|
assert b"identical" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_or_chain(ws):
|
|
await ws.execute("echo aaa > /data/diff_a.txt")
|
|
await ws.execute("echo bbb > /data/diff_b.txt")
|
|
io = await ws.execute(
|
|
"diff /data/diff_a.txt /data/diff_b.txt || echo different")
|
|
assert b"different" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_if_identical(ws):
|
|
await ws.execute("echo same > /data/diff_a.txt")
|
|
await ws.execute("echo same > /data/diff_b.txt")
|
|
cmd = ("if diff /data/diff_a.txt /data/diff_b.txt;"
|
|
" then echo same; else echo changed; fi")
|
|
io = await ws.execute(cmd)
|
|
assert b"same" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_if_different(ws):
|
|
await ws.execute("echo aaa > /data/diff_a.txt")
|
|
await ws.execute("echo bbb > /data/diff_b.txt")
|
|
cmd = ("if diff /data/diff_a.txt /data/diff_b.txt;"
|
|
" then echo same; else echo changed; fi")
|
|
io = await ws.execute(cmd)
|
|
assert b"changed" in io.stdout
|
|
|
|
|
|
# --- find (verify already correct) ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_existing(ws):
|
|
io = await ws.execute("find /data/subdir")
|
|
assert io.exit_code == 0
|
|
assert b"a.txt" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_with_name(ws):
|
|
io = await ws.execute("find /data/subdir -name a.txt")
|
|
assert io.exit_code == 0
|
|
assert b"a.txt" in io.stdout
|
|
|
|
|
|
# --- Complex combined scenarios ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_no_match_pipe_redirect(ws):
|
|
cmd = ("grep NONEXISTENT /data/log.txt > /data/result.txt"
|
|
" || echo none > /data/result.txt")
|
|
await ws.execute(cmd)
|
|
io = await ws.execute("cat /data/result.txt")
|
|
assert b"none" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_match_and_diff(ws):
|
|
await ws.execute("grep ERROR /data/log.txt > /data/errors.txt")
|
|
await ws.execute("echo 'ERROR fail\nERROR bad' > /data/expected.txt")
|
|
io = await ws.execute("diff /data/errors.txt /data/expected.txt")
|
|
assert io.exit_code in (0, 1)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rg_subshell_isolation(ws):
|
|
io = await ws.execute("(rg NONEXISTENT /data/hello.txt) || echo recovered")
|
|
assert b"recovered" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_background_exit_code(ws):
|
|
await ws.execute("grep ERROR /data/log.txt &")
|
|
io = await ws.execute("wait %1")
|
|
assert io.exit_code == 0
|
|
assert b"ERROR" in io.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grep_no_match_background(ws):
|
|
await ws.execute("grep NONEXISTENT /data/log.txt &")
|
|
io = await ws.execute("wait %1")
|
|
assert io.exit_code == 1
|