fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
"""Unit tests for artifacts_capture scratch/outputs filtering."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
import pytest
|
|
|
|
from application.sandbox import artifacts_capture as ac
|
|
from application.sandbox.artifacts_capture import _is_scratch, _matches_outputs
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestIsScratch:
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
["tmp/x.csv", "tmp/sub/y.json", "__pycache__/m.pyc", "pkg/__pycache__/m.pyc",
|
|
".cache/blob", ".ipynb_checkpoints/nb", "a.tmp", "b.lock", "c.pyc"],
|
|
)
|
|
def test_scratch_paths_excluded(self, path):
|
|
assert _is_scratch(path) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"path", ["report.pdf", "out/data.csv", "deck.pptx", "notes.txt", "tmpfile.txt"],
|
|
)
|
|
def test_real_outputs_kept(self, path):
|
|
assert _is_scratch(path) is False
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestMatchesOutputs:
|
|
def test_basename_and_path(self):
|
|
assert _matches_outputs("report.pdf", ["report.pdf"])
|
|
assert _matches_outputs("out/report.pdf", ["report.pdf"]) # basename also matches
|
|
|
|
def test_globs(self):
|
|
assert _matches_outputs("a/b.csv", ["*.csv"])
|
|
assert _matches_outputs("out/x.json", ["out/*.json"])
|
|
|
|
def test_no_match(self):
|
|
assert not _matches_outputs("report.pdf", ["*.csv"])
|
|
|
|
|
|
class _FakeMgr:
|
|
"""Serves a fixed {rel_path: bytes} workspace listing."""
|
|
|
|
def __init__(self, files):
|
|
self._files = files
|
|
|
|
def list_files(self, _sid):
|
|
return list(self._files)
|
|
|
|
def get_file(self, _sid, path):
|
|
return self._files[path]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCaptureFiltering:
|
|
@staticmethod
|
|
def _captured(monkeypatch, files, pre=None, outputs=None):
|
|
seen = []
|
|
|
|
def fake_persist(rel_path, data, **_kw):
|
|
seen.append(rel_path)
|
|
return {"artifact_id": rel_path, "version": 1,
|
|
"filename": rel_path.rsplit("/", 1)[-1], "mime_type": "x", "size": len(data)}
|
|
|
|
monkeypatch.setattr(ac, "persist_artifact", fake_persist)
|
|
ac.capture_artifacts(_FakeMgr(files), "sid", pre or {}, user_id="u", outputs=outputs)
|
|
return seen
|
|
|
|
def test_auto_skips_scratch(self, monkeypatch):
|
|
files = {"report.pdf": b"x", "tmp/scratch.csv": b"y", "__pycache__/m.pyc": b"z"}
|
|
assert self._captured(monkeypatch, files) == ["report.pdf"]
|
|
|
|
def test_inputs_never_captured(self, monkeypatch):
|
|
files = {"report.pdf": b"x", "inputs/source.csv": b"y"}
|
|
assert self._captured(monkeypatch, files) == ["report.pdf"]
|
|
|
|
def test_outputs_allow_list_only(self, monkeypatch):
|
|
files = {"report.pdf": b"x", "data.csv": b"y", "notes.txt": b"z"}
|
|
assert self._captured(monkeypatch, files, outputs=["report.pdf"]) == ["report.pdf"]
|
|
|
|
def test_outputs_bypass_scratch(self, monkeypatch):
|
|
# An explicit pattern wins over the scratch skip.
|
|
files = {"tmp/keep.csv": b"x", "skip.txt": b"y"}
|
|
assert self._captured(monkeypatch, files, outputs=["*.csv"]) == ["tmp/keep.csv"]
|
|
|
|
def test_unchanged_file_skipped(self, monkeypatch):
|
|
pre = {"report.pdf": (1, hashlib.sha256(b"x").hexdigest())}
|
|
assert self._captured(monkeypatch, {"report.pdf": b"x"}, pre=pre) == []
|
|
# Content change is captured.
|
|
assert self._captured(monkeypatch, {"report.pdf": b"xy"}, pre=pre) == ["report.pdf"]
|
|
|
|
|
|
class _CountingMgr:
|
|
"""Serves a fixed {rel_path: bytes} workspace and counts every get_file read."""
|
|
|
|
def __init__(self, files):
|
|
self._files = files
|
|
self.reads = 0
|
|
|
|
def list_files(self, _sid):
|
|
return list(self._files)
|
|
|
|
def get_file(self, _sid, path):
|
|
self.reads += 1
|
|
return self._files[path]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestReadSweepCap:
|
|
def test_snapshot_signature_scan_capped(self):
|
|
# An unchanged-file-heavy workspace can no longer be read in full each pass.
|
|
files = {f"f{i:04d}.txt": b"x" for i in range(ac.MAX_SCANNED_FILES + 50)}
|
|
mgr = _CountingMgr(files)
|
|
sigs = ac.snapshot_signatures(mgr, "sid")
|
|
assert mgr.reads == ac.MAX_SCANNED_FILES
|
|
assert len(sigs) == ac.MAX_SCANNED_FILES
|
|
|
|
def test_capture_read_sweep_capped(self, monkeypatch):
|
|
# Every get_file (even for unchanged, never-persisted files) counts toward the cap.
|
|
files = {f"f{i:04d}.txt": b"x" for i in range(ac.MAX_SCANNED_FILES + 50)}
|
|
pre = {name: (1, hashlib.sha256(b"x").hexdigest()) for name in files} # all unchanged
|
|
mgr = _CountingMgr(files)
|
|
monkeypatch.setattr(ac, "persist_artifact", lambda *a, **k: None)
|
|
captured = ac.capture_artifacts(mgr, "sid", pre, user_id="u")
|
|
assert captured == [] # nothing changed -> nothing persisted
|
|
assert mgr.reads == ac.MAX_SCANNED_FILES # but the sweep is still bounded
|