|
26 | 26 | from basic_memory.schemas.document import ( |
27 | 27 | DocumentAgentObservationV1, |
28 | 28 | DocumentAgentOutputV1, |
| 29 | + DocumentAgentRelationV1, |
29 | 30 | DocumentIngestionStage, |
30 | 31 | DocumentIngestionV1, |
31 | 32 | DocumentMarkdownV1, |
@@ -309,3 +310,145 @@ def test_pdf_citation_rejects_source_entry_that_disagrees_with_provenance( |
309 | 310 | assert redirected != markdown |
310 | 311 | with pytest.raises(ValidationError, match="trusted source PDF page"): |
311 | 312 | parse_document_markdown(redirected) |
| 313 | + |
| 314 | + |
| 315 | +def test_relation_context_cannot_borrow_declared_page_citation() -> None: |
| 316 | + with pytest.raises(ValidationError, match="cannot define generated"): |
| 317 | + DocumentAgentOutputV1( |
| 318 | + title="Citations", |
| 319 | + body="", |
| 320 | + observations=( |
| 321 | + DocumentAgentObservationV1( |
| 322 | + category="fact", content="Claim.", locator=DocumentPageLocatorV1(page=2) |
| 323 | + ), |
| 324 | + ), |
| 325 | + relations=( |
| 326 | + DocumentAgentRelationV1( |
| 327 | + relation_type="supports", |
| 328 | + target="Evidence", |
| 329 | + context="Borrowed [^document-page-2]", |
| 330 | + ), |
| 331 | + ), |
| 332 | + ) |
| 333 | + |
| 334 | + |
| 335 | +def test_pdf_citation_rejects_duplicate_source_ids(raw_pdf: DocumentMarkdownV1) -> None: |
| 336 | + enriched = enrich( |
| 337 | + raw_pdf, |
| 338 | + ( |
| 339 | + DocumentAgentObservationV1( |
| 340 | + category="fact", content="Claim.", locator=DocumentPageLocatorV1(page=2) |
| 341 | + ), |
| 342 | + ), |
| 343 | + ) |
| 344 | + assert enriched.frontmatter.sources is not None |
| 345 | + duplicate = enriched.model_copy( |
| 346 | + update={ |
| 347 | + "frontmatter": enriched.frontmatter.model_copy( |
| 348 | + update={"sources": enriched.frontmatter.sources * 2} |
| 349 | + ) |
| 350 | + } |
| 351 | + ) |
| 352 | + with pytest.raises(ValidationError, match="citation source IDs must be unique"): |
| 353 | + parse_document_markdown(assemble_document_markdown(duplicate)) |
| 354 | + |
| 355 | + |
| 356 | +def test_page_citation_rejects_non_pdf_source(raw_pdf: DocumentMarkdownV1) -> None: |
| 357 | + markdown = assemble_document_markdown(raw_pdf).replace( |
| 358 | + "media_type: application/pdf", "media_type: text/plain" |
| 359 | + ) |
| 360 | + non_pdf = parse_document_markdown(markdown) |
| 361 | + with pytest.raises(ValueError, match="page citations require a PDF source"): |
| 362 | + enrich( |
| 363 | + non_pdf, |
| 364 | + ( |
| 365 | + DocumentAgentObservationV1( |
| 366 | + category="fact", content="Claim.", locator=DocumentPageLocatorV1(page=2) |
| 367 | + ), |
| 368 | + ), |
| 369 | + ) |
| 370 | + |
| 371 | + |
| 372 | +def test_page_label_is_retained_when_later_observation_omits_it( |
| 373 | + raw_pdf: DocumentMarkdownV1, |
| 374 | +) -> None: |
| 375 | + enriched = enrich( |
| 376 | + raw_pdf, |
| 377 | + ( |
| 378 | + DocumentAgentObservationV1( |
| 379 | + category="fact", |
| 380 | + content="First.", |
| 381 | + locator=DocumentPageLocatorV1(page=2, page_label="iv"), |
| 382 | + ), |
| 383 | + DocumentAgentObservationV1( |
| 384 | + category="fact", content="Second.", locator=DocumentPageLocatorV1(page=2) |
| 385 | + ), |
| 386 | + ), |
| 387 | + ) |
| 388 | + parsed = parse_document_markdown(assemble_document_markdown(enriched)) |
| 389 | + assert parsed.frontmatter.sources is not None |
| 390 | + assert len(parsed.frontmatter.sources) == 1 |
| 391 | + assert parsed.frontmatter.sources[0].locator.page_label == "iv" |
| 392 | + |
| 393 | + |
| 394 | +def test_page_label_is_added_when_earlier_observation_omits_it(raw_pdf: DocumentMarkdownV1) -> None: |
| 395 | + enriched = enrich( |
| 396 | + raw_pdf, |
| 397 | + ( |
| 398 | + DocumentAgentObservationV1( |
| 399 | + category="fact", content="First.", locator=DocumentPageLocatorV1(page=2) |
| 400 | + ), |
| 401 | + DocumentAgentObservationV1( |
| 402 | + category="fact", |
| 403 | + content="Second.", |
| 404 | + locator=DocumentPageLocatorV1(page=2, page_label="iv"), |
| 405 | + ), |
| 406 | + ), |
| 407 | + ) |
| 408 | + parsed = parse_document_markdown(assemble_document_markdown(enriched)) |
| 409 | + assert parsed.frontmatter.sources is not None |
| 410 | + assert len(parsed.frontmatter.sources) == 1 |
| 411 | + assert parsed.frontmatter.sources[0].locator.page_label == "iv" |
| 412 | + |
| 413 | + |
| 414 | +def test_agent_body_cannot_borrow_declared_page_citation() -> None: |
| 415 | + with pytest.raises(ValidationError, match="cannot define generated"): |
| 416 | + DocumentAgentOutputV1( |
| 417 | + title="Citations", |
| 418 | + body="Unsupported claim [^document-page-2]", |
| 419 | + observations=( |
| 420 | + DocumentAgentObservationV1( |
| 421 | + category="fact", content="Claim.", locator=DocumentPageLocatorV1(page=2) |
| 422 | + ), |
| 423 | + ), |
| 424 | + ) |
| 425 | + |
| 426 | + |
| 427 | +def test_unlocated_observation_cannot_borrow_declared_page_citation() -> None: |
| 428 | + with pytest.raises(ValidationError, match="cannot define generated"): |
| 429 | + DocumentAgentOutputV1( |
| 430 | + title="Citations", |
| 431 | + body="", |
| 432 | + observations=( |
| 433 | + DocumentAgentObservationV1( |
| 434 | + category="fact", content="Claim.", locator=DocumentPageLocatorV1(page=2) |
| 435 | + ), |
| 436 | + DocumentAgentObservationV1(category="fact", content="Unlocated [^document-page-2]"), |
| 437 | + ), |
| 438 | + ) |
| 439 | + |
| 440 | + |
| 441 | +def test_observation_context_cannot_borrow_declared_page_citation() -> None: |
| 442 | + with pytest.raises(ValidationError, match="cannot define generated"): |
| 443 | + DocumentAgentOutputV1( |
| 444 | + title="Citations", |
| 445 | + body="", |
| 446 | + observations=( |
| 447 | + DocumentAgentObservationV1( |
| 448 | + category="fact", |
| 449 | + content="Claim.", |
| 450 | + locator=DocumentPageLocatorV1(page=2), |
| 451 | + context="Borrowed [^document-page-2]", |
| 452 | + ), |
| 453 | + ), |
| 454 | + ) |
0 commit comments