mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-13 15:40:00 +00:00
fix(subagents): make acceptance checks portable on Windows (#5162)
* fix(subagents): make acceptance checks portable on Windows * fix(subagents): reject drive-root path escapes * fix(subagents): preserve drive-root containment * fix(subagents): use Windows path casing rules * fix(subagents): reject drive-relative cd paths * fix(subagents): reject shell-dependent cd targets * fix(subagents): reject shell-dependent runner paths * fix(subagents): harden cross-shell acceptance checks * fix(subagents): reject ambiguous shell tokenization * fix(subagents): reject tokenizer segment drift * fix(subagents): reject ambiguous PowerShell syntax * fix(subagents): include all PowerShell quote delimiters * fix(subagents): fail closed on cross-family paths * fix(subagents): reject ambiguous Windows aliases * fix(subagents): reject PSDrive alias exclusions * fix(subagents): reject PSDrive-relative aliases --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
9f4a7823e2
commit
bec0acf6b5
@ -261,9 +261,8 @@ make test-live
|
|||||||
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
|
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
|
||||||
```
|
```
|
||||||
|
|
||||||
Direct pytest collection or execution of `tests/test_client_live.py` remains
|
Keep live tests opt-in via `DEER_FLOW_RUN_LIVE_TESTS=1`; guard POSIX-only
|
||||||
skipped unless `DEER_FLOW_RUN_LIVE_TESTS=1` is set. Do not add that opt-in to
|
markers with `os.name` for Windows collection.
|
||||||
default CI workflows.
|
|
||||||
|
|
||||||
Jina logging tests use dummy keys (`tests/test_jina_client.py`).
|
Jina logging tests use dummy keys (`tests/test_jina_client.py`).
|
||||||
Jina/Browserless/InfoQuest resolve URLs without rebuilding HTML.
|
Jina/Browserless/InfoQuest resolve URLs without rebuilding HTML.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -55,10 +55,13 @@ the async caller offloads the whole check with ``asyncio.to_thread``.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ntpath
|
||||||
import os
|
import os
|
||||||
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import stat
|
import stat
|
||||||
|
import unicodedata
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
@ -480,6 +483,39 @@ def _check_file_leaf(
|
|||||||
_SHELL_OPERATORS = ";&|"
|
_SHELL_OPERATORS = ";&|"
|
||||||
#: Leading ``VAR=value`` assignments are environment setup, not the executable.
|
#: Leading ``VAR=value`` assignments are environment setup, not the executable.
|
||||||
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
||||||
|
_WINDOWS_DRIVE_QUALIFIED_RE = re.compile(r"^[A-Za-z]:")
|
||||||
|
_WINDOWS_DRIVE_ABSOLUTE_RE = re.compile(r"^[A-Za-z]:/")
|
||||||
|
_WINDOWS_UNC_ABSOLUTE_RE = re.compile(r"^//[^/]+/[^/]+(?:/|$)")
|
||||||
|
#: A generated 8.3 component has at most six legal stem characters before a
|
||||||
|
#: one-digit ``~N`` tail and may retain an extension of at most three legal
|
||||||
|
#: characters. Lexical normalization cannot prove that it differs from the
|
||||||
|
#: corresponding long name on a volume where short-name generation is active.
|
||||||
|
_WINDOWS_SHORT_NAME_COMPONENT_RE = re.compile(r"^[A-Za-z0-9$%_'@~`!(){}^#&-]{1,6}~[1-9](?:\.[A-Za-z0-9$%_'@~`!(){}^#&-]{1,3})?$")
|
||||||
|
#: PowerShell paths may name a provider/PSDrive before ``:`` (for example,
|
||||||
|
#: ``FileSystem::C:/tmp`` or ``External:/tmp``). Those forms are absolute in
|
||||||
|
#: PowerShell but look relative to POSIX path normalization.
|
||||||
|
_POWERSHELL_DRIVE_QUALIFIED_RE = re.compile(r"^[^/\\:]+:")
|
||||||
|
#: A provider-qualified filesystem path carries either a drive designator or
|
||||||
|
#: a UNC root after PowerShell's ``Provider::`` prefix. Keep that delimiter
|
||||||
|
#: distinct from pytest's later ``::nodeid`` separator.
|
||||||
|
_POWERSHELL_PROVIDER_QUALIFIED_RE = re.compile(r"^[^/\\:]+::(?P<path>(?:[^/\\:]+:|//))")
|
||||||
|
#: Unlike drive-letter paths, a named PSDrive may have a multi-character name.
|
||||||
|
#: A separator after the colon distinguishes root-anchored from drive-relative
|
||||||
|
#: spellings whose resolution depends on that PSDrive's remembered location.
|
||||||
|
_POWERSHELL_DRIVE_ABSOLUTE_RE = re.compile(r"^[^/\\:]+:/")
|
||||||
|
#: ``cmd.exe`` expands paired-percent environment references before running
|
||||||
|
#: the command. Without shell provenance, a token such as ``%TEMP%`` cannot
|
||||||
|
#: be treated as the literal relative path seen by the POSIX parser.
|
||||||
|
_CMD_ENV_EXPANSION_RE = re.compile(r"%[^%\r\n]+%")
|
||||||
|
_CMD_DELAYED_ENV_EXPANSION_RE = re.compile(r"![^!\r\n]+!")
|
||||||
|
#: Shells supported by ``LocalSandbox`` do not agree on Bash brace expansion.
|
||||||
|
#: The evidence currently records no shell kind, so matching must reject it
|
||||||
|
#: before a POSIX parser can turn the expression into one harmless-looking
|
||||||
|
#: token.
|
||||||
|
_BASH_BRACE_EXPANSION_RE = re.compile(r"\{[^{}\r\n]*(?:,|\.\.)[^{}\r\n]*\}")
|
||||||
|
#: PowerShell treats seven typographic single and double quotes as string
|
||||||
|
#: delimiters, while POSIX ``shlex`` retains them as ordinary token characters.
|
||||||
|
_POWERSHELL_QUOTE_DELIMITERS = frozenset("‘’‚‛“”„")
|
||||||
|
|
||||||
|
|
||||||
def _carries_summary_shape(text: str) -> bool:
|
def _carries_summary_shape(text: str) -> bool:
|
||||||
@ -490,6 +526,58 @@ def _carries_summary_shape(text: str) -> bool:
|
|||||||
return bool(_TEST_PASS_SHAPE_RE.search(text) or _TEST_FAIL_SHAPE_RE.search(text) or _TEST_ZERO_SHAPE_RE.search(text))
|
return bool(_TEST_PASS_SHAPE_RE.search(text) or _TEST_FAIL_SHAPE_RE.search(text) or _TEST_ZERO_SHAPE_RE.search(text))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_cd_scope_path(path: str) -> tuple[str, bool] | None:
|
||||||
|
"""Normalize a ``cd`` path and retain Windows-absolute provenance.
|
||||||
|
|
||||||
|
``posixpath.normpath`` drops the slash from a bare Windows drive root and
|
||||||
|
collapses traversal above that root into a relative-looking path. Handle
|
||||||
|
drive-qualified absolute and UNC paths component by component so those
|
||||||
|
spellings remain absolute, and fail closed when ``..`` would cross the
|
||||||
|
drive or share root. Drive-relative forms such as ``C:tmp`` also fail
|
||||||
|
closed because their resolution depends on the process's remembered
|
||||||
|
directory for that drive.
|
||||||
|
"""
|
||||||
|
slash_path = path.replace("\\", "/")
|
||||||
|
if slash_path.startswith(("//?/", "//./")):
|
||||||
|
# Win32 device namespaces can alias ordinary drive/UNC paths, but the
|
||||||
|
# lexical checker has no filesystem provenance to resolve them.
|
||||||
|
return None
|
||||||
|
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||||||
|
return None
|
||||||
|
if _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return None
|
||||||
|
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||||||
|
unc_parts = slash_path[2:].split("/")
|
||||||
|
share_root = unc_parts[:2]
|
||||||
|
parts: list[str] = []
|
||||||
|
for part in unc_parts[2:]:
|
||||||
|
if not part or part == ".":
|
||||||
|
continue
|
||||||
|
if part == "..":
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
parts.pop()
|
||||||
|
else:
|
||||||
|
parts.append(part)
|
||||||
|
normalized = "//" + "/".join((*share_root, *parts))
|
||||||
|
return normalized, True
|
||||||
|
if not _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return posixpath.normpath(slash_path), False
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
for part in slash_path[3:].split("/"):
|
||||||
|
if not part or part == ".":
|
||||||
|
continue
|
||||||
|
if part == "..":
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
parts.pop()
|
||||||
|
else:
|
||||||
|
parts.append(part)
|
||||||
|
normalized = slash_path[:2] + "/" + "/".join(parts)
|
||||||
|
return normalized, True
|
||||||
|
|
||||||
|
|
||||||
def _cd_target_in_scope(target: str, thread_data: Mapping[str, Any] | None) -> bool:
|
def _cd_target_in_scope(target: str, thread_data: Mapping[str, Any] | None) -> bool:
|
||||||
"""Whether a preceding ``cd`` target provably keeps the criterion's
|
"""Whether a preceding ``cd`` target provably keeps the criterion's
|
||||||
relative path-like targets resolving inside the thread's data roots.
|
relative path-like targets resolving inside the thread's data roots.
|
||||||
@ -508,16 +596,38 @@ def _cd_target_in_scope(target: str, thread_data: Mapping[str, Any] | None) -> b
|
|||||||
"""
|
"""
|
||||||
if not target or target == "-" or target.startswith("~"):
|
if not target or target == "-" or target.startswith("~"):
|
||||||
return False
|
return False
|
||||||
normalized = os.path.normpath(target.replace("\\", "/"))
|
if ".." in target.replace("\\", "/").split("/"):
|
||||||
if normalized.startswith("/"):
|
# Lexical cleanup is not proof of containment: the OS follows a
|
||||||
|
# directory symlink before resolving the following ``..`` component.
|
||||||
|
return False
|
||||||
|
# After shell-dependent spellings have failed closed, keep deterministic
|
||||||
|
# POSIX lexical semantics even when this checker itself runs on Windows.
|
||||||
|
normalized_target = _normalize_cd_scope_path(target)
|
||||||
|
if normalized_target is None:
|
||||||
|
return False
|
||||||
|
normalized, is_windows_absolute = normalized_target
|
||||||
|
if _thread_uses_windows_paths(thread_data) and normalized.startswith("/") and not is_windows_absolute:
|
||||||
|
# ``/mnt/...`` is absolute under Bash but drive-rooted under
|
||||||
|
# PowerShell. Without the executing shell, a Windows workspace cannot
|
||||||
|
# prove that the virtual POSIX spelling stayed inside its data root.
|
||||||
|
return False
|
||||||
|
if normalized.startswith("/") or is_windows_absolute:
|
||||||
roots = [VIRTUAL_PATH_PREFIX]
|
roots = [VIRTUAL_PATH_PREFIX]
|
||||||
for key in ("workspace_path", "outputs_path", "uploads_path"):
|
for key in ("workspace_path", "outputs_path", "uploads_path"):
|
||||||
value = (thread_data or {}).get(key)
|
value = (thread_data or {}).get(key)
|
||||||
if isinstance(value, str) and value:
|
if isinstance(value, str) and value:
|
||||||
roots.append(value)
|
roots.append(value)
|
||||||
for root in roots:
|
for root in roots:
|
||||||
normalized_root = os.path.normpath(root.replace("\\", "/"))
|
normalized_root_result = _normalize_cd_scope_path(root)
|
||||||
if normalized == normalized_root or normalized.startswith(normalized_root + "/"):
|
if normalized_root_result is None:
|
||||||
|
continue
|
||||||
|
normalized_root, root_is_windows_absolute = normalized_root_result
|
||||||
|
use_windows_comparison = is_windows_absolute and root_is_windows_absolute
|
||||||
|
candidate_for_comparison = ntpath.normcase(normalized) if use_windows_comparison else normalized
|
||||||
|
root_for_comparison = ntpath.normcase(normalized_root) if use_windows_comparison else normalized_root
|
||||||
|
separator = "\\" if use_windows_comparison else "/"
|
||||||
|
root_prefix = root_for_comparison if root_for_comparison.endswith(separator) else root_for_comparison + separator
|
||||||
|
if candidate_for_comparison == root_for_comparison or candidate_for_comparison.startswith(root_prefix):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
return ".." not in normalized.split("/")
|
return ".." not in normalized.split("/")
|
||||||
@ -605,26 +715,175 @@ def _negated_value(token: str) -> str:
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def _negation_overlaps(criterion_token: str, negated_value: str) -> bool:
|
def _thread_uses_windows_paths(thread_data: Mapping[str, Any] | None) -> bool:
|
||||||
|
"""Whether the thread roots establish Windows filesystem semantics."""
|
||||||
|
for key in ("workspace_path", "outputs_path", "uploads_path"):
|
||||||
|
value = (thread_data or {}).get(key)
|
||||||
|
if not isinstance(value, str):
|
||||||
|
continue
|
||||||
|
slash_path = value.replace("\\", "/")
|
||||||
|
if _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path) or _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _selection_path_parts(value: str) -> tuple[str, str | None]:
|
||||||
|
"""Split a runner selection into its filesystem path and pytest nodeid."""
|
||||||
|
provider_match = _POWERSHELL_PROVIDER_QUALIFIED_RE.match(value)
|
||||||
|
nodeid_start = provider_match.end() if provider_match is not None else 0
|
||||||
|
marker_index = value.find("::", nodeid_start)
|
||||||
|
if marker_index < 0:
|
||||||
|
return value, None
|
||||||
|
return value[:marker_index], value[marker_index + 2 :]
|
||||||
|
|
||||||
|
|
||||||
|
def _without_powershell_provider(path: str) -> str:
|
||||||
|
"""Return the rooted portion of a provider-qualified filesystem path."""
|
||||||
|
provider_match = _POWERSHELL_PROVIDER_QUALIFIED_RE.match(path)
|
||||||
|
if provider_match is None:
|
||||||
|
return path
|
||||||
|
return path[provider_match.start("path") :]
|
||||||
|
|
||||||
|
|
||||||
|
def _has_parent_path_component(value: str) -> bool:
|
||||||
|
path, _nodeid = _selection_path_parts(value)
|
||||||
|
return ".." in path.replace("\\", "/").split("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _uses_windows_selection_semantics(value: str, *, windows_path_context: bool) -> bool:
|
||||||
|
path, _nodeid = _selection_path_parts(value)
|
||||||
|
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||||||
|
return bool(windows_path_context or _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) or _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path))
|
||||||
|
|
||||||
|
|
||||||
|
def _selection_path_kind(value: str) -> str:
|
||||||
|
path, _nodeid = _selection_path_parts(value)
|
||||||
|
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||||||
|
if _WINDOWS_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return "windows_drive_absolute"
|
||||||
|
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return "windows_unc_absolute"
|
||||||
|
if slash_path.startswith("/"):
|
||||||
|
return "posix_absolute"
|
||||||
|
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path) and not _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||||||
|
if _POWERSHELL_DRIVE_ABSOLUTE_RE.match(slash_path):
|
||||||
|
return "powershell_drive_absolute"
|
||||||
|
return "powershell_drive_relative"
|
||||||
|
return "relative"
|
||||||
|
|
||||||
|
|
||||||
|
def _windows_volume_identifier(value: str) -> tuple[str, str] | None:
|
||||||
|
"""Return a comparable drive, PSDrive, or UNC-root identifier."""
|
||||||
|
path, _nodeid = _selection_path_parts(value)
|
||||||
|
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||||||
|
if _WINDOWS_DRIVE_QUALIFIED_RE.match(slash_path):
|
||||||
|
return "drive", ntpath.normcase(slash_path[:2])
|
||||||
|
if _WINDOWS_UNC_ABSOLUTE_RE.match(slash_path):
|
||||||
|
server, share, *_rest = slash_path[2:].split("/")
|
||||||
|
return "unc", ntpath.normcase(f"//{server}/{share}")
|
||||||
|
if _POWERSHELL_DRIVE_QUALIFIED_RE.match(slash_path):
|
||||||
|
drive, _separator, _rest = slash_path.partition(":")
|
||||||
|
return "psdrive", ntpath.normcase(drive)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_ambiguous_windows_component(value: str, *, windows_path_context: bool) -> bool:
|
||||||
|
"""Whether ordinary Win32 cleanup may alias a textual path component.
|
||||||
|
|
||||||
|
Windows APIs normally discard trailing spaces and periods from path
|
||||||
|
components, while extended-length paths can preserve them. Generated 8.3
|
||||||
|
short names can also identify a longer component without any lexical
|
||||||
|
relationship. Without shell and filesystem provenance, these spellings
|
||||||
|
must make an overlap decision fail closed.
|
||||||
|
"""
|
||||||
|
if not _uses_windows_selection_semantics(value, windows_path_context=windows_path_context):
|
||||||
|
return False
|
||||||
|
path, _nodeid = _selection_path_parts(value)
|
||||||
|
path = _without_powershell_provider(path)
|
||||||
|
return any(component not in {"", ".", ".."} and (component.endswith((" ", ".")) or _WINDOWS_SHORT_NAME_COMPONENT_RE.fullmatch(component)) for component in path.replace("\\", "/").split("/"))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_selection_path(value: str, *, windows_path_context: bool) -> tuple[str, str | None, bool]:
|
||||||
|
"""Normalize safe lexical aliases without erasing pytest nodeid case.
|
||||||
|
|
||||||
|
Parent traversal is rejected by the caller because resolving ``..``
|
||||||
|
lexically can cross symlinks. ``.`` and duplicate separators are safe to
|
||||||
|
collapse. Drive-qualified and UNC paths always use ``ntpath`` semantics;
|
||||||
|
when the thread roots establish a Windows workspace, every runner path
|
||||||
|
does, including drive-rooted ``/tests`` and mapped virtual paths.
|
||||||
|
"""
|
||||||
|
path, nodeid = _selection_path_parts(value)
|
||||||
|
slash_path = _without_powershell_provider(path).replace("\\", "/")
|
||||||
|
is_windows_path = _uses_windows_selection_semantics(value, windows_path_context=windows_path_context)
|
||||||
|
if is_windows_path:
|
||||||
|
normalized = ntpath.normcase(ntpath.normpath(slash_path))
|
||||||
|
else:
|
||||||
|
normalized = posixpath.normpath(slash_path)
|
||||||
|
return normalized, nodeid, is_windows_path
|
||||||
|
|
||||||
|
|
||||||
|
def _nodeids_overlap(a: str | None, b: str | None) -> bool:
|
||||||
|
if a is None or b is None:
|
||||||
|
return True
|
||||||
|
return a == b or a.startswith(b + "::") or b.startswith(a + "::")
|
||||||
|
|
||||||
|
|
||||||
|
def _negation_overlaps(criterion_token: str, negated_value: str, *, windows_path_context: bool = False) -> bool:
|
||||||
"""Whether a negated value overlaps a matched criterion target: equal, or
|
"""Whether a negated value overlaps a matched criterion target: equal, or
|
||||||
one nested under the other at a path boundary (``tests`` vs
|
one nested under the other at a path boundary (``tests`` vs
|
||||||
``tests/unit/test_auth.py``) or a pytest nodeid boundary (``tests/x.py``
|
``tests/unit/test_auth.py``) or a pytest nodeid boundary (``tests/x.py``
|
||||||
vs ``tests/x.py::test_y``). Overlap means part of the criterion's
|
vs ``tests/x.py::test_y``). Overlap means part of the criterion's
|
||||||
selection never ran, so the passing summary may not cover it; unrelated
|
selection never ran, so the passing summary may not cover it; unrelated
|
||||||
exclusions (``--ignore tests/slow`` against ``pytest tests/unit``) do not
|
exclusions (``--ignore tests/slow`` against ``pytest tests/unit``) do not
|
||||||
overlap and keep matching."""
|
overlap and keep matching. Safe lexical aliases are normalized, while
|
||||||
a = criterion_token.replace("\\", "/").removeprefix("./").rstrip("/")
|
mixed path families fail closed because evidence does not carry enough
|
||||||
b = negated_value.replace("\\", "/").removeprefix("./").rstrip("/")
|
filesystem provenance to prove that their spellings are distinct."""
|
||||||
if not a or not b:
|
a_kind = _selection_path_kind(criterion_token)
|
||||||
|
b_kind = _selection_path_kind(negated_value)
|
||||||
|
if a_kind != b_kind:
|
||||||
|
# Relative selections resolve against an unrecorded cwd, and Windows
|
||||||
|
# can resolve POSIX-rooted spellings against the current drive or map
|
||||||
|
# a drive onto a UNC share. Cross-family spellings can therefore alias
|
||||||
|
# even when their lexical prefixes differ.
|
||||||
|
return True
|
||||||
|
a_volume = _windows_volume_identifier(criterion_token)
|
||||||
|
b_volume = _windows_volume_identifier(negated_value)
|
||||||
|
if a_volume is not None and b_volume is not None and a_volume != b_volume:
|
||||||
|
# Distinct drive letters can alias through SUBST or mapped drives, and
|
||||||
|
# distinct UNC roots can alias through DFS, DNS, or share mappings.
|
||||||
|
# The execution evidence records none of that volume provenance.
|
||||||
|
return True
|
||||||
|
if a_kind == "relative":
|
||||||
|
a_path, _a_nodeid = _selection_path_parts(criterion_token)
|
||||||
|
b_path, _b_nodeid = _selection_path_parts(negated_value)
|
||||||
|
a_is_drive_relative = bool(_WINDOWS_DRIVE_QUALIFIED_RE.match(a_path.replace("\\", "/")))
|
||||||
|
b_is_drive_relative = bool(_WINDOWS_DRIVE_QUALIFIED_RE.match(b_path.replace("\\", "/")))
|
||||||
|
if a_is_drive_relative != b_is_drive_relative:
|
||||||
|
# ``tests/x`` and ``D:tests/x`` can name the same path when the
|
||||||
|
# process cwd and D:'s remembered cwd coincide. Neither value is
|
||||||
|
# absolute, and the execution evidence carries neither cwd.
|
||||||
|
return True
|
||||||
|
a, a_nodeid, a_is_windows = _normalize_selection_path(criterion_token, windows_path_context=windows_path_context)
|
||||||
|
b, b_nodeid, b_is_windows = _normalize_selection_path(negated_value, windows_path_context=windows_path_context)
|
||||||
|
if not a or not b or a == "." or b == ".":
|
||||||
return False
|
return False
|
||||||
return a == b or a.startswith((b + "/", b + "::")) or b.startswith((a + "/", a + "::"))
|
if a == b:
|
||||||
|
return _nodeids_overlap(a_nodeid, b_nodeid)
|
||||||
|
if a_is_windows != b_is_windows:
|
||||||
|
# A drive-relative spelling can resolve to the same path as an
|
||||||
|
# ordinary relative token, but the per-drive cwd is not recorded.
|
||||||
|
return True
|
||||||
|
separator = "\\" if a_is_windows else "/"
|
||||||
|
a_prefix = a if a.endswith(separator) else a + separator
|
||||||
|
b_prefix = b if b.endswith(separator) else b + separator
|
||||||
|
return a.startswith(b_prefix) or b.startswith(a_prefix)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_command(command: str) -> str:
|
def _normalize_command(command: str) -> str:
|
||||||
return " ".join(command.split())
|
return " ".join(command.split())
|
||||||
|
|
||||||
|
|
||||||
def _shell_parse_line(line: str) -> tuple[str | None, list[list[str]], list[str]] | None:
|
def _shell_parse_line(line: str, *, posix: bool = True) -> tuple[str | None, list[list[str]], list[str]] | None:
|
||||||
"""Tokenize one physical line into segments plus the operators joining them.
|
"""Tokenize one physical line into segments plus the operators joining them.
|
||||||
|
|
||||||
Returns ``(leading_op, segments, ops)``: ``ops[i]`` is the operator
|
Returns ``(leading_op, segments, ops)``: ``ops[i]`` is the operator
|
||||||
@ -636,9 +895,11 @@ def _shell_parse_line(line: str) -> tuple[str | None, list[list[str]], list[str]
|
|||||||
``;`` would overstate what provably ran. Comments are stripped (a
|
``;`` would overstate what provably ran. Comments are stripped (a
|
||||||
``# pytest ...`` remark executes nothing) and quotes are honored, so an
|
``# pytest ...`` remark executes nothing) and quotes are honored, so an
|
||||||
operator inside an argument cannot split a segment. Returns ``None`` on
|
operator inside an argument cannot split a segment. Returns ``None`` on
|
||||||
malformed shell (unbalanced quotes).
|
malformed shell (unbalanced quotes). ``posix=False`` is reserved for the
|
||||||
|
raw-token safety pass: it keeps backslashes and surrounding quotes visible
|
||||||
|
before the normal POSIX parse can consume them as escaping syntax.
|
||||||
"""
|
"""
|
||||||
lexer = shlex.shlex(line, posix=True, punctuation_chars=_SHELL_OPERATORS)
|
lexer = shlex.shlex(line, posix=posix, punctuation_chars=_SHELL_OPERATORS)
|
||||||
lexer.whitespace_split = True
|
lexer.whitespace_split = True
|
||||||
lexer.commenters = "#"
|
lexer.commenters = "#"
|
||||||
try:
|
try:
|
||||||
@ -670,7 +931,7 @@ def _shell_parse_line(line: str) -> tuple[str | None, list[list[str]], list[str]
|
|||||||
return leading_op, segments, ops
|
return leading_op, segments, ops
|
||||||
|
|
||||||
|
|
||||||
def _shell_parse(command: str) -> tuple[list[list[str]], list[str]] | None:
|
def _shell_parse(command: str, *, posix: bool = True) -> tuple[list[list[str]], list[str]] | None:
|
||||||
"""Tokenize a shell command into segments plus the operators joining them.
|
"""Tokenize a shell command into segments plus the operators joining them.
|
||||||
|
|
||||||
Physical newlines are command separators with ``;`` semantics — bash
|
Physical newlines are command separators with ``;`` semantics — bash
|
||||||
@ -692,7 +953,7 @@ def _shell_parse(command: str) -> tuple[list[list[str]], list[str]] | None:
|
|||||||
segments: list[list[str]] = []
|
segments: list[list[str]] = []
|
||||||
ops: list[str] = []
|
ops: list[str] = []
|
||||||
for line in command.split("\n"):
|
for line in command.split("\n"):
|
||||||
parsed = _shell_parse_line(line)
|
parsed = _shell_parse_line(line, posix=posix)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
return None
|
return None
|
||||||
leading_op, line_segments, line_ops = parsed
|
leading_op, line_segments, line_ops = parsed
|
||||||
@ -709,6 +970,142 @@ def _shell_parse(command: str) -> tuple[list[list[str]], list[str]] | None:
|
|||||||
return segments, ops
|
return segments, ops
|
||||||
|
|
||||||
|
|
||||||
|
def _has_shell_ambiguous_whitespace(command: str) -> bool:
|
||||||
|
"""Whether whitespace can split differently across supported shells.
|
||||||
|
|
||||||
|
ASCII space, tab, LF, and CRLF are shared separators. PowerShell also
|
||||||
|
separates on bare CR, vertical tab, form feed, NEL, and Unicode separator
|
||||||
|
characters, while Python's POSIX ``shlex`` can retain them inside a token.
|
||||||
|
"""
|
||||||
|
command_without_crlf = command.replace("\r\n", "\n")
|
||||||
|
return any(character in "\r\v\f\x85" or (character != " " and unicodedata.category(character) in {"Zs", "Zl", "Zp"}) for character in command_without_crlf)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_cmd_control_operator_in_single_quotes(command: str) -> bool:
|
||||||
|
"""Whether POSIX single quotes hide cmd.exe control syntax.
|
||||||
|
|
||||||
|
Cmd does not use single quotes for grouping, so metacharacters within them
|
||||||
|
remain active there. Ordinary single-quoted text stays verifiable for the
|
||||||
|
POSIX execution path. Shell-specific ways to escape quotes are already
|
||||||
|
rejected by the other provenance checks below.
|
||||||
|
"""
|
||||||
|
in_double_quotes = False
|
||||||
|
in_single_quotes = False
|
||||||
|
for character in command:
|
||||||
|
if character == '"':
|
||||||
|
in_double_quotes = not in_double_quotes
|
||||||
|
elif character == "'" and not in_double_quotes:
|
||||||
|
in_single_quotes = not in_single_quotes
|
||||||
|
elif in_single_quotes and character in "&|<>()":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _has_unquoted_parenthesis(command: str) -> bool:
|
||||||
|
"""Whether a parenthesis appears outside a quoted string.
|
||||||
|
|
||||||
|
PowerShell evaluates an unquoted parenthesized command expression and
|
||||||
|
expands its output into native arguments. POSIX tokenization instead
|
||||||
|
leaves the parentheses attached to ordinary tokens, which can hide an
|
||||||
|
injected runner option. Parentheses inside double- or single-quoted
|
||||||
|
strings are data; cmd-specific control syntax inside single quotes is
|
||||||
|
rejected separately because cmd does not honor those quotes.
|
||||||
|
"""
|
||||||
|
in_double_quotes = False
|
||||||
|
in_single_quotes = False
|
||||||
|
for character in command:
|
||||||
|
if character == '"' and not in_single_quotes:
|
||||||
|
in_double_quotes = not in_double_quotes
|
||||||
|
elif character == "'" and not in_double_quotes:
|
||||||
|
in_single_quotes = not in_single_quotes
|
||||||
|
elif character in "()" and not in_double_quotes and not in_single_quotes:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _has_unquoted_single_quote(text: str) -> bool:
|
||||||
|
"""Whether *text* contains a single quote outside double quotes.
|
||||||
|
|
||||||
|
The raw ``posix=False`` token stream retains surrounding quotes. Cmd.exe
|
||||||
|
passes a POSIX single quote to the child process instead of using it for
|
||||||
|
grouping, so such a token cannot be compared with its quote-stripped
|
||||||
|
POSIX form without knowing which shell executed it.
|
||||||
|
"""
|
||||||
|
in_double_quotes = False
|
||||||
|
for character in text:
|
||||||
|
if character == '"':
|
||||||
|
in_double_quotes = not in_double_quotes
|
||||||
|
elif character == "'" and not in_double_quotes:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_segment_has_unquoted_single_quote(segment: list[str]) -> bool:
|
||||||
|
return any(_has_unquoted_single_quote(token) for token in segment)
|
||||||
|
|
||||||
|
|
||||||
|
def _matched_span_has_ambiguous_single_quotes(
|
||||||
|
expected_raw: list[list[str]],
|
||||||
|
actual_raw: list[list[str]],
|
||||||
|
actual: list[list[str]],
|
||||||
|
*,
|
||||||
|
start: int,
|
||||||
|
span: int,
|
||||||
|
thread_data: Mapping[str, Any] | None,
|
||||||
|
) -> bool:
|
||||||
|
"""Whether cmd.exe single-quote semantics can change a candidate match.
|
||||||
|
|
||||||
|
Every expected segment and the matching actual span define the invocation
|
||||||
|
being certified, so a POSIX single quote in any of them is ambiguous.
|
||||||
|
Before the span, only segments accepted as provably silent can contribute
|
||||||
|
to a successful match; a quoted ``cd`` or assignment can change the state
|
||||||
|
in which the runner executes and therefore also requires shell provenance.
|
||||||
|
Non-silent prefixes remain governed by output-attribution checks, preserving
|
||||||
|
authoritative failure results such as ``echo '12 passed'; make test``.
|
||||||
|
"""
|
||||||
|
if any(_raw_segment_has_unquoted_single_quote(segment) for segment in expected_raw):
|
||||||
|
return True
|
||||||
|
if any(_raw_segment_has_unquoted_single_quote(segment) for segment in actual_raw[start : start + span]):
|
||||||
|
return True
|
||||||
|
return any(_raw_segment_has_unquoted_single_quote(actual_raw[index]) and _is_silent_segment(actual[index], thread_data) for index in range(start))
|
||||||
|
|
||||||
|
|
||||||
|
def _command_requires_shell_provenance(command: str) -> bool:
|
||||||
|
"""Whether raw command tokens have shell-dependent Windows semantics.
|
||||||
|
|
||||||
|
The acceptance evidence does not currently record which shell executed a
|
||||||
|
command. Inspect a non-POSIX tokenization before the authoritative POSIX
|
||||||
|
parser can discard backslashes anywhere in the candidate command: PowerShell
|
||||||
|
and native Windows runners preserve them as path separators while POSIX
|
||||||
|
shells treat them as escapes. Cmd percent/bang references and ``^``, Bash
|
||||||
|
tilde/brace expansion, and PowerShell splatting, typographic quotes, or
|
||||||
|
unquoted parentheses can all rewrite the native argv differently. ``#``
|
||||||
|
starts a comment for the POSIX parser but is an ordinary argument to
|
||||||
|
cmd.exe. Cmd also does not treat single quotes as quoting, so cmd control
|
||||||
|
syntax hidden inside POSIX single quotes is unsafe. PowerShell recognizes
|
||||||
|
more separators than POSIX ``shlex``, including bare carriage returns and
|
||||||
|
Unicode separator characters; normal CRLF remains unambiguous. These forms
|
||||||
|
fail closed rather than certifying arguments that depend on an unknown
|
||||||
|
shell.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
_has_shell_ambiguous_whitespace(command)
|
||||||
|
or _has_cmd_control_operator_in_single_quotes(command)
|
||||||
|
or "#" in command
|
||||||
|
or "^" in command
|
||||||
|
or _CMD_DELAYED_ENV_EXPANSION_RE.search(command)
|
||||||
|
or _BASH_BRACE_EXPANSION_RE.search(command)
|
||||||
|
or any(quote in command for quote in _POWERSHELL_QUOTE_DELIMITERS)
|
||||||
|
or _has_unquoted_parenthesis(command)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
parsed = _shell_parse(command, posix=False)
|
||||||
|
if parsed is None:
|
||||||
|
return "\\" in command or bool(_CMD_ENV_EXPANSION_RE.search(command))
|
||||||
|
segments, _ops = parsed
|
||||||
|
return any("\\" in token or token.startswith(("~", "@")) or (token.startswith("(") and token.endswith(")")) or _CMD_ENV_EXPANSION_RE.search(token) for segment in segments for token in segment)
|
||||||
|
|
||||||
|
|
||||||
def _strip_env_assignments(tokens: list[str]) -> list[str]:
|
def _strip_env_assignments(tokens: list[str]) -> list[str]:
|
||||||
index = 0
|
index = 0
|
||||||
while index < len(tokens) and _ENV_ASSIGNMENT_RE.match(tokens[index]):
|
while index < len(tokens) and _ENV_ASSIGNMENT_RE.match(tokens[index]):
|
||||||
@ -878,7 +1275,7 @@ def _normalize_executable(token: str) -> str:
|
|||||||
return os.path.normpath(token.replace("\\", "/"))
|
return os.path.normpath(token.replace("\\", "/"))
|
||||||
|
|
||||||
|
|
||||||
def _segment_matches(expected: list[str], actual: list[str]) -> str:
|
def _segment_matches(expected: list[str], actual: list[str], *, windows_path_context: bool = False) -> str:
|
||||||
"""Match one segment against the criterion's, classifying extra flags.
|
"""Match one segment against the criterion's, classifying extra flags.
|
||||||
|
|
||||||
Returns ``"match"`` when the executable agrees — directional: a bare
|
Returns ``"match"`` when the executable agrees — directional: a bare
|
||||||
@ -1003,7 +1400,14 @@ def _segment_matches(expected: list[str], actual: list[str]) -> str:
|
|||||||
# before resolving ``..``, so a textually unrelated value can name
|
# before resolving ``..``, so a textually unrelated value can name
|
||||||
# the criterion's target).
|
# the criterion's target).
|
||||||
return "unprovable"
|
return "unprovable"
|
||||||
if any(_negation_overlaps(actual[position], value) for position in consumed if position != 0 for value in negated_values):
|
compared_values = [actual[position] for position in consumed if position != 0] + negated_values
|
||||||
|
if negated_values and any(_has_ambiguous_windows_component(value, windows_path_context=windows_path_context) for value in compared_values):
|
||||||
|
return "unprovable"
|
||||||
|
if negated_values and any(_has_parent_path_component(actual[position]) for position in consumed if position != 0):
|
||||||
|
# The matched target itself may normalize onto an excluded path, but
|
||||||
|
# collapsing its ``..`` components would be unsound across symlinks.
|
||||||
|
return "unprovable"
|
||||||
|
if any(_negation_overlaps(actual[position], value, windows_path_context=windows_path_context) for position in consumed if position != 0 for value in negated_values):
|
||||||
return "unprovable"
|
return "unprovable"
|
||||||
for position, token in enumerate(actual):
|
for position, token in enumerate(actual):
|
||||||
if position in consumed or position in negated or position in option_positions:
|
if position in consumed or position in negated or position in option_positions:
|
||||||
@ -1096,7 +1500,13 @@ def _criterion_connectors_preserved(expected_ops: list[str], executed_within_ops
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _commands_match(criterion_command: str, executed_command: str, *, executed_success: bool) -> str:
|
def _commands_match(
|
||||||
|
criterion_command: str,
|
||||||
|
executed_command: str,
|
||||||
|
*,
|
||||||
|
executed_success: bool,
|
||||||
|
thread_data: Mapping[str, Any] | None = None,
|
||||||
|
) -> str:
|
||||||
"""Shell-structure match with control-flow attribution.
|
"""Shell-structure match with control-flow attribution.
|
||||||
|
|
||||||
Returns ``"match"`` when the criterion's segment sequence appears as
|
Returns ``"match"`` when the criterion's segment sequence appears as
|
||||||
@ -1110,23 +1520,35 @@ def _commands_match(criterion_command: str, executed_command: str, *, executed_s
|
|||||||
"""
|
"""
|
||||||
expected_parsed = _shell_parse(criterion_command)
|
expected_parsed = _shell_parse(criterion_command)
|
||||||
actual_parsed = _shell_parse(executed_command)
|
actual_parsed = _shell_parse(executed_command)
|
||||||
|
expected_raw_parsed = _shell_parse(criterion_command, posix=False)
|
||||||
|
actual_raw_parsed = _shell_parse(executed_command, posix=False)
|
||||||
|
needs_shell_provenance = _command_requires_shell_provenance(criterion_command) or _command_requires_shell_provenance(executed_command)
|
||||||
if expected_parsed is None or actual_parsed is None:
|
if expected_parsed is None or actual_parsed is None:
|
||||||
# Malformed shell: only exact normalized equality survives.
|
# Malformed shell: only exact normalized equality survives.
|
||||||
expected_norm = _normalize_command(criterion_command)
|
expected_norm = _normalize_command(criterion_command)
|
||||||
return "match" if expected_norm and expected_norm == _normalize_command(executed_command) else "no_match"
|
if not expected_norm or expected_norm != _normalize_command(executed_command):
|
||||||
|
return "no_match"
|
||||||
|
has_ambiguous_single_quote = _has_unquoted_single_quote(criterion_command) or _has_unquoted_single_quote(executed_command)
|
||||||
|
return "unprovable" if needs_shell_provenance or has_ambiguous_single_quote else "match"
|
||||||
expected, expected_ops = expected_parsed
|
expected, expected_ops = expected_parsed
|
||||||
actual, ops = actual_parsed
|
actual, ops = actual_parsed
|
||||||
if not expected or not actual or len(expected) > len(actual):
|
if not expected or not actual or len(expected) > len(actual):
|
||||||
return "no_match"
|
return "no_match"
|
||||||
|
raw_segments_align = expected_raw_parsed is not None and actual_raw_parsed is not None and len(expected_raw_parsed[0]) == len(expected) and len(actual_raw_parsed[0]) == len(actual)
|
||||||
|
if not raw_segments_align:
|
||||||
|
needs_shell_provenance = True
|
||||||
|
expected_raw = expected_raw_parsed[0] if raw_segments_align and expected_raw_parsed is not None else []
|
||||||
|
actual_raw = actual_raw_parsed[0] if raw_segments_align and actual_raw_parsed is not None else []
|
||||||
span = len(expected)
|
span = len(expected)
|
||||||
saw_unprovable = False
|
saw_unprovable = False
|
||||||
|
windows_path_context = _thread_uses_windows_paths(thread_data)
|
||||||
for start in range(len(actual) - span + 1):
|
for start in range(len(actual) - span + 1):
|
||||||
if any(_segment_pollutes_state(segment) for segment in actual[:start]):
|
if any(_segment_pollutes_state(segment) for segment in actual[:start]):
|
||||||
# A preceding segment mutated shell state the matcher cannot see
|
# A preceding segment mutated shell state the matcher cannot see
|
||||||
# (PATH/exports): nothing later is provable.
|
# (PATH/exports): nothing later is provable.
|
||||||
saw_unprovable = True
|
saw_unprovable = True
|
||||||
continue
|
continue
|
||||||
outcomes = [_segment_matches(expected[i], actual[start + i]) for i in range(span)]
|
outcomes = [_segment_matches(expected[i], actual[start + i], windows_path_context=windows_path_context) for i in range(span)]
|
||||||
if any(outcome == "no_match" for outcome in outcomes):
|
if any(outcome == "no_match" for outcome in outcomes):
|
||||||
continue
|
continue
|
||||||
if any(outcome == "unprovable" for outcome in outcomes):
|
if any(outcome == "unprovable" for outcome in outcomes):
|
||||||
@ -1143,6 +1565,16 @@ def _commands_match(criterion_command: str, executed_command: str, *, executed_s
|
|||||||
saw_unprovable = True
|
saw_unprovable = True
|
||||||
continue
|
continue
|
||||||
if _span_attributable(ops[:start], ops[start : start + span - 1], ops[start + span - 1 :], executed_success):
|
if _span_attributable(ops[:start], ops[start : start + span - 1], ops[start + span - 1 :], executed_success):
|
||||||
|
if needs_shell_provenance or _matched_span_has_ambiguous_single_quotes(
|
||||||
|
expected_raw,
|
||||||
|
actual_raw,
|
||||||
|
actual,
|
||||||
|
start=start,
|
||||||
|
span=span,
|
||||||
|
thread_data=thread_data,
|
||||||
|
):
|
||||||
|
saw_unprovable = True
|
||||||
|
continue
|
||||||
return "match"
|
return "match"
|
||||||
saw_unprovable = True
|
saw_unprovable = True
|
||||||
return "unprovable" if saw_unprovable else "no_match"
|
return "unprovable" if saw_unprovable else "no_match"
|
||||||
@ -1190,7 +1622,12 @@ def _check_tests_passed_leaf(command: str, bash_executions: list[dict[str, Any]]
|
|||||||
matches: list[tuple[str, dict[str, Any]]] = []
|
matches: list[tuple[str, dict[str, Any]]] = []
|
||||||
for execution in bash_executions or []:
|
for execution in bash_executions or []:
|
||||||
status = str(execution.get("status") or "")
|
status = str(execution.get("status") or "")
|
||||||
outcome = _commands_match(command, str(execution.get("command") or ""), executed_success=status == "success")
|
outcome = _commands_match(
|
||||||
|
command,
|
||||||
|
str(execution.get("command") or ""),
|
||||||
|
executed_success=status == "success",
|
||||||
|
thread_data=thread_data,
|
||||||
|
)
|
||||||
if outcome == "match" and execution.get("command_truncated"):
|
if outcome == "match" and execution.get("command_truncated"):
|
||||||
# The recorded command lost its suffix to the evidence cap; a
|
# The recorded command lost its suffix to the evidence cap; a
|
||||||
# selection-changing tail (``-k smoke``) may have been cut away.
|
# selection-changing tail (``-k smoke``) may have been cut away.
|
||||||
|
|||||||
@ -24,6 +24,12 @@ THREAD_DATA = {
|
|||||||
"outputs_path": "/ws/thread/user-data/outputs",
|
"outputs_path": "/ws/thread/user-data/outputs",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WINDOWS_THREAD_DATA = {
|
||||||
|
"workspace_path": "D:/WS",
|
||||||
|
"uploads_path": "D:/WS/uploads",
|
||||||
|
"outputs_path": "D:/WS/outputs",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _reader(files: dict[str, str]):
|
def _reader(files: dict[str, str]):
|
||||||
def read(_runtime, path: str) -> str:
|
def read(_runtime, path: str) -> str:
|
||||||
@ -124,7 +130,7 @@ class TestFileLeaves:
|
|||||||
|
|
||||||
from deerflow.sandbox.tools import _resolve_local_read_path
|
from deerflow.sandbox.tools import _resolve_local_read_path
|
||||||
|
|
||||||
assert Path(_resolve_local_read_path(seen[0], THREAD_DATA)) == Path("/ws/thread/user-data/outputs/report.md") # type: ignore[arg-type]
|
assert Path(_resolve_local_read_path(seen[0], THREAD_DATA)) == Path("/ws/thread/user-data/outputs/report.md").resolve() # type: ignore[arg-type]
|
||||||
|
|
||||||
def test_non_empty_fails_on_empty_file(self):
|
def test_non_empty_fails_on_empty_file(self):
|
||||||
files = {"/mnt/user-data/outputs/report.md": ""}
|
files = {"/mnt/user-data/outputs/report.md": ""}
|
||||||
@ -656,7 +662,7 @@ class TestProbeInnerScriptRealLayouts:
|
|||||||
(outputs / "report.md").write_text("hello", encoding="utf-8")
|
(outputs / "report.md").write_text("hello", encoding="utf-8")
|
||||||
assert self._run_read_probe(str(outputs / "report.md"), str(outputs)) == "READABLE"
|
assert self._run_read_probe(str(outputs / "report.md"), str(outputs)) == "READABLE"
|
||||||
|
|
||||||
@pytest.mark.skipif(os.geteuid() == 0, reason="root reads through mode-000")
|
@pytest.mark.skipif(os.name == "nt" or os.geteuid() == 0, reason="mode-000 readability needs POSIX permissions and a non-root euid")
|
||||||
def test_read_probe_mode_000_is_unreadable(self, tmp_path):
|
def test_read_probe_mode_000_is_unreadable(self, tmp_path):
|
||||||
outputs = tmp_path / "outputs"
|
outputs = tmp_path / "outputs"
|
||||||
outputs.mkdir()
|
outputs.mkdir()
|
||||||
@ -1156,6 +1162,120 @@ class TestTestsPassedLeaf:
|
|||||||
|
|
||||||
assert verdict["leaves"][0]["holds"] is True
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_crlf_execution_with_test_command_last_still_matches(self):
|
||||||
|
"""A normal Windows CRLF command record has the same boundaries as
|
||||||
|
its LF equivalent and must not require shell provenance."""
|
||||||
|
executions = [_bash_execution("cd backend\r\npytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_bare_carriage_return_is_unprovable_before_powershell_line_split(self):
|
||||||
|
"""PowerShell treats a standalone CR as a command boundary, while a
|
||||||
|
POSIX parser may absorb it as whitespace and attribute a forged
|
||||||
|
passing summary to the preceding test run."""
|
||||||
|
executions = [_bash_execution("pytest tests/security\rWrite-Output '3 passed'", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("separator", ("\v", "\f", "\x85", "\u00a0", "\u2028", "\u2029"))
|
||||||
|
def test_powershell_only_whitespace_cannot_hide_runner_arguments(self, separator):
|
||||||
|
"""PowerShell separates arguments on these characters, while POSIX
|
||||||
|
shlex can absorb them into one apparently harmless token."""
|
||||||
|
command = f"pytest tests/security tests/unit{separator}--ignore{separator}tests/security"
|
||||||
|
executions = [_bash_execution(command, output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_single_quote_cannot_hide_a_control_operator(self):
|
||||||
|
"""cmd.exe does not quote ``&`` with single quotes, even though POSIX
|
||||||
|
shlex would hide the operator inside one argument."""
|
||||||
|
executions = [_bash_execution("pytest tests/security '& echo 3 passed'", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_single_quotes_cannot_change_matched_runner_argv(self):
|
||||||
|
"""POSIX removes ordinary single quotes, while cmd.exe passes them
|
||||||
|
through as part of the runner argument."""
|
||||||
|
executions = [_bash_execution("pytest 'tests/security'", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_single_quotes_in_criterion_are_unprovable(self):
|
||||||
|
executions = [_bash_execution("pytest tests/security", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest 'tests/security'"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_single_quotes_in_preceding_cd_are_unprovable(self):
|
||||||
|
"""A quoted POSIX ``cd`` target is not the same path under cmd.exe,
|
||||||
|
so it cannot establish the runner's working directory."""
|
||||||
|
executions = [_bash_execution("cd '/mnt/user-data/workspace' && pytest tests/security", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(
|
||||||
|
["tests_passed:pytest tests/security"],
|
||||||
|
thread_data=THREAD_DATA,
|
||||||
|
bash_executions=executions,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_mismatched_posix_and_raw_segments_are_unprovable(self):
|
||||||
|
"""A tokenizer disagreement must fail closed instead of indexing a
|
||||||
|
raw segment list that could not be produced."""
|
||||||
|
executions = [_bash_execution('cd foo";"bar && pytest tests/', output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], bash_executions=executions)
|
||||||
|
|
||||||
|
leaf = verdict["leaves"][0]
|
||||||
|
assert leaf["checked"] is False
|
||||||
|
assert leaf["holds"] is False
|
||||||
|
|
||||||
|
def test_powershell_parenthesized_expression_cannot_inject_runner_arguments(self):
|
||||||
|
"""PowerShell expands a parenthesized command expression into native
|
||||||
|
arguments, while POSIX tokenization leaves the closing parenthesis on
|
||||||
|
the exclusion path and can miss that the required target was skipped."""
|
||||||
|
command = 'pytest tests/security tests/unit (Write-Output "--ignore" "tests/security")'
|
||||||
|
executions = [_bash_execution(command, output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
leaf = verdict["leaves"][0]
|
||||||
|
assert leaf["checked"] is False
|
||||||
|
assert leaf["holds"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("opening_quote", "closing_quote"),
|
||||||
|
[("“", "”"), ("‘", "’"), ("‚", "‚"), ("‛", "‛"), ("„", "„")],
|
||||||
|
)
|
||||||
|
def test_powershell_quote_delimiters_cannot_hide_runner_exclusions(self, opening_quote: str, closing_quote: str):
|
||||||
|
command = f"pytest tests/security tests/unit {opening_quote}--deselect=tests/security/test_auth.py::test_required{closing_quote}"
|
||||||
|
executions = [_bash_execution(command, output_tail="3 passed, 1 deselected")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
leaf = verdict["leaves"][0]
|
||||||
|
assert leaf["checked"] is False
|
||||||
|
assert leaf["holds"] is False
|
||||||
|
|
||||||
|
def test_shared_whitespace_and_double_quotes_remain_verifiable(self):
|
||||||
|
executions = [_bash_execution('pytest\t"tests/security" -q', output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_parentheses_inside_double_quoted_path_remain_verifiable(self):
|
||||||
|
executions = [_bash_execution('pytest "tests/(security)"', output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(['tests_passed:pytest "tests/(security)"'], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_apostrophe_inside_double_quotes_remains_verifiable(self):
|
||||||
|
executions = [_bash_execution('pytest "tests/O\'Brien" -q', output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(['tests_passed:pytest "tests/O\'Brien"'], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
def test_multiline_background_operator_stays_unprovable(self):
|
def test_multiline_background_operator_stays_unprovable(self):
|
||||||
"""A trailing ``&`` at end of a line still separates (and backgrounds)
|
"""A trailing ``&`` at end of a line still separates (and backgrounds)
|
||||||
the next line's command."""
|
the next line's command."""
|
||||||
@ -1190,6 +1310,151 @@ class TestTestsPassedLeaf:
|
|||||||
|
|
||||||
assert verdict["leaves"][0]["checked"] is False
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"target",
|
||||||
|
(
|
||||||
|
r"..\..\tmp",
|
||||||
|
r"'\mnt\user-data\workspace\fake'",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_cd_with_backslashes_is_unprovable_before_posix_tokenization(self, target):
|
||||||
|
"""Shell provenance is absent, so a backslash-bearing ``cd`` target
|
||||||
|
cannot be interpreted safely as either POSIX escaping or a Windows
|
||||||
|
path separator."""
|
||||||
|
executions = [_bash_execution(f"cd {target} && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_runner_backslash_negation_is_unprovable_before_posix_tokenization(self):
|
||||||
|
"""PowerShell preserves the separator in ``tests\\security``, while
|
||||||
|
POSIX ``shlex`` consumes it and could hide that the required target was
|
||||||
|
excluded from the recorded run."""
|
||||||
|
executions = [_bash_execution(r"pytest tests/security tests/unit --ignore tests\security", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_hash_argument_is_unprovable_before_comment_stripping(self):
|
||||||
|
"""``cmd.exe`` passes ``#`` as an ordinary argument, while POSIX
|
||||||
|
parsing treats it as the start of a comment and can hide a following
|
||||||
|
exclusion from the acceptance matcher."""
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit # --ignore tests/security", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cmd_caret_escape_is_unprovable_before_posix_tokenization(self):
|
||||||
|
"""``cmd.exe`` removes ``^`` escaping before invoking the runner, so
|
||||||
|
the literal token seen by the matcher may hide an exclusion alias."""
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore tests^/security", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"command",
|
||||||
|
(
|
||||||
|
"pytest tests/security tests/unit {--ignore,tests/security}",
|
||||||
|
'pytest tests/security tests/unit @("--ignore","tests/security")',
|
||||||
|
'pytest tests/security tests/unit ("--ignore","tests/security")',
|
||||||
|
'pytest tests/security tests/unit @("--ignore=tests/security")',
|
||||||
|
'pytest tests/security tests/unit ("--ignore=tests/security")',
|
||||||
|
"pytest tests/security tests/unit @pytestArgs",
|
||||||
|
"pytest tests/security tests/unit --ignore ~/repo/tests/security",
|
||||||
|
"pytest tests/security tests/unit --ignore !TARGET!",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_shell_expansion_syntax_is_unprovable_before_tokenization(self, command):
|
||||||
|
"""Bash brace/tilde expansion and PowerShell splatting/arrays can
|
||||||
|
inject or rewrite runner arguments before the process is launched."""
|
||||||
|
executions = [_bash_execution(command, output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ("%TEMP%", "%USERPROFILE%/fake"))
|
||||||
|
def test_cd_with_cmd_environment_expansion_is_unprovable(self, target):
|
||||||
|
executions = [_bash_execution(f"cd {target} && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cd_with_unpaired_percent_remains_a_literal_relative_path(self):
|
||||||
|
executions = [_bash_execution("cd reports/100% && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_cd_to_out_of_scope_windows_drive_path_is_unprovable(self):
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "D:/ws/thread/user-data/workspace",
|
||||||
|
"outputs_path": "D:/ws/thread/user-data/outputs",
|
||||||
|
}
|
||||||
|
executions = [_bash_execution("cd D:/tmp/fake && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ("FileSystem::C:/tmp", "External:/tmp"))
|
||||||
|
def test_cd_to_powershell_provider_or_psdrive_path_is_unprovable(self, target):
|
||||||
|
"""Provider-qualified and named-PSDrive paths are absolute in
|
||||||
|
PowerShell but look relative to the POSIX path normalizer."""
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "D:/ws/thread/user-data/workspace",
|
||||||
|
"outputs_path": "D:/ws/thread/user-data/outputs",
|
||||||
|
}
|
||||||
|
executions = [_bash_execution(f"cd {target} && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ("C:", "C:tmp", "C:../tmp"))
|
||||||
|
def test_cd_to_windows_drive_relative_path_is_unprovable(self, target):
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "D:/ws/thread/user-data/workspace",
|
||||||
|
"outputs_path": "D:/ws/thread/user-data/outputs",
|
||||||
|
}
|
||||||
|
executions = [_bash_execution(f"cd {target} && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ("C:/", "C:/../tmp", "D:/ws/../../tmp"))
|
||||||
|
def test_cd_to_windows_drive_root_or_above_root_is_unprovable(self, target):
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "D:/ws/thread/user-data/workspace",
|
||||||
|
"outputs_path": "D:/ws/thread/user-data/outputs",
|
||||||
|
}
|
||||||
|
executions = [_bash_execution(f"cd {target} && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cd_to_in_scope_windows_drive_path_is_case_insensitive(self):
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "D:/ws/thread/user-data/workspace",
|
||||||
|
"outputs_path": "D:/ws/thread/user-data/outputs",
|
||||||
|
}
|
||||||
|
executions = [_bash_execution("cd d:/WS/thread/user-data/workspace/project && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_cd_to_windows_unicode_casefold_collision_is_unprovable(self):
|
||||||
|
thread_data = {"workspace_path": "D:/ws/Straße/workspace"}
|
||||||
|
executions = [_bash_execution("cd d:/WS/STRASSE/workspace/fake && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_cd_to_child_of_windows_drive_root_is_in_scope(self):
|
||||||
|
thread_data = {"workspace_path": "D:/"}
|
||||||
|
executions = [_bash_execution("cd d:/project && pytest tests/", output_tail="3 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"wrapped",
|
"wrapped",
|
||||||
(
|
(
|
||||||
@ -1197,6 +1462,7 @@ class TestTestsPassedLeaf:
|
|||||||
"cd && pytest tests/", # bare cd goes HOME
|
"cd && pytest tests/", # bare cd goes HOME
|
||||||
"cd - && pytest tests/", # prints OLDPWD
|
"cd - && pytest tests/", # prints OLDPWD
|
||||||
"cd backend/../../x && pytest tests/", # lexical walk-out
|
"cd backend/../../x && pytest tests/", # lexical walk-out
|
||||||
|
"cd linked/../safe && pytest tests/", # ``..`` can cross a directory symlink
|
||||||
"cd /mnt/user-data/../etc && pytest tests/", # normalized escape
|
"cd /mnt/user-data/../etc && pytest tests/", # normalized escape
|
||||||
"cd /ws/thread/user-data/workspace2 && pytest tests/", # sibling of an allowed root
|
"cd /ws/thread/user-data/workspace2 && pytest tests/", # sibling of an allowed root
|
||||||
),
|
),
|
||||||
@ -1557,6 +1823,370 @@ class TestTestsPassedLeaf:
|
|||||||
assert leaf["checked"] is False
|
assert leaf["checked"] is False
|
||||||
assert leaf["detail"] == "matching segment cannot be proven to have executed"
|
assert leaf["detail"] == "matching segment cannot be proven to have executed"
|
||||||
|
|
||||||
|
def test_windows_case_alias_of_target_that_is_excluded_is_unprovable(self):
|
||||||
|
"""Windows drive paths are case-insensitive for overlap purposes, so
|
||||||
|
a differently-cased exclusion can still remove the required target."""
|
||||||
|
executions = [_bash_execution("pytest D:/WS/tests/security D:/WS/tests/unit --ignore d:/ws/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest D:/WS/tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
leaf = verdict["leaves"][0]
|
||||||
|
assert leaf["checked"] is False
|
||||||
|
assert leaf["detail"] == "matching segment cannot be proven to have executed"
|
||||||
|
|
||||||
|
def test_windows_case_alias_exclusion_nested_under_target_is_unprovable(self):
|
||||||
|
executions = [_bash_execution("pytest D:/WS/tests --ignore d:/ws/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest D:/WS/tests"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unrelated_windows_exclusion_keeps_matching(self):
|
||||||
|
target = "D:/WS/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} D:/WS/tests/unit --ignore d:/ws/tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_different_absolute_drive_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
executions = [_bash_execution("pytest D:/ws/tests/security D:/ws/tests/unit --ignore E:/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest D:/ws/tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_different_unc_root_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
target = "//server1/share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} //server1/share/tests/unit --ignore //server2/share/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unrelated_exclusion_on_same_unc_root_keeps_matching(self):
|
||||||
|
target = "//server/share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} //server/share/tests/unit --ignore //server/share/tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_different_psdrive_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore Mirror:/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_psdrive_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore FileSystem::Mirror:/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_psdrive_relative_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore Data:tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_psdrive_relative_exclusion_fails_closed(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore FileSystem::data:tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_psdrive_absolute_exclusion_fails_closed_for_relative_criterion(self):
|
||||||
|
target = "Data:tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:tests/unit --ignore Data:/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_psdrive_relative_deselect_fails_closed_for_absolute_nodeid(self):
|
||||||
|
target = "Data:/tests/security/test_x.py::test_a"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --deselect Data:tests/security/test_x.py", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unrelated_exclusion_on_same_relative_psdrive_keeps_matching(self):
|
||||||
|
target = "Data:tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:tests/unit --ignore data:tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_unrelated_exclusion_on_same_psdrive_keeps_matching(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore data:/tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_same_psdrive_case_alias_exclusion_is_unprovable(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore data:/TESTS/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_path_on_same_psdrive_compares_lexically(self):
|
||||||
|
target = "Data:/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/unit --ignore FileSystem::data:/tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_provider_qualified_psdrive_preserves_pytest_nodeid_boundary(self):
|
||||||
|
target = "Data:/tests/x.py::TestA"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/y.py --deselect FileSystem::data:/tests/x.py::TestA", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_psdrive_keeps_pytest_nodeid_case_sensitive(self):
|
||||||
|
target = "Data:/tests/x.py::TestA"
|
||||||
|
executions = [_bash_execution(f"pytest {target} Data:/tests/y.py --deselect FileSystem::data:/tests/X.PY::testa", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_provider_qualified_unc_case_alias_exclusion_fails_closed(self):
|
||||||
|
target = "FileSystem:://srv/share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} FileSystem:://srv/share/tests/unit --ignore FileSystem:://srv/SHARE/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_distinct_unc_root_exclusion_fails_closed(self):
|
||||||
|
target = "FileSystem:://srv/share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} FileSystem:://srv/share/tests/unit --ignore FileSystem:://srv/other/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_unrelated_unc_exclusion_keeps_matching(self):
|
||||||
|
target = "FileSystem:://srv/share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} FileSystem:://srv/share/tests/unit --ignore FileSystem:://srv/share/tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_provider_qualified_unc_preserves_pytest_nodeid_boundary(self):
|
||||||
|
target = "FileSystem:://srv/share/tests/x.py::TestA"
|
||||||
|
executions = [_bash_execution(f"pytest {target} FileSystem:://srv/share/tests/y.py --deselect FileSystem:://srv/SHARE/tests/x.py::TestA", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_provider_qualified_unc_keeps_pytest_nodeid_case_sensitive(self):
|
||||||
|
target = "FileSystem:://srv/share/tests/x.py::TestA"
|
||||||
|
executions = [_bash_execution(f"pytest {target} FileSystem:://srv/share/tests/y.py --deselect FileSystem:://srv/SHARE/tests/X.PY::testa", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_windows_drive_root_exclusion_overlap_is_unprovable(self):
|
||||||
|
executions = [_bash_execution("pytest D:/ D:/WS/tests/unit --ignore d:/ws/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest D:/"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("criterion_target", "ignored_target"),
|
||||||
|
(
|
||||||
|
("D:/WS/tests/./security", "d:/ws/tests/security"),
|
||||||
|
("D:/WS/tests/security", "d:/ws/tests/./security"),
|
||||||
|
("D:/WS/tests//security", "d:/ws/tests/security"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_windows_lexical_alias_exclusion_is_unprovable(self, criterion_target, ignored_target):
|
||||||
|
executions = [_bash_execution(f"pytest {criterion_target} D:/WS/tests/unit --ignore {ignored_target}", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {criterion_target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_windows_relative_case_alias_exclusion_is_unprovable(self):
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore TESTS/SECURITY", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(
|
||||||
|
["tests_passed:pytest tests/security"],
|
||||||
|
thread_data=WINDOWS_THREAD_DATA,
|
||||||
|
bash_executions=executions,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_windows_rooted_path_case_alias_exclusion_is_unprovable(self):
|
||||||
|
target = "/Tests/Security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} /Tests/Unit --ignore /tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_windows_thread_rejects_shell_ambiguous_virtual_cd_target(self):
|
||||||
|
command = "cd /mnt/user-data/workspace && pytest tests/"
|
||||||
|
executions = [_bash_execution(command, output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:{command}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unc_cd_target_uses_windows_case_rules(self):
|
||||||
|
thread_data = {
|
||||||
|
"workspace_path": "//SERVER/Share/Workspace",
|
||||||
|
"uploads_path": "//SERVER/Share/Workspace/uploads",
|
||||||
|
"outputs_path": "//SERVER/Share/Workspace/outputs",
|
||||||
|
}
|
||||||
|
command = "cd //server/share/workspace && pytest tests/"
|
||||||
|
executions = [_bash_execution(command, output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:{command}"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ignored", ("D:tests/security", "C:WS/tests/security"))
|
||||||
|
def test_drive_relative_exclusion_fails_closed(self, ignored):
|
||||||
|
target = "D:/WS/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} D:/WS/tests/unit --ignore {ignored}", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_drive_relative_case_alias_uses_windows_semantics_without_thread_context(self):
|
||||||
|
target = "D:tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} D:tests/unit --ignore d:TESTS/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_different_drive_relative_exclusion_fails_closed_without_thread_context(self):
|
||||||
|
executions = [_bash_execution("pytest C:tests/security C:tests/unit --ignore D:tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest C:tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unrelated_exclusion_on_same_drive_relative_root_keeps_matching(self):
|
||||||
|
target = "C:tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} C:tests/unit --ignore c:tests/slow", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_drive_relative_and_plain_relative_exclusion_fail_closed_without_thread_context(self):
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore D:tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_drive_relative_and_plain_relative_exclusion_fail_closed_in_windows_context(self):
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore D:tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(
|
||||||
|
["tests_passed:pytest tests/security"],
|
||||||
|
thread_data=WINDOWS_THREAD_DATA,
|
||||||
|
bash_executions=executions,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("thread_data", "target", "ignored"),
|
||||||
|
(
|
||||||
|
(WINDOWS_THREAD_DATA, "tests/security", "D:/WS/tests/security"),
|
||||||
|
(WINDOWS_THREAD_DATA, "/mnt/user-data/workspace/tests/security", "D:/WS/tests/security"),
|
||||||
|
(THREAD_DATA, "tests/security", "/ws/thread/user-data/workspace/tests/security"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_mixed_path_forms_in_exclusion_fail_closed(self, thread_data, target, ignored):
|
||||||
|
executions = [_bash_execution(f"pytest {target} tests/unit --ignore {ignored}", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=thread_data, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("criterion_target", "additional_target", "ignored_target"),
|
||||||
|
(
|
||||||
|
("/tests/security", "/tests/unit", "D:/tests/security"),
|
||||||
|
("D:/WS/tests/security", "D:/WS/tests/unit", "/WS/tests/security"),
|
||||||
|
("//srv/share/tests/security", "//srv/share/tests/unit", "D:/tests/security"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_cross_family_absolute_exclusion_fails_closed(self, criterion_target, additional_target, ignored_target):
|
||||||
|
executions = [_bash_execution(f"pytest {criterion_target} {additional_target} --ignore {ignored_target}", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {criterion_target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_unc_case_alias_exclusion_is_unprovable(self):
|
||||||
|
target = "//SERVER/Share/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} //SERVER/Share/tests/unit --ignore //server/share/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("criterion_target", ("tests/./security", "tests//security"))
|
||||||
|
def test_posix_lexical_alias_exclusion_is_unprovable(self, criterion_target):
|
||||||
|
executions = [_bash_execution(f"pytest {criterion_target} tests/unit --ignore tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {criterion_target}"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_windows_trailing_dot_alias_exclusion_is_unprovable(self):
|
||||||
|
target = "D:/WS/tests/security."
|
||||||
|
executions = [_bash_execution(f"pytest {target} D:/WS/tests/unit --ignore d:/ws/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_windows_short_name_alias_exclusion_is_unprovable_without_thread_context(self):
|
||||||
|
target = "C:/longdirectoryname/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} C:/longdirectoryname/tests/unit --ignore C:/LONGDI~1/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_relative_windows_short_name_alias_exclusion_is_unprovable_in_windows_context(self):
|
||||||
|
target = "longdirectoryname/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} longdirectoryname/tests/unit --ignore LONGDI~1/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_non_short_name_tilde_component_keeps_matching(self):
|
||||||
|
target = "C:/LONGDI~X/tests/security"
|
||||||
|
executions = [_bash_execution(f"pytest {target} C:/LONGDI~X/tests/unit --ignore C:/other/tests/security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_parent_traversal_in_consumed_target_with_exclusion_is_unprovable(self):
|
||||||
|
executions = [_bash_execution("pytest tests/../security tests/unit --ignore security", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/../security"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["checked"] is False
|
||||||
|
|
||||||
|
def test_posix_relative_case_aliases_remain_distinct(self):
|
||||||
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore TESTS/SECURITY", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], thread_data=THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
def test_windows_nodeid_case_remains_distinct(self):
|
||||||
|
target = "D:/WS/tests/x.py::TestA"
|
||||||
|
executions = [_bash_execution(f"pytest {target} D:/WS/tests/y.py --deselect d:/ws/tests/X.PY::testa", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {target}"], thread_data=WINDOWS_THREAD_DATA, bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("criterion_target", "ignored_target"),
|
||||||
|
(
|
||||||
|
("/opt/WS/tests/security", "/opt/ws/tests/security"),
|
||||||
|
("D:/WS/Straße/tests/security", "d:/ws/STRASSE/tests/security"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_distinct_case_sensitive_exclusion_paths_do_not_overlap(self, criterion_target, ignored_target):
|
||||||
|
executions = [_bash_execution(f"pytest {criterion_target} tests/unit --ignore {ignored_target}", output_tail="12 passed")]
|
||||||
|
verdict = check_acceptance_criteria([f"tests_passed:pytest {criterion_target}"], bash_executions=executions)
|
||||||
|
|
||||||
|
assert verdict["leaves"][0]["holds"] is True
|
||||||
|
|
||||||
def test_unrelated_exclusion_does_not_block_the_match(self):
|
def test_unrelated_exclusion_does_not_block_the_match(self):
|
||||||
executions = [_bash_execution("pytest tests/security tests/unit --ignore tests/slow", output_tail="12 passed")]
|
executions = [_bash_execution("pytest tests/security tests/unit --ignore tests/slow", output_tail="12 passed")]
|
||||||
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
verdict = check_acceptance_criteria(["tests_passed:pytest tests/security"], bash_executions=executions)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user