-
Notifications
You must be signed in to change notification settings - Fork 1
claude-code-chat-browser: YAML escape fix — handle colons, hashes, tabs, bool literals in frontmatter export #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ac14821
YAML frontmatter: always-quote scalars for safe round-trip parsing
clean6378-max-it 6579447
Fix CI: newline-only YAML block scalars, ruff format, types-PyYAML
clean6378-max-it df37c92
YAML frontmatter: drop block scalars so all strings round-trip
clean6378-max-it eaa4a0d
test: use delimiter-aware frontmatter extraction in roundtrip test
clean6378-max-it e1fef70
YAML frontmatter: emit models_used and service_tiers as sequences
clean6378-max-it File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """YAML frontmatter escaping and round-trip tests for md_exporter.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
| import yaml | ||
| from hypothesis import given, settings, strategies as st | ||
|
|
||
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | ||
|
|
||
| from models.session import SessionDict | ||
| from utils.md_exporter import ( | ||
| _append_yaml_value, | ||
| _escape_yaml, | ||
| _session_frontmatter_dict, | ||
| session_to_markdown, | ||
| ) | ||
|
|
||
| FUZZ_SETTINGS = settings(max_examples=100) | ||
|
|
||
|
|
||
| def _extract_frontmatter_dict(markdown: str) -> dict: | ||
| lines = markdown.splitlines() | ||
| if not lines or lines[0].strip() != "---": | ||
| raise ValueError("missing opening frontmatter delimiter") | ||
| yaml_lines: list[str] = [] | ||
| for line in lines[1:]: | ||
| if line.strip() == "---": | ||
| break | ||
| yaml_lines.append(line) | ||
| else: | ||
| raise ValueError("missing closing frontmatter delimiter") | ||
| loaded = yaml.safe_load("\n".join(yaml_lines)) | ||
| return loaded if isinstance(loaded, dict) else {} | ||
|
|
||
|
|
||
| def _base_session(**overrides: object) -> SessionDict: | ||
| session: SessionDict = { | ||
| "session_id": "sess-001", | ||
| "title": "Hello", | ||
| "messages": [{"role": "user", "text": "hi"}], | ||
| "metadata": { | ||
| "session_id": "sess-001", | ||
| "models_used": ["claude-sonnet-4-20250514"], | ||
| "first_timestamp": "2026-01-02T12:00:00Z", | ||
| "last_timestamp": "2026-01-02T12:30:00Z", | ||
| "total_input_tokens": 120, | ||
| "total_output_tokens": 45, | ||
| "total_cache_read_tokens": 10, | ||
| "total_tool_calls": 2, | ||
| "tool_call_counts": {"Read": 2}, | ||
| "cwd": "/workspace", | ||
| "git_branch": "main", | ||
| "version": "1.0.0", | ||
| "permission_mode": "default", | ||
| }, | ||
| } | ||
| if overrides: | ||
| for key, value in overrides.items(): | ||
| if key == "metadata" and isinstance(value, dict): | ||
| session["metadata"].update(value) # type: ignore[typeddict-item] | ||
| else: | ||
| session[key] = value # type: ignore[literal-required] | ||
| return session | ||
|
|
||
|
|
||
| class TestYamlFrontmatterRoundtrip: | ||
| def test_yaml_frontmatter_roundtrip(self): | ||
| session = _base_session( | ||
| title="Fix: handle edge case #42", | ||
| metadata={ | ||
| "cwd": r"C:\Users\dev\project", | ||
| "git_branch": "feat#yaml", | ||
| "permission_mode": "true", | ||
| "stop_reasons": {"max_tokens": 1, "end_turn": 2}, | ||
| "tool_call_counts": {"Read": 1, "Fix: tool": 1}, | ||
| }, | ||
| ) | ||
| md = session_to_markdown(session) | ||
| assert _extract_frontmatter_dict(md) == _session_frontmatter_dict(session) | ||
|
|
||
| def test_multiline_title_uses_quoted_scalar(self): | ||
| session = _base_session(title="line one\nline two") | ||
| md = session_to_markdown(session) | ||
| assert 'title: "line one\\nline two"' in md.split("---")[1] | ||
| assert _extract_frontmatter_dict(md)["title"] == "line one\nline two" | ||
|
|
||
| def test_tab_and_hash_in_title(self): | ||
| session = _base_session(title="tab\there # not a comment") | ||
| md = session_to_markdown(session) | ||
| assert _extract_frontmatter_dict(md)["title"] == "tab\there # not a comment" | ||
|
|
||
| def test_models_used_serializes_as_yaml_sequence(self): | ||
| session = _base_session( | ||
| metadata={"models_used": ["claude-sonnet-4", "claude-opus-4"]}, | ||
| ) | ||
| md = session_to_markdown(session) | ||
| frontmatter = _extract_frontmatter_dict(md) | ||
| assert frontmatter["models_used"] == ["claude-sonnet-4", "claude-opus-4"] | ||
| assert "models_used:\n" in md.split("---")[1] or "models_used:" in md.split("---")[1] | ||
| assert ' - "claude-sonnet-4"' in md.split("---")[1] | ||
|
|
||
|
|
||
| @FUZZ_SETTINGS | ||
| @given(st.text()) | ||
| def test_escape_yaml_roundtrip(s: str) -> None: | ||
| """Double-quoted scalars round-trip for arbitrary text.""" | ||
| loaded = yaml.safe_load(f"key: {_escape_yaml(s)}") | ||
| assert loaded["key"] == s | ||
|
|
||
|
|
||
| @FUZZ_SETTINGS | ||
| @given(st.text()) | ||
| def test_yaml_string_field_roundtrip(s: str) -> None: | ||
| """Frontmatter string serializer round-trips arbitrary text.""" | ||
| lines: list[str] = [] | ||
| _append_yaml_value(lines, "key", s) | ||
| loaded = yaml.safe_load("\n".join(lines)) | ||
| assert loaded["key"] == s | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: cppalliance/claude-code-chat-browser
Length of output: 200
test_yaml_string_field_roundtripfails for strings ending in\n_append_yaml_value(lines, "key", "\n")serializes tokey: |-\n, which loads back as""because|-strips trailing line breaks.st.text()can shrink to this case, so the fuzz test is expected to fail unless trailing-newline strings are handled differently (for example,|or quoted escaping).🤖 Prompt for AI Agents