mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-15 09:11:24 +00:00
test(auto_resource): streamline ownership lookup regression coverage
This commit is contained in:
parent
8601d2d322
commit
b0830535f5
1 changed files with 57 additions and 65 deletions
|
|
@ -158,14 +158,12 @@ async def test_mixed_processors_share_history_and_keep_result_order(auto_resourc
|
|||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["text-step", "text-only-router"])
|
||||
@pytest.mark.parametrize("dated", [False, True], ids=["loose-root", "dated-resource"])
|
||||
async def test_text_only_batch_uses_history_without_image_processor(routed, dated, auto_resource_env, monkeypatch):
|
||||
"""A text-only pipeline neither needs an image processor nor scans history for dated paths."""
|
||||
async def test_text_only_batch_uses_history_without_image_processor(routed, auto_resource_env, monkeypatch):
|
||||
"""A text-only pipeline can reuse history without configuring an image processor."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
env.app_context.registry = R.copy()
|
||||
prefix = "resource/2026-01-01" if dated else "resource"
|
||||
notes = [env.write_note(f"daily/2026-01-01/report-{i}.md", f"[[{prefix}/report-{i}.txt]]") for i in range(3)]
|
||||
notes = [env.write_note(f"daily/2026-01-01/report-{i}.md", f"[[resource/report-{i}.txt]]") for i in range(3)]
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
options = {"app_context": env.app_context, "file_store": env.file_store}
|
||||
step = (
|
||||
|
|
@ -174,17 +172,16 @@ async def test_text_only_batch_uses_history_without_image_processor(routed, date
|
|||
else AutoTextResourceStep(**options)
|
||||
)
|
||||
|
||||
response = await env.run(step, [{"change": "deleted", "path": f"{prefix}/report-{i}.txt"} for i in range(3)])
|
||||
response = await env.run(step, [{"change": "deleted", "path": f"resource/report-{i}.txt"} for i in range(3)])
|
||||
|
||||
assert response.success is True
|
||||
assert all(not note.exists() for note in notes)
|
||||
_assert_history_reads(calls, 0 if dated else 1)
|
||||
assert calls[("daily_list", "2026-01-01")] == (3 if dated else 1)
|
||||
_assert_history_reads(calls)
|
||||
assert calls[("daily_list", "2026-01-01")] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
@pytest.mark.parametrize("dated", [False, True], ids=["loose-root", "dated-resource"])
|
||||
async def test_new_image_uses_live_post_write_resolution(routed, dated, auto_resource_env, monkeypatch):
|
||||
async def test_new_image_uses_live_post_write_resolution(dated, auto_resource_env, monkeypatch):
|
||||
"""Only dated input needs a pre-write daily_list; both paths read the newly written note."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
|
|
@ -200,7 +197,7 @@ async def test_new_image_uses_live_post_write_resolution(routed, dated, auto_res
|
|||
monkeypatch.setattr(FakeVisionModel, "__call__", observe_pre_write)
|
||||
with patch.object(BaseAutoResourceStep, "_today", return_value="2026-01-01"):
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[{"change": "added", "path": str(source)}],
|
||||
)
|
||||
|
||||
|
|
@ -212,14 +209,11 @@ async def test_new_image_uses_live_post_write_resolution(routed, dated, auto_res
|
|||
assert calls[("daily_list", "2026-01-01")] == (2 if dated else 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_reused_step_and_context_rebuild_history_next_invocation(routed, auto_resource_env, monkeypatch):
|
||||
async def test_reused_step_and_context_rebuild_history_next_invocation(auto_resource_env):
|
||||
"""External edits between invocations are visible; batch state never survives the call."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
original = env.write_note("daily/2026-01-01/original.md", "[[resource/photo.png]]")
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
step = env.processor(_model(), routed=routed)
|
||||
step = env.processor(_model(), routed=True)
|
||||
changes = [{"change": "deleted", "path": "resource/photo.png"}]
|
||||
context = RuntimeContext(changes=changes, user_value="preserve")
|
||||
context_keys = set(context.data)
|
||||
|
|
@ -242,20 +236,15 @@ async def test_reused_step_and_context_rebuild_history_next_invocation(routed, a
|
|||
assert set(context.data) == context_keys
|
||||
assert context["changes"] is changes
|
||||
assert context["user_value"] == "preserve"
|
||||
_assert_history_reads(calls, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_create_update_delete_recreate_maintains_ownership_across_midnight(
|
||||
routed,
|
||||
auto_resource_env,
|
||||
monkeypatch,
|
||||
):
|
||||
"""A card keeps its original day until deletion permits a fresh card on today's date."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
env.write_binary("resource/caption.png", image_bytes())
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
clock = {"day": "2026-01-01"}
|
||||
refresh = resource_module.refresh_day_index
|
||||
|
||||
|
|
@ -267,7 +256,7 @@ async def test_create_update_delete_recreate_maintains_ownership_across_midnight
|
|||
monkeypatch.setattr(BaseAutoResourceStep, "_today", lambda _step: clock["day"])
|
||||
monkeypatch.setattr(resource_module, "refresh_day_index", refresh_and_advance)
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[{"change": change, "path": "resource/caption.png"} for change in ("added", "modified", "deleted", "added")],
|
||||
)
|
||||
|
||||
|
|
@ -282,25 +271,19 @@ async def test_create_update_delete_recreate_maintains_ownership_across_midnight
|
|||
]
|
||||
assert not (env.workspace / "daily/2026-01-01/caption.md").exists()
|
||||
assert (env.workspace / "daily/2026-01-02/caption.md").is_file()
|
||||
# The three repeated events deliberately use the original uncached lookup.
|
||||
_assert_history_reads(calls, 4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
@pytest.mark.parametrize("first_change", ["added", "deleted"])
|
||||
async def test_repeated_source_retries_after_partial_write_or_delete(
|
||||
routed,
|
||||
first_change,
|
||||
auto_resource_env,
|
||||
monkeypatch,
|
||||
):
|
||||
"""A repeated source uses live lookup even when its previous event failed after mutation."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
env.write_binary("resource/caption.png", image_bytes())
|
||||
if first_change == "deleted":
|
||||
env.write_note("daily/2026-01-01/caption.md", "[[resource/caption.png]]")
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
clock = {"day": "2026-01-01"}
|
||||
refresh = resource_module.refresh_day_index
|
||||
refresh_calls = 0
|
||||
|
|
@ -317,7 +300,7 @@ async def test_repeated_source_retries_after_partial_write_or_delete(
|
|||
monkeypatch.setattr(resource_module, "refresh_day_index", fail_first_refresh)
|
||||
next_change = "modified" if first_change == "added" else "added"
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[{"change": change, "path": "resource/caption.png"} for change in (first_change, next_change)],
|
||||
)
|
||||
|
||||
|
|
@ -332,56 +315,57 @@ async def test_repeated_source_retries_after_partial_write_or_delete(
|
|||
assert recovered["metadata"]["path"] == f"daily/{day}/caption.md"
|
||||
assert recovered["metadata"]["created"] is (first_change == "deleted")
|
||||
assert (env.workspace / f"daily/{day}/caption.md").is_file()
|
||||
_assert_history_reads(calls, 2)
|
||||
assert len(list((env.workspace / "daily").glob("*/caption.md"))) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
@pytest.mark.parametrize("mixed_paths", [False, True], ids=["relative-paths", "absolute-then-relative"])
|
||||
async def test_repeated_existing_updates_keep_original_day_and_path(
|
||||
routed,
|
||||
mixed_paths,
|
||||
auto_resource_env,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Repeated events use the same logical source even when absolute and relative inputs differ."""
|
||||
"""Non-consecutive updates recognize the same source in absolute and relative form."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
source = env.write_binary("resource/photo.png", image_bytes())
|
||||
note = env.write_note("daily/2026-01-01/original.md", "[[resource/photo.png]]")
|
||||
env.write_binary("resource/other.png", image_bytes())
|
||||
other = env.write_note("daily/2026-01-01/other.md", "[[resource/other.png]]")
|
||||
before = note.read_bytes()
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
model = _model()
|
||||
changes = [
|
||||
{"change": "modified", "path": str(source) if mixed_paths else "resource/photo.png"},
|
||||
{"change": "modified", "path": str(source)},
|
||||
{"change": "modified", "path": "resource/other.png"},
|
||||
{"change": "modified", "path": "resource/photo.png"},
|
||||
]
|
||||
|
||||
with patch.object(BaseAutoResourceStep, "_today", return_value="2026-01-02"):
|
||||
response = await env.run(env.processor(model, routed=routed), changes)
|
||||
response = await env.run(env.processor(model, routed=True), changes)
|
||||
|
||||
assert response.success is True
|
||||
metadata = [result["metadata"] for result in response.metadata["results"]]
|
||||
assert [item["path"] for item in metadata] == ["daily/2026-01-01/original.md"] * 2
|
||||
assert [item["path"] for item in metadata] == [
|
||||
"daily/2026-01-01/original.md",
|
||||
"daily/2026-01-01/other.md",
|
||||
"daily/2026-01-01/original.md",
|
||||
]
|
||||
assert all(item["action"] == "modified" and item["created"] is False for item in metadata)
|
||||
assert len(model.calls) == 2
|
||||
assert len(model.calls) == 3
|
||||
assert note.read_bytes() != before
|
||||
assert other.is_file()
|
||||
assert not (env.workspace / "daily/2026-01-02").exists()
|
||||
# Unchanged paths alone cannot prove that the repeated source used live lookup.
|
||||
_assert_history_reads(calls, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_cross_day_duplicate_only_fails_ambiguous_resource(routed, auto_resource_env, monkeypatch):
|
||||
async def test_cross_day_duplicate_only_fails_ambiguous_resource(auto_resource_env):
|
||||
"""Ambiguous ownership prevents mutation without blocking another valid resource."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
first = env.write_note("daily/2026-01-01/first.md", "[[resource/duplicate.png]]")
|
||||
second = env.write_note("daily/2026-01-02/second.md", "[[resource/duplicate.png]]")
|
||||
valid = env.write_note("daily/2026-01-02/valid.md", "[[resource/valid.png]]")
|
||||
before = {note: note.read_bytes() for note in (first, second)}
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[
|
||||
{"change": "deleted", "path": "resource/duplicate.png"},
|
||||
{"change": "deleted", "path": "resource/valid.png"},
|
||||
|
|
@ -397,20 +381,16 @@ async def test_cross_day_duplicate_only_fails_ambiguous_resource(routed, auto_re
|
|||
assert deleted["metadata"]["action"] == "deleted"
|
||||
assert not valid.exists()
|
||||
assert all(note.read_bytes() == contents for note, contents in before.items())
|
||||
_assert_history_reads(calls)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_same_day_duplicates_keep_first_match_after_deletion(routed, auto_resource_env, monkeypatch):
|
||||
async def test_same_day_duplicates_keep_first_match_after_deletion(auto_resource_env):
|
||||
"""Deleting the first same-day match exposes the next one, preserving existing behavior."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
first = env.write_note("daily/2026-01-01/first.md", "[[resource/photo.png]]")
|
||||
second = env.write_note("daily/2026-01-01/second.md", "[[resource/photo.png]]")
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
with patch.object(BaseAutoResourceStep, "_today", return_value="2026-01-02"):
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[{"change": "deleted", "path": "resource/photo.png"} for _ in range(3)],
|
||||
)
|
||||
|
||||
|
|
@ -420,16 +400,36 @@ async def test_same_day_duplicates_keep_first_match_after_deletion(routed, auto_
|
|||
assert [item["path"] for item in metadata[:2]] == ["daily/2026-01-01/first.md", "daily/2026-01-01/second.md"]
|
||||
assert not first.exists()
|
||||
assert not second.exists()
|
||||
_assert_history_reads(calls, 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_initial_read_failure_does_not_publish_partial_history(routed, auto_resource_env, monkeypatch):
|
||||
async def test_loose_and_dated_changes_keep_separate_lookup_modes(auto_resource_env):
|
||||
"""A dated event must not inherit a loose event's cached path, including on repeated deletion."""
|
||||
env = auto_resource_env
|
||||
sources = ["resource/first.png", "resource/2026-01-02/dated.png", "resource/last.png"]
|
||||
notes = [
|
||||
env.write_note("daily/2026-01-01/first.md", f"[[{sources[0]}]]"),
|
||||
env.write_note("daily/2026-01-02/first.md", f"[[{sources[1]}]]"),
|
||||
env.write_note("daily/2026-01-02/second.md", f"[[{sources[1]}]]"),
|
||||
env.write_note("daily/2026-01-01/last.md", f"[[{sources[2]}]]"),
|
||||
]
|
||||
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=True),
|
||||
[{"change": "deleted", "path": source} for source in (sources[0], sources[1], sources[1], sources[2])],
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
metadata = [result["metadata"] for result in response.metadata["results"]]
|
||||
assert [item["action"] for item in metadata] == ["deleted"] * 4
|
||||
assert [item["path"] for item in metadata] == [note.relative_to(env.workspace).as_posix() for note in notes]
|
||||
assert all(not note.exists() for note in notes)
|
||||
|
||||
|
||||
async def test_initial_read_failure_does_not_publish_partial_history(auto_resource_env, monkeypatch):
|
||||
"""A transient history read failure makes the next item retry the initial scan."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
owned_note = env.write_note("daily/2026-01-01/original.md", "[[resource/photo.png]]")
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
daily_list = env.app_context.jobs["daily_list"]
|
||||
failed_once = False
|
||||
|
||||
|
|
@ -442,7 +442,7 @@ async def test_initial_read_failure_does_not_publish_partial_history(routed, aut
|
|||
|
||||
monkeypatch.setitem(env.app_context.jobs, "daily_list", fail_once_mid_scan)
|
||||
response = await env.run(
|
||||
env.processor(_model(), routed=routed),
|
||||
env.processor(_model(), routed=True),
|
||||
[
|
||||
{"change": "deleted", "path": "resource/missing.png"},
|
||||
{"change": "deleted", "path": "resource/photo.png"},
|
||||
|
|
@ -458,8 +458,6 @@ async def test_initial_read_failure_does_not_publish_partial_history(routed, aut
|
|||
assert recovered["metadata"]["path"] == "daily/2026-01-01/original.md"
|
||||
assert recovered["metadata"]["action"] == "deleted"
|
||||
assert not owned_note.exists()
|
||||
assert calls["history_enumerations"] == 2
|
||||
assert [calls[("parse", day)] for day in _HISTORY_DAYS] == [2, 1, 1]
|
||||
|
||||
|
||||
async def test_empty_history_is_initialized_once(auto_resource_env, monkeypatch):
|
||||
|
|
@ -478,12 +476,10 @@ async def test_empty_history_is_initialized_once(auto_resource_env, monkeypatch)
|
|||
assert calls["history_enumerations"] == 1
|
||||
|
||||
|
||||
async def test_router_exception_restores_context_and_next_call_rebuilds(auto_resource_env, monkeypatch):
|
||||
async def test_router_exception_restores_context_and_next_call_rebuilds(auto_resource_env):
|
||||
"""Even an exception after dispatch cannot leak batch state into a reused context."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
original = env.write_note("daily/2026-01-01/original.md", "[[resource/photo.png]]")
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
step = env.processor(_model(), routed=True)
|
||||
changes = [{"change": "deleted", "path": "resource/photo.png"}]
|
||||
context = RuntimeContext(changes=changes, user_value="preserve")
|
||||
|
|
@ -509,18 +505,15 @@ async def test_router_exception_restores_context_and_next_call_rebuilds(auto_res
|
|||
assert response.metadata["results"][0]["metadata"]["path"] == "daily/2026-01-02/recreated.md"
|
||||
assert not recreated.exists()
|
||||
assert set(context.data) == context_keys
|
||||
_assert_history_reads(calls, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routed", [False, True], ids=["image", "unified-router"])
|
||||
async def test_cancelled_invocation_restores_context_and_rebuilds(routed, auto_resource_env, monkeypatch):
|
||||
async def test_cancelled_invocation_restores_context_and_rebuilds(routed, auto_resource_env):
|
||||
"""Cancel at the model boundary after lookup, without mocking the resource processing path."""
|
||||
env = auto_resource_env
|
||||
_seed_history(env)
|
||||
env.write_binary("resource/photo.png", image_bytes())
|
||||
original = env.write_note("daily/2026-01-01/original.md", "[[resource/photo.png]]")
|
||||
before = original.read_bytes()
|
||||
calls = _observe_history(env, monkeypatch)
|
||||
entered = asyncio.Event()
|
||||
|
||||
# The inherited structured method deliberately raises to exercise plain-call fallback.
|
||||
|
|
@ -560,4 +553,3 @@ async def test_cancelled_invocation_restores_context_and_rebuilds(routed, auto_r
|
|||
assert response.metadata["results"][0]["metadata"]["path"] == "daily/2026-01-02/recreated.md"
|
||||
assert not recreated.exists()
|
||||
assert set(context.data) == context_keys
|
||||
_assert_history_reads(calls, 2)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue