Answer the review, and keep the opening node's bank
Nine findings from a review of the phase-14 stack. The one about a retry withdrawing a memory is not a bug — a memory anchored to a node describes that node, and it goes when the node goes. The root is the exception, and it is the only one: migration 62 parked every memory written before memories had coordinates on depth 0, so withdrawing the opening node would retire a whole bank nobody attached there. A memory with no source range covers no story and now stays; a summary that genuinely ends there is still withdrawn. The rest are repairs. * The adventure list quoted whichever attempt was written last rather than the one the story tells, so switching back left the index disagreeing with the page. * A v1 import gave a typed memory no depth, rebuilding the NULL the migration exists to remove — invisible until the imported adventure forked. * The action cap counted a v1 file's turns, and a turn expands into a row per saved attempt, so a file inside the cap could write a multiple of it. * Forking a live node on a borrowed ancestor promoted a sibling on a branch the caller never named. It is a branch switch, and now says so. * Switching attempts left the state, status and memory panels reading the previous take: the story does not change length, so nothing keyed on its length noticed. Same class as the branch-switch bug this phase already fixed. * A retry after switching back numbered the new attempt into the middle of the group instead of the end. * The cursor backfill numbered every action in the table once per adventure; correlated to the adventure being updated, it is an index lookup instead. * Renaming a branch answered own_actions=0. And one behaviour change recorded rather than repaired: script-visible history and actionCount no longer count blank-text rows. That is the right shape and there is no reading compatible with both, so plan/14 says so. 409 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
This commit is contained in:
committed by
Parth
co-authored by
Claude Opus 5
parent
2d38a162d4
commit
4af6e17406
@@ -533,6 +533,75 @@ def test_a_v2_bundle_brings_its_anchors_back(client):
|
||||
db.close()
|
||||
|
||||
|
||||
def test_a_v1_memory_that_summarises_nothing_lands_on_the_root(client):
|
||||
"""The import has to answer the question migration 62 answered.
|
||||
|
||||
A v1 file has no depths, and a memory the player typed has no `sourceEnd`
|
||||
to derive one from — so it used to come back with a NULL depth, which is the
|
||||
exact state SP7 removed from the schema. `Path._entry_clause` compares
|
||||
`depth <= max_depth` and a NULL fails it, so the memory would read fine
|
||||
until the imported adventure was forked and then vanish from the new branch.
|
||||
"""
|
||||
payload = {
|
||||
"format": bundle.LEGACY_FORMAT, "title": "Old backup",
|
||||
"memoryCursor": 0, "summaryCursor": 0,
|
||||
"actions": [
|
||||
{"index": 0, "type": "start", "text": OPENING},
|
||||
{"index": 1, "type": "story", "text": "A corridor."},
|
||||
],
|
||||
"memories": [
|
||||
{"text": "Kira is the innkeeper's daughter"}, # typed
|
||||
{"text": "The corridor, summarised", "sourceStart": 1, "sourceEnd": 1},
|
||||
],
|
||||
}
|
||||
copy = _imported(client, payload)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = {m.text: m for m in db.query(models.Memory)
|
||||
.filter(models.Memory.adventure_id == copy).all()}
|
||||
assert rows["Kira is the innkeeper's daughter"].depth == 0, (
|
||||
"a typed memory anchors at the root, which every branch can see"
|
||||
)
|
||||
assert rows["The corridor, summarised"].depth == 1, "derived from its range"
|
||||
assert all(m.depth is not None for m in rows.values())
|
||||
assert all(m.branch_id is not None for m in rows.values())
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_the_action_cap_counts_the_rows_a_v1_file_expands_into(client, monkeypatch):
|
||||
"""The cap has to count what gets written, not what the file lists.
|
||||
|
||||
A v1 turn carries its retries in a `variants` array, and SP4 made every
|
||||
attempt a row — so one entry can become ten. Counting entries lets a file
|
||||
inside the cap write a multiple of it, and the body limit is no help: the
|
||||
text is tiny, it is the row count that is the cost.
|
||||
"""
|
||||
monkeypatch.setattr(auth, "MULTI_USER", True)
|
||||
monkeypatch.setattr(limits, "MAX_ACTIONS_PER_ADVENTURE", 6)
|
||||
monkeypatch.setattr(limits, "_BUNDLE_LIST_CAPS",
|
||||
{**limits._BUNDLE_LIST_CAPS, "actions": 6})
|
||||
payload = {
|
||||
"format": bundle.LEGACY_FORMAT, "title": "Small file, many rows",
|
||||
"memoryCursor": 0, "summaryCursor": 0,
|
||||
"actions": [{"index": 0, "type": "start", "text": OPENING}] + [
|
||||
{
|
||||
"index": i, "type": "ai", "text": "Take four.",
|
||||
"variants": [{"text": f"Take {n}."} for n in range(4)],
|
||||
"variantIndex": 3,
|
||||
}
|
||||
for i in range(1, 4)
|
||||
],
|
||||
}
|
||||
assert len(payload["actions"]) <= 6, "the file itself is inside the cap"
|
||||
before = _adventure_count()
|
||||
|
||||
r = _import(client, payload)
|
||||
assert r.status_code == 409, r.text
|
||||
assert _adventure_count() == before, "and nothing was written"
|
||||
|
||||
|
||||
def test_an_unknown_format_is_refused(client):
|
||||
r = _import(client, {"format": "ai-dnd-adventure-v3", "title": "From the future"})
|
||||
assert r.status_code == 400, r.text
|
||||
|
||||
Reference in New Issue
Block a user