fix(sources): query reference edge by in/out in retry endpoint (#899)

POST /sources/{id}/retry looked up a source's notebooks with
`SELECT notebook FROM reference WHERE source = $source_id`, but `reference`
is a graph edge (RELATE source->reference->notebook) with only `in`/`out`
columns. The query matched nothing, so `notebook_ids` was always empty and
the endpoint returned 400 for every source. Mirror the working query used in
the source-list path: `SELECT VALUE out FROM reference WHERE in = $source_id`.

Adds regression tests asserting retry re-queues a linked source and only 400s
when a source is genuinely unlinked.
This commit is contained in:
Luis Novo 2026-06-16 04:29:28 -03:00 committed by GitHub
parent d39af07660
commit a05c9d2de2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 64 additions and 4 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- `POST /sources/{id}/retry` no longer returns `400 "Source is not associated with any notebooks"` for every source; it now queries the `reference` graph edge by its `in`/`out` columns instead of a non-existent `source` column (#861)
## [1.9.0] - 2026-06-02
### Added

View file

@ -842,10 +842,15 @@ async def retry_source_processing(source_id: str):
)
# Continue with retry if we can't check status
# Get notebooks that this source belongs to
query = "SELECT notebook FROM reference WHERE source = $source_id"
references = await repo_query(query, {"source_id": source_id})
notebook_ids = [str(ref["notebook"]) for ref in references]
# Get notebooks that this source belongs to. `reference` is a graph edge
# (RELATE source->reference->notebook), so it only has `in`/`out` columns —
# there is no `source`/`notebook` column. Mirror the working query at the
# source-list path above. See issue #861.
references = await repo_query(
"SELECT VALUE out FROM reference WHERE in = $source_id",
{"source_id": ensure_record_id(source.id or source_id)},
)
notebook_ids = [str(nb_id) for nb_id in references] if references else []
if not notebook_ids:
raise HTTPException(

View file

@ -138,5 +138,57 @@ class TestAsyncSourceAssetPersistence:
assert source.asset is None
class TestRetrySourceProcessing:
"""POST /sources/{id}/retry must find a source's notebooks via the reference
edge's in/out columns, not a non-existent `source` column (#861)."""
@pytest.mark.asyncio
@patch("api.routers.sources.CommandService.submit_command_job", new_callable=AsyncMock)
@patch("api.routers.sources.repo_query", new_callable=AsyncMock)
@patch("api.routers.sources.Source.get", new_callable=AsyncMock)
async def test_retry_finds_notebooks_and_requeues(
self, mock_get, mock_repo_query, mock_submit, client
):
source = MagicMock()
source.id = "source:1"
source.command = None
source.title = "My source"
source.topics = []
source.full_text = None
source.asset = MagicMock(file_path=None, url="https://example.com/post")
source.save = AsyncMock()
source.get_embedded_chunks = AsyncMock(return_value=0)
mock_get.return_value = source
# The corrected query returns the linked notebook(s)
mock_repo_query.return_value = ["notebook:1"]
mock_submit.return_value = "123"
response = client.post("/api/sources/source:1/retry")
assert response.status_code == 200
# Regression guard: must query the reference edge by its `in` column
called_query = mock_repo_query.await_args.args[0]
assert "WHERE in = $source_id" in called_query
assert "SELECT VALUE out FROM reference" in called_query
@pytest.mark.asyncio
@patch("api.routers.sources.repo_query", new_callable=AsyncMock)
@patch("api.routers.sources.Source.get", new_callable=AsyncMock)
async def test_retry_400_only_when_truly_unlinked(
self, mock_get, mock_repo_query, client
):
source = MagicMock()
source.id = "source:1"
source.command = None
mock_get.return_value = source
mock_repo_query.return_value = [] # genuinely no notebooks
response = client.post("/api/sources/source:1/retry")
assert response.status_code == 400
assert "not associated with any notebooks" in response.json()["detail"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])