Files
item_bank/tests/test_notebook.py
T
2026-08-01 16:50:55 +08:00

235 lines
10 KiB
Python

"""笔记本行为:轻量记录、我的笔记、待复习、知识点导航、多维检索。"""
CAPTURED = {"type": "unclassified", "stem_markdown": "拍照记下来的一道题"}
async def _create(client, headers, **payload):
r = await client.post("/api/questions", json=payload, headers=headers)
assert r.status_code == 201, r.text
return r.json()
class TestCaptureFirst:
"""主路径要轻:拍完就能存,不强制补题型/答案。"""
async def test_save_without_type_or_answer(self, client, auth_headers):
h = await auth_headers()
# 连 type 都不传
note = await _create(client, h, stem_markdown="只有题干")
assert note["type"] == "unclassified"
assert note["correct_answer"] is None
assert note["options"] is None
async def test_unclassified_note_is_searchable(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, **CAPTURED)
r = await client.get("/api/questions?type=unclassified", headers=h)
assert r.json()["total"] == 1
async def test_can_upgrade_note_later(self, client, auth_headers):
"""先随手记,之后想练习时再补题型和答案。"""
h = await auth_headers()
note = await _create(client, h, **CAPTURED)
r = await client.put(
f"/api/questions/{note['id']}",
json={
"type": "single_choice",
"options": [
{"key": "A", "text_markdown": "1"},
{"key": "B", "text_markdown": "2"},
],
"correct_answer": ["B"],
},
headers=h,
)
assert r.status_code == 200
assert r.json()["type"] == "single_choice"
assert r.json()["correct_answer"] == ["B"]
class TestMyNote:
""""我的笔记"是一等公民,且与 AI 产出分离。"""
async def test_write_and_read_my_note(self, client, auth_headers):
h = await auth_headers()
note = await _create(
client, h, stem_markdown="题目", my_note_markdown="我错在没看清定义域"
)
assert note["my_note_markdown"] == "我错在没看清定义域"
async def test_my_note_is_searchable(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="题目甲", my_note_markdown="关键是换元法")
await _create(client, h, stem_markdown="题目乙")
r = await client.get("/api/questions?q=换元法", headers=h)
assert r.json()["total"] == 1
assert r.json()["items"][0]["stem_markdown"] == "题目甲"
async def test_my_note_and_ai_explanation_coexist(self, client, auth_headers):
h = await auth_headers()
note = await _create(
client,
h,
stem_markdown="题目",
my_note_markdown="我的反思",
explanation_markdown="AI 的讲解",
)
assert note["my_note_markdown"] == "我的反思"
assert note["explanation_markdown"] == "AI 的讲解"
class TestNeedsReview:
async def test_mark_and_unmark(self, client, auth_headers):
h = await auth_headers()
note = await _create(client, h, stem_markdown="要复习的题")
assert note["needs_review"] is False
r = await client.put(
f"/api/questions/{note['id']}", json={"needs_review": True}, headers=h
)
assert r.json()["needs_review"] is True
review = (await client.get("/api/notebook/review", headers=h)).json()
assert len(review) == 1
assert review[0]["marked"] is True
assert review[0]["last_wrong"] is False
await client.put(
f"/api/questions/{note['id']}", json={"needs_review": False}, headers=h
)
assert (await client.get("/api/notebook/review", headers=h)).json() == []
async def test_marked_without_ever_answering(self, client, auth_headers):
"""笔记本的逻辑:我抄进来的就是错题,不必先答一遍来证明。"""
h = await auth_headers()
await _create(client, h, stem_markdown="没答过但要复习", needs_review=True)
review = (await client.get("/api/notebook/review", headers=h)).json()
assert len(review) == 1
assert review[0]["wrong_count"] == 0
async def test_filter_by_needs_review(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="要复习", needs_review=True)
await _create(client, h, stem_markdown="不用复习")
r = await client.get("/api/questions?needs_review=true", headers=h)
assert r.json()["total"] == 1
async def test_review_isolated_per_user(self, client, auth_headers):
ha = await auth_headers("alice")
hb = await auth_headers("bob")
await _create(client, ha, stem_markdown="alice 的题", needs_review=True)
assert len((await client.get("/api/notebook/review", headers=ha)).json()) == 1
assert (await client.get("/api/notebook/review", headers=hb)).json() == []
class TestOverview:
async def test_overview_shape(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="导数题", tags=["导数", "微积分"])
await _create(client, h, stem_markdown="待复习的导数题", tags=["导数"], needs_review=True)
await _create(client, h, stem_markdown="没归类的题")
ov = (await client.get("/api/notebook/overview", headers=h)).json()
assert ov["total_notes"] == 3
assert ov["review_count"] == 1
assert ov["untagged_count"] == 1
tags = {t["name"]: (t["count"], t["needs_review_count"]) for t in ov["tags"]}
assert tags["导数"] == (2, 1)
assert tags["微积分"] == (1, 0)
# 最近记录倒序
assert ov["recent"][0]["stem_markdown"] == "没归类的题"
async def test_empty_overview(self, client, auth_headers):
h = await auth_headers()
ov = (await client.get("/api/notebook/overview", headers=h)).json()
assert ov["total_notes"] == 0
assert ov["tags"] == []
assert ov["recent"] == []
async def test_overview_isolated_per_user(self, client, auth_headers):
ha = await auth_headers("alice")
hb = await auth_headers("bob")
await _create(client, ha, stem_markdown="alice 的题", tags=["私有标签"])
ov = (await client.get("/api/notebook/overview", headers=hb)).json()
assert ov["total_notes"] == 0
assert ov["tags"] == []
async def test_no_accuracy_stats_endpoint(self, client, auth_headers):
"""这是笔记本,不做正确率统计。"""
h = await auth_headers()
assert (await client.get("/api/practice/stats", headers=h)).status_code == 404
class TestSearchDimensions:
"""按知识点 + 时间 + 难度 多维检索。"""
async def test_difficulty_filters(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="", difficulty=1)
await _create(client, h, stem_markdown="", difficulty=3)
await _create(client, h, stem_markdown="", difficulty=5)
async def total(qs):
return (await client.get(f"/api/questions{qs}", headers=h)).json()["total"]
assert await total("?difficulty=3") == 1
assert await total("?difficulty_min=3") == 2
assert await total("?difficulty_max=3") == 2
assert await total("?difficulty_min=2&difficulty_max=4") == 1
async def test_multi_tag_is_intersection(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="两个标签", tags=["导数", "极限"])
await _create(client, h, stem_markdown="一个标签", tags=["导数"])
async def total(qs):
return (await client.get(f"/api/questions{qs}", headers=h)).json()["total"]
assert await total("?tag=导数") == 2
assert await total("?tag=导数&tag=极限") == 1
async def test_time_range(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="现在记的")
async def total(qs):
return (await client.get(f"/api/questions{qs}", headers=h)).json()["total"]
assert await total("?created_after=2020-01-01T00:00:00") == 1
assert await total("?created_after=2099-01-01T00:00:00") == 0
assert await total("?created_before=2099-01-01T00:00:00") == 1
assert await total("?created_before=2020-01-01T00:00:00") == 0
async def test_untagged_filter(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="有标签", tags=["导数"])
await _create(client, h, stem_markdown="没标签")
r = await client.get("/api/questions?untagged=true", headers=h)
assert r.json()["total"] == 1
assert r.json()["items"][0]["stem_markdown"] == "没标签"
async def test_combined_filters(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="命中", tags=["导数"], difficulty=5, needs_review=True)
await _create(client, h, stem_markdown="难度不符", tags=["导数"], difficulty=1, needs_review=True)
await _create(client, h, stem_markdown="标签不符", tags=["极限"], difficulty=5, needs_review=True)
r = await client.get(
"/api/questions?tag=导数&difficulty_min=4&needs_review=true", headers=h
)
assert r.json()["total"] == 1
assert r.json()["items"][0]["stem_markdown"] == "命中"
async def test_sort_orders(self, client, auth_headers):
h = await auth_headers()
await _create(client, h, stem_markdown="先记的")
await _create(client, h, stem_markdown="后记的")
desc = (await client.get("/api/questions?sort=created_desc", headers=h)).json()
assert desc["items"][0]["stem_markdown"] == "后记的"
asc = (await client.get("/api/questions?sort=created_asc", headers=h)).json()
assert asc["items"][0]["stem_markdown"] == "先记的"