631 lines
22 KiB
Python
631 lines
22 KiB
Python
"""
|
||
Notes app task definitions.
|
||
"""
|
||
# -- Task Index (auto-generated, do not edit) --
|
||
# 15 tasks | L1×2 L2×9 L3×2 L4×2
|
||
#
|
||
# [L1] ReadNotesCount 看看笔记里有几条便签
|
||
# [L2] ChangeViewMode 把笔记的视图模式改成{mode}
|
||
# [L2] CreateNewNote 在笔记里新建一条便签,标题写「{title}」
|
||
# [L2] AddNewTodo 在笔记的待办里添加一条「{text}」
|
||
# [L2] PinNote 把笔记里标题为「{note_title}」的便签置顶
|
||
# [L2] ReadNoteContent 看看笔记里标题为「{note_title}」的便签写了什么内容
|
||
# [L2] ReadTodoText 看看笔记里的待办事项有哪些
|
||
# [L2] DeleteTodo 把笔记待办里的「{todo_text}」删掉
|
||
# [L2] DeleteAllCompletedTodos 把笔记待办里已完成的事项全部删掉
|
||
# [L3] RestoreFromTrash 把笔记回收站里的「{note_title}」恢复回来
|
||
# [L3] SearchNoteTitle 在笔记里搜索「{keyword}」,告诉我搜到的便签标题
|
||
# [L1] CreateFolderAndMoveNote 在笔记里新建一个「{folder_name}」文件夹,然后把「{note_title}」移到这个文件夹里
|
||
# [L2] CreateNoteWithReminder 在笔记里新建一条标题为「{title}」的便签,写上「{content}」,设一个提醒,然后告诉我提醒时间
|
||
# [L4] PrivateNotesWorkflow 把笔记里「{note_title}」设为私密,然后告诉我现在私密便签里总共有几条
|
||
# [L4] TodoBatchWorkflow 在笔记待办里加一条「{new_todo}」,然后把「{existing_todo}」标为已完成,最后告诉我还有几条没完成的待办
|
||
# -- End Task Index --
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
from typing import Any
|
||
|
||
from bench_env.task.base import BaseTask
|
||
from bench_env.task.common_tasks import (
|
||
AnswerTask,
|
||
CriteriaTask,
|
||
build_answer_checks,
|
||
match_time,
|
||
)
|
||
from bench_env.task.judge import JudgeInput
|
||
from bench_env.task.notes.app import (
|
||
NOTES_CREATE_TITLES,
|
||
NOTES_MOVE_FOLDER_NAMES,
|
||
NOTES_NEW_FOLDER_NAMES,
|
||
NOTES_NEW_TODO_TEXTS,
|
||
NOTES_PRIVATE_CREATE_CONTENTS,
|
||
NOTES_PRIVATE_CREATE_TITLES,
|
||
NOTES_REMINDER_CREATE_CONTENTS,
|
||
NOTES_REMINDER_CREATE_TITLES,
|
||
Notes,
|
||
)
|
||
|
||
NOTES_ONLY_CHANGES = ["notes"]
|
||
TODOS_ONLY_CHANGES = ["todos"]
|
||
FOLDER_ONLY_CHANGES = ["folders", "selectedFolderId"]
|
||
FOLDER_AND_NOTES_CHANGES = ["folders", "selectedFolderId", "notes"]
|
||
|
||
|
||
# =============================================================================
|
||
# L1 — Atomic operations & simple queries
|
||
# =============================================================================
|
||
class ReadNotesCount(AnswerTask):
|
||
templates = ["看看笔记里有几条便签"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "query"
|
||
composition = "atomic"
|
||
difficulty = "L1"
|
||
capabilities = ["extract"]
|
||
answer_fields = [{"type": "number", "label": "便签数量"}]
|
||
|
||
def get_answer(self, input: JudgeInput) -> Any:
|
||
return len(Notes(input.apps_init["notes"]).visible_notes)
|
||
|
||
|
||
# =============================================================================
|
||
# L2 — Multi-step operations & simple queries
|
||
# =============================================================================
|
||
|
||
|
||
class ChangeViewMode(CriteriaTask):
|
||
templates = ["把笔记的视图模式改成{mode}"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "atomic"
|
||
difficulty = "L2"
|
||
capabilities = ["settings", "nav"]
|
||
parameters = {
|
||
"mode": {
|
||
"type": "enum",
|
||
"values": {"列表": "list", "宫格": "grid"},
|
||
"default": "list",
|
||
"description": "目标视图模式",
|
||
},
|
||
}
|
||
criteria = {"settings.notesViewMode": "{mode}"}
|
||
|
||
async def _post_sample(self, env):
|
||
await self._invert_criteria(env)
|
||
|
||
|
||
class CreateNewNote(BaseTask):
|
||
templates = ["在笔记里新建一条便签,标题写「{title}」"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["create"]
|
||
parameters = {
|
||
"title": {
|
||
"type": "enum",
|
||
"values": NOTES_CREATE_TITLES,
|
||
"default": "下周计划",
|
||
"description": "新建便签标题",
|
||
},
|
||
}
|
||
expected_changes = NOTES_ONLY_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
note = notes.find_note_by_title(self.p.title)
|
||
return [
|
||
{
|
||
"field": "notes.note_created",
|
||
"expected": self.p.title,
|
||
"actual": note.get("title") if note else None,
|
||
"passed": note is not None,
|
||
}
|
||
]
|
||
|
||
|
||
class AddNewTodo(BaseTask):
|
||
templates = ["在笔记的待办里添加一条「{text}」"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["create"]
|
||
parameters = {
|
||
"text": {
|
||
"type": "enum",
|
||
"values": NOTES_NEW_TODO_TEXTS,
|
||
"default": "买菜",
|
||
"description": "新增待办内容",
|
||
},
|
||
}
|
||
expected_changes = TODOS_ONLY_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
todo = notes.find_todo_by_text(self.p.text)
|
||
return [
|
||
{
|
||
"field": "todos.todo_created",
|
||
"expected": self.p.text,
|
||
"actual": todo.get("text") if todo else None,
|
||
"passed": todo is not None,
|
||
}
|
||
]
|
||
|
||
|
||
class PinNote(BaseTask):
|
||
templates = ["把笔记里标题为「{note_title}」的便签置顶"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["edit"]
|
||
parameters = {
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "目标便签标题",
|
||
},
|
||
"_note": {
|
||
"sampler": Notes._sample_visible_note,
|
||
"fields": {"note_title": "note_title"},
|
||
},
|
||
}
|
||
expected_changes = NOTES_ONLY_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
note = notes.find_note_by_title(self.p.note_title)
|
||
actual = bool((note or {}).get("pinned"))
|
||
return [
|
||
{
|
||
"field": "notes.note_pinned",
|
||
"expected": True,
|
||
"actual": actual,
|
||
"passed": note is not None and actual,
|
||
}
|
||
]
|
||
|
||
|
||
class ReadNoteContent(AnswerTask):
|
||
templates = [
|
||
"看看笔记里标题为「{note_title}」的便签写了什么内容",
|
||
"帮我查一下「{note_title}」这条便签里面的内容",
|
||
]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "query"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["extract"]
|
||
parameters = {
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "目标便签标题",
|
||
},
|
||
"keyword1": {
|
||
"type": "string",
|
||
"default": "牛奶",
|
||
"description": "内容关键词1(内容第一行)",
|
||
},
|
||
"keyword2": {
|
||
"type": "string",
|
||
"default": "鸡蛋",
|
||
"description": "内容关键词2(内容第二行)",
|
||
},
|
||
"_note_target": {
|
||
"sampler": Notes._sample_note_with_content_target,
|
||
"fields": {
|
||
"note_title": "note_title",
|
||
"keyword1": "keyword1",
|
||
"keyword2": "keyword2",
|
||
},
|
||
},
|
||
}
|
||
|
||
answer_fields=[{"type": "text", "label": "便签内容", "hint": "请输入内容全文"}]
|
||
|
||
def get_answer(self, input: JudgeInput) -> str:
|
||
notes_state = input.apps_init["notes"]
|
||
for note in notes_state.get("notes") or []:
|
||
if str(note.get("title") or "") == str(self.p.note_title):
|
||
return str(note.get("content") or "")
|
||
return ""
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
answer_text = str(input.answer or "")
|
||
return [
|
||
{
|
||
"field": "answer.keyword1",
|
||
"expected": self.p.keyword1,
|
||
"actual": answer_text,
|
||
"passed": self.p.keyword1 in answer_text,
|
||
},
|
||
{
|
||
"field": "answer.keyword2",
|
||
"expected": self.p.keyword2,
|
||
"actual": answer_text,
|
||
"passed": self.p.keyword2 in answer_text,
|
||
},
|
||
]
|
||
|
||
|
||
class ReadTodoText(AnswerTask):
|
||
templates = ["看看笔记里的待办事项有哪些"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "query"
|
||
composition = "atomic"
|
||
difficulty = "L2"
|
||
capabilities = ["extract", "nav"]
|
||
answer_fields = [{"type": "text", "label": "待办事项", "hint": "如:开会", "repeatable": True, "compare": "set"}]
|
||
|
||
def get_answer(self, input: JudgeInput) -> Any:
|
||
notes = Notes(input.apps_init["notes"])
|
||
return {
|
||
f"todo_{index + 1}": str(todo.get("text") or "")
|
||
for index, todo in enumerate(notes.incomplete_todos)
|
||
}
|
||
|
||
def get_expected_response(self, input: JudgeInput) -> list:
|
||
notes = Notes(input.apps_init["notes"])
|
||
return [[str(todo.get("text") or "") for todo in notes.incomplete_todos]]
|
||
|
||
|
||
# =============================================================================
|
||
# L3 — Multi-step operations & queries
|
||
# =============================================================================
|
||
class DeleteTodo(BaseTask):
|
||
templates = ["把笔记待办里的「{todo_text}」删掉"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["delete", "nav"]
|
||
parameters = {
|
||
"todo_text": {
|
||
"type": "string",
|
||
"default": "预约牙医",
|
||
"description": "目标待办文本",
|
||
},
|
||
"_todo": {
|
||
"sampler": Notes._sample_incomplete_todo,
|
||
"fields": {"todo_text": "todo_text"},
|
||
},
|
||
}
|
||
expected_changes = TODOS_ONLY_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"], init=input.apps_init["notes"])
|
||
return [
|
||
notes.check_todo_deleted(self.p.todo_text),
|
||
notes.check_other_init_todos_preserved(exclude_text=self.p.todo_text),
|
||
]
|
||
|
||
|
||
class DeleteAllCompletedTodos(BaseTask):
|
||
templates = ["把笔记待办里已完成的事项全部删掉"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
capabilities = ["delete", "nav"]
|
||
expected_changes = TODOS_ONLY_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"], init=input.apps_init["notes"])
|
||
return [
|
||
notes.check_all_completed_todos_deleted(),
|
||
notes.check_incomplete_todos_preserved(),
|
||
]
|
||
|
||
|
||
class RestoreFromTrash(BaseTask):
|
||
templates = ["把笔记回收站里的「{note_title}」恢复回来"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L3"
|
||
capabilities = ["edit", "nav"]
|
||
parameters = {
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "回收站目标便签标题",
|
||
},
|
||
"_note": {
|
||
"sampler": Notes._sample_visible_note,
|
||
"fields": {"note_title": "note_title"},
|
||
},
|
||
}
|
||
expected_changes = NOTES_ONLY_CHANGES
|
||
|
||
async def _post_sample(self, env):
|
||
state = await env.get_state()
|
||
notes_state = copy.deepcopy((state.get("apps") or {}).get("notes") or {})
|
||
notes_list = copy.deepcopy(notes_state.get("notes") or [])
|
||
now = int((((state.get("os") or {}).get("time") or {}).get("timestamp") or 0) or 0)
|
||
patched = False
|
||
for note in notes_list:
|
||
if str(note.get("title") or "").strip() != str(self.p.note_title):
|
||
continue
|
||
note["trashedAt"] = now or int(note.get("updatedAt", 0) or 1)
|
||
note["updatedAt"] = now or int(note.get("updatedAt", 0) or 1)
|
||
patched = True
|
||
break
|
||
if not patched:
|
||
raise ValueError(f"RestoreFromTrash target note not found: {self.p.note_title}")
|
||
await env.set_state(
|
||
{"apps": {"notes": {"notes": notes_list}}},
|
||
deep=True,
|
||
reload=False,
|
||
)
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
note = notes.find_note_by_title(self.p.note_title)
|
||
trashed_at = (note or {}).get("trashedAt")
|
||
return [
|
||
{
|
||
"field": "notes.note_restored",
|
||
"expected": None,
|
||
"actual": trashed_at,
|
||
"passed": note is not None and trashed_at is None,
|
||
}
|
||
]
|
||
|
||
|
||
class SearchNoteTitle(AnswerTask):
|
||
templates = [
|
||
"在笔记里搜索「{keyword}」,告诉我搜到的便签标题",
|
||
"帮我在笔记里搜一下「{keyword}」,看看有哪条便签",
|
||
]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "query"
|
||
composition = "deep_dive"
|
||
difficulty = "L3"
|
||
capabilities = ["search", "extract"]
|
||
parameters = {
|
||
"keyword": {
|
||
"type": "string",
|
||
"default": "购物",
|
||
"description": "搜索关键词",
|
||
},
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "匹配到的便签标题",
|
||
},
|
||
"_search_target": {
|
||
"sampler": Notes._sample_search_target,
|
||
"fields": {"keyword": "keyword", "note_title": "note_title"},
|
||
},
|
||
}
|
||
answer_fields = [{"type": "text", "label": "便签标题", "hint": "如:工作计划"}]
|
||
|
||
def get_answer(self, input: JudgeInput) -> Any:
|
||
return self.p.note_title
|
||
# =============================================================================
|
||
# L4 — Deep multi-step & hybrid tasks
|
||
# =============================================================================
|
||
|
||
|
||
class CreateFolderAndMoveNote(BaseTask):
|
||
templates = ["在笔记里新建一个「{folder_name}」文件夹,然后把「{note_title}」移到这个文件夹里"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "operate"
|
||
composition = "sequential"
|
||
difficulty = "L1"
|
||
max_steps = 30
|
||
capabilities = ["create", "edit", "nav"]
|
||
parameters = {
|
||
"folder_name": {
|
||
"type": "enum",
|
||
"values": NOTES_MOVE_FOLDER_NAMES,
|
||
"default": "重要",
|
||
"description": "新建文件夹名称",
|
||
},
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "目标便签标题",
|
||
},
|
||
"_note": {
|
||
"sampler": Notes._sample_visible_note,
|
||
"fields": {"note_title": "note_title"},
|
||
},
|
||
}
|
||
expected_changes = FOLDER_AND_NOTES_CHANGES
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
folder = notes.find_folder_by_name(self.p.folder_name)
|
||
note = notes.find_note_by_title(self.p.note_title)
|
||
target_folder_id = str((folder or {}).get("id") or "")
|
||
actual_folder_id = str((note or {}).get("folderId") or "")
|
||
return [
|
||
{
|
||
"field": "folders.folder_created",
|
||
"expected": self.p.folder_name,
|
||
"actual": folder.get("name") if folder else None,
|
||
"passed": folder is not None,
|
||
},
|
||
{
|
||
"field": "notes.note_folder",
|
||
"expected": target_folder_id,
|
||
"actual": actual_folder_id,
|
||
"passed": bool(folder and note and actual_folder_id == target_folder_id),
|
||
},
|
||
]
|
||
class CreateNoteWithReminder(BaseTask):
|
||
templates = ["在笔记里新建一条标题为「{title}」的便签,写上「{content}」,设一个提醒,然后告诉我提醒时间"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "hybrid"
|
||
composition = "sequential"
|
||
difficulty = "L2"
|
||
max_steps = 45
|
||
capabilities = ["create", "edit", "extract"]
|
||
parameters = {
|
||
"title": {
|
||
"type": "enum",
|
||
"values": NOTES_REMINDER_CREATE_TITLES,
|
||
"default": "明天开会",
|
||
"description": "新建便签标题",
|
||
},
|
||
"content": {
|
||
"type": "enum",
|
||
"values": NOTES_REMINDER_CREATE_CONTENTS,
|
||
"default": "记得带文件",
|
||
"description": "新建便签内容",
|
||
},
|
||
}
|
||
expected_changes = NOTES_ONLY_CHANGES
|
||
answer_fields = [{"type": "text", "label": "提醒时间", "hint": "如:09:15"}]
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"])
|
||
note = notes.latest_note_by_title(self.p.title)
|
||
content = str((note or {}).get("content") or "")
|
||
alarm_at = (note or {}).get("alarmAt")
|
||
answer_text = str(input.answer or "")
|
||
labels = (
|
||
Notes.reminder_time_labels(int(alarm_at))
|
||
if isinstance(alarm_at, (int, float)) and alarm_at > 0
|
||
else []
|
||
)
|
||
expected_time = labels[-1] if labels else None
|
||
return [
|
||
{
|
||
"field": "notes.note_created",
|
||
"expected": self.p.title,
|
||
"actual": note.get("title") if note else None,
|
||
"passed": note is not None,
|
||
},
|
||
{
|
||
"field": "notes.note_content",
|
||
"expected": self.p.content,
|
||
"actual": content,
|
||
"passed": note is not None and self.p.content in content,
|
||
},
|
||
{
|
||
"field": "notes.note_reminder",
|
||
"expected": "alarmAt > 0",
|
||
"actual": alarm_at,
|
||
"passed": isinstance(alarm_at, (int, float)) and alarm_at > 0,
|
||
},
|
||
{
|
||
"field": "answer.reminder_time",
|
||
"expected": expected_time,
|
||
"actual": input.answer,
|
||
"passed": match_time(str(expected_time), answer_text) if expected_time is not None else False,
|
||
},
|
||
]
|
||
|
||
|
||
class PrivateNotesWorkflow(BaseTask):
|
||
templates = [
|
||
"把笔记里「{note_title}」设为私密,然后告诉我现在私密便签里总共有几条",
|
||
"把「{note_title}」这条便签设成私密的,看看私密区域一共有多少条便签",
|
||
]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "hybrid"
|
||
composition = "deep_dive"
|
||
difficulty = "L4"
|
||
capabilities = ["edit", "nav", "extract"]
|
||
parameters = {
|
||
"note_title": {
|
||
"type": "string",
|
||
"default": "购物清单",
|
||
"description": "目标便签标题",
|
||
},
|
||
"_note": {
|
||
"sampler": Notes._sample_visible_note,
|
||
"fields": {"note_title": "note_title"},
|
||
},
|
||
}
|
||
expected_changes = NOTES_ONLY_CHANGES
|
||
answer_fields = [{"type": "number", "label": "私密便签总数"}]
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"], init=input.apps_init["notes"])
|
||
note = notes.find_note_by_title(self.p.note_title)
|
||
actual_private = bool((note or {}).get("isPrivate"))
|
||
# sampler 保证 init 中该笔记非私密,期望私密数 = init 私密数 + 1
|
||
expected_count = len(notes.init.private_notes) + 1
|
||
answer_checks = build_answer_checks(expected_count, input.answer)
|
||
return [
|
||
{
|
||
"field": "notes.note_private",
|
||
"expected": True,
|
||
"actual": actual_private,
|
||
"passed": note is not None and actual_private,
|
||
},
|
||
*answer_checks,
|
||
]
|
||
|
||
|
||
class TodoBatchWorkflow(BaseTask):
|
||
templates = ["在笔记待办里加一条「{new_todo}」,然后把「{existing_todo}」标为已完成,最后告诉我还有几条没完成的待办"]
|
||
apps = ["notes"]
|
||
scope = "S1"
|
||
objective = "hybrid"
|
||
composition = "deep_dive"
|
||
difficulty = "L4"
|
||
capabilities = ["create", "edit", "extract"]
|
||
parameters = {
|
||
"new_todo": {
|
||
"type": "enum",
|
||
"values": ["整理衣柜", "打扫房间", "洗车"],
|
||
"default": "整理衣柜",
|
||
"description": "新增待办内容",
|
||
},
|
||
"existing_todo": {
|
||
"type": "string",
|
||
"default": "明天去车站",
|
||
"description": "原有未完成待办",
|
||
},
|
||
"_todo": {
|
||
"sampler": Notes._sample_incomplete_todo,
|
||
"fields": {"existing_todo": "todo_text"},
|
||
},
|
||
}
|
||
expected_changes = TODOS_ONLY_CHANGES
|
||
answer_fields = [{"type": "number", "label": "未完成待办数"}]
|
||
|
||
def check_goals(self, input: JudgeInput) -> list[dict[str, Any]]:
|
||
notes = Notes(input.apps["notes"], init=input.apps_init["notes"])
|
||
new_todo = notes.find_new_todo_by_text(self.p.new_todo)
|
||
existing_todo = notes.find_todo_by_text(self.p.existing_todo)
|
||
# init incomplete count +1 (new_todo added) -1 (existing_todo completed) = unchanged
|
||
expected_count = len(notes.init.incomplete_todos)
|
||
answer_checks = build_answer_checks(expected_count, input.answer)
|
||
return [
|
||
{
|
||
"field": "todos.todo_created",
|
||
"expected": self.p.new_todo,
|
||
"actual": new_todo.get("text") if new_todo else None,
|
||
"passed": new_todo is not None,
|
||
},
|
||
{
|
||
"field": "todos.todo_completed",
|
||
"expected": True,
|
||
"actual": bool((existing_todo or {}).get("completed")),
|
||
"passed": existing_todo is not None and bool(existing_todo.get("completed")),
|
||
},
|
||
*answer_checks,
|
||
]
|