Skip to content

test(skills): tighten TestUpdateSkill.test_mtime_conflict to pin §5 c4 no-write semantics #778

@memtomem

Description

@memtomem

Background

ADR-0001 §5 c4 ("conflict path covered by a test fixture") requires
pinning conflict semantics, not just the response label. PR #777
(closes #772) backfilled the strict assertion set for Phase B
(Commands). PR #777's review surfaced that Phase A (Skills) — the
oldest of the three phase tests — still asserts only the response
label, leaving the same hygiene gap §5 c4 is designed to prevent.

This is a follow-up to #772 / PR #777, scoped to Phase A only.

Blocked on PR #777 merging. The strictness map below is the
post-merge snapshot. PR #777 is the one that adds the Commands row
entry; until it lands on main, :578 will not exist there. Pick
this up after gh pr view 777 --json mergedAt is non-null and rebase
any line refs against the new main before opening a PR.

Current 409-fixture strictness map (post-PR #777)

Phase Stale-mtime 409 fixture status: "aborted" no-write content mtime_ns echo
A Skills test_web_routes_context.py:224 (TestUpdateSkill.test_mtime_conflict) ✗ MISSING ✗ MISSING
B Commands test_web_routes_context.py:578 (TestCommandCRUD.test_mtime_conflict, after PR #777 merges) ✗ (out of scope)
C Agents test_web_routes_context.py:772 (TestAgentCRUD.test_mtime_conflict_after_external_write)

The Agents test at :801-802 is the strictest reference shape:

assert data["mtime_ns"] == str(agent_path.stat().st_mtime_ns)
assert "overwritten" not in agent_path.read_text(encoding="utf-8")

A regression that returns 409 + status: "aborted" and then writes
body.content would currently pass TestUpdateSkill.test_mtime_conflict
unchanged.

Scope

Tighten the existing Skills test in place. One file, one method, no
new test.

packages/memtomem/tests/test_web_routes_context.py:224-232
TestUpdateSkill.test_mtime_conflict should additionally assert:

  1. data["mtime_ns"] echoes the manifest's on-disk
    st_mtime_ns — pins the retry-hint contract from
    context_skills.py:199-208.
  2. The skill manifest content on disk is unchanged after the 409 —
    pins the no-write semantics §5 c4 actually targets.

Bind the manifest path explicitly so both assertions reuse the same
variable:

manifest_path = tmp_path / ".memtomem" / "skills" / "conflict" / SKILL_MANIFEST
# ... PUT with stale mtime_ns ...
assert data["mtime_ns"] == str(manifest_path.stat().st_mtime_ns)
assert manifest_path.read_text(encoding="utf-8") == "# Test skill\n"

("# Test skill\n" is the default _make_skill writes — see line 43
of the test file.)

The 409 path itself already exists in production code at
packages/memtomem/src/memtomem/web/routes/context_skills.py:199-208
(current_mtime_ns != body_mtime_ns → 409 + status: "aborted").
This issue is purely test-coverage hardening — no production change.

Suggested mutation-validation step

Before commit, swap the order of atomic_write_text and the
mtime-guard in context_skills.py:199-211 so the write happens
before the conflict check. The hardened test should fail (current
test would still pass). Revert the mutation before commit. PR #777
followed this same pattern.

Acceptance

  • TestUpdateSkill.test_mtime_conflict asserts data["mtime_ns"]
    echoes the on-disk mtime AND the manifest content is unchanged.
  • uv run pytest -m "not ollama" packages/memtomem/tests/test_web_routes_context.py green.
  • uv run ruff check packages/memtomem/tests/test_web_routes_context.py green.
  • PR body documents the mutation-validation result.

Out of scope

  • Production code changes to context_skills.py — the 409 path is
    already correct.
  • Phase B (Commands) mtime_ns echo assertion — the gap exists
    (Commands lacks the echo) but is not a §5 c4 requirement
    ("conflict semantics" = no-write, which Commands now pins). File
    separately if the team wants strict three-phase echo parity.
  • Phase C (Agents) — already strict.

Refs: #772 (Phase B sibling), #777 (Phase B PR), #761 (parent RFC),
#769 (ADR §5).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions