diff --git a/roboflow/cli/handlers/image.py b/roboflow/cli/handlers/image.py index 382a3f18..ae421860 100644 --- a/roboflow/cli/handlers/image.py +++ b/roboflow/cli/handlers/image.py @@ -104,7 +104,7 @@ def search_images( Use --export to download matching results as a dataset. """ if project: - # Project-scoped search (legacy behavior) + # _handle_search scopes by injecting a `project:` RoboQL filter. args = ctx_to_args(ctx, query=query, project=project, limit=limit, cursor=cursor) _handle_search(args) elif export: @@ -422,10 +422,17 @@ def _handle_search(args): # noqa: ANN001 output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'") return + # search/v1 only scopes via a `project:` RoboQL filter (body params are + # ignored). Leading space = implicit AND; `AND (...)` 500s on free-text queries. + query = args.query + project = getattr(args, "project", None) + if project: + query = f"project:{project} {args.query}" + result = rfapi.workspace_search( api_key=api_key, workspace_url=workspace_url, - query=args.query, + query=query, page_size=args.limit, continuation_token=args.cursor, ) diff --git a/tests/cli/test_image_handler.py b/tests/cli/test_image_handler.py index 3c386a04..9497630a 100644 --- a/tests/cli/test_image_handler.py +++ b/tests/cli/test_image_handler.py @@ -462,9 +462,30 @@ def test_search(self, mock_workspace_search): sys.stdout = old mock_workspace_search.assert_called_once() + # -p must scope via a `project:` filter prepended to the query. + called_query = mock_workspace_search.call_args.kwargs["query"] + self.assertEqual(called_query, "project:proj tag:test") result = json.loads(buf.getvalue()) self.assertEqual(result["total"], 0) + @patch("roboflow.adapters.rfapi.workspace_search") + def test_search_without_project_is_unscoped(self, mock_workspace_search): + from roboflow.cli.handlers.image import _handle_search + + mock_workspace_search.return_value = {"results": [], "total": 0} + args = _make_args(json=True, query="tag:test", project=None, limit=10, cursor=None) + + buf = io.StringIO() + old = sys.stdout + sys.stdout = buf + try: + _handle_search(args) + finally: + sys.stdout = old + + called_query = mock_workspace_search.call_args.kwargs["query"] + self.assertEqual(called_query, "tag:test") + class TestImageAnnotate(unittest.TestCase): """Test the annotate handler."""