235 lines
10 KiB
Python
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"] == "先记的"
|