Add json/csv support to the posting script (issue #012)

TEXT_KINDS and TEXT_SUFFIXES now include json/csv, with matching
sniff_text_kind branches so both explicit --kind and positional auto-mode
work. Added validate_payload's JSON shape check mirroring the server.
Updated SKILL.md/docs/llm_posting_helper.md with real usage examples,
replacing the stale --as-markdown workaround guidance (kept only for
genuinely unstructured code, which still has no dedicated kind). Also
fixed an unrelated stale doc line found in the same table: html_full's
validation was documented as requiring <!doctype AND <html, but only
<html is actually required.

Verified via dry-run (explicit --kind, auto-mode, invalid-JSON rejection)
and one real end-to-end post through the script against the live dev
instance.

Added tests/scripts/test_daybook_post.py - 17 tests, plain unittest, zero
new dependencies, loaded via importlib since the script isn't a package.
Closes the test-coverage question the issue raised.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-02 15:38:47 +01:00
parent 532b4bd4eb
commit e137725cab
6 changed files with 177 additions and 8 deletions

View file

@ -0,0 +1,117 @@
import importlib.util
import tempfile
import unittest
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "daybook-posting"
/ "scripts"
/ "daybook_post.py"
)
def _load_module():
spec = importlib.util.spec_from_file_location("daybook_post", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
daybook_post = _load_module()
class SniffTextKindTestCase(unittest.TestCase):
def test_markdown_suffixes(self):
for suffix in (".md", ".markdown", ".txt"):
self.assertEqual(daybook_post.sniff_text_kind(suffix, "anything"), "markdown")
def test_json_suffix(self):
self.assertEqual(daybook_post.sniff_text_kind(".json", '{"a": 1}'), "json")
def test_csv_suffix(self):
self.assertEqual(daybook_post.sniff_text_kind(".csv", "a,b\n1,2"), "csv")
def test_html_full_sniffed_from_content(self):
self.assertEqual(
daybook_post.sniff_text_kind(".html", "<!doctype html><html></html>"), "html_full"
)
def test_deck_sniffed_from_content(self):
self.assertEqual(daybook_post.sniff_text_kind(".html", "<section>slide</section>"), "deck")
def test_html_fragment_is_the_fallback(self):
self.assertEqual(daybook_post.sniff_text_kind(".html", "<p>hi</p>"), "html_fragment")
class ValidatePayloadTestCase(unittest.TestCase):
def test_valid_json_passes(self):
daybook_post.validate_payload({"kind": "json", "content": '{"a": 1}'}, None) # no raise
def test_invalid_json_raises(self):
with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "json", "content": "{not json"}, None)
def test_deck_without_section_raises(self):
with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "deck", "content": "<h1>no slides</h1>"}, None)
def test_html_fragment_with_nested_html_raises(self):
with self.assertRaises(ValueError):
daybook_post.validate_payload(
{"kind": "html_fragment", "content": "<html><body>x</body></html>"}, None
)
def test_html_full_without_html_tag_raises(self):
with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "html_full", "content": "<p>x</p>"}, None)
def test_blank_markdown_raises(self):
with self.assertRaises(ValueError):
daybook_post.validate_payload({"kind": "markdown", "content": " "}, None)
def test_csv_has_no_shape_check(self):
daybook_post.validate_payload({"kind": "csv", "content": "anything at all"}, None) # no raise
def test_upload_path_skips_all_checks(self):
daybook_post.validate_payload({"upload_path": Path("whatever.png")}, None) # no raise
class ResolveAutoPathTestCase(unittest.TestCase):
def _args(self, path):
class Args:
pass
args = Args()
args.path = str(path)
args.as_markdown = False
args.title = None
return args
def test_json_file_infers_json_kind(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "data.json"
path.write_text('{"a": 1}', encoding="utf-8")
args = daybook_post.resolve_auto_path(self._args(path))
self.assertEqual(args.kind, "json")
self.assertEqual(args.content_file, str(path))
def test_csv_file_infers_csv_kind(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "data.csv"
path.write_text("a,b\n1,2", encoding="utf-8")
args = daybook_post.resolve_auto_path(self._args(path))
self.assertEqual(args.kind, "csv")
def test_image_file_becomes_multipart_upload(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "chart.png"
path.write_bytes(b"\x89PNG\r\n")
args = daybook_post.resolve_auto_path(self._args(path))
self.assertIsNone(args.kind)
self.assertEqual(args.file, str(path))
if __name__ == "__main__":
unittest.main()