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
400 lines
12 KiB
Python
400 lines
12 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.accessor.base import Accessor, NOOPAccessor
|
|
from mirage.commands.registry import command
|
|
from mirage.commands.spec import SPECS
|
|
from mirage.io.types import ByteSource, IOResult
|
|
from mirage.resource.ram import RAMResource
|
|
from mirage.types import MountMode, PathSpec
|
|
from mirage.workspace.snapshot import apply_state_dict, read_tar
|
|
from mirage.workspace.workspace import Workspace
|
|
|
|
|
|
@command("history", resource="ram", spec=SPECS["history"])
|
|
async def fake_history(
|
|
accessor: Accessor = NOOPAccessor(),
|
|
paths: list[PathSpec] | None = None,
|
|
*texts: str,
|
|
**_extra: object,
|
|
) -> tuple[ByteSource | None, IOResult]:
|
|
return b"FAKE\n", IOResult()
|
|
|
|
|
|
def _ws() -> Workspace:
|
|
return Workspace({"/data": (RAMResource(), MountMode.WRITE)})
|
|
|
|
|
|
def _exec(ws: Workspace, cmd: str, **kw) -> IOResult:
|
|
return asyncio.run(ws.execute(cmd, **kw))
|
|
|
|
|
|
def _stdout(io: IOResult) -> str:
|
|
if io.stdout is None:
|
|
return ""
|
|
if isinstance(io.stdout, memoryview):
|
|
return bytes(io.stdout).decode()
|
|
if isinstance(io.stdout, (bytes, bytearray)):
|
|
return bytes(io.stdout).decode()
|
|
return ""
|
|
|
|
|
|
def test_history_command_per_session():
|
|
ws = _ws()
|
|
ws.create_session("s2")
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "pwd")
|
|
_exec(ws, "ls /", session_id="s2")
|
|
mine = _stdout(_exec(ws, "history"))
|
|
other = _stdout(_exec(ws, "history", session_id="s2"))
|
|
assert "ls /data" in mine
|
|
assert "pwd" in mine
|
|
assert "ls /data" not in other
|
|
assert "ls /" in other
|
|
|
|
|
|
def test_history_builtin_wins_over_mount_command():
|
|
res = RAMResource()
|
|
res.register(fake_history)
|
|
ws = Workspace({"/data": (res, MountMode.WRITE)})
|
|
_exec(ws, "ls /data")
|
|
out = _stdout(_exec(ws, "history", cwd="/data"))
|
|
assert "FAKE" not in out
|
|
assert "ls /data" in out
|
|
|
|
|
|
def test_cat_bash_history_all_sessions():
|
|
ws = _ws()
|
|
ws.create_session("s2")
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "pwd", session_id="s2")
|
|
io = _exec(ws, "cat /.bash_history")
|
|
assert io.exit_code == 0
|
|
out = _stdout(io)
|
|
assert "ls /data" in out
|
|
assert "pwd" in out
|
|
assert out.count("#") >= 2
|
|
|
|
|
|
def test_grep_and_tail_bash_history():
|
|
ws = _ws()
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "pwd")
|
|
grep_io = _exec(ws, "grep pwd /.bash_history")
|
|
assert grep_io.exit_code == 0
|
|
grep_out = _stdout(grep_io)
|
|
assert "pwd" in grep_out
|
|
assert "ls /data" not in grep_out
|
|
tail_io = _exec(ws, "tail -n 2 /.bash_history")
|
|
assert tail_io.exit_code == 0
|
|
assert "pwd" in _stdout(tail_io)
|
|
|
|
|
|
def test_ls_root_hides_dotfile_ls_a_shows():
|
|
ws = _ws()
|
|
plain = _stdout(_exec(ws, "ls /"))
|
|
dotted = _stdout(_exec(ws, "ls -a /"))
|
|
assert ".bash_history" not in plain
|
|
assert ".bash_history" in dotted
|
|
|
|
|
|
def test_history_c_clears_only_my_session_file_unchanged():
|
|
ws = _ws()
|
|
ws.create_session("s2")
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "pwd", session_id="s2")
|
|
clear_io = _exec(ws, "history -c")
|
|
assert clear_io.exit_code == 0
|
|
mine = _stdout(_exec(ws, "history"))
|
|
other = _stdout(_exec(ws, "history", session_id="s2"))
|
|
file_out = _stdout(_exec(ws, "cat /.bash_history"))
|
|
assert "ls /data" not in mine
|
|
assert "pwd" in other
|
|
assert "ls /data" in file_out
|
|
|
|
|
|
def test_append_to_bash_history_rejected():
|
|
ws = _ws()
|
|
io = _exec(ws, "echo hacked >> /.bash_history")
|
|
assert io.exit_code != 0
|
|
|
|
|
|
def test_sessions_path_no_longer_resolves():
|
|
ws = _ws()
|
|
io = _exec(ws, "ls /.sessions")
|
|
assert io.exit_code != 0
|
|
|
|
|
|
def test_unmount_history_view_rejected():
|
|
ws = _ws()
|
|
with pytest.raises(ValueError, match="history view"):
|
|
asyncio.run(ws.unmount("/.bash_history"))
|
|
|
|
|
|
def test_snapshot_roundtrip_preserves_tombstones(tmp_path):
|
|
ws = _ws()
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "history -c")
|
|
_exec(ws, "pwd")
|
|
snap = tmp_path / "ws.tar"
|
|
asyncio.run(ws.snapshot(snap))
|
|
dst = asyncio.run(Workspace.load(snap))
|
|
mine = _stdout(_exec(dst, "history"))
|
|
file_out = _stdout(_exec(dst, "cat /.bash_history"))
|
|
assert "ls /data" not in mine
|
|
assert "pwd" in mine
|
|
assert "ls /data" in file_out
|
|
|
|
|
|
def test_workspace_history_method():
|
|
ws = _ws()
|
|
_exec(ws, "ls /data")
|
|
_exec(ws, "pwd")
|
|
events = asyncio.run(ws.history())
|
|
assert [e["command"] for e in events] == ["ls /data", "pwd"]
|
|
assert all(e["type"] == "command" for e in events)
|
|
|
|
|
|
def test_history_s_appends_without_executing():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
io = _exec(ws, "history -s rm -rf /data")
|
|
assert io.exit_code == 0
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert "rm -rf /data" in out
|
|
assert _exec(ws, "ls /data/x").exit_code != 0
|
|
|
|
|
|
def test_history_d_deletes_and_renumbers():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
_exec(ws, "echo keep")
|
|
io = _exec(ws, "history -d 1")
|
|
assert io.exit_code == 0
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert "pwd" not in out.split("history -d 1")[0]
|
|
assert out.startswith("1 echo keep")
|
|
|
|
|
|
def test_history_d_attached_offset_deletes():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
_exec(ws, "echo keep")
|
|
io = _exec(ws, "history -d1")
|
|
assert io.exit_code == 0
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert out.startswith("1 echo keep")
|
|
|
|
|
|
def test_history_d_out_of_range():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
io = _exec(ws, "history -d 99")
|
|
assert io.exit_code == 1
|
|
assert b"99: history position out of range" in io.stderr
|
|
|
|
|
|
def test_history_d_non_numeric():
|
|
ws = _ws()
|
|
io = _exec(ws, "history -d abc")
|
|
assert io.exit_code == 1
|
|
assert b"abc: history position out of range" in io.stderr
|
|
|
|
|
|
def test_history_d_trailing_garbage_rejected():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
io = _exec(ws, "history -d 1abc")
|
|
assert io.exit_code == 1
|
|
assert b"1abc: history position out of range" in io.stderr
|
|
|
|
|
|
def test_history_count_trailing_garbage_rejected():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
io = _exec(ws, "history 3abc")
|
|
assert io.exit_code == 1
|
|
assert b"3abc: numeric argument required" in io.stderr
|
|
|
|
|
|
def test_history_d_negative_offset_deletes_last():
|
|
ws = _ws()
|
|
_exec(ws, "echo first")
|
|
_exec(ws, "echo last")
|
|
io = _exec(ws, "history -d -1")
|
|
assert io.exit_code == 0
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert "echo first" in out
|
|
lines = [ln for ln in out.splitlines() if "echo last" in ln]
|
|
assert lines == []
|
|
|
|
|
|
def test_history_d_requires_argument():
|
|
ws = _ws()
|
|
io = _exec(ws, "history -d")
|
|
assert io.exit_code == 2
|
|
assert b"-d: option requires an argument" in io.stderr
|
|
assert b"history: usage:" in io.stderr
|
|
|
|
|
|
def test_history_invalid_option_usage_exit_2():
|
|
ws = _ws()
|
|
io = _exec(ws, "history -z")
|
|
assert io.exit_code == 2
|
|
assert b"-z: invalid option" in io.stderr
|
|
assert b"history: usage:" in io.stderr
|
|
|
|
|
|
def test_history_p_prints_without_storing_args():
|
|
ws = _ws()
|
|
io = _exec(ws, "history -p hello world")
|
|
assert io.exit_code == 0
|
|
assert _stdout(io) == "hello\nworld\n"
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert "1 history -p hello world" in out
|
|
|
|
|
|
def test_history_ps_suppresses_print():
|
|
ws = _ws()
|
|
io = _exec(ws, "history -ps echo hi")
|
|
assert io.exit_code == 0
|
|
assert _stdout(io) == ""
|
|
out = _stdout(_exec(ws, "history"))
|
|
assert "echo hi" in out
|
|
|
|
|
|
def test_history_zero_lists_nothing():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
_exec(ws, "echo two")
|
|
io = _exec(ws, "history 0")
|
|
assert io.exit_code == 0
|
|
assert _stdout(io) == ""
|
|
|
|
|
|
def test_history_sync_flags_are_noops():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
for flag in ("-a", "-r", "-w", "-n"):
|
|
io = _exec(ws, f"history {flag}")
|
|
assert io.exit_code == 0
|
|
assert _stdout(io) == ""
|
|
|
|
|
|
def test_history_cluster_cw():
|
|
ws = _ws()
|
|
_exec(ws, "pwd")
|
|
io = _exec(ws, "history -cw")
|
|
assert io.exit_code == 0
|
|
out = _stdout(_exec(ws, "history 1"))
|
|
assert "pwd" not in out
|
|
|
|
|
|
def test_find_view_via_generic():
|
|
ws = _ws()
|
|
out = _stdout(_exec(ws, "find /.bash_history"))
|
|
assert out == "/.bash_history\n"
|
|
out = _stdout(_exec(ws, "find /.bash_history -name '*.bash*'"))
|
|
assert out == "/.bash_history\n"
|
|
out = _stdout(_exec(ws, "find /.bash_history -type d"))
|
|
assert out == ""
|
|
|
|
|
|
def test_substitution_records_only_outer_line():
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
_exec(ws, "wc -l $(find /data -name '*.txt')")
|
|
commands = [e["command"] for e in asyncio.run(ws.history())]
|
|
assert commands == [
|
|
"echo hi > /data/f.txt",
|
|
"wc -l $(find /data -name '*.txt')",
|
|
]
|
|
|
|
|
|
def test_substitution_keeps_inner_ops_in_audit():
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
_exec(ws, "echo $(cat /data/f.txt)")
|
|
events = asyncio.run(ws.observer.events())
|
|
reads = [e for e in events if e["type"] == "op" and e["op"] == "read"]
|
|
assert any(e["path"] == "/data/f.txt" for e in reads)
|
|
|
|
|
|
def test_outer_ops_after_substitution_not_lost():
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
_exec(ws, "wc -l $(find /data -name '*.txt')")
|
|
events = asyncio.run(ws.observer.events())
|
|
reads = [(e["op"], e["path"]) for e in events if e["type"] == "op"]
|
|
assert ("read", "/data/f.txt") in reads
|
|
|
|
|
|
def test_eval_xargs_source_record_single_entry():
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
_exec(ws, "eval cat /data/f.txt")
|
|
_exec(ws, "echo /data/f.txt | xargs cat")
|
|
_exec(ws, "echo 'cat /data/f.txt' > /data/s.sh")
|
|
_exec(ws, "source /data/s.sh")
|
|
commands = [e["command"] for e in asyncio.run(ws.history())]
|
|
assert commands == [
|
|
"echo hi > /data/f.txt",
|
|
"eval cat /data/f.txt",
|
|
"echo /data/f.txt | xargs cat",
|
|
"echo 'cat /data/f.txt' > /data/s.sh",
|
|
"source /data/s.sh",
|
|
]
|
|
|
|
|
|
def test_unrecorded_execute_skips_history_keeps_caller_ops():
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
_exec(ws, "cat /data/f.txt", record=False)
|
|
commands = [e["command"] for e in asyncio.run(ws.history())]
|
|
assert commands == ["echo hi > /data/f.txt"]
|
|
|
|
|
|
async def _raise_induced(io):
|
|
raise RuntimeError("induced")
|
|
|
|
|
|
def test_in_place_restore_rewinds_history(tmp_path):
|
|
src = _ws()
|
|
_exec(src, "echo from-snapshot")
|
|
snap = tmp_path / "s.tar"
|
|
asyncio.run(src.snapshot(snap))
|
|
dst = _ws()
|
|
_exec(dst, "echo pre-restore")
|
|
asyncio.run(apply_state_dict(dst, read_tar(snap)))
|
|
cmds = [e["command"] for e in asyncio.run(dst.history())]
|
|
assert cmds == ["echo from-snapshot"]
|
|
|
|
|
|
def test_failed_line_ops_still_in_audit(monkeypatch):
|
|
ws = _ws()
|
|
_exec(ws, "echo hi > /data/f.txt")
|
|
monkeypatch.setattr(ws, "apply_io", _raise_induced)
|
|
io = _exec(ws, "cat /data/f.txt")
|
|
assert io.exit_code == 1
|
|
events = asyncio.run(ws.observer.events())
|
|
reads = [e for e in events if e["type"] == "op" and e["op"] == "read"]
|
|
assert any(e["path"] == "/data/f.txt" for e in reads)
|
|
last_cmd = [e for e in events if e["type"] == "command"][-1]
|
|
assert last_cmd["command"] == "cat /data/f.txt"
|
|
assert last_cmd["exit_code"] == 1
|