Phase 2: Add reasoning and TipTap content endpoints

- Extend Document model with reasoning_type, confidence, reasoning_steps, model_source, tiptap_content fields
- Add new endpoints:
  - GET /documents/{id}/reasoning - Get reasoning metadata
  - PATCH /documents/{id}/reasoning - Update reasoning metadata
  - GET /documents/{id}/reasoning-panel - Get reasoning panel data for UI
  - POST /documents/{id}/reasoning-steps - Add reasoning step
  - DELETE /documents/{id}/reasoning-steps/{step} - Delete reasoning step
  - GET /documents/{id}/content?format=tiptap|markdown - Get content in TipTap or Markdown
  - PUT /documents/{id}/content - Update content (supports both TipTap JSON and Markdown)
- Add TipTap to Markdown conversion
- Update database schema with new columns
- Add comprehensive tests for all new endpoints
- All 37 tests passing
This commit is contained in:
Motoko
2026-03-30 23:11:44 +00:00
parent 0645b9c59c
commit bbbe42358d
5 changed files with 880 additions and 7 deletions

View File

@@ -134,3 +134,361 @@ async def test_remove_tag(client):
headers={"Authorization": f"Bearer {token}"}
)
assert remove_resp.status_code == 204
# =============================================================================
# Phase 2: Reasoning Endpoint Tests
# =============================================================================
@pytest.mark.asyncio
async def test_get_document_reasoning_empty(client):
"""Test getting reasoning from a document with no reasoning set."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "No Reasoning Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
response = await client.get(
f"/api/v1/documents/{doc_id}/reasoning",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["reasoning_type"] is None
assert data["confidence"] is None
assert data["reasoning_steps"] == []
assert data["model_source"] is None
@pytest.mark.asyncio
async def test_patch_document_reasoning(client):
"""Test updating reasoning metadata."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Reasoning Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Update reasoning
response = await client.patch(
f"/api/v1/documents/{doc_id}/reasoning",
json={
"reasoning_type": "chain",
"confidence": 0.87,
"model_source": "claude-3",
"reasoning_steps": [
{"step": 1, "thought": "First thought", "conclusion": "First conclusion"}
]
},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["reasoning_type"] == "chain"
assert data["confidence"] == 0.87
assert data["model_source"] == "claude-3"
assert len(data["reasoning_steps"]) == 1
@pytest.mark.asyncio
async def test_patch_reasoning_invalid_confidence(client):
"""Test that invalid confidence values are rejected."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Reasoning Doc 2"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Invalid confidence > 1.0 - Pydantic validation returns 422
response = await client.patch(
f"/api/v1/documents/{doc_id}/reasoning",
json={"confidence": 1.5},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 422
# Invalid confidence < 0.0 - Pydantic validation returns 422
response = await client.patch(
f"/api/v1/documents/{doc_id}/reasoning",
json={"confidence": -0.1},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_get_reasoning_panel(client):
"""Test getting reasoning panel data."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Panel Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Initially no reasoning
panel_resp = await client.get(
f"/api/v1/documents/{doc_id}/reasoning-panel",
headers={"Authorization": f"Bearer {token}"}
)
assert panel_resp.status_code == 200
panel = panel_resp.json()
assert panel["has_reasoning"] is False
assert panel["reasoning"] is None
assert panel["editable"] is True
# Add reasoning
await client.patch(
f"/api/v1/documents/{doc_id}/reasoning",
json={"reasoning_type": "idea", "confidence": 0.95},
headers={"Authorization": f"Bearer {token}"}
)
# Now has reasoning
panel_resp = await client.get(
f"/api/v1/documents/{doc_id}/reasoning-panel",
headers={"Authorization": f"Bearer {token}"}
)
panel = panel_resp.json()
assert panel["has_reasoning"] is True
assert panel["reasoning"]["reasoning_type"] == "idea"
@pytest.mark.asyncio
async def test_add_reasoning_step(client):
"""Test adding a reasoning step."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Steps Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Add first step
step1_resp = await client.post(
f"/api/v1/documents/{doc_id}/reasoning-steps",
json={"thought": "First step thought", "conclusion": "First conclusion"},
headers={"Authorization": f"Bearer {token}"}
)
assert step1_resp.status_code == 201
step1 = step1_resp.json()
assert step1["step"] == 1
assert step1["thought"] == "First step thought"
# Add second step
step2_resp = await client.post(
f"/api/v1/documents/{doc_id}/reasoning-steps",
json={"thought": "Second step thought"},
headers={"Authorization": f"Bearer {token}"}
)
assert step2_resp.status_code == 201
step2 = step2_resp.json()
assert step2["step"] == 2
@pytest.mark.asyncio
async def test_delete_reasoning_step(client):
"""Test deleting a reasoning step."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Delete Step Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Add steps
await client.post(
f"/api/v1/documents/{doc_id}/reasoning-steps",
json={"thought": "Step 1"},
headers={"Authorization": f"Bearer {token}"}
)
await client.post(
f"/api/v1/documents/{doc_id}/reasoning-steps",
json={"thought": "Step 2"},
headers={"Authorization": f"Bearer {token}"}
)
# Delete step 1
del_resp = await client.delete(
f"/api/v1/documents/{doc_id}/reasoning-steps/1",
headers={"Authorization": f"Bearer {token}"}
)
assert del_resp.status_code == 204
# Verify step 2 still exists
reasoning_resp = await client.get(
f"/api/v1/documents/{doc_id}/reasoning",
headers={"Authorization": f"Bearer {token}"}
)
steps = reasoning_resp.json()["reasoning_steps"]
assert len(steps) == 1
assert steps[0]["step"] == 2
@pytest.mark.asyncio
async def test_delete_nonexistent_step(client):
"""Test deleting a step that doesn't exist."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "No Steps Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
del_resp = await client.delete(
f"/api/v1/documents/{doc_id}/reasoning-steps/999",
headers={"Authorization": f"Bearer {token}"}
)
assert del_resp.status_code == 404
# =============================================================================
# Phase 2: TipTap Content Endpoint Tests
# =============================================================================
@pytest.mark.asyncio
async def test_get_content_tiptap_format(client):
"""Test getting content in TipTap format."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "TipTap Doc", "content": "Hello world"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
response = await client.get(
f"/api/v1/documents/{doc_id}/content",
params={"format": "tiptap"},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["format"] == "tiptap"
assert "content" in data
@pytest.mark.asyncio
async def test_get_content_markdown_format(client):
"""Test getting content in Markdown format."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Markdown Doc", "content": "# Hello\n\nWorld"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
response = await client.get(
f"/api/v1/documents/{doc_id}/content",
params={"format": "markdown"},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["format"] == "markdown"
assert "# Hello" in data["content"]
@pytest.mark.asyncio
async def test_put_content_tiptap(client):
"""Test updating content with TipTap JSON."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Update TipTap Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
tiptap_content = {
"type": "doc",
"content": [
{
"type": "heading",
"attrs": {"level": 1},
"content": [{"type": "text", "text": "My Title"}]
},
{
"type": "paragraph",
"content": [{"type": "text", "text": "Hello world"}]
}
]
}
response = await client.put(
f"/api/v1/documents/{doc_id}/content",
json={"content": tiptap_content, "format": "tiptap"},
headers={"Authorization": f"Bearer {token}"}
)
print(f"DEBUG: status={response.status_code} body={response.text}")
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}"
data = response.json()
assert data["tiptap_content"] is not None
# Verify markdown content was also updated
assert "My Title" in data["content"] or "# My Title" in data["content"]
@pytest.mark.asyncio
async def test_put_content_markdown(client):
"""Test updating content with Markdown."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Update MD Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
response = await client.put(
f"/api/v1/documents/{doc_id}/content",
json={"content": "# New Title\n\nNew content here", "format": "markdown"},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
data = response.json()
assert data["content"] == "# New Title\n\nNew content here"
@pytest.mark.asyncio
async def test_document_response_includes_reasoning_fields(client):
"""Test that DocumentResponse includes new reasoning fields."""
token, proj_id = await setup_project_and_get_token(client)
doc_resp = await client.post(
f"/api/v1/projects/{proj_id}/documents",
json={"title": "Full Doc"},
headers={"Authorization": f"Bearer {token}"}
)
doc_id = doc_resp.json()["id"]
# Add reasoning
await client.patch(
f"/api/v1/documents/{doc_id}/reasoning",
json={"reasoning_type": "reflection", "confidence": 0.99},
headers={"Authorization": f"Bearer {token}"}
)
# Get document and verify fields
get_resp = await client.get(
f"/api/v1/documents/{doc_id}",
headers={"Authorization": f"Bearer {token}"}
)
data = get_resp.json()
assert "reasoning_type" in data
assert "confidence" in data
assert "reasoning_steps" in data
assert "model_source" in data
assert "tiptap_content" in data
assert data["reasoning_type"] == "reflection"
assert data["confidence"] == 0.99