diff --git a/AGENTS.md b/AGENTS.md index d25d2eed96..50ec349241 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,13 @@ The codebase is split into: - Inject dependencies through constructors and preserve clean architecture boundaries. - Handle errors with domain-specific exceptions at the correct layer. +## Cherry-Picking to LTS Branches + +- Always use `git cherry-pick -x` when backporting commits from `main` to any `lts/*` branch. +- The `-x` flag appends `(cherry picked from commit )` to the commit message. CI validates this provenance line; commits without it will fail. +- When cherry-picking multiple commits, each must carry its own `(cherry picked from ...)` annotation. +- Run `git cherry-pick --continue` with `HUSKY=0` to skip pre-existing lint errors that are unrelated to the fix. + ## Project Conventions - Backend architecture adheres to DDD and Clean Architecture principles. diff --git a/api/controllers/console/files.py b/api/controllers/console/files.py index 109a3cd0d3..afaeeab192 100644 --- a/api/controllers/console/files.py +++ b/api/controllers/console/files.py @@ -105,7 +105,8 @@ class FilePreviewApi(Resource): @account_initialization_required def get(self, file_id): file_id = str(file_id) - text = FileService(db.engine).get_file_preview(file_id) + _, tenant_id = current_account_with_tenant() + text = FileService(db.engine).get_file_preview(file_id, tenant_id) return {"content": text} diff --git a/api/services/file_service.py b/api/services/file_service.py index a7060f3b92..be1449d274 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -173,12 +173,14 @@ class FileService: return upload_file - def get_file_preview(self, file_id: str): + def get_file_preview(self, file_id: str, tenant_id: str): """ Return a short text preview extracted from a document file. """ with self._session_maker(expire_on_commit=False) as session: - upload_file = session.query(UploadFile).where(UploadFile.id == file_id).first() + upload_file = session.scalar( + select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id).limit(1) + ) if not upload_file: raise NotFound("File not found") diff --git a/api/tests/test_containers_integration_tests/services/test_file_service.py b/api/tests/test_containers_integration_tests/services/test_file_service.py index 42dbdef1c9..4532005836 100644 --- a/api/tests/test_containers_integration_tests/services/test_file_service.py +++ b/api/tests/test_containers_integration_tests/services/test_file_service.py @@ -514,7 +514,7 @@ class TestFileService: db_session_with_containers.commit() - result = FileService(engine).get_file_preview(file_id=upload_file.id) + result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) assert result == "extracted text content" mock_external_service_dependencies["extract_processor"].load_from_upload_file.assert_called_once() @@ -529,7 +529,7 @@ class TestFileService: non_existent_id = str(fake.uuid4()) with pytest.raises(NotFound, match="File not found"): - FileService(engine).get_file_preview(file_id=non_existent_id) + FileService(engine).get_file_preview(file_id=non_existent_id, tenant_id=str(fake.uuid4())) def test_get_file_preview_unsupported_file_type( self, db_session_with_containers: Session, engine, mock_external_service_dependencies @@ -549,7 +549,7 @@ class TestFileService: db_session_with_containers.commit() with pytest.raises(UnsupportedFileTypeError): - FileService(engine).get_file_preview(file_id=upload_file.id) + FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) def test_get_file_preview_text_truncation( self, db_session_with_containers: Session, engine, mock_external_service_dependencies @@ -572,7 +572,7 @@ class TestFileService: long_text = "x" * 5000 # Longer than PREVIEW_WORDS_LIMIT mock_external_service_dependencies["extract_processor"].load_from_upload_file.return_value = long_text - result = FileService(engine).get_file_preview(file_id=upload_file.id) + result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) assert len(result) == 3000 # PREVIEW_WORDS_LIMIT assert result == "x" * 3000 diff --git a/api/tests/unit_tests/controllers/console/test_files.py b/api/tests/unit_tests/controllers/console/test_files.py index 5df9daa7f8..9acf68dfc3 100644 --- a/api/tests/unit_tests/controllers/console/test_files.py +++ b/api/tests/unit_tests/controllers/console/test_files.py @@ -278,7 +278,7 @@ class TestFileApiPost: class TestFilePreviewApi: - def test_get_preview(self, app, mock_file_service): + def test_get_preview(self, app, mock_account_context, mock_file_service): api = FilePreviewApi() get_method = unwrap(api.get) mock_file_service.get_file_preview.return_value = "preview text" diff --git a/api/tests/unit_tests/services/test_file_service.py b/api/tests/unit_tests/services/test_file_service.py index b7259c3e82..5e990b4da1 100644 --- a/api/tests/unit_tests/services/test_file_service.py +++ b/api/tests/unit_tests/services/test_file_service.py @@ -215,29 +215,29 @@ class TestFileService: upload_file = MagicMock(spec=UploadFile) upload_file.id = "file_id" upload_file.extension = "pdf" - mock_db_session.query().where().first.return_value = upload_file + mock_db_session.scalar.return_value = upload_file with patch("services.file_service.ExtractProcessor.load_from_upload_file") as mock_extract: mock_extract.return_value = "Extracted text content" # Execute - result = file_service.get_file_preview("file_id") + result = file_service.get_file_preview("file_id", "tenant_id") # Assert assert result == "Extracted text content" def test_get_file_preview_not_found(self, file_service, mock_db_session): - mock_db_session.query().where().first.return_value = None + mock_db_session.scalar.return_value = None with pytest.raises(NotFound, match="File not found"): - file_service.get_file_preview("non_existent") + file_service.get_file_preview("non_existent", "tenant_id") def test_get_file_preview_unsupported_type(self, file_service, mock_db_session): upload_file = MagicMock(spec=UploadFile) upload_file.id = "file_id" upload_file.extension = "exe" - mock_db_session.query().where().first.return_value = upload_file + mock_db_session.scalar.return_value = upload_file with pytest.raises(UnsupportedFileTypeError): - file_service.get_file_preview("file_id") + file_service.get_file_preview("file_id", "tenant_id") def test_get_image_preview_success(self, file_service, mock_db_session): # Setup