Skip to content

feat(MAA): 适配库存保持功能 - #314

Open
1w1w11w1 wants to merge 1 commit into
AUTO-MAS-Project:devfrom
1w1w11w1:codex/adapt-maa-inventory-keep
Open

feat(MAA): 适配库存保持功能#314
1w1w11w1 wants to merge 1 commit into
AUTO-MAS-Project:devfrom
1w1w11w1:codex/adapt-maa-inventory-keep

Conversation

@1w1w11w1

@1w1w11w1 1w1w11w1 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

摘要

  • 适配 MAA DepotMaintain 任务,并在未启用时保持旧版 MAA 配置兼容
  • 从 MAA item_index.json 读取物品选项,在用户表单中支持设置关卡、物品与目标库存
  • 补充配置、Schema、生成的 OpenAPI 客户端、版本记录与定向测试

验证

  • python -m pytest tests/test_maa_depot_maintain.py -q:2 passed
  • Python 编译、git diff --check、新组件 ESLint/Prettier 通过
  • 完成桌面与 390px 窄屏组件检查;未进行真实 MAA 端到端运行
  • 全量 yarn lintyarn typecheckpytest 仍受 dev 既有无关错误影响

Summary by Sourcery

新增对配置和执行 MAA DepotMaintain(仓库维护)任务的支持,同时保持与旧版本 MAA 的兼容性。

新特性:

  • 在 MAA 任务配置中暴露新的 DepotMaintain 任务,包括启用开关以及基于 JSON 的目标库存计划定义。
  • 提供前端的 DepotMaintain 配置区域,允许用户通过从 MAA 资源加载的选项来管理关卡、物品和目标库存计划。
  • 新增一个 API 接口和后端逻辑,用于读取 MAA item_index.json 并返回用于 DepotMaintain 配置的过滤后的物品选项。

增强:

  • 扩展 MAA 任务列表和中文标签以包含 DepotMaintain,并为不可刷取的库存物品定义需要排除的物品 ID。

测试:

  • 增加单元测试,覆盖 DepotMaintain 任务计划构建以及从 MAA item_index.json 生成物品选项的逻辑。
Original summary in English

Summary by Sourcery

Add support for configuring and executing MAA DepotMaintain (inventory maintain) tasks while preserving compatibility with older MAA versions.

New Features:

  • Expose a new DepotMaintain task in MAA task configuration, including enable flag and JSON-based plan definitions for target inventory.
  • Provide a frontend DepotMaintain configuration section that lets users manage stage, item, and target stock plans using options loaded from MAA resources.
  • Add an API endpoint and backend logic to read MAA item_index.json and return filtered item options for DepotMaintain configuration.

Enhancements:

  • Extend MAA task list and Chinese labels to include DepotMaintain and define excluded item IDs for non-farmable inventory items.

Tests:

  • Add unit tests covering DepotMaintain task plan building and item option generation from MAA item_index.json.

@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

端到端适配 MAA DepotMaintain(仓库维护)任务支持:后端配置与 API、前端用户编辑 UI、任务构建逻辑、常量、schema/OpenAPI 模型以及测试;同时在该功能被禁用或 MAA 版本不支持时保持兼容性。

获取 MAA 仓库物品选项的时序图

sequenceDiagram
    actor User
    participant MAAUserEditView as MAAUserEditView
    participant Service as Service
    participant ScriptsRouter as ApiScriptsRouter
    participant Config as Config

    User ->> MAAUserEditView: loadDepotItemOptions
    MAAUserEditView ->> Service: getMaaDepotItemsApiScriptsMaaDepotItemsPost
    Service ->> ScriptsRouter: POST /api/scripts/maa/depot/items
    ScriptsRouter ->> Config: get_maa_depot_items
    Config ->> Config: get_maa_depot_items
    Config -->> ScriptsRouter: list[dict[label,value]]
    ScriptsRouter -->> Service: ComboBoxOut
    Service -->> MAAUserEditView: ComboBoxOut
    MAAUserEditView -->> User: depotItemOptions updated
Loading

构建 DepotMaintain MAA 任务的时序图

sequenceDiagram
    actor User
    participant DepotSection as DepotMaintainConfigSection
    participant MaaConfig as MaaUserConfig
    participant AutoProxy as AutoProxyTask
    participant Builder as _build_depot_maintain_task
    participant MAA as MAA

    User ->> DepotSection: toggle Task.IfDepotMaintain
    User ->> DepotSection: edit DepotMaintainPlans
    DepotSection ->> MaaConfig: save Task.IfDepotMaintain
    DepotSection ->> MaaConfig: save Task.DepotMaintainPlans

    User ->> AutoProxy: set_maa
    AutoProxy ->> MaaConfig: get Task.IfDepotMaintain
    AutoProxy ->> MaaConfig: get Task.DepotMaintainPlans
    AutoProxy ->> Builder: _build_depot_maintain_task
    Builder -->> AutoProxy: DepotMaintainTask dict
    AutoProxy ->> MAA: write TaskQueue with DepotMaintain
    MAA -->> User: DepotMaintain task executed
Loading

文件级改动

Change Details Files
为 MAA DepotMaintain 任务配置和物品选项添加服务端支持,包括任务构建、仓库物品过滤以及 API 端点。
  • 引入 _build_depot_maintain_task 用于校验/清洗用户的仓库计划,并为 MAA 生成 DepotMaintainTask 负载
  • 扩展 MAA_TASKS/MAA_TASKS_ZH,加入 DepotMaintain,并定义构建物品选项时使用的不可刷取物品 ID 排除集合
  • DepotMaintain 接入 AutoProxy set_maa:在禁用时跳过写入新任务;使用已保存的 DepotMaintainPlans 覆盖 task_set 条目;只入队 task_set 中存在的任务
  • 添加 AppConfig.get_maa_depot_items,从 MAA 脚本资源路径中读取 item_index.json,过滤出数值型且未被排除的物品,并返回排序后的 label/value 对
  • 通过新的 POST /api/scripts/maa/depot/items 端点暴露 get_maa_depot_items,该端点包装配置调用并返回 ComboBoxOut 或错误信息
app/task/MAA/AutoProxy.py
app/utils/constants.py
app/core/config.py
app/api/scripts.py
扩展配置和 schema,用于存储 DepotMaintain 功能开关和计划,并据此重新生成 OpenAPI TypeScript 客户端。
  • 添加配置项 Task_IfDepotMaintainTask_DepotMaintainPlans,分别由 BoolValidator/JSONValidator(list) 支持
  • 更新 MaaUserConfig_Task Pydantic 模型,包含 IfDepotMaintainDepotMaintainPlans 字段
  • 重新生成前端 API 模型,使 MaaUserConfig_Task 中的新任务字段得到反映
  • GetServiceService 中都加入新端点 getMaaDepotItemsApiScriptsMaaDepotItemsPost,并提供正确的类型和文档
app/models/config.py
app/models/schema.py
frontend/src/api/models/MaaUserConfig_Task.ts
frontend/src/api/services/GetService.ts
frontend/src/api/services/Service.ts
添加前端 UI,用于配置 DepotMaintain 计划,从后端加载仓库物品选项,并为新用户提供默认任务值。
  • 创建 DepotMaintainConfigSection.vue,包含启用开关、物品错误警告、计划表格、添加/删除操作,以及基于 JSON 的计划序列化/反序列化
  • DepotMaintainConfigSection 集成到 MAAUserEdit.vue 中,包括 props 和保存事件的连线
  • 添加前端状态和加载逻辑 depotItemOptions,包含错误/加载处理,并在挂载时调用 loadDepotItemOptions
  • 扩展 getDefaultMAAUserDataTask 部分:新增 IfDepotMaintain 默认值 falseDepotMaintainPlans 默认 '[]'
frontend/src/views/EditView/User/MAAUserEdit.vue
frontend/src/views/MAAUserEdit/DepotMaintainConfigSection.vue
添加针对 DepotMaintain 任务构建以及仓库物品选项生成的定向测试,使用临时 MAA 资源验证过滤和排序。
  • 测试 _build_depot_maintain_task 能过滤掉无效、空或非正数计划,并生成正确的 PlanList
  • 测试 AppConfig.get_maa_depot_items 能读取 item_index.json,排除非数值和被排除的 ID,并返回排序后的可刷取物品及其标签
tests/test_maa_depot_maintain.py

Tips and commands

Interacting with Sourcery

  • 触发新的审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在某条审查评论下回复,请求 Sourcery 基于该评论创建 issue。你也可以在审查评论中回复 @sourcery-ai issue 来创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来随时(重新)生成摘要。
  • 生成 reviewer’s guide: 在 pull request 上评论 @sourcery-ai guide,即可随时(重新)生成 reviewer’s guide。
  • 批量解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不希望再看到它们,这会很有用。
  • 驳回所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss,即可驳回所有现有的 Sourcery 审查。若你想从头开始一次新的审查,尤其有用——不要忘记再评论一次 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查特性,例如 Sourcery 自动生成的 pull request 摘要、reviewer’s guide 等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adapt MAA DepotMaintain task support end-to-end: backend config and API, frontend user-edit UI, task building logic, constants, schema/OpenAPI models, and tests, while keeping compatibility when the feature is disabled or MAA version doesn’t support it.

Sequence diagram for fetching MAA depot item options

sequenceDiagram
    actor User
    participant MAAUserEditView as MAAUserEditView
    participant Service as Service
    participant ScriptsRouter as ApiScriptsRouter
    participant Config as Config

    User ->> MAAUserEditView: loadDepotItemOptions
    MAAUserEditView ->> Service: getMaaDepotItemsApiScriptsMaaDepotItemsPost
    Service ->> ScriptsRouter: POST /api/scripts/maa/depot/items
    ScriptsRouter ->> Config: get_maa_depot_items
    Config ->> Config: get_maa_depot_items
    Config -->> ScriptsRouter: list[dict[label,value]]
    ScriptsRouter -->> Service: ComboBoxOut
    Service -->> MAAUserEditView: ComboBoxOut
    MAAUserEditView -->> User: depotItemOptions updated
Loading

Sequence diagram for building DepotMaintain MAA task

sequenceDiagram
    actor User
    participant DepotSection as DepotMaintainConfigSection
    participant MaaConfig as MaaUserConfig
    participant AutoProxy as AutoProxyTask
    participant Builder as _build_depot_maintain_task
    participant MAA as MAA

    User ->> DepotSection: toggle Task.IfDepotMaintain
    User ->> DepotSection: edit DepotMaintainPlans
    DepotSection ->> MaaConfig: save Task.IfDepotMaintain
    DepotSection ->> MaaConfig: save Task.DepotMaintainPlans

    User ->> AutoProxy: set_maa
    AutoProxy ->> MaaConfig: get Task.IfDepotMaintain
    AutoProxy ->> MaaConfig: get Task.DepotMaintainPlans
    AutoProxy ->> Builder: _build_depot_maintain_task
    Builder -->> AutoProxy: DepotMaintainTask dict
    AutoProxy ->> MAA: write TaskQueue with DepotMaintain
    MAA -->> User: DepotMaintain task executed
Loading

File-Level Changes

Change Details Files
Add server-side support for MAA DepotMaintain task configuration and item options, including task building, depot item filtering, and API endpoint.
  • Introduce _build_depot_maintain_task to validate/sanitize user depot plans and produce a DepotMaintainTask payload for MAA
  • Extend MAA_TASKS/MAA_TASKS_ZH with DepotMaintain and define an exclusion set of non-farmable item IDs used when building item options
  • Wire DepotMaintain into AutoProxy set_maa: skip writing the new task when disabled, override task_set entry using stored DepotMaintainPlans, and only enqueue tasks that exist in task_set
  • Add AppConfig.get_maa_depot_items to read item_index.json from the MAA script resource path, filter numeric, non-excluded items, and return sorted label/value pairs
  • Expose get_maa_depot_items via a new /api/scripts/maa/depot/items POST endpoint that wraps the config call and returns ComboBoxOut or an error
app/task/MAA/AutoProxy.py
app/utils/constants.py
app/core/config.py
app/api/scripts.py
Extend configuration and schema to store DepotMaintain feature flags and plans, and regenerate the OpenAPI TypeScript client accordingly.
  • Add Task_IfDepotMaintain and Task_DepotMaintainPlans config items backed by BoolValidator/JSONValidator(list)
  • Update MaaUserConfig_Task Pydantic model to include IfDepotMaintain and DepotMaintainPlans
  • Regenerate frontend API models to reflect the new task fields in MaaUserConfig_Task
  • Include the new endpoint getMaaDepotItemsApiScriptsMaaDepotItemsPost in both GetService and Service with proper typing and documentation
app/models/config.py
app/models/schema.py
frontend/src/api/models/MaaUserConfig_Task.ts
frontend/src/api/services/GetService.ts
frontend/src/api/services/Service.ts
Add frontend UI for configuring DepotMaintain plans, loading depot item options from backend, and default task values for new users.
  • Create DepotMaintainConfigSection.vue with enable switch, item error alert, plan table, add/remove actions, and JSON-backed plan serialization/deserialization
  • Integrate DepotMaintainConfigSection into MAAUserEdit.vue, including wiring props and save events
  • Add frontend state and loader for depotItemOptions, plus error/loading handling, and invoke loadDepotItemOptions on mount
  • Extend getDefaultMAAUserData Task section with IfDepotMaintain default false and DepotMaintainPlans default '[]'
frontend/src/views/EditView/User/MAAUserEdit.vue
frontend/src/views/MAAUserEdit/DepotMaintainConfigSection.vue
Add targeted tests for DepotMaintain task building and depot item option generation using temporary MAA resources to validate filtering and ordering.
  • Test _build_depot_maintain_task filters invalid, empty, or non-positive plans and produces the correct PlanList
  • Test AppConfig.get_maa_depot_items reads item_index.json, excludes non-numeric and excluded IDs, and returns sorted farmable items with labels
tests/test_maa_depot_maintain.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并给出了一些总体反馈:

  • DepotMaintainPlans 的 JSON 处理在后端和 DepotMaintainConfigSection 中是重复且无类型约束的;建议引入一个共享的强类型模型或用于解析/序列化计划的工具方法,以减少运行时 JSON 错误并保持 schema 一致。
  • 在 get_maa_depot_items 中,所有异常都会被转换成一个 500 的 ComboBoxOut,而且没有任何日志;在失败路径上增加结构化日志,可以更容易排查线上环境中缺失或无效 item_index.json 的问题。
供 AI 代理使用的提示
Please address the comments from this code review:

## Overall Comments
- DepotMaintainPlans 的 JSON 处理在后端和 DepotMaintainConfigSection 中是重复且无类型约束的;建议引入一个共享的强类型模型或用于解析/序列化计划的工具方法,以减少运行时 JSON 错误并保持 schema 一致。
- 在 get_maa_depot_items 中,所有异常都会被转换成一个 500 的 ComboBoxOut,而且没有任何日志;在失败路径上增加结构化日志,可以更容易排查线上环境中缺失或无效 item_index.json 的问题。

## Individual Comments

### Comment 1
<location path="tests/test_maa_depot_maintain.py" line_range="13-22" />
<code_context>
+from app.task.MAA.AutoProxy import _build_depot_maintain_task
+
+
+def test_build_depot_maintain_task_filters_invalid_plans() -> None:
+    task = _build_depot_maintain_task(
+        json.dumps(
+            [
+                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
+                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
+                {"Stage": "", "DropId": "2001", "DropCount": 50},
+                "invalid",
+            ]
+        )
+    )
+
+    assert task["TaskType"] == "DepotMaintain"
+    assert task["PlanList"] == [
+        {
+            "Stage": "1-7",
</code_context>
<issue_to_address>
**suggestion (testing):**`_build_depot_maintain_task` 中为更多边界情况和完整的任务结构添加测试覆盖

这个辅助函数的校验比当前测试所覆盖的要严格:`DropCount` 必须是 `int`(而不是 `bool`)、为正数,并且 `Stage``DropId` 都必须是非空字符串。建议增加一些用例:`DropCount` 为 bool、非 int 或负数,以及缺少必需键或键类型错误的情况,以防后续重构削弱这些校验。

另外,该辅助函数返回的是一个完整的任务字典(包含 `IsEnable``UpdateDepot``SkipDuringActivity` 等),但当前测试只断言了 `TaskType``PlanList`。在这里(或单独的测试中)对这些额外的标志位进行断言,可以防止任务结构在不经意间发生变化。

建议实现:

```python
import json
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace

from app.core.config import AppConfig
from app.models.config import MaaConfig
from app.task.MAA.AutoProxy import _build_depot_maintain_task


def test_build_depot_maintain_task_filters_invalid_plans() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                # valid plan
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
                # invalid: DropCount zero
                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
                # invalid: empty Stage
                {"Stage": "", "DropId": "2001", "DropCount": 50},
                # invalid: DropCount is bool (should not be treated as int)
                {"Stage": "2-3", "DropId": "30013", "DropCount": True},
                # invalid: DropCount is non-int type (str)
                {"Stage": "3-4", "DropId": "30014", "DropCount": "10"},
                # invalid: DropCount is negative
                {"Stage": "4-5", "DropId": "30015", "DropCount": -1},
                # invalid: missing Stage
                {"DropId": "30016", "DropCount": 20},
                # invalid: missing DropId
                {"Stage": "5-6", "DropCount": 30},
                # invalid: missing DropCount
                {"Stage": "6-7", "DropId": "30017"},
                # invalid: Stage wrong type
                {"Stage": 123, "DropId": "30018", "DropCount": 40},
                # invalid: DropId wrong type
                {"Stage": "7-8", "DropId": 456, "DropCount": 50},
                # invalid: completely wrong type
                "invalid",
            ]
        )
    )

    # task shape assertions
    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert "PlanList" in task
    assert isinstance(task["PlanList"], list)

    # only the single valid plan should be kept
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # guard the rest of the task flags / shape
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)


def test_build_depot_maintain_task_task_shape_with_minimal_valid_input() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
            ]
        )
    )

    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # assert the helper consistently sets the task-level flags
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)

```

如果 `_build_depot_maintain_task` 除了 `IsEnable``UpdateDepot``SkipDuringActivity` 之外还设置了其他任务级别的键(例如与调度相关的字段),也可以考虑将这些键加入断言。请根据该辅助函数实际返回的结构调整两处测试中的键列表;如果你知道这些标志位的期望值(例如 `IsEnable` 总是 `True`),还可以将断言从类型检查加强为具体值检查。
</issue_to_address>

### Comment 2
<location path="tests/test_maa_depot_maintain.py" line_range="39-48" />
<code_context>
+    ]
+
+
+def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
+    with TemporaryDirectory() as temp_dir:
+        root = Path(temp_dir)
+        resource = root / "resource"
+        resource.mkdir()
+        (resource / "item_index.json").write_text(
+            json.dumps(
+                {
+                    "1stact": {"name": "活动道具"},
+                    "4002": {"name": "源石"},
+                    "30012": {"name": "固源岩"},
+                    "2001": {"name": "基础作战记录"},
+                }
+            ),
+            encoding="utf-8",
+        )
+
+        script_id = uuid.uuid4()
+        script_config = MaaConfig()
+        script_config.Info_Path.setValue(str(root))
+        owner = SimpleNamespace(ScriptConfig={script_id: script_config})
+
+        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))
+
+    assert items == [
+        {"label": "基础作战记录", "value": "2001"},
+        {"label": "固源岩", "value": "30012"},
</code_context>
<issue_to_address>
**suggestion (testing):** 扩展 `get_maa_depot_items` 的测试,以涵盖错误分支、被排除的 ID 以及缺失名称的情况

当前测试覆盖了主要的成功路径(数值型 ID 和排序),但还有几个关键分支尚未测试:

* 非 MAA 脚本:当 `ScriptConfig[script_id]` 不是 `MaaConfig` 时,`get_maa_depot_items` 应抛出 `TypeError`。可以添加一个使用其他配置类型或简单占位对象的用例,并断言该异常。
* 缺失 `item_index.json`:当 `Info_Path` 下不存在 `resource/item_index.json` 时,应该抛出带有预期信息的 `FileNotFoundError`。可以添加一个测试,让 `Info_Path` 指向一个不包含该文件的目录。
* 被排除的 ID:通过包含一个 ID 属于 `MAA_DEPOT_EXCLUDED_ITEM_IDS` 的物品,并断言该物品不会被返回,来验证这一排除集合。
* 缺失 `name`:验证没有 `name` 的物品(例如 `{"30012": {}}`)会以 `label=item_id` 的形式返回。

覆盖这些场景可以显著增强仓库物品 API 在正常和异常条件下的行为健壮性。

建议实现:

```python
def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    "1stact": {"name": "活动道具"},
                    "4002": {"name": "源石"},
                    "30012": {"name": "固源岩"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "固源岩", "value": "30012"},
    ]


def test_get_maa_depot_items_non_maa_script_raises_type_error() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)

        script_id = uuid.uuid4()
        # Use a dummy/non-MaaConfig config object to trigger the type check
        non_maa_config = SimpleNamespace(Info_Path=str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: non_maa_config})

        with pytest.raises(TypeError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_missing_item_index_raises_file_not_found() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        # Point Info_Path to a directory without resource/item_index.json
        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        with pytest.raises(FileNotFoundError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_excludes_configured_item_ids() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        # Include a known excluded numeric ID and a non-excluded one
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # Assumes 4002 is in MAA_DEPOT_EXCLUDED_ITEM_IDS
                    "4002": {"name": "源石"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Only the non-excluded item should be returned
    assert items == [{"label": "基础作战记录", "value": "2001"}]
    # And the excluded ID should not appear in the returned values
    assert "4002" not in {item["value"] for item in items}


def test_get_maa_depot_items_uses_item_id_as_label_when_name_missing() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # No name provided for 30012, should fall back to ID as label
                    "30012": {},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Both items should be present; 30012 should use its ID as label
    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "30012", "value": "30012"},
    ]

```

这些测试基于以下前提:
1.`tests/test_maa_depot_maintain.py` 文件顶部已经导入了 `pytest`2. 该文件已经像现有测试一样导入了 `TemporaryDirectory``Path``json``uuid``asyncio``SimpleNamespace``MaaConfig``AppConfig`3. `test_get_maa_depot_items_excludes_configured_item_ids` 中使用的被排除 ID(`"4002"`)确实是 `MAA_DEPOT_EXCLUDED_ITEM_IDS` 中的值之一。如果实际被排除的是其他 ID,请相应调整该测试中的 JSON 和断言。
4. 如果 `get_maa_depot_items` 抛出的 `FileNotFoundError` 包含特定的错误信息或路径,你也可以使用 `pytest.raises(..., match="expected message")` 来收紧断言。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The DepotMaintainPlans JSON handling is duplicated and untyped between backend and DepotMaintainConfigSection; consider introducing a shared typed model or utility for parsing/serializing plans to reduce runtime JSON errors and keep the schema consistent.
  • In get_maa_depot_items, all exceptions are converted into a 500 ComboBoxOut without any logging; adding structured logging for the failure path would make it easier to diagnose missing/invalid item_index.json issues in production.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The DepotMaintainPlans JSON handling is duplicated and untyped between backend and DepotMaintainConfigSection; consider introducing a shared typed model or utility for parsing/serializing plans to reduce runtime JSON errors and keep the schema consistent.
- In get_maa_depot_items, all exceptions are converted into a 500 ComboBoxOut without any logging; adding structured logging for the failure path would make it easier to diagnose missing/invalid item_index.json issues in production.

## Individual Comments

### Comment 1
<location path="tests/test_maa_depot_maintain.py" line_range="13-22" />
<code_context>
+from app.task.MAA.AutoProxy import _build_depot_maintain_task
+
+
+def test_build_depot_maintain_task_filters_invalid_plans() -> None:
+    task = _build_depot_maintain_task(
+        json.dumps(
+            [
+                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
+                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
+                {"Stage": "", "DropId": "2001", "DropCount": 50},
+                "invalid",
+            ]
+        )
+    )
+
+    assert task["TaskType"] == "DepotMaintain"
+    assert task["PlanList"] == [
+        {
+            "Stage": "1-7",
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for more edge cases and the full task shape in `_build_depot_maintain_task`

The helper enforces stricter validation than this test currently covers: `DropCount` must be an `int` (not `bool`), positive, and both `Stage` and `DropId` must be non-empty strings. It would be helpful to add cases where `DropCount` is a bool, non-int, or negative, and where required keys are missing or have the wrong type, so refactors don’t weaken validation.

Also, the helper returns a full task dict (`IsEnable`, `UpdateDepot`, `SkipDuringActivity`, etc.), but the test only asserts `TaskType` and `PlanList`. Asserting the other flags (either here or in a dedicated test) would guard against unintended changes to the task shape.

Suggested implementation:

```python
import json
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace

from app.core.config import AppConfig
from app.models.config import MaaConfig
from app.task.MAA.AutoProxy import _build_depot_maintain_task


def test_build_depot_maintain_task_filters_invalid_plans() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                # valid plan
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
                # invalid: DropCount zero
                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
                # invalid: empty Stage
                {"Stage": "", "DropId": "2001", "DropCount": 50},
                # invalid: DropCount is bool (should not be treated as int)
                {"Stage": "2-3", "DropId": "30013", "DropCount": True},
                # invalid: DropCount is non-int type (str)
                {"Stage": "3-4", "DropId": "30014", "DropCount": "10"},
                # invalid: DropCount is negative
                {"Stage": "4-5", "DropId": "30015", "DropCount": -1},
                # invalid: missing Stage
                {"DropId": "30016", "DropCount": 20},
                # invalid: missing DropId
                {"Stage": "5-6", "DropCount": 30},
                # invalid: missing DropCount
                {"Stage": "6-7", "DropId": "30017"},
                # invalid: Stage wrong type
                {"Stage": 123, "DropId": "30018", "DropCount": 40},
                # invalid: DropId wrong type
                {"Stage": "7-8", "DropId": 456, "DropCount": 50},
                # invalid: completely wrong type
                "invalid",
            ]
        )
    )

    # task shape assertions
    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert "PlanList" in task
    assert isinstance(task["PlanList"], list)

    # only the single valid plan should be kept
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # guard the rest of the task flags / shape
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)


def test_build_depot_maintain_task_task_shape_with_minimal_valid_input() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
            ]
        )
    )

    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # assert the helper consistently sets the task-level flags
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)

```

If `_build_depot_maintain_task` sets additional task-level keys beyond `IsEnable`, `UpdateDepot`, and `SkipDuringActivity` (for example, scheduling-related fields), you may want to expand the assertions to cover them as well. Adjust the list of keys in the two tests to match the actual shape returned by the helper, and, if you know the expected values for the flags (e.g. `IsEnable` is always `True`), you can strengthen the assertions from type checks to exact value checks.
</issue_to_address>

### Comment 2
<location path="tests/test_maa_depot_maintain.py" line_range="39-48" />
<code_context>
+    ]
+
+
+def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
+    with TemporaryDirectory() as temp_dir:
+        root = Path(temp_dir)
+        resource = root / "resource"
+        resource.mkdir()
+        (resource / "item_index.json").write_text(
+            json.dumps(
+                {
+                    "1stact": {"name": "活动道具"},
+                    "4002": {"name": "源石"},
+                    "30012": {"name": "固源岩"},
+                    "2001": {"name": "基础作战记录"},
+                }
+            ),
+            encoding="utf-8",
+        )
+
+        script_id = uuid.uuid4()
+        script_config = MaaConfig()
+        script_config.Info_Path.setValue(str(root))
+        owner = SimpleNamespace(ScriptConfig={script_id: script_config})
+
+        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))
+
+    assert items == [
+        {"label": "基础作战记录", "value": "2001"},
+        {"label": "固源岩", "value": "30012"},
</code_context>
<issue_to_address>
**suggestion (testing):** Extend `get_maa_depot_items` tests to cover error paths, excluded IDs, and missing names

The current test covers the main success case (numeric IDs and sorting), but several key branches remain untested:

* Non-MAA script: when `ScriptConfig[script_id]` is not a `MaaConfig`, `get_maa_depot_items` should raise `TypeError`. Add a case using a different config type/dummy object to assert this.
* Missing `item_index.json`: when `resource/item_index.json` is absent under `Info_Path`, it should raise `FileNotFoundError` with the expected message. Add a test that points `Info_Path` to a directory without that file.
* Excluded IDs: verify `MAA_DEPOT_EXCLUDED_ITEM_IDS` by including an item whose ID is in that set and asserting it is not returned.
* Missing `name`: verify that items without a `name` (e.g. `{"30012": {}}`) are returned with `label=item_id`.

Covering these cases will make the depot item API’s behavior in both normal and error conditions much more robust.

Suggested implementation:

```python
def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    "1stact": {"name": "活动道具"},
                    "4002": {"name": "源石"},
                    "30012": {"name": "固源岩"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "固源岩", "value": "30012"},
    ]


def test_get_maa_depot_items_non_maa_script_raises_type_error() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)

        script_id = uuid.uuid4()
        # Use a dummy/non-MaaConfig config object to trigger the type check
        non_maa_config = SimpleNamespace(Info_Path=str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: non_maa_config})

        with pytest.raises(TypeError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_missing_item_index_raises_file_not_found() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        # Point Info_Path to a directory without resource/item_index.json
        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        with pytest.raises(FileNotFoundError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_excludes_configured_item_ids() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        # Include a known excluded numeric ID and a non-excluded one
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # Assumes 4002 is in MAA_DEPOT_EXCLUDED_ITEM_IDS
                    "4002": {"name": "源石"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Only the non-excluded item should be returned
    assert items == [{"label": "基础作战记录", "value": "2001"}]
    # And the excluded ID should not appear in the returned values
    assert "4002" not in {item["value"] for item in items}


def test_get_maa_depot_items_uses_item_id_as_label_when_name_missing() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # No name provided for 30012, should fall back to ID as label
                    "30012": {},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Both items should be present; 30012 should use its ID as label
    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "30012", "value": "30012"},
    ]

```

These tests assume:
1. `pytest` is imported at the top of `tests/test_maa_depot_maintain.py`.
2. `TemporaryDirectory`, `Path`, `json`, `uuid`, `asyncio`, `SimpleNamespace`, `MaaConfig`, and `AppConfig` are already imported as in the existing tests.
3. The excluded ID used in `test_get_maa_depot_items_excludes_configured_item_ids` (`"4002"`) matches one of the values in `MAA_DEPOT_EXCLUDED_ITEM_IDS`. If a different ID is actually excluded, adjust the JSON and assertions in that test accordingly.
4. If `get_maa_depot_items` raises `FileNotFoundError` with a specific message or path, you may want to tighten the assertion using `pytest.raises(..., match="expected message")`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +13 to +22
def test_build_depot_maintain_task_filters_invalid_plans() -> None:
task = _build_depot_maintain_task(
json.dumps(
[
{"Stage": "1-7", "DropId": "30012", "DropCount": 100},
{"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
{"Stage": "", "DropId": "2001", "DropCount": 50},
"invalid",
]
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing):_build_depot_maintain_task 中为更多边界情况和完整的任务结构添加测试覆盖

这个辅助函数的校验比当前测试所覆盖的要严格:DropCount 必须是 int(而不是 bool)、为正数,并且 StageDropId 都必须是非空字符串。建议增加一些用例:DropCount 为 bool、非 int 或负数,以及缺少必需键或键类型错误的情况,以防后续重构削弱这些校验。

另外,该辅助函数返回的是一个完整的任务字典(包含 IsEnableUpdateDepotSkipDuringActivity 等),但当前测试只断言了 TaskTypePlanList。在这里(或单独的测试中)对这些额外的标志位进行断言,可以防止任务结构在不经意间发生变化。

建议实现:

import json
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace

from app.core.config import AppConfig
from app.models.config import MaaConfig
from app.task.MAA.AutoProxy import _build_depot_maintain_task


def test_build_depot_maintain_task_filters_invalid_plans() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                # valid plan
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
                # invalid: DropCount zero
                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
                # invalid: empty Stage
                {"Stage": "", "DropId": "2001", "DropCount": 50},
                # invalid: DropCount is bool (should not be treated as int)
                {"Stage": "2-3", "DropId": "30013", "DropCount": True},
                # invalid: DropCount is non-int type (str)
                {"Stage": "3-4", "DropId": "30014", "DropCount": "10"},
                # invalid: DropCount is negative
                {"Stage": "4-5", "DropId": "30015", "DropCount": -1},
                # invalid: missing Stage
                {"DropId": "30016", "DropCount": 20},
                # invalid: missing DropId
                {"Stage": "5-6", "DropCount": 30},
                # invalid: missing DropCount
                {"Stage": "6-7", "DropId": "30017"},
                # invalid: Stage wrong type
                {"Stage": 123, "DropId": "30018", "DropCount": 40},
                # invalid: DropId wrong type
                {"Stage": "7-8", "DropId": 456, "DropCount": 50},
                # invalid: completely wrong type
                "invalid",
            ]
        )
    )

    # task shape assertions
    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert "PlanList" in task
    assert isinstance(task["PlanList"], list)

    # only the single valid plan should be kept
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # guard the rest of the task flags / shape
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)


def test_build_depot_maintain_task_task_shape_with_minimal_valid_input() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
            ]
        )
    )

    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # assert the helper consistently sets the task-level flags
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)

如果 _build_depot_maintain_task 除了 IsEnableUpdateDepotSkipDuringActivity 之外还设置了其他任务级别的键(例如与调度相关的字段),也可以考虑将这些键加入断言。请根据该辅助函数实际返回的结构调整两处测试中的键列表;如果你知道这些标志位的期望值(例如 IsEnable 总是 True),还可以将断言从类型检查加强为具体值检查。

Original comment in English

suggestion (testing): Add coverage for more edge cases and the full task shape in _build_depot_maintain_task

The helper enforces stricter validation than this test currently covers: DropCount must be an int (not bool), positive, and both Stage and DropId must be non-empty strings. It would be helpful to add cases where DropCount is a bool, non-int, or negative, and where required keys are missing or have the wrong type, so refactors don’t weaken validation.

Also, the helper returns a full task dict (IsEnable, UpdateDepot, SkipDuringActivity, etc.), but the test only asserts TaskType and PlanList. Asserting the other flags (either here or in a dedicated test) would guard against unintended changes to the task shape.

Suggested implementation:

import json
import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace

from app.core.config import AppConfig
from app.models.config import MaaConfig
from app.task.MAA.AutoProxy import _build_depot_maintain_task


def test_build_depot_maintain_task_filters_invalid_plans() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                # valid plan
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
                # invalid: DropCount zero
                {"Stage": "CE-6", "DropId": "4001", "DropCount": 0},
                # invalid: empty Stage
                {"Stage": "", "DropId": "2001", "DropCount": 50},
                # invalid: DropCount is bool (should not be treated as int)
                {"Stage": "2-3", "DropId": "30013", "DropCount": True},
                # invalid: DropCount is non-int type (str)
                {"Stage": "3-4", "DropId": "30014", "DropCount": "10"},
                # invalid: DropCount is negative
                {"Stage": "4-5", "DropId": "30015", "DropCount": -1},
                # invalid: missing Stage
                {"DropId": "30016", "DropCount": 20},
                # invalid: missing DropId
                {"Stage": "5-6", "DropCount": 30},
                # invalid: missing DropCount
                {"Stage": "6-7", "DropId": "30017"},
                # invalid: Stage wrong type
                {"Stage": 123, "DropId": "30018", "DropCount": 40},
                # invalid: DropId wrong type
                {"Stage": "7-8", "DropId": 456, "DropCount": 50},
                # invalid: completely wrong type
                "invalid",
            ]
        )
    )

    # task shape assertions
    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert "PlanList" in task
    assert isinstance(task["PlanList"], list)

    # only the single valid plan should be kept
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # guard the rest of the task flags / shape
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)


def test_build_depot_maintain_task_task_shape_with_minimal_valid_input() -> None:
    task = _build_depot_maintain_task(
        json.dumps(
            [
                {"Stage": "1-7", "DropId": "30012", "DropCount": 100},
            ]
        )
    )

    assert isinstance(task, dict)
    assert task["TaskType"] == "DepotMaintain"
    assert task["PlanList"] == [
        {
            "Stage": "1-7",
            "DropId": "30012",
            "DropCount": 100,
            "UseMedicine": False,
            "MedicineCount": 0,
            "UseStone": False,
            "StoneCount": 0,
        }
    ]

    # assert the helper consistently sets the task-level flags
    for key in ("IsEnable", "UpdateDepot", "SkipDuringActivity"):
        assert key in task
        assert isinstance(task[key], bool)

If _build_depot_maintain_task sets additional task-level keys beyond IsEnable, UpdateDepot, and SkipDuringActivity (for example, scheduling-related fields), you may want to expand the assertions to cover them as well. Adjust the list of keys in the two tests to match the actual shape returned by the helper, and, if you know the expected values for the flags (e.g. IsEnable is always True), you can strengthen the assertions from type checks to exact value checks.

Comment on lines +39 to +48
def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
with TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
resource = root / "resource"
resource.mkdir()
(resource / "item_index.json").write_text(
json.dumps(
{
"1stact": {"name": "活动道具"},
"4002": {"name": "源石"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 扩展 get_maa_depot_items 的测试,以涵盖错误分支、被排除的 ID 以及缺失名称的情况

当前测试覆盖了主要的成功路径(数值型 ID 和排序),但还有几个关键分支尚未测试:

  • 非 MAA 脚本:当 ScriptConfig[script_id] 不是 MaaConfig 时,get_maa_depot_items 应抛出 TypeError。可以添加一个使用其他配置类型或简单占位对象的用例,并断言该异常。
  • 缺失 item_index.json:当 Info_Path 下不存在 resource/item_index.json 时,应该抛出带有预期信息的 FileNotFoundError。可以添加一个测试,让 Info_Path 指向一个不包含该文件的目录。
  • 被排除的 ID:通过包含一个 ID 属于 MAA_DEPOT_EXCLUDED_ITEM_IDS 的物品,并断言该物品不会被返回,来验证这一排除集合。
  • 缺失 name:验证没有 name 的物品(例如 {"30012": {}})会以 label=item_id 的形式返回。

覆盖这些场景可以显著增强仓库物品 API 在正常和异常条件下的行为健壮性。

建议实现:

def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    "1stact": {"name": "活动道具"},
                    "4002": {"name": "源石"},
                    "30012": {"name": "固源岩"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "固源岩", "value": "30012"},
    ]


def test_get_maa_depot_items_non_maa_script_raises_type_error() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)

        script_id = uuid.uuid4()
        # Use a dummy/non-MaaConfig config object to trigger the type check
        non_maa_config = SimpleNamespace(Info_Path=str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: non_maa_config})

        with pytest.raises(TypeError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_missing_item_index_raises_file_not_found() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        # Point Info_Path to a directory without resource/item_index.json
        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        with pytest.raises(FileNotFoundError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_excludes_configured_item_ids() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        # Include a known excluded numeric ID and a non-excluded one
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # Assumes 4002 is in MAA_DEPOT_EXCLUDED_ITEM_IDS
                    "4002": {"name": "源石"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Only the non-excluded item should be returned
    assert items == [{"label": "基础作战记录", "value": "2001"}]
    # And the excluded ID should not appear in the returned values
    assert "4002" not in {item["value"] for item in items}


def test_get_maa_depot_items_uses_item_id_as_label_when_name_missing() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # No name provided for 30012, should fall back to ID as label
                    "30012": {},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Both items should be present; 30012 should use its ID as label
    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "30012", "value": "30012"},
    ]

这些测试基于以下前提:

  1. tests/test_maa_depot_maintain.py 文件顶部已经导入了 pytest
  2. 该文件已经像现有测试一样导入了 TemporaryDirectoryPathjsonuuidasyncioSimpleNamespaceMaaConfigAppConfig
  3. test_get_maa_depot_items_excludes_configured_item_ids 中使用的被排除 ID("4002")确实是 MAA_DEPOT_EXCLUDED_ITEM_IDS 中的值之一。如果实际被排除的是其他 ID,请相应调整该测试中的 JSON 和断言。
  4. 如果 get_maa_depot_items 抛出的 FileNotFoundError 包含特定的错误信息或路径,你也可以使用 pytest.raises(..., match="expected message") 来收紧断言。
Original comment in English

suggestion (testing): Extend get_maa_depot_items tests to cover error paths, excluded IDs, and missing names

The current test covers the main success case (numeric IDs and sorting), but several key branches remain untested:

  • Non-MAA script: when ScriptConfig[script_id] is not a MaaConfig, get_maa_depot_items should raise TypeError. Add a case using a different config type/dummy object to assert this.
  • Missing item_index.json: when resource/item_index.json is absent under Info_Path, it should raise FileNotFoundError with the expected message. Add a test that points Info_Path to a directory without that file.
  • Excluded IDs: verify MAA_DEPOT_EXCLUDED_ITEM_IDS by including an item whose ID is in that set and asserting it is not returned.
  • Missing name: verify that items without a name (e.g. {"30012": {}}) are returned with label=item_id.

Covering these cases will make the depot item API’s behavior in both normal and error conditions much more robust.

Suggested implementation:

def test_get_maa_depot_items_uses_numeric_farmable_items() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    "1stact": {"name": "活动道具"},
                    "4002": {"name": "源石"},
                    "30012": {"name": "固源岩"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "固源岩", "value": "30012"},
    ]


def test_get_maa_depot_items_non_maa_script_raises_type_error() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)

        script_id = uuid.uuid4()
        # Use a dummy/non-MaaConfig config object to trigger the type check
        non_maa_config = SimpleNamespace(Info_Path=str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: non_maa_config})

        with pytest.raises(TypeError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_missing_item_index_raises_file_not_found() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        # Point Info_Path to a directory without resource/item_index.json
        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        with pytest.raises(FileNotFoundError):
            asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))


def test_get_maa_depot_items_excludes_configured_item_ids() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        # Include a known excluded numeric ID and a non-excluded one
        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # Assumes 4002 is in MAA_DEPOT_EXCLUDED_ITEM_IDS
                    "4002": {"name": "源石"},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Only the non-excluded item should be returned
    assert items == [{"label": "基础作战记录", "value": "2001"}]
    # And the excluded ID should not appear in the returned values
    assert "4002" not in {item["value"] for item in items}


def test_get_maa_depot_items_uses_item_id_as_label_when_name_missing() -> None:
    with TemporaryDirectory() as temp_dir:
        root = Path(temp_dir)
        resource = root / "resource"
        resource.mkdir()

        (resource / "item_index.json").write_text(
            json.dumps(
                {
                    # No name provided for 30012, should fall back to ID as label
                    "30012": {},
                    "2001": {"name": "基础作战记录"},
                }
            ),
            encoding="utf-8",
        )

        script_id = uuid.uuid4()
        script_config = MaaConfig()
        script_config.Info_Path.setValue(str(root))
        owner = SimpleNamespace(ScriptConfig={script_id: script_config})

        items = asyncio.run(AppConfig.get_maa_depot_items(owner, str(script_id)))

    # Both items should be present; 30012 should use its ID as label
    assert items == [
        {"label": "基础作战记录", "value": "2001"},
        {"label": "30012", "value": "30012"},
    ]

These tests assume:

  1. pytest is imported at the top of tests/test_maa_depot_maintain.py.
  2. TemporaryDirectory, Path, json, uuid, asyncio, SimpleNamespace, MaaConfig, and AppConfig are already imported as in the existing tests.
  3. The excluded ID used in test_get_maa_depot_items_excludes_configured_item_ids ("4002") matches one of the values in MAA_DEPOT_EXCLUDED_ITEM_IDS. If a different ID is actually excluded, adjust the JSON and assertions in that test accordingly.
  4. If get_maa_depot_items raises FileNotFoundError with a specific message or path, you may want to tighten the assertion using pytest.raises(..., match="expected message").

@HarcoChen HarcoChen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉没问题

Comment thread app/models/schema.py
Comment on lines +489 to +492
IfDepotMaintain: Optional[bool] = Field(default=None, description="库存保持")
DepotMaintainPlans: Optional[str] = Field(
default=None, description="库存保持计划 JSON"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

咱啥时候该搞个ruff.toml了

Comment thread app/utils/constants.py
MAA_DEPOT_EXCLUDED_ITEM_IDS = {
"3213",
"3223",
"3233",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里为什么要做硬编码呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants