Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

671 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Notes task correctness tests.
"""
from __future__ import annotations
import copy
import datetime
import inspect
import json
import re
from pathlib import Path
from typing import Any
import pytest
from bench_env.task.base import BaseTask
from bench_env.task.common_tasks import AnswerTask, CriteriaTask
from bench_env.task.notes import tasks as _tasks_module
from bench_env.task.notes.app import Notes
from bench_env.tests.conftest import make_judge_input
ALL_TASK_CLASSES: list[type[BaseTask]] = [
obj
for _, obj in inspect.getmembers(_tasks_module, inspect.isclass)
if issubclass(obj, BaseTask) and obj is not BaseTask and obj.__module__ == _tasks_module.__name__
]
ALL_TASK_IDS = [cls.__name__ for cls in ALL_TASK_CLASSES]
ANSWER_TASK_CLASSES = [cls for cls in ALL_TASK_CLASSES if issubclass(cls, AnswerTask)]
TEST_OS_STATE = {"time": {"timestamp": 1742025600000}}
DEFAULT_ROUTE = {"app": "notes", "path": "/"}
def _parse_timestamp(value: Any) -> int:
if isinstance(value, (int, float)):
return int(value)
return int(datetime.datetime.fromisoformat(str(value)).timestamp() * 1000)
def _load_defaults() -> dict[str, Any]:
path = Path(__file__).resolve().parents[3] / "system" / "Notes" / "data" / "defaults.json"
return json.loads(path.read_text(encoding="utf-8"))
def _make_base_state() -> dict[str, Any]:
defaults = _load_defaults()
notes = [
{**note, "updatedAt": _parse_timestamp(note["updatedAt"])}
for note in defaults["sampleNotes"]
]
todos = [
{**todo, "updatedAt": _parse_timestamp(todo["updatedAt"])}
for todo in defaults["sampleTodos"]
]
return {
"notes": notes,
"todos": todos,
"folders": [
{"id": "all", "name": "全部", "system": True},
{"id": "call", "name": "通话笔记", "system": True},
{"id": "unfiled", "name": "未分类", "system": True},
],
"selectedFolderId": "all",
"settings": copy.deepcopy(defaults["settings"]),
}
BASE_STATE = _make_base_state()
def _make_task_input(
init_state: dict[str, Any],
curr_state: dict[str, Any],
*,
route: dict[str, Any] | None = None,
answer: str | None = None,
):
return make_judge_input(
{"apps": {"notes": init_state}, "os": TEST_OS_STATE},
{"apps": {"notes": curr_state}, "os": TEST_OS_STATE},
route=route or DEFAULT_ROUTE,
answer=answer,
)
def _set_by_path(state: dict[str, Any], path: str, value: Any) -> None:
current = state
parts = path.split(".")
for part in parts[:-1]:
current = current[part]
current[parts[-1]] = value
def _resolve_criteria_value(value: Any, params: dict[str, Any]) -> Any:
if isinstance(value, str):
match = re.fullmatch(r"\{(\w+)\}", value)
if match:
return params[match.group(1)]
return value
def _natural_answer(expected: Any) -> str:
if isinstance(expected, dict):
return "".join(str(value) for value in expected.values())
if isinstance(expected, (int, float)):
return f"答案是{expected:g}" if isinstance(expected, float) else f"答案是{expected}"
if isinstance(expected, str):
return f"答案是{expected}"
return str(expected)
def _wrong_answer(expected: Any) -> str:
if isinstance(expected, dict):
return "答案是错误内容"
if isinstance(expected, (int, float)):
wrong = float(expected) + 1
return f"答案是{wrong:g}"
if isinstance(expected, str):
return "答案是错误内容"
return "错误答案"
def _require_note(state: dict[str, Any], title: str) -> dict[str, Any]:
for note in state["notes"]:
if str(note.get("title") or "").strip() == str(title).strip():
return note
raise ValueError(f"note not found: {title}")
def _require_todo(state: dict[str, Any], text: str) -> dict[str, Any]:
for todo in state["todos"]:
if str(todo.get("text") or "").strip() == str(text).strip():
return todo
raise ValueError(f"todo not found: {text}")
def _add_note(
state: dict[str, Any],
title: str,
*,
content: str = "",
folder_id: str = "unfiled",
pinned: bool = False,
is_private: bool = False,
trashed_at: int | None = None,
alarm_at: int | None = None,
) -> dict[str, Any]:
next_ts = max(int(note.get("updatedAt", 0) or 0) for note in state["notes"]) + 1000
note = {
"id": f"test_note_{len(state['notes']) + 1}",
"title": title,
"content": content,
"updatedAt": next_ts,
"folderId": folder_id,
}
if pinned:
note["pinned"] = True
if is_private:
note["isPrivate"] = True
if trashed_at is not None:
note["trashedAt"] = trashed_at
if alarm_at is not None:
note["alarmAt"] = alarm_at
state["notes"].insert(0, note)
return note
def _add_folder(state: dict[str, Any], name: str) -> dict[str, Any]:
folder = {
"id": f"xf_test_{len(state['folders']) + 1}",
"name": name,
"system": False,
}
state["folders"].append(folder)
state["selectedFolderId"] = folder["id"]
return folder
def _add_todo(state: dict[str, Any], text: str, *, completed: bool = False) -> dict[str, Any]:
next_ts = max(int(todo.get("updatedAt", 0) or 0) for todo in state["todos"]) + 1000
todo = {
"id": f"test_todo_{len(state['todos']) + 1}",
"text": text,
"completed": completed,
"updatedAt": next_ts,
}
state["todos"].insert(0, todo)
return todo
def _positive_answer_case(task: AnswerTask, *, curr_state: dict[str, Any] | None = None):
curr = copy.deepcopy(curr_state) if curr_state is not None else copy.deepcopy(BASE_STATE)
probe = _make_task_input(copy.deepcopy(BASE_STATE), curr)
expected = task.get_answer(probe)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=_natural_answer(expected))
def _negative_answer_case(task: AnswerTask, *, curr_state: dict[str, Any] | None = None):
curr = copy.deepcopy(curr_state) if curr_state is not None else copy.deepcopy(BASE_STATE)
probe = _make_task_input(copy.deepcopy(BASE_STATE), curr)
expected = task.get_answer(probe)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=_wrong_answer(expected))
def _positive_criteria_case(task: CriteriaTask):
curr = copy.deepcopy(BASE_STATE)
route = DEFAULT_ROUTE
for path, raw_value in task.criteria.items():
value = _resolve_criteria_value(raw_value, task.params)
if path == "route":
route = {"app": "notes", "path": str(value)}
continue
_set_by_path(curr, path, value)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, route=route)
def _negative_criteria_case(task: CriteriaTask):
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
class TestTaskDefinitions:
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_instantiation(self, cls):
task = cls()
assert task.name == cls.__name__
assert task.templates
assert "notes" in task.apps
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_description_renders(self, cls):
task = cls()
task._env_state = {"os": TEST_OS_STATE}
desc = task.description
assert desc
assert "{" not in desc
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_required_class_attrs(self, cls):
assert cls.scope in ("S1", "S2", "S3")
assert cls.objective in ("operate", "query", "hybrid")
assert cls.composition in ("atomic", "sequential", "transfer", "deep_dive")
assert cls.difficulty in ("L1", "L2", "L3", "L4")
@pytest.mark.parametrize("cls", ALL_TASK_CLASSES, ids=ALL_TASK_IDS)
def test_parameter_defaults_present(self, cls):
for key, schema in cls.parameters.items():
if key.startswith("_"):
continue
assert "default" in schema, f"{cls.__name__}.parameters['{key}'] missing default"
@pytest.mark.parametrize("cls", ANSWER_TASK_CLASSES, ids=[cls.__name__ for cls in ANSWER_TASK_CLASSES])
def test_answer_task_has_answer_or_get_answer(self, cls):
has_answer_attr = cls.answer is not None
has_get_answer_override = cls.get_answer is not AnswerTask.get_answer
assert has_answer_attr or has_get_answer_override
class TestNotesAccessor:
@pytest.fixture
def notes_app(self) -> Notes:
return Notes(copy.deepcopy(BASE_STATE))
@pytest.fixture
def notes_variant(self) -> Notes:
state = copy.deepcopy(BASE_STATE)
_require_note(state, "购物清单")["isPrivate"] = True
target = _require_note(state, "客户来电记录")
target["trashedAt"] = int(target["updatedAt"]) + 500
_require_todo(state, "给妈妈打电话")["completed"] = True
return Notes(state)
def test_basic_collections(self, notes_app: Notes):
assert len(notes_app.notes) == 5
assert len(notes_app.todos) == 5
assert len(notes_app.folders) == 3
assert notes_app.settings["notesViewMode"] == "grid"
assert notes_app.latest_note is not None
assert notes_app.longest_note is not None
def test_visible_private_trashed_views(self, notes_variant: Notes):
assert len(notes_variant.visible_notes) == 3
assert len(notes_variant.private_notes) == 1
assert len(notes_variant.trashed_notes) == 1
assert len(notes_variant.incomplete_todos) == 2
assert len(notes_variant.completed_todos) == 3
def test_note_lookup_helpers(self, notes_app: Notes):
assert notes_app.find_note_by_title("购物清单") is not None
assert notes_app.latest_note_by_title("购物清单") is not None
assert notes_app.find_note_containing("灵隐寺")["title"] == "杭州旅行计划"
assert notes_app.find_note_with_keywords(["西湖", "灵隐寺"])["title"] == "杭州旅行计划"
def test_todo_and_folder_lookup_helpers(self, notes_app: Notes):
assert notes_app.find_todo_by_text("给妈妈打电话") is not None
assert notes_app.find_folder_by_name("未分类")["id"] == "unfiled"
def test_check_latest_contains(self, notes_app: Notes):
check = notes_app.check_latest_contains("牛奶", "鸡蛋", field="latest")
assert check["field"] == "latest"
assert check["passed"] is True
def test_check_latest_contains_any_of(self, notes_app: Notes):
check = notes_app.check_latest_contains_any_of(["不存在", "牛奶"], field="latest_any")
assert check["field"] == "latest_any"
assert check["passed"] is True
def test_check_latest_norm_contains(self):
state = copy.deepcopy(BASE_STATE)
_add_note(state, "热门整理", content="杭州旅行| 阅读摘抄")
notes_app = Notes(state)
check = notes_app.check_latest_norm_contains("杭州旅行", "阅读摘抄", field="latest_norm")
assert check["field"] == "latest_norm"
assert check["passed"] is True
def test_check_no_new_notes(self):
unchanged = Notes(copy.deepcopy(BASE_STATE), init=copy.deepcopy(BASE_STATE))
assert unchanged.check_no_new_notes()["passed"] is True
current = copy.deepcopy(BASE_STATE)
_add_note(current, "临时备忘", content="不应该新增")
changed = Notes(current, init=copy.deepcopy(BASE_STATE))
check = changed.check_no_new_notes(field="no_new_trip_note")
assert check["field"] == "no_new_trip_note"
assert check["passed"] is False
def test_reminder_time_labels(self):
labels = Notes.reminder_time_labels(1741597200000)
assert len(labels) == 4
assert any(":" in label for label in labels)
def _read_notes_count_positive():
return _positive_answer_case(_tasks_module.ReadNotesCount())
def _read_notes_count_negative():
return _negative_answer_case(_tasks_module.ReadNotesCount())
def _change_view_mode_positive():
return _positive_criteria_case(_tasks_module.ChangeViewMode())
def _change_view_mode_negative():
return _negative_criteria_case(_tasks_module.ChangeViewMode())
def _create_new_note_positive():
task = _tasks_module.CreateNewNote()
curr = copy.deepcopy(BASE_STATE)
_add_note(curr, task.p.title)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _create_new_note_negative():
task = _tasks_module.CreateNewNote()
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
def _add_new_todo_positive():
task = _tasks_module.AddNewTodo()
curr = copy.deepcopy(BASE_STATE)
_add_todo(curr, task.p.text)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _add_new_todo_negative():
task = _tasks_module.AddNewTodo()
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
def _pin_note_positive():
task = _tasks_module.PinNote()
curr = copy.deepcopy(BASE_STATE)
_require_note(curr, task.p.note_title)["pinned"] = True
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _pin_note_negative():
task = _tasks_module.PinNote()
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
def _read_note_content_positive():
return _positive_answer_case(_tasks_module.ReadNoteContent())
def _read_note_content_negative():
return _negative_answer_case(_tasks_module.ReadNoteContent())
def _read_todo_text_positive():
return _positive_answer_case(_tasks_module.ReadTodoText())
def _read_todo_text_negative():
return _negative_answer_case(_tasks_module.ReadTodoText())
def _delete_todo_positive():
task = _tasks_module.DeleteTodo()
curr = copy.deepcopy(BASE_STATE)
target = _require_todo(curr, task.p.todo_text)
curr["todos"] = [t for t in curr["todos"] if t["id"] != target["id"]]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _delete_todo_negative():
task = _tasks_module.DeleteTodo()
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
def _delete_todo_negative_wrong_target():
# 删错了目标:把另一条未完成 todo 删掉,目标 todo 仍在
task = _tasks_module.DeleteTodo()
curr = copy.deepcopy(BASE_STATE)
other = next(
t for t in curr["todos"]
if not t.get("completed") and str(t.get("text")) != task.p.todo_text
)
curr["todos"] = [t for t in curr["todos"] if t["id"] != other["id"]]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _delete_todo_negative_text_cleared():
# 把目标的 text 清空(错把"删除"理解成"清空文本"),条目依然在
task = _tasks_module.DeleteTodo()
curr = copy.deepcopy(BASE_STATE)
_require_todo(curr, task.p.todo_text)["text"] = ""
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _delete_all_completed_todos_positive():
task = _tasks_module.DeleteAllCompletedTodos()
curr = copy.deepcopy(BASE_STATE)
curr["todos"] = [t for t in curr["todos"] if not t.get("completed")]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _delete_all_completed_todos_negative():
task = _tasks_module.DeleteAllCompletedTodos()
return task, _make_task_input(copy.deepcopy(BASE_STATE), copy.deepcopy(BASE_STATE))
def _delete_all_completed_todos_negative_partial():
# 只删了部分 completed todo,另一条 completed 还在
task = _tasks_module.DeleteAllCompletedTodos()
curr = copy.deepcopy(BASE_STATE)
first_completed = next(t for t in curr["todos"] if t.get("completed"))
curr["todos"] = [t for t in curr["todos"] if t["id"] != first_completed["id"]]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _delete_all_completed_todos_negative_also_deleted_incomplete():
# 完成 todo 全删了,但顺带把一条未完成 todo 也删了
task = _tasks_module.DeleteAllCompletedTodos()
curr = copy.deepcopy(BASE_STATE)
curr["todos"] = [t for t in curr["todos"] if not t.get("completed")]
victim = next(t for t in curr["todos"] if not t.get("completed"))
curr["todos"] = [t for t in curr["todos"] if t["id"] != victim["id"]]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _restore_from_trash_positive():
task = _tasks_module.RestoreFromTrash()
init_state = copy.deepcopy(BASE_STATE)
note = _require_note(init_state, task.p.note_title)
note["trashedAt"] = int(note["updatedAt"]) + 500
curr = copy.deepcopy(init_state)
_require_note(curr, task.p.note_title)["trashedAt"] = None
return task, _make_task_input(init_state, curr)
def _restore_from_trash_negative():
task = _tasks_module.RestoreFromTrash()
init_state = copy.deepcopy(BASE_STATE)
note = _require_note(init_state, task.p.note_title)
note["trashedAt"] = int(note["updatedAt"]) + 500
return task, _make_task_input(init_state, copy.deepcopy(init_state))
def _search_note_title_positive():
return _positive_answer_case(_tasks_module.SearchNoteTitle())
def _search_note_title_negative():
return _negative_answer_case(_tasks_module.SearchNoteTitle())
def _create_folder_and_move_note_positive():
task = _tasks_module.CreateFolderAndMoveNote()
curr = copy.deepcopy(BASE_STATE)
folder = _add_folder(curr, task.p.folder_name)
_require_note(curr, task.p.note_title)["folderId"] = folder["id"]
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _create_folder_and_move_note_negative():
task = _tasks_module.CreateFolderAndMoveNote()
curr = copy.deepcopy(BASE_STATE)
_add_folder(curr, task.p.folder_name)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr)
def _create_note_with_reminder_positive():
task = _tasks_module.CreateNoteWithReminder()
curr = copy.deepcopy(BASE_STATE)
ts = 1741597200000
_add_note(curr, task.p.title, content=task.p.content, alarm_at=ts)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=Notes.reminder_time_labels(ts)[0])
def _create_note_with_reminder_positive_natural_time():
task = _tasks_module.CreateNoteWithReminder()
curr = copy.deepcopy(BASE_STATE)
ts = 1741597200000
_add_note(curr, task.p.title, content=task.p.content, alarm_at=ts)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer="提醒时间是下午5点00分")
def _create_note_with_reminder_negative_wrong_answer():
task = _tasks_module.CreateNoteWithReminder()
curr = copy.deepcopy(BASE_STATE)
ts = 1741597200000
_add_note(curr, task.p.title, content=task.p.content, alarm_at=ts)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer="提醒时间是明天")
def _create_note_with_reminder_negative_wrong_state():
task = _tasks_module.CreateNoteWithReminder()
curr = copy.deepcopy(BASE_STATE)
ts = 1741597200000
_add_note(curr, task.p.title, content="错误内容", alarm_at=ts)
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=Notes.reminder_time_labels(ts)[0])
def _private_notes_workflow_positive():
task = _tasks_module.PrivateNotesWorkflow()
curr = copy.deepcopy(BASE_STATE)
_require_note(curr, task.p.note_title)["isPrivate"] = True
answer = _natural_answer(len(Notes(curr).private_notes))
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=answer)
def _private_notes_workflow_negative_wrong_answer():
task = _tasks_module.PrivateNotesWorkflow()
curr = copy.deepcopy(BASE_STATE)
_require_note(curr, task.p.note_title)["isPrivate"] = True
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer="答案是999")
def _private_notes_workflow_negative_wrong_state():
task = _tasks_module.PrivateNotesWorkflow()
curr = copy.deepcopy(BASE_STATE)
answer = _natural_answer(len(Notes(curr).private_notes))
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=answer)
def _todo_batch_workflow_positive():
task = _tasks_module.TodoBatchWorkflow()
curr = copy.deepcopy(BASE_STATE)
_add_todo(curr, task.p.new_todo)
_require_todo(curr, task.p.existing_todo)["completed"] = True
answer = _natural_answer(len(Notes(curr).incomplete_todos))
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=answer)
def _todo_batch_workflow_negative_wrong_answer():
task = _tasks_module.TodoBatchWorkflow()
curr = copy.deepcopy(BASE_STATE)
_add_todo(curr, task.p.new_todo)
_require_todo(curr, task.p.existing_todo)["completed"] = True
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer="答案是999")
def _todo_batch_workflow_negative_wrong_state():
task = _tasks_module.TodoBatchWorkflow()
curr = copy.deepcopy(BASE_STATE)
_add_todo(curr, task.p.new_todo)
answer = _natural_answer(len(Notes(curr).incomplete_todos))
return task, _make_task_input(copy.deepcopy(BASE_STATE), curr, answer=answer)
OFFLINE_JUDGE_POSITIVE_CASES = [
("ReadNotesCount", _read_notes_count_positive),
("ChangeViewMode", _change_view_mode_positive),
("CreateNewNote", _create_new_note_positive),
("AddNewTodo", _add_new_todo_positive),
("PinNote", _pin_note_positive),
("ReadNoteContent", _read_note_content_positive),
("ReadTodoText", _read_todo_text_positive),
("DeleteTodo", _delete_todo_positive),
("DeleteAllCompletedTodos", _delete_all_completed_todos_positive),
("RestoreFromTrash", _restore_from_trash_positive),
("SearchNoteTitle", _search_note_title_positive),
("CreateFolderAndMoveNote", _create_folder_and_move_note_positive),
("CreateNoteWithReminder", _create_note_with_reminder_positive),
("PrivateNotesWorkflow", _private_notes_workflow_positive),
("TodoBatchWorkflow", _todo_batch_workflow_positive),
]
OFFLINE_JUDGE_NEGATIVE_CASES = [
("ReadNotesCount", _read_notes_count_negative),
("ChangeViewMode", _change_view_mode_negative),
("CreateNewNote", _create_new_note_negative),
("AddNewTodo", _add_new_todo_negative),
("PinNote", _pin_note_negative),
("ReadNoteContent", _read_note_content_negative),
("ReadTodoText", _read_todo_text_negative),
("DeleteTodo", _delete_todo_negative),
("DeleteTodo", _delete_todo_negative_wrong_target),
("DeleteTodo", _delete_todo_negative_text_cleared),
("DeleteAllCompletedTodos", _delete_all_completed_todos_negative),
("DeleteAllCompletedTodos", _delete_all_completed_todos_negative_partial),
("DeleteAllCompletedTodos", _delete_all_completed_todos_negative_also_deleted_incomplete),
("RestoreFromTrash", _restore_from_trash_negative),
("SearchNoteTitle", _search_note_title_negative),
("CreateFolderAndMoveNote", _create_folder_and_move_note_negative),
("CreateNoteWithReminder", _create_note_with_reminder_negative_wrong_answer),
("CreateNoteWithReminder", _create_note_with_reminder_negative_wrong_state),
("PrivateNotesWorkflow", _private_notes_workflow_negative_wrong_answer),
("PrivateNotesWorkflow", _private_notes_workflow_negative_wrong_state),
("TodoBatchWorkflow", _todo_batch_workflow_negative_wrong_answer),
("TodoBatchWorkflow", _todo_batch_workflow_negative_wrong_state),
]
EXTRA_POSITIVE_CASES = [
("CreateNoteWithReminder_natural_time", _create_note_with_reminder_positive_natural_time),
]
OFFLINE_JUDGE_TASK_NAMES = {cls.__name__ for cls in ALL_TASK_CLASSES}
class TestTaskJudgeMatrixOffline:
def test_offline_judge_matrix_complete(self):
positive = {name for name, _ in OFFLINE_JUDGE_POSITIVE_CASES}
negative = {name for name, _ in OFFLINE_JUDGE_NEGATIVE_CASES}
assert positive == OFFLINE_JUDGE_TASK_NAMES
assert negative == OFFLINE_JUDGE_TASK_NAMES
for task_name in [
"CreateNoteWithReminder",
"PrivateNotesWorkflow",
"TodoBatchWorkflow",
]:
hybrid_negatives = [name for name, _ in OFFLINE_JUDGE_NEGATIVE_CASES if name == task_name]
assert len(hybrid_negatives) >= 2
@pytest.mark.parametrize(
"task_name,builder",
OFFLINE_JUDGE_POSITIVE_CASES,
ids=[name for name, _ in OFFLINE_JUDGE_POSITIVE_CASES],
)
def test_positive_case(self, task_name, builder):
task, inp = builder()
result = task.evaluate(inp)
assert result.success, f"{task_name} positive failed: issues={result.issues}, warnings={result.warnings}"
@pytest.mark.parametrize(
"task_name,builder",
OFFLINE_JUDGE_NEGATIVE_CASES,
ids=[name for name, _ in OFFLINE_JUDGE_NEGATIVE_CASES],
)
def test_negative_case(self, task_name, builder):
task, inp = builder()
result = task.evaluate(inp)
assert not result.success, f"{task_name} negative unexpectedly passed"
@pytest.mark.parametrize(
"task_name,builder",
EXTRA_POSITIVE_CASES,
ids=[name for name, _ in EXTRA_POSITIVE_CASES],
)
def test_extra_positive_case(self, task_name, builder):
task, inp = builder()
result = task.evaluate(inp)
assert result.success, f"{task_name} extra positive failed: issues={result.issues}, warnings={result.warnings}"