| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- """
- Tests for PaaS Run Workspace — persistent run directories.
- Tests cover:
- 1. create_run_workspace() creates correct directory structure
- 2. save_run_artifacts() writes output/trace/cost files
- 3. Multiple runs preserve all history (no deletion)
- 4. Context carries workspace_path through fork/extend
- 5. Workspace not created when agent_dir is empty
- """
- import json
- import os
- import shutil
- import tempfile
- import pytest
- from agentpaas.engine.sandbox import (
- create_run_workspace,
- save_run_artifacts,
- build_change_manifest,
- ExecutionResult,
- )
- from lambdagent.core import Context, TraceEntry
- @pytest.fixture
- def agent_dir():
- """Create a temporary agent directory for testing."""
- d = tempfile.mkdtemp(prefix="test_agent_")
- yield d
- shutil.rmtree(d, ignore_errors=True)
- # ============================================================
- # 1. create_run_workspace()
- # ============================================================
- class TestCreateRunWorkspace:
- def test_creates_directory_structure(self, agent_dir):
- """Creates workspace/run_YYYYMMDD_HHMMSS/ with subdirs"""
- ws = create_run_workspace(agent_dir, "run_abc123", "hello", {"type": "simple"})
- assert os.path.isdir(ws)
- assert os.path.isdir(os.path.join(ws, "code"))
- assert os.path.isdir(os.path.join(ws, "results"))
- assert os.path.isdir(os.path.join(ws, "final"))
- def test_saves_input_json(self, agent_dir):
- """input.json contains run_id and input text"""
- ws = create_run_workspace(agent_dir, "run_abc123", "test input", {})
- with open(os.path.join(ws, "input.json")) as f:
- data = json.load(f)
- assert data["run_id"] == "run_abc123"
- assert data["input"] == "test input"
- def test_saves_config_yml(self, agent_dir):
- """config.yml is a snapshot of the agent config"""
- config = {"type": "react", "systemPrompt": "You are helpful"}
- ws = create_run_workspace(agent_dir, "run_abc123", "input", config)
- assert os.path.isfile(os.path.join(ws, "config.yml"))
- def test_workspace_under_agent_dir(self, agent_dir):
- """Workspace is under {agent_dir}/workspace/"""
- ws = create_run_workspace(agent_dir, "run_abc123", "input", {})
- assert ws.startswith(os.path.join(agent_dir, "workspace"))
- def test_run_dir_has_timestamp(self, agent_dir):
- """Run directory name matches run_YYYYMMDD_HHMMSS pattern"""
- ws = create_run_workspace(agent_dir, "run_abc123", "input", {})
- dirname = os.path.basename(ws)
- assert dirname.startswith("run_20")
- # ============================================================
- # 2. save_run_artifacts()
- # ============================================================
- class TestSaveRunArtifacts:
- def test_saves_output_json(self, agent_dir):
- ws = create_run_workspace(agent_dir, "run_123", "input", {})
- save_run_artifacts(ws, "final result", [], 1000, status="completed")
- with open(os.path.join(ws, "output.json")) as f:
- data = json.load(f)
- assert data["output"] == "final result"
- assert data["status"] == "completed"
- def test_saves_trace_json(self, agent_dir):
- ws = create_run_workspace(agent_dir, "run_123", "input", {})
- trace = [TraceEntry("agent1", "id1", "in", "out", 100.0, "model", 50)]
- save_run_artifacts(ws, "result", trace, 500)
- with open(os.path.join(ws, "trace.json")) as f:
- data = json.load(f)
- assert len(data) == 1
- assert data[0]["term_name"] == "agent1"
- assert data[0]["tokens_used"] == 50
- def test_saves_cost_json(self, agent_dir):
- ws = create_run_workspace(agent_dir, "run_123", "input", {})
- save_run_artifacts(ws, "result", [], 1500, input_tokens=100, steps=5)
- with open(os.path.join(ws, "cost.json")) as f:
- data = json.load(f)
- assert data["duration_ms"] == 1500
- assert data["input_tokens"] == 100
- assert data["steps"] == 5
- def test_saves_on_failure(self, agent_dir):
- ws = create_run_workspace(agent_dir, "run_123", "input", {})
- save_run_artifacts(ws, "", [], 500, error="boom", status="failed")
- with open(os.path.join(ws, "output.json")) as f:
- data = json.load(f)
- assert data["status"] == "failed"
- assert data["error"] == "boom"
- def test_noop_without_workspace(self):
- """save_run_artifacts does nothing if workspace_path is empty"""
- save_run_artifacts("", "result", [], 100) # Should not raise
- save_run_artifacts("/nonexistent/path", "result", [], 100) # Should not raise
- # ============================================================
- # 3. Multiple Runs Preserve History
- # ============================================================
- class TestMultipleRuns:
- def test_multiple_runs_coexist(self, agent_dir):
- """Each run creates a separate directory, old ones are not deleted"""
- import time
- ws1 = create_run_workspace(agent_dir, "run_aaa", "input1", {"v": 1})
- time.sleep(1.1) # Ensure different timestamp
- ws2 = create_run_workspace(agent_dir, "run_bbb", "input2", {"v": 2})
- assert ws1 != ws2
- assert os.path.isdir(ws1) # First run still exists
- assert os.path.isdir(ws2) # Second run also exists
- # Both have their own input files
- with open(os.path.join(ws1, "input.json")) as f:
- assert json.load(f)["run_id"] == "run_aaa"
- with open(os.path.join(ws2, "input.json")) as f:
- assert json.load(f)["run_id"] == "run_bbb"
- def test_workspace_dir_lists_all_runs(self, agent_dir):
- """workspace/ directory contains all run directories"""
- import time
- create_run_workspace(agent_dir, "run_1", "i1", {})
- time.sleep(1.1)
- create_run_workspace(agent_dir, "run_2", "i2", {})
- ws_dir = os.path.join(agent_dir, "workspace")
- runs = os.listdir(ws_dir)
- assert len(runs) == 2
- assert all(r.startswith("run_") for r in runs)
- # ============================================================
- # 4. Context Carries workspace_path
- # ============================================================
- class TestContextWorkspace:
- def test_context_workspace_path(self):
- ctx = Context(workspace_path="/tmp/test_ws", run_id="run_123")
- assert ctx.workspace_path == "/tmp/test_ws"
- assert ctx.run_id == "run_123"
- def test_fork_preserves_workspace(self):
- ctx = Context(workspace_path="/tmp/ws", run_id="run_abc")
- forked = ctx.fork()
- assert forked.workspace_path == "/tmp/ws"
- assert forked.run_id == "run_abc"
- def test_extend_preserves_workspace(self):
- ctx = Context(workspace_path="/tmp/ws", run_id="run_abc")
- child = ctx.extend(x=42)
- assert child.workspace_path == "/tmp/ws"
- assert child.run_id == "run_abc"
- def test_default_workspace_is_none(self):
- ctx = Context()
- assert ctx.workspace_path is None
- assert ctx.run_id is None
- # ============================================================
- # 5. No Workspace When agent_dir Empty
- # ============================================================
- class TestNoWorkspaceWithoutAgentDir:
- def test_execution_result_default_empty(self):
- r = ExecutionResult()
- assert r.workspace_path == ""
- # ============================================================
- # 6. Change Manifest (修改清单)
- # ============================================================
- class TestChangeManifest:
- def test_save_run_artifacts_writes_manifest(self, agent_dir):
- """save_run_artifacts auto-generates manifest.json"""
- ws = create_run_workspace(agent_dir, "run_m1", "input", {})
- save_run_artifacts(ws, "result", [], 100)
- assert os.path.isfile(os.path.join(ws, "manifest.json"))
- def test_manifest_lists_agent_produced_files(self, agent_dir):
- """Files the agent wrote under the run dir appear in the manifest"""
- ws = create_run_workspace(agent_dir, "run_m2", "input", {})
- with open(os.path.join(ws, "code", "main.py"), "w") as f:
- f.write("print('hi')\n")
- with open(os.path.join(ws, "results", "data.csv"), "w") as f:
- f.write("a,b\n1,2\n")
- manifest = build_change_manifest(ws)
- paths = {e["path"] for e in manifest["files"]}
- assert "code/main.py" in paths
- assert "results/data.csv" in paths
- assert manifest["file_count"] == 2
- assert manifest["total_bytes"] > 0
- def test_manifest_excludes_framework_metadata(self, agent_dir):
- """input/config/output/trace/cost/manifest themselves are not counted"""
- ws = create_run_workspace(agent_dir, "run_m3", "input", {})
- save_run_artifacts(ws, "result", [], 100) # writes output/trace/cost + manifest
- with open(os.path.join(ws, "manifest.json")) as f:
- manifest = json.load(f)
- paths = {e["path"] for e in manifest["files"]}
- assert paths.isdisjoint({
- "input.json", "config.yml", "output.json",
- "trace.json", "cost.json", "manifest.json",
- })
- def test_manifest_records_sha256(self, agent_dir):
- """Each produced file carries a sha256 digest for the evidence chain"""
- ws = create_run_workspace(agent_dir, "run_m4", "input", {})
- with open(os.path.join(ws, "final", "report.txt"), "w") as f:
- f.write("hello world")
- manifest = build_change_manifest(ws)
- entry = next(e for e in manifest["files"] if e["path"] == "final/report.txt")
- # sha256("hello world")
- assert entry["sha256"] == (
- "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
- )
- def test_manifest_carries_run_id(self, agent_dir):
- """run_id from input.json is propagated into the manifest"""
- ws = create_run_workspace(agent_dir, "run_m5", "input", {})
- manifest = build_change_manifest(ws)
- assert manifest["run_id"] == "run_m5"
- def test_manifest_git_field_present(self, agent_dir):
- """git section is always present (available False outside a repo)"""
- ws = create_run_workspace(agent_dir, "run_m6", "input", {})
- manifest = build_change_manifest(ws)
- assert "git" in manifest
- assert "available" in manifest["git"]
- def test_manifest_noop_on_invalid_path(self):
- """Invalid path returns {} and does not raise"""
- assert build_change_manifest("") == {}
- assert build_change_manifest("/nonexistent/xyz") == {}
- def test_empty_run_yields_empty_manifest(self, agent_dir):
- """A run that produced nothing still yields a valid (empty) manifest"""
- ws = create_run_workspace(agent_dir, "run_m7", "input", {})
- manifest = build_change_manifest(ws) # only framework metadata present
- assert manifest["file_count"] == 0
- assert manifest["files"] == []
|