"""create_storage 工厂测试:覆盖 mode 字段严格拒绝 + 正常路径。""" from common.storage.base import AsyncStorageBackend from common.storage.exceptions import StorageConfigError from common.storage.factory import ( PURPOSE_BUCKETS, USAGE_TYPE_TO_PURPOSE, build_storage_config, create_storage, ) # ── mode 字段严格拒绝 ────────────────────────────────────────────── def test_create_storage_rejects_mode_sync(): """显式 mode='sync' 现在必须拒绝 —— 同步抽象已砍掉。""" import pytest with pytest.raises(StorageConfigError) as exc_info: create_storage({"type": "local", "base_dir": "/tmp", "mode": "sync"}) msg = str(exc_info.value) assert "不再接受 'mode' 字段" in msg def test_create_storage_rejects_mode_async(): """显式 mode='async' 也必须拒绝 —— 只有 async 一条路,不需要再声明。""" import pytest with pytest.raises(StorageConfigError) as exc_info: create_storage({"type": "s3", "bucket": "x", "mode": "async"}) msg = str(exc_info.value) assert "不再接受 'mode' 字段" in msg def test_create_storage_rejects_any_mode_value(): """任何 mode 字段(包含未来可能新增的合法值)都拒绝 —— 简化语义。""" import pytest for value in ("async", "sync", "dual", ""): with pytest.raises(StorageConfigError): create_storage({"type": "local", "base_dir": "/tmp", "mode": value}) # ── 正常路径 ──────────────────────────────────────────────────────── def test_create_storage_returns_async_subclass(): s = create_storage({"type": "local", "base_dir": "/tmp/cc-factory-test"}) assert isinstance(s, AsyncStorageBackend) # 确认是真 AsyncStorageBackend,不是同步兼容形态 assert type(s).__name__ == "LocalStorageBackend" def test_create_storage_missing_type_raises(): import pytest with pytest.raises(StorageConfigError) as exc_info: create_storage({"base_dir": "/tmp"}) assert "缺少 'type' 字段" in str(exc_info.value) def test_create_storage_unknown_type_raises(): import pytest with pytest.raises(StorageConfigError) as exc_info: create_storage({"type": "nonexistent"}) assert "未知的存储后端" in str(exc_info.value) def test_create_storage_does_not_mutate_input_dict(): """调用方传入的字典不能被改写。""" cfg = {"type": "local", "base_dir": "/tmp/cc-no-mutate"} cfg_id = id(cfg) snapshot = dict(cfg) create_storage(cfg) assert dict(cfg) == snapshot, "factory should not mutate caller's dict" assert id(cfg) == cfg_id def test_create_storage_kwargs_mismatch_raises_wrapped_error(): import pytest with pytest.raises(StorageConfigError) as exc_info: create_storage({"type": "local", "base_dir": 12345}) # base_dir 必须是 str msg = str(exc_info.value) assert "参数不匹配" in msg # ── build_storage_config 不再产生 mode 字段 ───────────────────────── def test_build_storage_config_local_has_no_mode(monkeypatch): """local 分支的输出 dict 不能含 'mode'。""" from common import config as common_config monkeypatch.setattr(common_config.settings, "storage_backend", "local", raising=False) monkeypatch.setattr(common_config.settings, "local_storage_base_dir", "/tmp", raising=False) cfg = build_storage_config("workspace") assert "mode" not in cfg, f"local cfg must not have 'mode', got: {cfg}" assert cfg["type"] == "local" assert "base_dir" in cfg def test_build_storage_config_s3_has_no_mode(monkeypatch): """s3 分支的输出 dict 也不能含 'mode'。""" from common import config as common_config monkeypatch.setattr(common_config.settings, "storage_backend", "s3", raising=False) monkeypatch.setattr(common_config.settings, "s3_workspace_bucket", "wb", raising=False) monkeypatch.setattr(common_config.settings, "s3_endpoint", "http://s3", raising=False) monkeypatch.setattr(common_config.settings, "s3_access_key", "ak", raising=False) monkeypatch.setattr(common_config.settings, "s3_secret_key", "sk", raising=False) cfg = build_storage_config("workspace") assert "mode" not in cfg, f"s3 cfg must not have 'mode', got: {cfg}" assert cfg["type"] == "s3" def test_build_storage_config_unknown_bucket_raises(): import pytest from common.storage.exceptions import StorageConfigError with pytest.raises(StorageConfigError): build_storage_config("not-a-real-bucket") def test_purpose_buckets_constant_complete(): """PURPOSE_BUCKETS 必须覆盖 USAGE_TYPE_TO_PURPOSE 中所有 purpose。""" purposes = set(USAGE_TYPE_TO_PURPOSE.values()) assert purposes.issubset(set(PURPOSE_BUCKETS)), ( f"missing buckets for purposes: {purposes - set(PURPOSE_BUCKETS)}" )