← Files NovelistARCHIVED FILE

skills/novel-story-telling/tests/test_chapter_markup.py

4.79 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

"""Regression tests for chapter prose Markdown guardrails."""

from __future__ import annotations

import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path


SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
SPEC = importlib.util.spec_from_file_location(
    "check_continuity_under_test",
    SCRIPTS / "check_continuity.py",
)
assert SPEC and SPEC.loader
check_continuity = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = check_continuity
SPEC.loader.exec_module(check_continuity)


class DraftMarkupTests(unittest.TestCase):
    def test_canonical_prose_markup_is_accepted(self) -> None:
        draft = """Narration.

*“Spoken dialogue.”*

*“승인해.”* 레아가 말했다.

레아가 물었다. *“정말?”*

*An interior thought.*

**Semantic emphasis** and `machine.literal`.

---

A new scene.
"""
        self.assertEqual(check_continuity.draft_markup_issues(draft), [])

    def test_dialogue_requires_an_italic_curly_quote_span(self) -> None:
        invalid = (
            "“Plain dialogue.”",
            "Narration. “Inline dialogue.”",
            '*"Straight dialogue."*',
            "*“”*",
            "**“Bold dialogue.”**",
        )
        for draft in invalid:
            with self.subTest(draft=draft):
                codes = {
                    code
                    for code, _line, _message in check_continuity.draft_markup_issues(draft)
                }
                self.assertTrue(
                    {"chapter-dialogue-format", "chapter-dialogue-quotes"} & codes
                )

    def test_dialogue_follows_normal_markdown_paragraph_boundaries(self) -> None:
        draft = "Narration.\n*“The same paragraph after a soft break.”*"
        self.assertEqual(check_continuity.draft_markup_issues(draft), [])

    def test_dialogue_allows_nested_single_quotes_and_ignores_code_quotes(self) -> None:
        draft = """*“그는 ‘가지 마’라고 말했다.”*

The log emitted `“READY”`.
"""
        self.assertEqual(check_continuity.draft_markup_issues(draft), [])

    def test_five_dialogue_only_paragraphs_require_a_readability_break(self) -> None:
        draft = "\n\n".join(f"*“Line {index}.”*" for index in range(1, 6))
        findings = check_continuity.draft_markup_issues(draft)
        self.assertIn(
            "chapter-dialogue-attribution",
            {code for code, _line, _message in findings},
        )

    def test_attribution_breaks_a_dialogue_only_run(self) -> None:
        draft = """*“One.”*

*“Two.”*

*“Three.”*

*“Four.”*

*“Five.”* Rhea crossed her arms.
"""
        self.assertEqual(check_continuity.draft_markup_issues(draft), [])

    def test_legacy_scene_break_is_rejected(self) -> None:
        findings = check_continuity.draft_markup_issues("Before.\n\n* * *\n\nAfter.")
        self.assertIn("chapter-scene-break", {code for code, _line, _message in findings})

    def test_prose_list_blockquote_fence_and_unbalanced_markers_are_rejected(self) -> None:
        draft = """- dialogue disguised as a list

> dialogue disguised as a quote

```
log
```

*unfinished thought

`unfinished literal
"""
        codes = {code for code, _line, _message in check_continuity.draft_markup_issues(draft)}
        self.assertTrue(
            {
                "chapter-prose-list",
                "chapter-prose-blockquote",
                "chapter-prose-code-block",
                "chapter-italics",
                "chapter-inline-code",
            }.issubset(codes)
        )

    def test_scene_break_requires_blank_lines(self) -> None:
        findings = check_continuity.draft_markup_issues("Before.\n---\nAfter.")
        self.assertIn(
            "chapter-scene-break-spacing",
            {code for code, _line, _message in findings},
        )

    def test_additional_h2_section_is_rejected(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            chapters = root / "chapters"
            chapters.mkdir()
            (chapters / "001.test.md").write_text(
                '''---
id: chapter-001
type: chapter
number: 1
title: "Test"
slug: test
status: draft
pov: ""
timeline: ""
setting: ""
word_target: 100
characters: []
materials: []
macguffins: []
plot_threads: []
outline: ""
published: false
created: 2026-07-15
updated: 2026-07-15
tags: []
---
# Test

## Synopsis

Synopsis.

## Draft

This text would be published.

## Interlude

This text must not be silently omitted.

## Revision Notes

None.
''',
                encoding="utf-8",
            )

            report = check_continuity.audit(root)
            codes = {finding["code"] for finding in report["issues"]}
            self.assertIn("chapter-section-layout", codes)


if __name__ == "__main__":
    unittest.main()

SHA-256: 8b92025747db472e43fd234a3e8690168b9e69e0896dd1187aae9572605ef78f