feat: initial commit from workspace-mcp
Some checks failed
Check Maintainer Edits Enabled / check-maintainer-edits (pull_request) Has been cancelled
Check Maintainer Edits Enabled / check-maintainer-edits-internal (pull_request) Has been cancelled
Docker Build and Push to GHCR / build-and-push (pull_request) Has been cancelled
Ruff / ruff (pull_request) Has been cancelled

This commit is contained in:
2026-03-17 19:23:33 -05:00
commit 395f0e2029
138 changed files with 41691 additions and 0 deletions

0
tests/gdocs/__init__.py Normal file
View File

View File

@@ -0,0 +1,455 @@
"""Tests for the Google Docs to Markdown converter."""
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from gdocs.docs_markdown import (
convert_doc_to_markdown,
format_comments_appendix,
format_comments_inline,
parse_drive_comments,
)
# --- Fixtures ---
SIMPLE_DOC = {
"title": "Simple Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Hello world\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "This is ", "textStyle": {}}},
{"textRun": {"content": "bold", "textStyle": {"bold": True}}},
{"textRun": {"content": " and ", "textStyle": {}}},
{
"textRun": {
"content": "italic",
"textStyle": {"italic": True},
}
},
{"textRun": {"content": " text.\n", "textStyle": {}}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
]
},
}
HEADINGS_DOC = {
"title": "Headings",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [{"textRun": {"content": "Title\n", "textStyle": {}}}],
"paragraphStyle": {"namedStyleType": "TITLE"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Heading one\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "HEADING_1"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Heading two\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "HEADING_2"},
}
},
]
},
}
TABLE_DOC = {
"title": "Table Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"table": {
"rows": 2,
"columns": 2,
"tableRows": [
{
"tableCells": [
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Name\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Age\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
]
},
{
"tableCells": [
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Alice\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "30\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
]
},
],
}
},
]
},
}
LIST_DOC = {
"title": "List Test",
"lists": {
"kix.list001": {
"listProperties": {
"nestingLevels": [
{"glyphType": "GLYPH_TYPE_UNSPECIFIED", "glyphSymbol": "\u2022"},
]
}
}
},
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Item one\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Item two\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
}
},
]
},
}
# --- Converter tests ---
class TestTextFormatting:
def test_plain_text(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "Hello world" in md
def test_bold(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "**bold**" in md
def test_italic(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "*italic*" in md
class TestHeadings:
def test_title(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "# Title" in md
def test_h1(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "# Heading one" in md
def test_h2(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "## Heading two" in md
class TestTables:
def test_table_header(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| Name | Age |" in md
def test_table_separator(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| --- | --- |" in md
def test_table_row(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| Alice | 30 |" in md
class TestLists:
def test_unordered(self):
md = convert_doc_to_markdown(LIST_DOC)
assert "- Item one" in md
assert "- Item two" in md
CHECKLIST_DOC = {
"title": "Checklist Test",
"lists": {
"kix.checklist001": {
"listProperties": {
"nestingLevels": [
{"glyphType": "GLYPH_TYPE_UNSPECIFIED"},
]
}
}
},
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Buy groceries\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.checklist001", "nestingLevel": 0},
}
},
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Walk the dog\n",
"textStyle": {"strikethrough": True},
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.checklist001", "nestingLevel": 0},
}
},
]
},
}
class TestChecklists:
def test_unchecked(self):
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "- [ ] Buy groceries" in md
def test_checked(self):
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "- [x] Walk the dog" in md
def test_checked_no_strikethrough(self):
"""Checked items should not have redundant ~~strikethrough~~ markdown."""
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "~~Walk the dog~~" not in md
def test_regular_bullet_not_checklist(self):
"""Bullet lists with glyphSymbol should remain as plain bullets."""
md = convert_doc_to_markdown(LIST_DOC)
assert "[ ]" not in md
assert "[x]" not in md
class TestEmptyDoc:
def test_empty(self):
md = convert_doc_to_markdown({"title": "Empty", "body": {"content": []}})
assert md.strip() == ""
# --- Comment parsing tests ---
class TestParseComments:
def test_filters_resolved(self):
response = {
"comments": [
{
"content": "open",
"resolved": False,
"author": {"displayName": "A"},
"replies": [],
},
{
"content": "closed",
"resolved": True,
"author": {"displayName": "B"},
"replies": [],
},
]
}
result = parse_drive_comments(response, include_resolved=False)
assert len(result) == 1
assert result[0]["content"] == "open"
def test_includes_resolved(self):
response = {
"comments": [
{
"content": "open",
"resolved": False,
"author": {"displayName": "A"},
"replies": [],
},
{
"content": "closed",
"resolved": True,
"author": {"displayName": "B"},
"replies": [],
},
]
}
result = parse_drive_comments(response, include_resolved=True)
assert len(result) == 2
def test_anchor_text(self):
response = {
"comments": [
{
"content": "note",
"resolved": False,
"author": {"displayName": "A"},
"quotedFileContent": {"value": "highlighted text"},
"replies": [],
}
]
}
result = parse_drive_comments(response)
assert result[0]["anchor_text"] == "highlighted text"
# --- Comment formatting tests ---
class TestInlineComments:
def test_inserts_footnote(self):
md = "Some text here."
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "text",
"replies": [],
"resolved": False,
}
]
result = format_comments_inline(md, comments)
assert "text[^c1]" in result
assert "[^c1]: **Alice**: Note." in result
def test_unmatched_goes_to_appendix(self):
md = "No match."
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "missing",
"replies": [],
"resolved": False,
}
]
result = format_comments_inline(md, comments)
assert "## Comments" in result
assert "> missing" in result
class TestAppendixComments:
def test_structure(self):
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "some text",
"replies": [],
"resolved": False,
}
]
result = format_comments_appendix(comments)
assert "## Comments" in result
assert "> some text" in result
assert "**Alice**: Note." in result
def test_empty(self):
assert format_comments_appendix([]).strip() == ""

View File

@@ -0,0 +1,139 @@
"""
Tests for update_paragraph_style batch operation support.
Covers the helpers, validation, and batch manager integration.
"""
import pytest
from unittest.mock import AsyncMock, Mock
from gdocs.docs_helpers import (
build_paragraph_style,
create_update_paragraph_style_request,
)
from gdocs.managers.validation_manager import ValidationManager
class TestBuildParagraphStyle:
def test_no_params_returns_empty(self):
style, fields = build_paragraph_style()
assert style == {}
assert fields == []
def test_heading_zero_maps_to_normal_text(self):
style, fields = build_paragraph_style(heading_level=0)
assert style["namedStyleType"] == "NORMAL_TEXT"
def test_heading_maps_to_named_style(self):
style, _ = build_paragraph_style(heading_level=3)
assert style["namedStyleType"] == "HEADING_3"
def test_heading_out_of_range_raises(self):
with pytest.raises(ValueError):
build_paragraph_style(heading_level=7)
def test_line_spacing_scaled_to_percentage(self):
style, _ = build_paragraph_style(line_spacing=1.5)
assert style["lineSpacing"] == 150.0
def test_dimension_field_uses_pt_unit(self):
style, _ = build_paragraph_style(indent_start=36.0)
assert style["indentStart"] == {"magnitude": 36.0, "unit": "PT"}
def test_multiple_params_combined(self):
style, fields = build_paragraph_style(
heading_level=2, alignment="CENTER", space_below=12.0
)
assert len(fields) == 3
assert style["alignment"] == "CENTER"
class TestCreateUpdateParagraphStyleRequest:
def test_returns_none_when_no_styles(self):
assert create_update_paragraph_style_request(1, 10) is None
def test_produces_correct_api_structure(self):
result = create_update_paragraph_style_request(1, 10, heading_level=1)
inner = result["updateParagraphStyle"]
assert inner["range"] == {"startIndex": 1, "endIndex": 10}
assert inner["paragraphStyle"]["namedStyleType"] == "HEADING_1"
assert inner["fields"] == "namedStyleType"
class TestValidateParagraphStyleParams:
@pytest.fixture()
def vm(self):
return ValidationManager()
def test_all_none_rejected(self, vm):
is_valid, _ = vm.validate_paragraph_style_params()
assert not is_valid
def test_wrong_types_rejected(self, vm):
assert not vm.validate_paragraph_style_params(heading_level=1.5)[0]
assert not vm.validate_paragraph_style_params(alignment=123)[0]
assert not vm.validate_paragraph_style_params(line_spacing="double")[0]
def test_negative_indent_start_rejected(self, vm):
is_valid, msg = vm.validate_paragraph_style_params(indent_start=-5.0)
assert not is_valid
assert "non-negative" in msg
def test_negative_indent_first_line_allowed(self, vm):
"""Hanging indent requires negative first-line indent."""
assert vm.validate_paragraph_style_params(indent_first_line=-18.0)[0]
def test_batch_validation_wired_up(self, vm):
valid_ops = [
{
"type": "update_paragraph_style",
"start_index": 1,
"end_index": 20,
"heading_level": 2,
},
]
assert vm.validate_batch_operations(valid_ops)[0]
no_style_ops = [
{"type": "update_paragraph_style", "start_index": 1, "end_index": 20},
]
assert not vm.validate_batch_operations(no_style_ops)[0]
class TestBatchManagerIntegration:
@pytest.fixture()
def manager(self):
from gdocs.managers.batch_operation_manager import BatchOperationManager
return BatchOperationManager(Mock())
def test_build_request_and_description(self, manager):
op = {
"type": "update_paragraph_style",
"start_index": 1,
"end_index": 50,
"heading_level": 2,
"alignment": "CENTER",
"line_spacing": 1.5,
}
request, desc = manager._build_operation_request(op, "update_paragraph_style")
assert "updateParagraphStyle" in request
assert "heading: H2" in desc
assert "1.5x" in desc
@pytest.mark.asyncio
async def test_end_to_end_execute(self, manager):
manager._execute_batch_requests = AsyncMock(return_value={"replies": [{}]})
success, message, meta = await manager.execute_batch_operations(
"doc-123",
[
{
"type": "update_paragraph_style",
"start_index": 1,
"end_index": 20,
"heading_level": 1,
}
],
)
assert success
assert meta["operations_count"] == 1