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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user