mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-12 04:01:20 +00:00
Merge origin/master into feat/remove-str-replace-editor-from-minimal
This commit is contained in:
+179
-58
@@ -126,7 +126,7 @@ ADVANCED_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "advanced"
|
||||
)
|
||||
ADVANCED_SNAPSHOT_FILENAMES = (
|
||||
"result.json", "session.v2.jsonl", "session.1.v2.jsonl", "session.2.v2.jsonl",
|
||||
"result.json", "session.v3.jsonl", "session.1.v3.jsonl", "session.2.v3.jsonl",
|
||||
)
|
||||
MINIMAL_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "minimal"
|
||||
@@ -134,11 +134,15 @@ MINIMAL_SNAPSHOT_DIRECTORY = (
|
||||
if IS_WINDOWS:
|
||||
MINIMAL_SNAPSHOT_DIRECTORY /= "win-x64"
|
||||
MINIMAL_SNAPSHOT_FILENAMES = ("model-visible.json",)
|
||||
IN_HISTORY_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "minimal-in-history"
|
||||
)
|
||||
IN_HISTORY_SNAPSHOT_FILENAMES = ("prompt-history.json",)
|
||||
RESTART_SNAPSHOT_DIRECTORY = (
|
||||
Path(__file__).resolve().parent / "snapshots" / "python-sdk-single-exe" / "restart"
|
||||
)
|
||||
RESTART_SNAPSHOT_FILENAMES = (
|
||||
"result.json", "requests.json", "session.1.v2.jsonl", "session.2.v2.jsonl",
|
||||
"result.json", "requests.json", "session.1.v3.jsonl", "session.2.v3.jsonl",
|
||||
)
|
||||
MCP_SERVER_SCRIPT = """\
|
||||
import json
|
||||
@@ -313,7 +317,8 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
messages = body.get("messages")
|
||||
if not isinstance(messages, list) or not messages:
|
||||
raise AssertionError(f"model request has no messages: {body}")
|
||||
latest = messages[-1]
|
||||
# A system prompt update may follow the tool result without replacing it.
|
||||
latest = next(message for message in reversed(messages) if message.get("role") != "system")
|
||||
if not isinstance(latest, dict):
|
||||
raise AssertionError(f"model request has an invalid latest message: {body}")
|
||||
|
||||
@@ -734,7 +739,7 @@ def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--scenario",
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "runner", "direct"),
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "runner", "direct"),
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument("--exe", type=Path)
|
||||
@@ -753,10 +758,10 @@ def main() -> None:
|
||||
parser.error("--scenario sdk-profile-plugin requires --installed-wheel")
|
||||
if args.installed_wheel:
|
||||
args.exe = assert_installed_wheel_environment()
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "runner", "direct"} and args.exe is None:
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "runner", "direct"} and args.exe is None:
|
||||
parser.error("--exe is required for custom, minimal, fs-search, spawn-node, snapshot, restart, runner, and direct scenarios")
|
||||
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-snapshot", "sdk-restart"}:
|
||||
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-snapshot, sdk-restart, or all")
|
||||
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-minimal-in-history", "sdk-snapshot", "sdk-restart"}:
|
||||
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-minimal-in-history, sdk-snapshot, sdk-restart, or all")
|
||||
if args.exe is not None and not args.exe.is_file():
|
||||
parser.error(f"runtime executable does not exist: {args.exe}")
|
||||
|
||||
@@ -781,6 +786,9 @@ def main() -> None:
|
||||
if args.scenario in {"all", "sdk-minimal"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_minimal(model.url, args.exe.resolve(), args.update_snapshots)
|
||||
if args.scenario in {"all", "sdk-minimal-in-history"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_minimal(model.url, args.exe.resolve(), args.update_snapshots, in_history=True)
|
||||
if args.scenario in {"all", "sdk-fs-search"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_fs_search(model.url, args.exe.resolve())
|
||||
@@ -876,13 +884,9 @@ def smoke_sdk_live() -> None:
|
||||
session_id = "installed-wheel-live-api"
|
||||
shell_tool = "pwsh" if IS_WINDOWS else "bash"
|
||||
create_prompt = (
|
||||
f"Use the {shell_tool} tool to create the file at the absolute path below with exactly one line "
|
||||
f"containing {LIVE_API_SENTINEL}. Then reply with exactly {LIVE_API_SENTINEL}.\n{marker}"
|
||||
)
|
||||
verify_prompt = (
|
||||
"The file created in the previous turn now contains a new value written by the test. "
|
||||
"Use a tool to read its current contents without modifying it. "
|
||||
"Reply with exactly its current single line, not the value from the previous turn."
|
||||
f"Use the {shell_tool} tool to create the file at the absolute path below with exact UTF-8 "
|
||||
f"content {LIVE_API_SENTINEL}, with no newline or byte-order mark. "
|
||||
f"Then reply with exactly {LIVE_API_SENTINEL}.\n{marker}"
|
||||
)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
@@ -898,26 +902,37 @@ def smoke_sdk_live() -> None:
|
||||
request_timeout_seconds=180,
|
||||
) as harness:
|
||||
created = harness.run(create_prompt, session_id=session_id)
|
||||
assert_live_turn("create", created, LIVE_API_SENTINEL)
|
||||
assert_live_turn("create", created)
|
||||
if not marker.is_file():
|
||||
raise AssertionError(f"real-model tool turn did not create {marker}")
|
||||
if marker.read_text(encoding="utf-8").splitlines() != [LIVE_API_SENTINEL]:
|
||||
raise AssertionError(f"real-model tool turn wrote unexpected text to {marker}")
|
||||
# Only a fresh filesystem observation can reveal the verification answer.
|
||||
verification_value = secrets.token_hex(16)
|
||||
marker.write_text(verification_value + "\n", encoding="utf-8")
|
||||
verified = harness.run(verify_prompt, session_id=session_id)
|
||||
assert_live_turn("verify", verified, verification_value)
|
||||
raise AssertionError(f"create turn did not create {marker}")
|
||||
if marker.read_bytes() != LIVE_API_SENTINEL.encode("utf-8"):
|
||||
raise AssertionError(f"create turn wrote unexpected bytes to {marker}")
|
||||
|
||||
if not marker.is_file():
|
||||
raise AssertionError(f"real-model tool turn did not create {marker}")
|
||||
if marker.read_text(encoding="utf-8").splitlines() != [verification_value]:
|
||||
raise AssertionError(f"real-model tool turn wrote unexpected text to {marker}")
|
||||
# The challenge is absent from the prior turn and the verification prompt.
|
||||
challenge = secrets.token_hex(32).encode("ascii")
|
||||
marker.write_bytes(challenge)
|
||||
with tempfile.TemporaryDirectory(prefix="receipt-", dir=root) as receipt_directory:
|
||||
receipt = Path(receipt_directory) / "receipt.txt"
|
||||
verify_prompt = (
|
||||
"The file created in the previous turn has changed externally. "
|
||||
"Use a tool to read that same file and copy its exact current content to the "
|
||||
"new receipt path below, without changing the source file. "
|
||||
"Preserve every byte; do not add a newline or byte-order mark. "
|
||||
f"Then reply with exactly {LIVE_API_SENTINEL}.\n{receipt}"
|
||||
)
|
||||
verified = harness.run(verify_prompt, session_id=session_id)
|
||||
assert_live_turn("verify", verified)
|
||||
if not receipt.is_file():
|
||||
raise AssertionError(f"verify turn did not create receipt {receipt}")
|
||||
if receipt.read_bytes() != challenge:
|
||||
raise AssertionError(f"verify turn wrote unexpected bytes to receipt {receipt}")
|
||||
if not marker.is_file() or marker.read_bytes() != challenge:
|
||||
raise AssertionError(f"verify turn changed source file {marker}")
|
||||
assert_zstd_session_log(sessions)
|
||||
|
||||
|
||||
def assert_live_turn(label: str, result: RunResult, expected: str) -> None:
|
||||
"""Require a completed model-requested tool turn with the expected response."""
|
||||
def assert_live_turn(label: str, result: RunResult) -> None:
|
||||
"""Require completed model tool use and the exact smoke answer for each live turn."""
|
||||
if result.finish_reason != "completed":
|
||||
event_types = [event.get("type") for event in result.events]
|
||||
turn_end_data = next(
|
||||
@@ -934,10 +949,9 @@ def assert_live_turn(label: str, result: RunResult, expected: str) -> None:
|
||||
f"{label} turn made no model-requested tool call; "
|
||||
f"final={result.final_response!r}"
|
||||
)
|
||||
if result.final_response.strip() != expected:
|
||||
if result.final_response.strip() != LIVE_API_SENTINEL:
|
||||
raise AssertionError(f"{label} turn returned {result.final_response!r}")
|
||||
|
||||
|
||||
def safe_turn_end(value: object) -> object:
|
||||
"""Project a live-provider failure without retaining credential-bearing text."""
|
||||
if not isinstance(value, dict):
|
||||
@@ -1023,7 +1037,9 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None:
|
||||
assert_session_log(sessions, root, EXPECTED_TEXT, CODE_WORKER_TEXT, WORKFLOW_WORKER_TEXT)
|
||||
|
||||
|
||||
def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -> None:
|
||||
def smoke_sdk_minimal(
|
||||
base_url: str, executable: Path, update_snapshots: bool, *, in_history: bool = False,
|
||||
) -> None:
|
||||
"""Exercise the shipped standalone minimal profile through the packaged executable."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
@@ -1033,6 +1049,19 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -
|
||||
root = Path(temporary).resolve()
|
||||
dsh_home = root / "home"
|
||||
sessions = dsh_home / "sessions"
|
||||
patches = ()
|
||||
if in_history:
|
||||
patch = root / "in-history.patch.yml"
|
||||
patch.write_text(json.dumps([
|
||||
{"id": "llm-deepseek", "config": {"models": [
|
||||
{"id": "smoke-model", "systemPromptUpdate": "in-history"},
|
||||
]}},
|
||||
{"insert": [{
|
||||
"id": "in-history-prompt",
|
||||
"name": (Path(__file__).resolve().parent / "fixtures/python-sdk-in-history-prompt.mjs").as_uri(),
|
||||
}]},
|
||||
]))
|
||||
patches = (str(patch),)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
@@ -1040,6 +1069,7 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -
|
||||
dsh_bin=str(executable),
|
||||
dsh_home=str(dsh_home),
|
||||
profile="sdk-minimal",
|
||||
patches=patches,
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
@@ -1051,10 +1081,18 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -
|
||||
raise AssertionError(f"minimal agent run emitted no final response: {result.events}")
|
||||
assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2")
|
||||
|
||||
files = build_minimal_snapshot_files(MockModelHandler.requests[first_request:], root)
|
||||
compare_snapshot_files(
|
||||
files, update_snapshots, MINIMAL_SNAPSHOT_DIRECTORY, MINIMAL_SNAPSHOT_FILENAMES,
|
||||
)
|
||||
requests = MockModelHandler.requests[first_request:]
|
||||
if in_history:
|
||||
logs = read_session_logs(sessions)
|
||||
files = build_in_history_snapshot_files(result, requests, logs[result.session_id])
|
||||
compare_snapshot_files(
|
||||
files, update_snapshots, IN_HISTORY_SNAPSHOT_DIRECTORY, IN_HISTORY_SNAPSHOT_FILENAMES,
|
||||
)
|
||||
else:
|
||||
files = build_minimal_snapshot_files(requests, root)
|
||||
compare_snapshot_files(
|
||||
files, update_snapshots, MINIMAL_SNAPSHOT_DIRECTORY, MINIMAL_SNAPSHOT_FILENAMES,
|
||||
)
|
||||
|
||||
|
||||
def smoke_sdk_fs_search(base_url: str, executable: Path) -> None:
|
||||
@@ -1288,8 +1326,15 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
|
||||
methods = [notification.method for notification in result.notifications]
|
||||
if methods.count("subagent.started") != 2 or methods.count("subagent.finished") != 2:
|
||||
raise AssertionError(f"advanced snapshot emitted unexpected subagent lifecycle: {methods}")
|
||||
if not any(event.get("type") == "tool/code-dispatch" for event in result.events):
|
||||
raise AssertionError("advanced snapshot emitted no tool/code-dispatch event")
|
||||
ptc_events = [event for event in result.events
|
||||
if event.get("type") in ("tool/ptc-dispatch-start", "tool/ptc-dispatch")]
|
||||
if [event["type"] for event in ptc_events] != ["tool/ptc-dispatch-start", "tool/ptc-dispatch"]:
|
||||
raise AssertionError(f"advanced snapshot emitted unexpected PTC dispatch events: {ptc_events}")
|
||||
for event in ptc_events:
|
||||
data = event["data"]
|
||||
identity = (data.get("rootCallId"), data.get("parentCallId"), data.get("subCallId"))
|
||||
if identity != ("advanced-code", "advanced-code", "advanced-code:ptc:1"):
|
||||
raise AssertionError(f"advanced snapshot emitted unexpected PTC dispatch identity: {identity}")
|
||||
|
||||
logs = read_session_logs(sessions)
|
||||
child_ids = snapshot_child_ids(result)
|
||||
@@ -1627,8 +1672,25 @@ def session_header_version(content: str, label: str) -> int:
|
||||
return version
|
||||
|
||||
|
||||
def assert_current_session_version(version: int, label: str) -> None:
|
||||
"""Require generated logs to use the source writer generation, independent of goldens."""
|
||||
source = Path(__file__).resolve().parents[1] / "packages/core/session/src/types.ts"
|
||||
declarations = re.findall(
|
||||
r"^export const SESSION_FORMAT_VERSION = ([0-9]+)$",
|
||||
source.read_text(encoding="utf-8"),
|
||||
re.MULTILINE,
|
||||
)
|
||||
if len(declarations) != 1:
|
||||
raise AssertionError(f"{source}: expected one literal SESSION_FORMAT_VERSION declaration")
|
||||
current_version = int(declarations[0])
|
||||
if version != current_version:
|
||||
raise AssertionError(
|
||||
f"{label}: expected current Session format v{current_version}, got v{version}",
|
||||
)
|
||||
|
||||
|
||||
def assert_persisted_session_version(path: Path, content: str) -> int:
|
||||
"""Require a raw persistence basename and header to name the same generation."""
|
||||
"""Require generated persistence filenames and headers to use the current generation."""
|
||||
filename_version = persisted_session_filename_version(path)
|
||||
if filename_version is None:
|
||||
raise AssertionError(f"non-canonical Session persistence filename: {path.name}")
|
||||
@@ -1638,6 +1700,7 @@ def assert_persisted_session_version(path: Path, content: str) -> int:
|
||||
f"{path.name}: filename declares Session format v{filename_version}, "
|
||||
f"header declares v{header_version}",
|
||||
)
|
||||
assert_current_session_version(header_version, path.name)
|
||||
return header_version
|
||||
|
||||
|
||||
@@ -1745,6 +1808,56 @@ def snapshot_child_ids(result: "RunResult") -> list[str]:
|
||||
return child_ids
|
||||
|
||||
|
||||
def build_in_history_snapshot_files(
|
||||
result: "RunResult",
|
||||
requests: list[dict[str, object]],
|
||||
log: list[dict[str, object]],
|
||||
) -> dict[str, str]:
|
||||
"""Assert live requests, SDK subscriptions, and persistence retain both prompts."""
|
||||
systems = [event for event in result.events if event.get("type") == "system/message"]
|
||||
assert len(systems) == 2, systems
|
||||
assert [event.get("surfaceOp") for event in systems] == ["append", "append"], systems
|
||||
prompts = [message_text(event["data"]["message"]["content"]) for event in systems]
|
||||
assert "Python SDK prompt version 1." in prompts[0], prompts
|
||||
assert "Python SDK prompt version 2." not in prompts[0], prompts
|
||||
assert "Python SDK prompt version 2." in prompts[1], prompts
|
||||
assert "Python SDK prompt version 1." not in prompts[1], prompts
|
||||
assert prompts[0] != prompts[1], prompts
|
||||
assert [event for event in log if event.get("type") == "system/message"] == systems
|
||||
subscribed = [
|
||||
notification.payload["event"]
|
||||
for notification in result.notifications
|
||||
if notification.method == "session.event"
|
||||
and notification.payload.get("event", {}).get("type") == "system/message"
|
||||
]
|
||||
assert subscribed == systems, subscribed
|
||||
contexts = [event["data"] for event in result.events if event.get("type") == "request/context"]
|
||||
assert contexts and all(context.get("systemPromptUpdate") == "in-history" for context in contexts), contexts
|
||||
assert len([event for event in result.events if event.get("type") == "request/header"]) == 1
|
||||
assert all(event.get("surfaceOp") in (None, "append") for event in result.events)
|
||||
first_tool = next(index for index, event in enumerate(result.events) if event.get("type") == "tool/result")
|
||||
assert result.events.index(systems[1]) > first_tool
|
||||
assert len(requests) == 4, requests
|
||||
request_prompts = []
|
||||
for index, request in enumerate(requests):
|
||||
messages = request["messages"]
|
||||
assert messages[0]["role"] == "system" and message_text(messages[0]["content"]) == prompts[0]
|
||||
assert request["tools"] == requests[0]["tools"], "prompt update changed tool schemas"
|
||||
positions = [position for position, message in enumerate(messages) if message["role"] == "system"]
|
||||
texts = [message_text(messages[position]["content"]) for position in positions]
|
||||
assert texts == (prompts[:1] if index == 0 else prompts), texts
|
||||
if index > 0:
|
||||
assert messages[positions[1] - 1]["role"] == "tool", messages
|
||||
request_prompts.append(texts)
|
||||
evidence = {
|
||||
"requestSystemPrompts": request_prompts,
|
||||
"systemMessageOperations": [event["surfaceOp"] for event in systems],
|
||||
"subscribedSystemPrompts": prompts,
|
||||
"requestContexts": contexts,
|
||||
}
|
||||
return {"prompt-history.json": json.dumps(evidence, indent=2, ensure_ascii=False) + "\n"}
|
||||
|
||||
|
||||
def build_minimal_snapshot_files(
|
||||
requests: list[dict[str, object]],
|
||||
cwd: Path,
|
||||
@@ -2021,7 +2134,7 @@ def normalize_snapshot_value(
|
||||
dt = member.get("dt")
|
||||
if isinstance(dt, list):
|
||||
member["dt"] = [0] * len(dt)
|
||||
if isinstance(normalized.get("id"), str) and normalized.get("role") in ("assistant", "user"):
|
||||
if isinstance(normalized.get("id"), str) and normalized.get("role") in ("assistant", "system", "user"):
|
||||
if not normalized["id"].startswith("{{message:"):
|
||||
normalized["id"] = "{{messageId}}"
|
||||
if normalized.get("type") in ("feedback/message-put", "feedback/message-delete"):
|
||||
@@ -2033,11 +2146,12 @@ def normalize_snapshot_value(
|
||||
item["createdAt"] = 0
|
||||
item["updatedAt"] = 0
|
||||
scrub_snapshot_header(normalized)
|
||||
scrub_snapshot_system_message(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def scrub_snapshot_header(value: dict[object, object]) -> None:
|
||||
"""Tokenize full request-header bulk while retaining tool names."""
|
||||
"""Tokenize full request-header tool schemas while retaining tool names."""
|
||||
data = value.get("data")
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
@@ -2045,8 +2159,6 @@ def scrub_snapshot_header(value: dict[object, object]) -> None:
|
||||
header = data.get("header")
|
||||
if not isinstance(header, dict):
|
||||
return
|
||||
if "system" in header:
|
||||
header["system"] = "{{system}}"
|
||||
tools = header.get("tools")
|
||||
if isinstance(tools, list):
|
||||
header["tools"] = [
|
||||
@@ -2055,6 +2167,20 @@ def scrub_snapshot_header(value: dict[object, object]) -> None:
|
||||
]
|
||||
|
||||
|
||||
def scrub_snapshot_system_message(value: dict[object, object]) -> None:
|
||||
"""Tokenize the rendered prompt text of a `system/message` surface node."""
|
||||
if value.get("type") != "system/message":
|
||||
return
|
||||
data = value.get("data")
|
||||
message = data.get("message") if isinstance(data, dict) else None
|
||||
content = message.get("content") if isinstance(message, dict) else None
|
||||
if not isinstance(content, list):
|
||||
return
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
block["text"] = "{{system}}"
|
||||
|
||||
|
||||
def render_jsonl(records: list[object]) -> str:
|
||||
"""Render parsed JSON values as compact, newline-terminated JSONL."""
|
||||
return "".join(
|
||||
@@ -2192,19 +2318,6 @@ def normalize_session_format_comparison(
|
||||
normalized.pop("time", None)
|
||||
if source_session_version == 1 and normalized.get("type") == "assistant/message":
|
||||
normalized.pop("sourceEventSeqs", None)
|
||||
if normalized.get("type") == "session-log-deepseek/delivery-accepted":
|
||||
data = normalized.get("data")
|
||||
if isinstance(data, dict):
|
||||
data.pop("throughSeq", None)
|
||||
data.pop("sessionFormatVersion", None)
|
||||
data["sessionFormatVersion"] = SESSION_FORMAT_PROVENANCE
|
||||
if normalized.get("kind") == "session-reference":
|
||||
references = normalized.get("references")
|
||||
if isinstance(references, list):
|
||||
for reference in references:
|
||||
if isinstance(reference, dict):
|
||||
reference.pop("capturedFormatVersion", None)
|
||||
reference["capturedFormatVersion"] = SESSION_FORMAT_PROVENANCE
|
||||
return normalized
|
||||
|
||||
|
||||
@@ -2237,10 +2350,18 @@ def compare_snapshot_files(
|
||||
directory: Path,
|
||||
filenames: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Write or exactly compare one scenario's expected snapshot files."""
|
||||
"""Compare ordered artifact roles and Session content across generations, or write generated filenames."""
|
||||
scenario = directory.name
|
||||
if tuple(files) != filenames:
|
||||
|
||||
def role_name(name: str) -> str:
|
||||
parsed = parse_snapshot_session_filename(name)
|
||||
return name if parsed is None else snapshot_session_filename(parsed[0], 0)
|
||||
|
||||
if tuple(map(role_name, files)) != tuple(map(role_name, filenames)):
|
||||
raise AssertionError(f"{scenario} snapshot builder produced {tuple(files)}, expected {filenames}")
|
||||
for name, content in files.items():
|
||||
if parse_snapshot_session_filename(name) is not None:
|
||||
assert_current_session_version(session_header_version(content, name), name)
|
||||
if update:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
for name, content in files.items():
|
||||
|
||||
Reference in New Issue
Block a user