Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
243 lines
9.0 KiB
Python
243 lines
9.0 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from artemis.utils.notes import (
|
|
append_note_content,
|
|
list_notes_info,
|
|
read_note_content,
|
|
save_note_content,
|
|
save_verified_note,
|
|
update_note_content,
|
|
)
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.parametrize("operation", ["save", "append", "update"])
|
|
@pytest.mark.parametrize("path_style", ["native", "posix"])
|
|
def test_note_rejects_misplaced_artifact_and_accepts_exact_receipt(tmp_path, operation, path_style):
|
|
artifact = tmp_path / "artifacts" / "android" / "截图 one.png"
|
|
artifact.parent.mkdir(parents=True)
|
|
artifact.write_bytes(b"evidence")
|
|
save_note_content(tmp_path, "evidence", "original")
|
|
note = tmp_path / "notes" / "evidence.md"
|
|
before = note.read_bytes()
|
|
missing = tmp_path / artifact.name
|
|
wrong = str(missing) if path_style == "native" else missing.as_posix()
|
|
|
|
def write(content):
|
|
if operation == "save":
|
|
save_note_content(tmp_path, "evidence", content)
|
|
elif operation == "append":
|
|
append_note_content(tmp_path, "evidence", content)
|
|
else:
|
|
update_note_content(tmp_path, "evidence", "original", content)
|
|
|
|
with pytest.raises(ValueError, match="exact trace_path") as error:
|
|
write(f"Screenshot: `{wrong}`。")
|
|
assert str(artifact) in str(error.value)
|
|
assert note.read_bytes() == before
|
|
assert artifact.read_bytes() == b"evidence"
|
|
write(f"Screenshot: `{artifact}`")
|
|
assert str(artifact) in note.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_note_does_not_guess_between_duplicate_artifact_names(tmp_path):
|
|
files = [
|
|
tmp_path / "artifacts" / "android" / "screen.png",
|
|
tmp_path / "artifacts" / "plugins" / "cua-driver" / "screen.png",
|
|
]
|
|
for path in files:
|
|
path.parent.mkdir(parents=True)
|
|
path.write_bytes(b"evidence")
|
|
with pytest.raises(ValueError) as error:
|
|
save_note_content(tmp_path, "evidence", str(tmp_path / "screen.png"))
|
|
assert all(str(path) in str(error.value) for path in files)
|
|
assert not (tmp_path / "notes" / "evidence.md").exists()
|
|
|
|
|
|
def test_note_allows_existing_root_files_and_unknown_future_paths(tmp_path):
|
|
artifact = tmp_path / "artifacts" / "android" / "screen.png"
|
|
artifact.parent.mkdir(parents=True)
|
|
artifact.write_bytes(b"artifact")
|
|
root_file = tmp_path / "screen.png"
|
|
root_file.write_bytes(b"different root file")
|
|
content = f"{root_file}\n{tmp_path / 'future.png'}\n{tmp_path / 'screen.png.backup'}"
|
|
save_note_content(tmp_path, "plan", content)
|
|
assert read_note_content(tmp_path, "plan") == content
|
|
|
|
|
|
@pytest.mark.parametrize("content", ["# 报告\n结果\n", "line\r\nsecond", ""])
|
|
def test_verified_note_receipt_matches_actual_file_bytes(tmp_path, content):
|
|
import hashlib
|
|
|
|
receipt = save_verified_note(tmp_path, "report.md", content)
|
|
data = (tmp_path / receipt["path"]).read_bytes()
|
|
assert receipt["path"] == "notes/report.md"
|
|
assert receipt["verified"] is True and receipt["bytes"] == len(data)
|
|
assert receipt["sha256"] == hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def test_verified_note_does_not_confirm_incomplete_write(tmp_path, monkeypatch):
|
|
from artemis.utils import notes
|
|
|
|
def partial_write(base_dir, key, content):
|
|
save_note_content(base_dir, key, content[:1])
|
|
|
|
monkeypatch.setattr(notes, "save_note_content", partial_write)
|
|
with pytest.raises(OSError, match="读回不一致"):
|
|
save_verified_note(tmp_path, "report", "完整报告")
|
|
|
|
|
|
def test_list_notes_info(tmp_path):
|
|
base_dir = tmp_path
|
|
|
|
# Create some notes
|
|
save_note_content(base_dir, "note1", "line1\nline2\nline3")
|
|
save_note_content(base_dir, "note2", "line1\nline2")
|
|
|
|
info = list_notes_info(base_dir)
|
|
assert info == {"note1": 3, "note2": 2}
|
|
|
|
|
|
def test_read_note_content_range(tmp_path):
|
|
base_dir = tmp_path
|
|
save_note_content(base_dir, "note1", "line1\nline2\nline3\nline4")
|
|
|
|
# Read full
|
|
assert read_note_content(base_dir, "note1") == "line1\nline2\nline3\nline4"
|
|
|
|
# Read range
|
|
assert read_note_content(base_dir, "note1", start_line=2, end_line=3) == "line2\nline3"
|
|
|
|
# Read from start to line
|
|
assert read_note_content(base_dir, "note1", end_line=2) == "line1\nline2"
|
|
|
|
# Read from line to end
|
|
assert read_note_content(base_dir, "note1", start_line=3) == "line3\nline4"
|
|
|
|
|
|
def test_update_note_exact_match(tmp_path):
|
|
base_dir = tmp_path
|
|
original_content = 'def demo():\n print("Hello, World!")\n return True\n'
|
|
save_note_content(base_dir, "task_plan", original_content)
|
|
|
|
# Exact match
|
|
warning = update_note_content(
|
|
base_dir,
|
|
"task_plan",
|
|
' print("Hello, World!")',
|
|
' print("Hello, Jetski!")',
|
|
)
|
|
assert warning is None
|
|
|
|
updated_content = read_note_content(base_dir, "task_plan")
|
|
assert 'print("Hello, Jetski!")' in updated_content
|
|
assert 'print("Hello, World!")' not in updated_content
|
|
|
|
|
|
def test_update_note_relaxed_match(tmp_path):
|
|
base_dir = tmp_path
|
|
# Multi-line content with checkboxes and indentation
|
|
original_content = (
|
|
"- [ ] Step 1: Research the issue\n - [ ] Step 2: Write code\n - [ ] Step 3: Run tests\n"
|
|
)
|
|
save_note_content(base_dir, "task_plan", original_content)
|
|
|
|
# Relaxed match: case difference, quote difference, checkbox bullet difference, trailing whitespace
|
|
target = " * [ ] step 2: write code "
|
|
replacement = " - [/] Step 2: In progress"
|
|
|
|
warning = update_note_content(base_dir, "task_plan", target, replacement)
|
|
|
|
# Warning should be returned
|
|
assert warning is not None
|
|
assert "We did our best to apply changes despite some inaccuracies" in warning
|
|
|
|
updated_content = read_note_content(base_dir, "task_plan")
|
|
assert "Step 2: In progress" in updated_content
|
|
assert "Step 2: Write code" not in updated_content
|
|
# Check indentation is preserved from replacement
|
|
assert " - [/] Step 2: In progress\n" in updated_content
|
|
|
|
|
|
def test_update_note_fuzzy_match(tmp_path):
|
|
base_dir = tmp_path
|
|
original_content = 'print("Alpha message")\nprint("Gamma message")\nprint("Delta message")\n'
|
|
save_note_content(base_dir, "task_plan", original_content)
|
|
|
|
# Fuzzy match: typo (Gemma vs Gamma)
|
|
target = 'print("Gemma message")'
|
|
replacement = 'print("Gamma updated!")'
|
|
|
|
warning = update_note_content(base_dir, "task_plan", target, replacement)
|
|
assert warning is not None
|
|
assert "We did our best to apply changes despite some inaccuracies" in warning
|
|
|
|
updated_content = read_note_content(base_dir, "task_plan")
|
|
assert 'print("Gamma updated!")' in updated_content
|
|
assert 'print("Gamma message")' not in updated_content
|
|
|
|
|
|
def test_update_note_ambiguous_match(tmp_path):
|
|
base_dir = tmp_path
|
|
# Multiple lines that could match target
|
|
original_content = 'print("Beta message")\nprint("Zeta message")\nprint("Eta message")\n'
|
|
save_note_content(base_dir, "task_plan", original_content)
|
|
|
|
# Target is highly ambiguous (Xeta is almost equally close to Beta and Zeta)
|
|
target = 'print("Xeta message")'
|
|
replacement = 'print("Ambiguous match!")'
|
|
|
|
# Should raise ValueError due to ambiguity or inability to find unique match
|
|
with pytest.raises(ValueError) as exc_info:
|
|
update_note_content(base_dir, "task_plan", target, replacement)
|
|
|
|
assert "Target string 'print(\"Xeta message\")' not found" in str(
|
|
exc_info.value
|
|
) or "not unique" in str(exc_info.value)
|
|
|
|
|
|
def test_update_note_not_found(tmp_path):
|
|
base_dir = tmp_path
|
|
original_content = 'print("Alpha message")\n'
|
|
save_note_content(base_dir, "task_plan", original_content)
|
|
|
|
# Totally different target
|
|
target = 'print("Something totally different")'
|
|
replacement = 'print("Hello")'
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
update_note_content(base_dir, "task_plan", target, replacement)
|
|
assert "not found" in str(exc_info.value)
|
|
|
|
|
|
def test_append_note_content(tmp_path):
|
|
base_dir = tmp_path
|
|
|
|
# Case 1: File does not exist
|
|
append_note_content(base_dir, "new_note", "hello")
|
|
assert read_note_content(base_dir, "new_note") == "hello\n"
|
|
|
|
# Case 2: File exists and ends with a newline
|
|
append_note_content(base_dir, "new_note", "world")
|
|
assert read_note_content(base_dir, "new_note") == "hello\nworld\n"
|
|
|
|
# Case 3: File exists and does not end with a newline
|
|
file_path = base_dir / "notes" / "no_newline.md"
|
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
file_path.write_text("no newline", encoding="utf-8")
|
|
|
|
append_note_content(base_dir, "no_newline", "added text")
|
|
assert read_note_content(base_dir, "no_newline") == "no newline\nadded text\n"
|