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
143 lines
5.2 KiB
Python
143 lines
5.2 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 ast
|
|
import pathlib
|
|
|
|
import pytest
|
|
|
|
BUILTIN = pathlib.Path(
|
|
__file__).resolve().parents[3] / "mirage" / "commands" / "builtin"
|
|
|
|
GUARDED_COMMANDS = ("cat", "head", "tail", "wc", "du", "file", "nl", "md5")
|
|
|
|
# Backends whose command genuinely operates on a single resource and rejects
|
|
# or has no multi-file semantics. Each entry needs a one-line reason so the
|
|
# allowlist stays honest. Keyed by (command, backend).
|
|
ALLOWLIST = {
|
|
("wc", "mongodb"): "counts documents of one collection via count API",
|
|
}
|
|
|
|
|
|
def _command_funcs(tree: ast.Module) -> list[ast.AsyncFunctionDef]:
|
|
funcs = []
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.AsyncFunctionDef):
|
|
continue
|
|
for dec in node.decorator_list:
|
|
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
name = getattr(target, "id", getattr(target, "attr", None))
|
|
if name == "command":
|
|
funcs.append(node)
|
|
return funcs
|
|
|
|
|
|
def _calls_resolve_glob(func: ast.AST) -> bool:
|
|
for node in ast.walk(func):
|
|
if isinstance(node, ast.Call):
|
|
target = node.func
|
|
name = getattr(target, "id", getattr(target, "attr", None))
|
|
if name == "resolve_glob":
|
|
return True
|
|
return False
|
|
|
|
|
|
def _indexes_first(func: ast.AST) -> bool:
|
|
for node in ast.walk(func):
|
|
if (isinstance(node, ast.Subscript)
|
|
and isinstance(node.value, ast.Name)
|
|
and node.value.id in ("paths", "resolved")):
|
|
idx = node.slice
|
|
if isinstance(idx, ast.Constant) and idx.value == 0:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _loops_paths(func: ast.AST) -> bool:
|
|
for node in ast.walk(func):
|
|
if isinstance(node, (ast.For, ast.AsyncFor)) and isinstance(
|
|
node.iter, ast.Name) and node.iter.id in ("paths", "resolved"):
|
|
return True
|
|
# comprehension form: {... for p in paths}
|
|
for comp in ast.walk(node) if isinstance(node,
|
|
(ast.DictComp, ast.ListComp,
|
|
ast.SetComp,
|
|
ast.GeneratorExp)) else []:
|
|
if isinstance(comp, ast.comprehension) and isinstance(
|
|
comp.iter,
|
|
ast.Name) and comp.iter.id in ("paths", "resolved"):
|
|
return True
|
|
return False
|
|
|
|
|
|
_MULTI_HELPERS = ("format_multi", "generic_grep", "generic_rg", "grep", "rg",
|
|
"generic_du", "generic_file", "file_cmd", "du_multi",
|
|
"generic_nl", "generic_md5")
|
|
|
|
|
|
def _passes_full_list(func: ast.AST) -> bool:
|
|
"""Heuristic: the resolved list is handed to a *_multi / generic helper.
|
|
|
|
Covers backends that delegate multi-file handling to a generic routine
|
|
(head_multi, tail_multi, format_multi, du_multi, file_cmd, generic_grep,
|
|
generic_rg) by passing the whole ``paths``/``resolved`` list as the first
|
|
argument.
|
|
"""
|
|
for node in ast.walk(func):
|
|
if not isinstance(node, ast.Call):
|
|
continue
|
|
target = node.func
|
|
name = getattr(target, "id", getattr(target, "attr", "")) or ""
|
|
if name.endswith("_multi") or name in _MULTI_HELPERS:
|
|
for arg in node.args:
|
|
if isinstance(arg,
|
|
ast.Name) and arg.id in ("paths", "resolved"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _iter_command_files():
|
|
for path in sorted(BUILTIN.glob("*/*.py")):
|
|
cmd = path.stem
|
|
if cmd not in GUARDED_COMMANDS:
|
|
continue
|
|
backend = path.parent.name
|
|
yield cmd, backend, path
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cmd,backend,path",
|
|
[
|
|
pytest.param(c, b, p, id=f"{c}:{b}")
|
|
for c, b, p in _iter_command_files()
|
|
],
|
|
)
|
|
def test_command_handles_multiple_files(cmd, backend, path):
|
|
if (cmd, backend) in ALLOWLIST:
|
|
pytest.skip(ALLOWLIST[(cmd, backend)])
|
|
tree = ast.parse(path.read_text())
|
|
for func in _command_funcs(tree):
|
|
if not _calls_resolve_glob(func):
|
|
continue
|
|
if _loops_paths(func) or _passes_full_list(func):
|
|
continue
|
|
if _indexes_first(func):
|
|
pytest.fail(
|
|
f"{cmd}/{backend}: command resolves a glob but indexes "
|
|
f"paths[0]/resolved[0] without looping all paths. This drops "
|
|
f"every file after the first (the multi-file bug). Loop all "
|
|
f"paths or delegate to a *_multi helper. If this backend is "
|
|
f"genuinely single-resource, add it to ALLOWLIST with a "
|
|
f"reason.")
|