mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-08-22 07:23:49 +00:00
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:
parent
d39af07660
commit
a05c9d2de2
3 changed files with 64 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue