e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from deeptutor.book.context import build_book_context, normalize_book_references
|
|
from deeptutor.book.models import (
|
|
Block,
|
|
BlockStatus,
|
|
BlockType,
|
|
Book,
|
|
Chapter,
|
|
Page,
|
|
PageStatus,
|
|
Spine,
|
|
)
|
|
|
|
|
|
class FakeBookStorage:
|
|
def __init__(self) -> None:
|
|
self.book = Book(
|
|
id="book-1",
|
|
title="Fourier Notes",
|
|
description="A compact guide",
|
|
language="en",
|
|
)
|
|
self.spine = Spine(
|
|
book_id="book-1",
|
|
chapters=[
|
|
Chapter(
|
|
id="ch-1",
|
|
title="Signals",
|
|
summary="Signals are decomposed into bases.",
|
|
learning_objectives=["Define a signal", "Explain basis functions"],
|
|
)
|
|
],
|
|
exploration_summary="The source material emphasizes intuition.",
|
|
)
|
|
self.page = Page(
|
|
id="page-1",
|
|
book_id="book-1",
|
|
chapter_id="ch-1",
|
|
title="Signal Basics",
|
|
learning_objectives=["Understand time and frequency domains"],
|
|
status=PageStatus.READY,
|
|
blocks=[
|
|
Block(
|
|
type=BlockType.TEXT,
|
|
status=BlockStatus.READY,
|
|
title="Intro",
|
|
payload={
|
|
"body": "A signal maps time to values. <think>hidden reasoning</think>",
|
|
},
|
|
),
|
|
Block(
|
|
type=BlockType.SECTION,
|
|
status=BlockStatus.READY,
|
|
title="Core idea",
|
|
payload={
|
|
"intro": "We compare signals with reusable patterns.",
|
|
"subsections": [
|
|
{
|
|
"heading": "Basis",
|
|
"body": "A basis lets us rebuild complex signals.",
|
|
}
|
|
],
|
|
"key_takeaway": "A coefficient measures alignment.",
|
|
},
|
|
),
|
|
Block(
|
|
type=BlockType.CODE,
|
|
status=BlockStatus.READY,
|
|
title="Demo",
|
|
payload={
|
|
"language": "python",
|
|
"explanation": "Sample a sine wave.",
|
|
"code": "print('sample')\n" * 200,
|
|
},
|
|
),
|
|
Block(
|
|
type=BlockType.INTERACTIVE,
|
|
status=BlockStatus.READY,
|
|
title="Widget",
|
|
payload={
|
|
"description": "Drag frequency to see phase changes.",
|
|
"code": {"content": "<html>" + ("x" * 5000) + "</html>"},
|
|
},
|
|
),
|
|
Block(
|
|
type=BlockType.QUIZ,
|
|
status=BlockStatus.ERROR,
|
|
title="Check",
|
|
error="provider timeout",
|
|
),
|
|
],
|
|
)
|
|
|
|
def load_book(self, book_id: str) -> Book | None:
|
|
return self.book if book_id == "book-1" else None
|
|
|
|
def load_spine(self, book_id: str) -> Spine | None:
|
|
return self.spine if book_id == "book-1" else None
|
|
|
|
def load_page(self, book_id: str, page_id: str) -> Page | None:
|
|
return self.page if book_id == "book-1" and page_id == "page-1" else None
|
|
|
|
|
|
def test_normalize_book_references_merges_and_filters() -> None:
|
|
refs = normalize_book_references(
|
|
[
|
|
{"book_id": "book-1", "page_ids": ["page-1", "page-1", "page-2"]},
|
|
{"book_id": "", "page_ids": ["x"]},
|
|
{"book_id": "book-1", "page_ids": ["page-3"]},
|
|
{"book_id": "book-2", "page_ids": "bad"},
|
|
]
|
|
)
|
|
|
|
assert [ref.model_dump() for ref in refs] == [
|
|
{"book_id": "book-1", "page_ids": ["page-1", "page-2", "page-3"]}
|
|
]
|
|
|
|
|
|
def test_normalize_book_references_keeps_empty_page_ids() -> None:
|
|
"""Empty page_ids means 'whole book' — ref must be kept for book_id resolution."""
|
|
refs = normalize_book_references([{"book_id": "book-1", "page_ids": []}])
|
|
|
|
assert len(refs) == 1
|
|
assert refs[0].book_id == "book-1"
|
|
assert refs[0].page_ids == []
|
|
|
|
|
|
def test_build_book_context_serializes_readable_page_content() -> None:
|
|
result = build_book_context(
|
|
[{"book_id": "book-1", "page_ids": ["page-1"]}],
|
|
storage=FakeBookStorage(), # type: ignore[arg-type]
|
|
max_chars=8000,
|
|
block_char_limit=1500,
|
|
)
|
|
|
|
assert result.references == [{"book_id": "book-1", "page_ids": ["page-1"]}]
|
|
assert result.warnings == []
|
|
assert "# Book: Fourier Notes" in result.text
|
|
assert "Chapter: Signals" in result.text
|
|
assert "A signal maps time to values." in result.text
|
|
assert "hidden reasoning" not in result.text
|
|
assert "A basis lets us rebuild complex signals." in result.text
|
|
assert "Code (python):" in result.text
|
|
assert "...[truncated]" in result.text
|
|
assert "Drag frequency to see phase changes." in result.text
|
|
assert "<html>" not in result.text
|
|
assert "Status: error" in result.text
|
|
assert "provider timeout" in result.text
|
|
|
|
|
|
def test_build_book_context_reports_missing_refs() -> None:
|
|
result = build_book_context(
|
|
[{"book_id": "missing", "page_ids": ["page-1"]}],
|
|
storage=FakeBookStorage(), # type: ignore[arg-type]
|
|
)
|
|
|
|
assert result.text == ""
|
|
assert result.warnings == ["book_not_found:missing"]
|