Skip to content

fix: release v2.0.2 and restore fortune pre-cache#23

Merged
FlanChanXwO merged 6 commits into
masterfrom
fix-configurable-messages-setu-fortune
May 18, 2026
Merged

fix: release v2.0.2 and restore fortune pre-cache#23
FlanChanXwO merged 6 commits into
masterfrom
fix-configurable-messages-setu-fortune

Conversation

@FlanChanXwO
Copy link
Copy Markdown
Owner

@FlanChanXwO FlanChanXwO commented May 18, 2026

Summary

  • bump plugin metadata to v2.0.2
  • document the 2.0.2 fixes in CHANGELOG.md
  • sync AGENTS.md with the current AstrBot plugin architecture and release state
  • restore Fortune daily pre-rendered image cache behavior
  • pin ASTRBOT_ROOT during plugin tests to stop AstrBot core runtime files from landing under the plugin directory

Verification

  • PYTHONPATH=/Users/flanchan/Development/SourceCode/GithubProjects/AstrbotPluginDev/data/plugins python -m pytest tests/infrastructure/test_fortune_pregeneration.py tests/test_main_command_routing.py tests/test_main_config_source.py tests/modules/test_fortune_module.py tests/application/test_setu_use_case.py -q
  • RUFF_CACHE_DIR=.ruff_cache python -m ruff check .

Notes

  • Existing local and edits were left uncommitted and are not included in this PR.

Summary by Sourcery

发布插件版本 v2.0.2,恢复自动运势预生成功能,并在运行时与测试行为上更安全。

Bug 修复:

  • 在启用运势自动刷新时,为最近活跃用户恢复每日预生成并缓存渲染后的运势卡片图片。
  • 通过在测试夹具中固定 ASTRBOT_ROOT,确保 AstrBot 核心运行时数据和测试不再写入插件源码目录下。

功能增强:

  • 扩展运势仓库和服务以记录活跃用户的用户名和群组上下文,并据此驱动图片缓存的预生成。
  • 通过引入午夜触发的后台任务来改进运势自动刷新行为,该任务会基于最近的用户活动预生成运势图片。
  • AGENTS.md 中完善 AstrBot agent/项目指引,包括架构概览、运行时数据规则以及开发工作流。

文档:

  • CHANGELOG.md 中记录 2.0.2 版本的变更,包括恢复的运势缓存以及工作流清理。
  • 重写 AGENTS.md,更详细地描述插件的架构、约定、发布规则和测试指南。

测试:

  • 为运势预生成和图片缓存创建添加测试,包括内存版和基于 SQLite 的仓库行为,以及受配置保护的自动刷新逻辑。

杂项:

  • 从非标准的 .github/workflow/ 目录中移除废弃的 GitHub Actions 工作流文件。
  • 将插件元数据版本从 v2.0.1 更新到 v2.0.2。
Original summary in English

Summary by Sourcery

Release plugin version v2.0.2 with restored automated fortune pre-generation and safer runtime/test behavior.

Bug Fixes:

  • Restore daily pre-generation and caching of rendered fortune card images for recently active users when fortune auto-refresh is enabled.
  • Ensure AstrBot core runtime data and tests no longer write under the plugin source directory by pinning ASTRBOT_ROOT via test fixtures.

Enhancements:

  • Extend fortune repository and service to track usernames and group context for active users and to drive image cache pre-generation.
  • Improve fortune auto-refresh behavior by introducing a midnight-triggered background task that pregenerates fortune images based on recent activity.
  • Refine the AstrBot agent/project guidelines in AGENTS.md, including architecture overview, runtime data rules, and development workflows.

Documentation:

  • Document the 2.0.2 changes in CHANGELOG.md, including restored fortune caching and workflow cleanup.
  • Overhaul AGENTS.md to describe the plugin’s architecture, conventions, release rules, and testing guidelines in more detail.

Tests:

  • Add tests for fortune pre-generation and image cache creation, including in-memory and SQLite-backed repository behavior and config-guarded auto-refresh.

Chores:

  • Remove obsolete GitHub Actions workflow files from the non-standard .github/workflow/ directory.
  • Bump plugin metadata version from v2.0.1 to v2.0.2.

Copilot AI review requested due to automatic review settings May 18, 2026 08:33
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 18, 2026

Reviewer's Guide

通过恢复带有渲染图片缓存的 Fortune 自动预生成功能、在签运持久化中加入用户名和活跃请求查询、在插件生命周期中接入夜间预生成循环,并相应更新文档/元数据和 CI 工作流,实现 AstrBot 插件 v2.0.2。

恢复后的 Fortune 自动预生成和图片缓存顺序图

sequenceDiagram
    participant SetuPlugin
    participant FortuneCommandHandler
    participant FortuneService
    participant FortuneRepository

    SetuPlugin->>SetuPlugin: _fortune_auto_refresh_enabled(config)
    alt auto_refresh_enabled
        SetuPlugin->>SetuPlugin: _fortune_pregenerate_loop()
        loop daily
            SetuPlugin->>SetuPlugin: asyncio.sleep(_seconds_until_next_midnight())
            SetuPlugin->>SetuPlugin: _pregenerate_active_fortune_images()
            SetuPlugin->>FortuneCommandHandler: pregenerate_active_fortune_images(days)
            FortuneCommandHandler->>FortuneService: pregenerate_active_user_records(days, include_existing=True)
            FortuneService->>FortuneRepository: get_active_fortune_requests(days, date_str)
            FortuneRepository-->>FortuneService: list[FortuneGenerationRequest]
            loop each active request
                FortuneService->>FortuneRepository: get_today_fortune(request)
                alt existing_record
                    FortuneService-->>FortuneCommandHandler: FortuneRecord
                else new_record
                    FortuneService->>FortuneService: get_or_create_fortune(request)
                    FortuneService-->>FortuneCommandHandler: FortuneRecord
                end
                FortuneCommandHandler->>FortuneService: get_cached_image(user_id, date_str)
                alt no_cached_image
                    FortuneCommandHandler->>FortuneCommandHandler: _render_fortune_image(record, service)
                    FortuneCommandHandler-->>SetuPlugin: increment cached_count
                end
            end
            FortuneCommandHandler-->>SetuPlugin: cached_count
        end
    end
Loading

File-Level Changes

Change Details Files
Add username-aware fortune persistence and active-request querying to support pre-generation.
  • 通过新增可为空的 username 列扩展 SQLite fortune schema,并迁移现有数据库
  • 在 SELECT/INSERT 语句中包含 username,对缺失的用户名使用已存储值或 user_id 作为默认值
  • 在 fortune repository port 和 SQLite 实现中新增 get_active_fortune_requests,返回按活跃度排序的每个用户最新签运生成请求
src/infrastructure/persistence/sqlite_fortune_repository.py
src/application/ports/fortune_repository.py
Restore and refine Fortune pre-generation, including rendered card cache pre-rendering and plugin scheduling.
  • 将 FortuneService.pregenerate_active_users 重构为 pregenerate_active_user_records,以支持返回记录并使用活跃请求数据
  • 新增 FortuneCommandHandler.pregenerate_active_fortune_images,为活跃用户生成并缓存渲染图片,并遵循 fortune.enabled 与 auto_refresh 标志
  • 在 SetuPlugin.initialize/terminate 中接入夜间预生成循环,使用 asyncio 任务在午夜后运行并记录结果日志
src/domain/fortune/service.py
src/infrastructure/astrbot/commands/fortune.py
main.py
Improve Fortune rendering flow and tests to ensure image-first pre-caching behavior.
  • 简化 _render_fortune_image,使其只依赖 FortuneRecord 和 FortuneService,并在存在缓存图片时优先使用缓存
  • 新增使用内存型 fortune repository 的专用测试套件,用于验证预生成、auto_refresh 限制逻辑以及 SQLite 活跃请求构造
  • 调整 fortune 命令模块中的 import,以提高清晰度和一致性
src/infrastructure/astrbot/commands/fortune.py
tests/infrastructure/test_fortune_pregeneration.py
Update project documentation and metadata for v2.0.2 and clarify repository/agent conventions.
  • 重写 AGENTS.md,详细说明插件架构、运行时规则、测试命令以及发布/PR 规范
  • 在 CHANGELOG 中新增 2.0.2 章节,描述自动预缓存恢复以及测试数据隔离修复
  • 将 metadata.yaml 版本从 v2.0.1 提升到 v2.0.2,并移除位于非标准路径 .github/workflow 下的已废弃 GitHub Actions workflow
AGENTS.md
CHANGELOG.md
metadata.yaml
.github/workflow/release-from-changelog.yml

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 来(重新)生成摘要。
  • 生成评审指南: 在 pull request 中评论 @sourcery-ai 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 摘要、评审指南等。
  • 更改评审语言。
  • 添加、删除或编辑自定义评审指令。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Implements AstrBot plugin v2.0.2 by restoring Fortune auto pre-generation with rendered image caching, enriching fortune persistence with usernames and active-request queries, wiring a nightly pre-generation loop into the plugin lifecycle, and updating docs/metadata and CI workflows accordingly.

Sequence diagram for restored fortune auto pre-generation and image caching

sequenceDiagram
    participant SetuPlugin
    participant FortuneCommandHandler
    participant FortuneService
    participant FortuneRepository

    SetuPlugin->>SetuPlugin: _fortune_auto_refresh_enabled(config)
    alt auto_refresh_enabled
        SetuPlugin->>SetuPlugin: _fortune_pregenerate_loop()
        loop daily
            SetuPlugin->>SetuPlugin: asyncio.sleep(_seconds_until_next_midnight())
            SetuPlugin->>SetuPlugin: _pregenerate_active_fortune_images()
            SetuPlugin->>FortuneCommandHandler: pregenerate_active_fortune_images(days)
            FortuneCommandHandler->>FortuneService: pregenerate_active_user_records(days, include_existing=True)
            FortuneService->>FortuneRepository: get_active_fortune_requests(days, date_str)
            FortuneRepository-->>FortuneService: list[FortuneGenerationRequest]
            loop each active request
                FortuneService->>FortuneRepository: get_today_fortune(request)
                alt existing_record
                    FortuneService-->>FortuneCommandHandler: FortuneRecord
                else new_record
                    FortuneService->>FortuneService: get_or_create_fortune(request)
                    FortuneService-->>FortuneCommandHandler: FortuneRecord
                end
                FortuneCommandHandler->>FortuneService: get_cached_image(user_id, date_str)
                alt no_cached_image
                    FortuneCommandHandler->>FortuneCommandHandler: _render_fortune_image(record, service)
                    FortuneCommandHandler-->>SetuPlugin: increment cached_count
                end
            end
            FortuneCommandHandler-->>SetuPlugin: cached_count
        end
    end
Loading

File-Level Changes

Change Details Files
Add username-aware fortune persistence and active-request querying to support pre-generation.
  • Extend SQLite fortune schema with a nullable username column and migrate existing databases
  • Include username in SELECT/INSERT statements and default missing usernames to stored values or user_id
  • Add get_active_fortune_requests to the fortune repository port and SQLite implementation, returning latest per-user generation requests ordered by activity
src/infrastructure/persistence/sqlite_fortune_repository.py
src/application/ports/fortune_repository.py
Restore and refine Fortune pre-generation, including rendered card cache pre-rendering and plugin scheduling.
  • Refactor FortuneService.pregenerate_active_users into pregenerate_active_user_records that supports returning records and using active request data
  • Add FortuneCommandHandler.pregenerate_active_fortune_images to generate and cache rendered images for active users, respecting fortune.enabled and auto_refresh flags
  • Wire a nightly pre-generation loop into SetuPlugin.initialize/terminate using an asyncio task that runs after midnight and logs outcomes
src/domain/fortune/service.py
src/infrastructure/astrbot/commands/fortune.py
main.py
Improve Fortune rendering flow and tests to ensure image-first pre-caching behavior.
  • Simplify _render_fortune_image to only depend on FortuneRecord and FortuneService, relying on cached images when present
  • Add a dedicated test suite with an in-memory fortune repository to validate pre-generation, auto_refresh gating, and SQLite active-request construction
  • Adjust imports in the fortune command module for clarity and consistency
src/infrastructure/astrbot/commands/fortune.py
tests/infrastructure/test_fortune_pregeneration.py
Update project documentation and metadata for v2.0.2 and clarify repository/agent conventions.
  • Rewrite AGENTS.md to describe the plugin architecture, runtime rules, testing commands, and release/PR practices in detail
  • Add a 2.0.2 section to CHANGELOG describing auto pre-cache restoration and test data isolation fixes
  • Bump metadata.yaml version from v2.0.1 to v2.0.2 and remove an obsolete GitHub Actions workflow under the non-standard .github/workflow path
AGENTS.md
CHANGELOG.md
metadata.yaml
.github/workflow/release-from-changelog.yml

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

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题

面向 AI 代理的提示
请根据这次代码评审中的评论进行修改:

## 单独评论

### 评论 1
<location path="src/infrastructure/astrbot/commands/fortune.py" line_range="366-371" />
<code_context>
+            days=days, include_existing=True
+        )
+
+        cached_count = 0
+        for record in records:
+            if await service.get_cached_image(record.user_id, record.date_str):
+                continue
+            image_bytes = await self._render_fortune_image(record, service)
+            if image_bytes:
+                cached_count += 1
+        return cached_count
</code_context>
<issue_to_address>
**建议:** 单次渲染错误会中止整个预生成过程

由于这是一个后台预生成任务,建议对每条记录单独处理失败情况,这样某个用户/日期的数据出错时,不会阻塞后续记录的处理。例如:

```python
for record in records:
    try:
        if await service.get_cached_image(record.user_id, record.date_str):
            continue
        image_bytes = await self._render_fortune_image(record, service)
        if image_bytes:
            cached_count += 1
    except Exception as exc:
        logger.warning(
            "[fortune] Failed to pregenerate cache for %s: %s",
            record.user_id,
            exc,
        )
```

这样可以在继续处理其他记录的同时,记录失败日志以便排查。

建议的实现:

```python
        cached_count = 0
        for record in records:
            try:
                if await service.get_cached_image(record.user_id, record.date_str):
                    continue
                image_bytes = await self._render_fortune_image(record, service)
                if image_bytes:
                    cached_count += 1
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "[fortune] Failed to pregenerate cache for %s: %s",
                    record.user_id,
                    exc,
                )
        return cached_count

```

1. 确保该模块中定义了 `logger`,例如:
   - 在文件顶部 `import logging`,以及
   - `logger = logging.getLogger(__name__)`2. 如果你的 lint 规则不同(例如不需要 `# noqa: BLE001`),请根据现有代码风格调整或删除该注释。
</issue_to_address>

### 评论 2
<location path="tests/infrastructure/test_fortune_pregeneration.py" line_range="84-85" />
<code_context>
+        return 0
+
+
+@pytest.mark.asyncio
+async def test_pregenerate_active_fortune_images_writes_rendered_cache(
+    monkeypatch,
+) -> None:
</code_context>
<issue_to_address>
**建议(测试):** 增加一个已有缓存图片的测试用例,以验证 `pregenerate_active_fortune_images` 会跳过渲染并且不会增加缓存计数。

为覆盖新的行为,请添加一个测试:将 `MemoryFortuneRepo.get_cached_image_path` 配置为对某个用户/日期返回非 None 的值,并断言:(1) 对该记录不会调用 `handler._renderer.render_to_image`(例如通过 spy/计数器来验证),(2) `pregenerate_active_fortune_images` 返回 `0`,或者至少不会把该用户计入返回值中。这样可以帮助捕获重新渲染已缓存图片的回归问题。

建议的实现:

```python
@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_writes_rendered_cache(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            # 在真实实现中这里会写入图片文件;在测试中我们只返回传入的
            # 路径,以满足 handler 的预期。
            return output_path

    renderer = DummyRenderer()
    # 根据你的代码库情况,下面可能需要替换为实际用于预生成的 handler
    # 类或工厂。
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # 没有缓存的一个有效 fortune 应当被渲染并计数。
    assert count == 1
    assert renderer.render_call_count == 1
    assert ("user-1", "2026-05-17") in repo.cached_images

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_skips_when_cache_exists(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    # 通过强制 get_cached_image_path 返回非 None 的 Path,模拟该用户/日期
    # 已经存在缓存图片。
    async def fake_get_cached_image_path(user_id: str, date_str: str) -> Path | None:
        assert user_id == "user-1"
        assert date_str == "2026-05-17"
        return Path(f"/tmp/{user_id}_{date_str}.jpg")

    monkeypatch.setattr(
        repo,
        "get_cached_image_path",
        fake_get_cached_image_path,
        raising=True,
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            return output_path

    renderer = DummyRenderer()
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # 由于返回了缓存图片路径,handler 应当跳过该记录的渲染,并且不将其
    # 计入预生成数量。
    assert renderer.render_call_count == 0
    assert count == 0

```

上述修改假设:

1. 存在一个 `FortunePregenerationHandler`(或类似命名)的类:
   - 在构造函数中接收 `repo``renderer`- 暴露一个异步方法 `pregenerate_active_fortune_images(today: str) -> int`- 在内部使用 `self._renderer.render_to_image(...)`,并调用 `repo.get_cached_image_path(...)` 决定是否需要渲染。
2. `MemoryFortuneRepo` 具有与断言兼容的 `cached_images` 属性。

如果你的实际 handler 或函数名/签名不同,你需要:
1.`FortunePregenerationHandler(...)` 替换为真实用于预生成的类或函数(或者调整测试以直接调用该函数,而不是通过 handler)。
2. 更新 `await handler.pregenerate_active_fortune_images(today=today)` 的调用,以符合真实 API。
3. 如果 `get_cached_image_path` 是同步的或签名不同,请相应调整 `fake_get_cached_image_path`,并在必要时移除 `async`。

需要在最终实现中保持的关键行为是:
-`get_cached_image_path` 返回非 None 的路径时,不得对该记录调用 `render_to_image`- 这类记录不得计入 `pregenerate_active_fortune_images` 返回的数量中。
</issue_to_address>

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/infrastructure/astrbot/commands/fortune.py" line_range="366-371" />
<code_context>
+            days=days, include_existing=True
+        )
+
+        cached_count = 0
+        for record in records:
+            if await service.get_cached_image(record.user_id, record.date_str):
+                continue
+            image_bytes = await self._render_fortune_image(record, service)
+            if image_bytes:
+                cached_count += 1
+        return cached_count
</code_context>
<issue_to_address>
**suggestion:** A single rendering error will abort the entire pregeneration pass

Since this runs as a background pregeneration job, consider handling failures per record so one bad user/date doesn’t stop the rest from being processed. For example:

```python
for record in records:
    try:
        if await service.get_cached_image(record.user_id, record.date_str):
            continue
        image_bytes = await self._render_fortune_image(record, service)
        if image_bytes:
            cached_count += 1
    except Exception as exc:
        logger.warning(
            "[fortune] Failed to pregenerate cache for %s: %s",
            record.user_id,
            exc,
        )
```

This keeps processing other records while still logging failures for investigation.

Suggested implementation:

```python
        cached_count = 0
        for record in records:
            try:
                if await service.get_cached_image(record.user_id, record.date_str):
                    continue
                image_bytes = await self._render_fortune_image(record, service)
                if image_bytes:
                    cached_count += 1
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "[fortune] Failed to pregenerate cache for %s: %s",
                    record.user_id,
                    exc,
                )
        return cached_count

```

1. Ensure this module has a `logger` defined, e.g.:
   - `import logging` at the top of the file, and
   - `logger = logging.getLogger(__name__)`.
2. If your linting rules differ (e.g. no need for `# noqa: BLE001`), adjust or remove that comment to match your existing code style.
</issue_to_address>

### Comment 2
<location path="tests/infrastructure/test_fortune_pregeneration.py" line_range="84-85" />
<code_context>
+        return 0
+
+
+@pytest.mark.asyncio
+async def test_pregenerate_active_fortune_images_writes_rendered_cache(
+    monkeypatch,
+) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case where existing cached images are present so `pregenerate_active_fortune_images` skips rendering and does not increment the cached count.

To cover the new behavior, please add a test that configures `MemoryFortuneRepo.get_cached_image_path` to return a non-None value for a user/date and asserts that: (1) `handler._renderer.render_to_image` is not called for that record (e.g., via a spy/counter), and (2) `pregenerate_active_fortune_images` returns `0` or otherwise does not include that user in the count. This will help catch regressions where we re-render already cached images.

Suggested implementation:

```python
@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_writes_rendered_cache(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            # In a real implementation this would write an image file; here we just
            # return the provided path to satisfy the handler's expectations.
            return output_path

    renderer = DummyRenderer()
    # Depending on your codebase this may need to be updated to the actual handler
    # class or factory used for pregeneration.
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # One active fortune with no cache should be rendered and counted.
    assert count == 1
    assert renderer.render_call_count == 1
    assert ("user-1", "2026-05-17") in repo.cached_images

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_skips_when_cache_exists(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    # Simulate that a cached image already exists for this user/date by forcing
    # get_cached_image_path to return a non-None Path.
    async def fake_get_cached_image_path(user_id: str, date_str: str) -> Path | None:
        assert user_id == "user-1"
        assert date_str == "2026-05-17"
        return Path(f"/tmp/{user_id}_{date_str}.jpg")

    monkeypatch.setattr(
        repo,
        "get_cached_image_path",
        fake_get_cached_image_path,
        raising=True,
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            return output_path

    renderer = DummyRenderer()
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # Because a cached image path is returned, the handler should skip rendering
    # for this record and not include it in the pregenerated count.
    assert renderer.render_call_count == 0
    assert count == 0

```

The above changes assume:

1. There is a `FortunePregenerationHandler` (or similarly named) class that:
   - Accepts `repo` and `renderer` in its constructor.
   - Exposes an async method `pregenerate_active_fortune_images(today: str) -> int`.
   - Uses `self._renderer.render_to_image(...)` internally and consults `repo.get_cached_image_path(...)` to decide whether to render.
2. `MemoryFortuneRepo` has a `cached_images` attribute compatible with the assertions.

If your actual handler or function names / signatures differ, you will need to:
1. Replace `FortunePregenerationHandler(...)` with the real class or function used for pregeneration (or adjust the tests to call the function directly rather than going through a handler).
2. Update the call `await handler.pregenerate_active_fortune_images(today=today)` to match the real API.
3. If `get_cached_image_path` is synchronous or has a different signature, adjust `fake_get_cached_image_path` accordingly and remove `async` if necessary.

The key behaviors to preserve in the final implementation are:
- When `get_cached_image_path` returns a non-None path, `render_to_image` must not be called for that record.
- Such records must not be counted in the value returned by `pregenerate_active_fortune_images`.
</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 thread src/infrastructure/astrbot/commands/fortune.py Outdated
Comment on lines +84 to +85
@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_writes_rendered_cache(
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.

建议(测试): 增加一个已有缓存图片的测试用例,以验证 pregenerate_active_fortune_images 会跳过渲染并且不会增加缓存计数。

为覆盖新的行为,请添加一个测试:将 MemoryFortuneRepo.get_cached_image_path 配置为对某个用户/日期返回非 None 的值,并断言:(1) 对该记录不会调用 handler._renderer.render_to_image(例如通过 spy/计数器来验证),(2) pregenerate_active_fortune_images 返回 0,或者至少不会把该用户计入返回值中。这样可以帮助捕获重新渲染已缓存图片的回归问题。

建议的实现:

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_writes_rendered_cache(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            # 在真实实现中这里会写入图片文件;在测试中我们只返回传入的
            # 路径,以满足 handler 的预期。
            return output_path

    renderer = DummyRenderer()
    # 根据你的代码库情况,下面可能需要替换为实际用于预生成的 handler
    # 类或工厂。
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # 没有缓存的一个有效 fortune 应当被渲染并计数。
    assert count == 1
    assert renderer.render_call_count == 1
    assert ("user-1", "2026-05-17") in repo.cached_images

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_skips_when_cache_exists(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    # 通过强制 get_cached_image_path 返回非 None 的 Path,模拟该用户/日期
    # 已经存在缓存图片。
    async def fake_get_cached_image_path(user_id: str, date_str: str) -> Path | None:
        assert user_id == "user-1"
        assert date_str == "2026-05-17"
        return Path(f"/tmp/{user_id}_{date_str}.jpg")

    monkeypatch.setattr(
        repo,
        "get_cached_image_path",
        fake_get_cached_image_path,
        raising=True,
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            return output_path

    renderer = DummyRenderer()
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # 由于返回了缓存图片路径,handler 应当跳过该记录的渲染,并且不将其
    # 计入预生成数量。
    assert renderer.render_call_count == 0
    assert count == 0

上述修改假设:

  1. 存在一个 FortunePregenerationHandler(或类似命名)的类:
    • 在构造函数中接收 reporenderer
    • 暴露一个异步方法 pregenerate_active_fortune_images(today: str) -> int
    • 在内部使用 self._renderer.render_to_image(...),并调用 repo.get_cached_image_path(...) 决定是否需要渲染。
  2. MemoryFortuneRepo 具有与断言兼容的 cached_images 属性。

如果你的实际 handler 或函数名/签名不同,你需要:

  1. FortunePregenerationHandler(...) 替换为真实用于预生成的类或函数(或者调整测试以直接调用该函数,而不是通过 handler)。
  2. 更新 await handler.pregenerate_active_fortune_images(today=today) 的调用,以符合真实 API。
  3. 如果 get_cached_image_path 是同步的或签名不同,请相应调整 fake_get_cached_image_path,并在必要时移除 async

需要在最终实现中保持的关键行为是:

  • get_cached_image_path 返回非 None 的路径时,不得对该记录调用 render_to_image
  • 这类记录不得计入 pregenerate_active_fortune_images 返回的数量中。
Original comment in English

suggestion (testing): Add a test case where existing cached images are present so pregenerate_active_fortune_images skips rendering and does not increment the cached count.

To cover the new behavior, please add a test that configures MemoryFortuneRepo.get_cached_image_path to return a non-None value for a user/date and asserts that: (1) handler._renderer.render_to_image is not called for that record (e.g., via a spy/counter), and (2) pregenerate_active_fortune_images returns 0 or otherwise does not include that user in the count. This will help catch regressions where we re-render already cached images.

Suggested implementation:

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_writes_rendered_cache(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            # In a real implementation this would write an image file; here we just
            # return the provided path to satisfy the handler's expectations.
            return output_path

    renderer = DummyRenderer()
    # Depending on your codebase this may need to be updated to the actual handler
    # class or factory used for pregeneration.
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # One active fortune with no cache should be rendered and counted.
    assert count == 1
    assert renderer.render_call_count == 1
    assert ("user-1", "2026-05-17") in repo.cached_images

@pytest.mark.asyncio
async def test_pregenerate_active_fortune_images_skips_when_cache_exists(
    monkeypatch,
) -> None:
    today = date.today().isoformat()
    repo = MemoryFortuneRepo(
        [
            FortuneGenerationRequest(
                user_id="user-1",
                username="测试用户",
                date_str="2026-05-17",
                group_id="group-1",
            )
        ]
    )

    # Simulate that a cached image already exists for this user/date by forcing
    # get_cached_image_path to return a non-None Path.
    async def fake_get_cached_image_path(user_id: str, date_str: str) -> Path | None:
        assert user_id == "user-1"
        assert date_str == "2026-05-17"
        return Path(f"/tmp/{user_id}_{date_str}.jpg")

    monkeypatch.setattr(
        repo,
        "get_cached_image_path",
        fake_get_cached_image_path,
        raising=True,
    )

    class DummyRenderer:
        def __init__(self) -> None:
            self.render_call_count = 0

        async def render_to_image(self, fortune: str, output_path: Path) -> Path:  # type: ignore[override]
            self.render_call_count += 1
            return output_path

    renderer = DummyRenderer()
    handler = FortunePregenerationHandler(repo=repo, renderer=renderer)  # type: ignore[name-defined]

    count = await handler.pregenerate_active_fortune_images(today=today)

    # Because a cached image path is returned, the handler should skip rendering
    # for this record and not include it in the pregenerated count.
    assert renderer.render_call_count == 0
    assert count == 0

The above changes assume:

  1. There is a FortunePregenerationHandler (or similarly named) class that:
    • Accepts repo and renderer in its constructor.
    • Exposes an async method pregenerate_active_fortune_images(today: str) -> int.
    • Uses self._renderer.render_to_image(...) internally and consults repo.get_cached_image_path(...) to decide whether to render.
  2. MemoryFortuneRepo has a cached_images attribute compatible with the assertions.

If your actual handler or function names / signatures differ, you will need to:

  1. Replace FortunePregenerationHandler(...) with the real class or function used for pregeneration (or adjust the tests to call the function directly rather than going through a handler).
  2. Update the call await handler.pregenerate_active_fortune_images(today=today) to match the real API.
  3. If get_cached_image_path is synchronous or has a different signature, adjust fake_get_cached_image_path accordingly and remove async if necessary.

The key behaviors to preserve in the final implementation are:

  • When get_cached_image_path returns a non-None path, render_to_image must not be called for that record.
  • Such records must not be counted in the value returned by pregenerate_active_fortune_images.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

本 PR 发布 v2.0.2,并恢复 Fortune 跨日后为近期活跃用户预生成并缓存运势卡片图片的行为,同时更新测试环境与仓库文档。

Changes:

  • 新增 Fortune 活跃用户请求查询、预生成记录返回与图片缓存预渲染流程。
  • 在插件初始化时启动 Fortune 日切预缓存后台任务,并在终止时取消。
  • 更新版本、CHANGELOG、AGENTS 指南,并删除错误路径下的旧 workflow 文件。

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
main.py Adds Fortune pre-generation background loop lifecycle.
src/application/ports/fortune_repository.py Extends Fortune repository port with active generation request lookup.
src/domain/fortune/service.py Refactors active-user pre-generation to return generated/existing records.
src/infrastructure/astrbot/commands/fortune.py Adds rendered Fortune card cache pre-generation.
src/infrastructure/persistence/sqlite_fortune_repository.py Stores usernames and implements active Fortune request query.
tests/infrastructure/test_fortune_pregeneration.py Adds coverage for Fortune pre-generation and SQLite request building.
tests/conftest.py Attempts to pin ASTRBOT_ROOT before AstrBot imports.
metadata.yaml Bumps plugin version to v2.0.2.
CHANGELOG.md Documents v2.0.2 fixes and workflow cleanup.
AGENTS.md Refreshes repository architecture and contributor guidance.
.github/workflow/release-from-changelog.yml Removes obsolete workflow from the non-standard path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

records.append(existing)
continue

records.append(await self.get_or_create_fortune(request))
Comment thread tests/conftest.py
Comment on lines +12 to +15
for parent in Path(__file__).resolve().parents:
if (parent / "astrbot" / "core" / "__init__.py").exists():
os.environ.setdefault("ASTRBOT_ROOT", str(parent))
break
Copilot AI review requested due to automatic review settings May 18, 2026 09:46
@FlanChanXwO FlanChanXwO review requested due to automatic review settings May 18, 2026 09:46
@FlanChanXwO FlanChanXwO merged commit 40f5b46 into master May 18, 2026
2 checks passed
@FlanChanXwO FlanChanXwO deleted the fix-configurable-messages-setu-fortune branch May 18, 2026 10:06
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