* test(lora): hold the import job to test cancel before and after the commit (#575) Squashed from PR #594. Closes #575. * fix: address review-bot findings on merged PRs (#593) Squashed from PR #595. Closes #593. * fix(maintenance): list every protected item with its reason (#586) Squashed from PR #596. Closes #586.
540 lines
23 KiB
Python
540 lines
23 KiB
Python
"""Pair category and tags repair (#550): the ``pair_metadata`` maintenance plan.
|
|
|
|
Before #546 a Pair node run saved its category and tags on the saved pair
|
|
only, so the positive prompt it created stayed Uncategorized and untagged.
|
|
``pair_metadata`` reviews those prompts and fills only their empty fields
|
|
from the prompt's non-archived pairs when every pair with a value agrees.
|
|
It never touches a negative, a saved pair or ``updated_at``, and a prompt
|
|
the Pair node did not create, that was edited, archived, or whose pairs
|
|
disagree, is kept (``550-design.md``).
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from database import maintenance # noqa: E402
|
|
from database import pair_metadata_repair as repair # noqa: E402
|
|
|
|
try:
|
|
from tests.test_maintenance_plans import MaintenanceDatabaseCase # noqa: E402
|
|
except ImportError: # discovered with tests/ as the top-level directory
|
|
from test_maintenance_plans import MaintenanceDatabaseCase # noqa: E402
|
|
|
|
OPERATION = "pair_metadata"
|
|
|
|
|
|
class PairMetadataCase(MaintenanceDatabaseCase):
|
|
def pair(self, positive, negative="", **metadata):
|
|
"""A pre-#546 Pair save: the pair gets the metadata, the prompt none."""
|
|
pair_id = self.db.save_prompt_pair(positive, negative, **metadata)
|
|
return pair_id, self.positive_of(pair_id)
|
|
|
|
def positive_of(self, pair_id):
|
|
return self.sql(
|
|
"SELECT positive_prompt_id FROM prompt_pairs WHERE id=?", pair_id
|
|
)[0][0]
|
|
|
|
def sql(self, statement, *args):
|
|
with self.db.model.get_connection() as conn:
|
|
return [tuple(row) for row in conn.execute(statement, args)]
|
|
|
|
def run_sql(self, statement, *args):
|
|
with self.db.model.get_connection() as conn:
|
|
conn.execute(statement, args)
|
|
|
|
def category(self, prompt_id):
|
|
return self.sql("SELECT category FROM prompts WHERE id=?", prompt_id)[0][0]
|
|
|
|
def tags(self, prompt_id):
|
|
return sorted(
|
|
row[0]
|
|
for row in self.sql(
|
|
"SELECT t.name FROM prompt_tags pt JOIN tags t ON t.id=pt.tag_id "
|
|
"WHERE pt.prompt_id=?",
|
|
prompt_id,
|
|
)
|
|
)
|
|
|
|
def preview(self, operation=OPERATION, roots=None):
|
|
return self.db.preview_maintenance(operation, [])
|
|
|
|
def apply(self, draft):
|
|
return self.db.apply_maintenance(
|
|
draft.operation, draft.fingerprint, draft.targets, []
|
|
)
|
|
|
|
def edit_stamp(self, prompt_id, updated_at="2099-01-01T00:00:00+00:00"):
|
|
self.run_sql(
|
|
"UPDATE prompts SET updated_at=? WHERE id=?", updated_at, prompt_id
|
|
)
|
|
|
|
def details(self, draft):
|
|
return [(row["label"], row["detail"]) for row in draft.details]
|
|
|
|
|
|
class TestClassification(PairMetadataCase):
|
|
def test_fills_empty_category_and_tags_from_the_pair(self):
|
|
pair_id, prompt = self.pair(
|
|
"a cat on a sofa", "blurry", category="portrait", tags=["b", "a"]
|
|
)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.operation, OPERATION)
|
|
self.assertEqual(draft.targets, [[prompt, True, True]])
|
|
self.assertEqual(draft.scope["count"], 1)
|
|
self.assertEqual(
|
|
draft.scope["label"], "1 prompt to fill: 1 category, 1 tag set"
|
|
)
|
|
self.assertEqual(draft.protected, {"count": 0, "reasons": []})
|
|
self.assertEqual(
|
|
draft.media,
|
|
{
|
|
"deletes_files": False,
|
|
"label": "No image file or saved pair is changed.",
|
|
},
|
|
)
|
|
self.assertIn(
|
|
(
|
|
"a cat on a sofa",
|
|
f"Fills category “portrait” and 2 tags: a, b (pair {pair_id})",
|
|
),
|
|
self.details(draft),
|
|
)
|
|
self.assertEqual(draft.details[-1]["label"], "Record")
|
|
self.assertEqual(draft.details[-1]["detail"], repair.RECORD_NOTE)
|
|
|
|
def test_empty_plan(self):
|
|
self.save("plain prompt")
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [])
|
|
self.assertEqual(draft.scope["count"], 0)
|
|
self.assertEqual(
|
|
draft.scope["label"], "No Pair prompts are missing a category or tags"
|
|
)
|
|
self.assertEqual(draft.details, [])
|
|
|
|
def test_prompt_already_complete_is_not_counted(self):
|
|
_pair, prompt = self.pair("done", category="portrait", tags=["a"])
|
|
self.run_sql("UPDATE prompts SET category='portrait' WHERE id=?", prompt)
|
|
self.db.set_prompt_tags(prompt, ["a"])
|
|
self.edit_stamp(
|
|
prompt, self.sql("SELECT created_at FROM prompts WHERE id=?", prompt)[0][0]
|
|
)
|
|
self.assertEqual(self.preview().scope["count"], 0)
|
|
|
|
def test_whitespace_category_is_blank_on_prompt_and_pair(self):
|
|
_p1, blank_prompt = self.pair("ws prompt", category=" portrait ")
|
|
self.run_sql("UPDATE prompts SET category=' ' WHERE id=?", blank_prompt)
|
|
_p2, blank_pair = self.pair("ws pair", category=" ")
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[blank_prompt, True, False]])
|
|
self.assertIn("“portrait”", self.details(draft)[0][1])
|
|
self.assertNotIn(blank_pair, [t[0] for t in draft.targets])
|
|
|
|
def test_mixed_fields_category_fills_while_tags_conflict(self):
|
|
_a, prompt = self.pair("mixed", "n1", category="portrait", tags=["a"])
|
|
self.pair("mixed", "n2", category="portrait", tags=["b"])
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[prompt, True, False]])
|
|
self.assertEqual(draft.scope["count"], 1)
|
|
self.assertEqual(draft.protected["count"], 1)
|
|
self.assertEqual(draft.protected["reasons"], ["1 where pairs disagree"])
|
|
self.assertIn(("mixed", "Kept: pairs disagree on tags"), self.details(draft))
|
|
|
|
def test_mixed_fields_tags_fill_while_category_conflicts(self):
|
|
_a, prompt = self.pair("mixed2", "n1", category="portrait", tags=["a"])
|
|
self.pair("mixed2", "n2", category="landscape", tags=["a"])
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[prompt, False, True]])
|
|
self.assertIn(
|
|
("mixed2", "Kept: pairs disagree on category: landscape, portrait"),
|
|
self.details(draft),
|
|
)
|
|
|
|
def test_category_set_and_tags_empty_fills_tags_only(self):
|
|
_a, prompt = self.pair("has cat", category="portrait", tags=["a"])
|
|
self.run_sql("UPDATE prompts SET category='mine' WHERE id=?", prompt)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[prompt, False, True]])
|
|
|
|
def test_pairs_without_a_value_are_ignored(self):
|
|
_a, prompt = self.pair("partial", "n1", category="portrait", tags=["a"])
|
|
self.pair("partial", "n2") # no category, no tags
|
|
self.assertEqual(self.preview().targets, [[prompt, True, True]])
|
|
|
|
def test_equal_values_across_pairs_fill(self):
|
|
_a, prompt = self.pair("same", "n1", category="portrait", tags=["a", "b"])
|
|
self.pair("same", "n2", category="portrait", tags=["b", "a"])
|
|
self.assertEqual(self.preview().targets, [[prompt, True, True]])
|
|
|
|
def test_category_is_case_sensitive(self):
|
|
self.pair("case", "n1", category="Portrait")
|
|
self.pair("case", "n2", category="portrait")
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [])
|
|
self.assertEqual(draft.protected["count"], 1)
|
|
|
|
def test_edited_prompt_is_protected(self):
|
|
cases = {
|
|
"later": "2099-01-01T00:00:00+00:00",
|
|
"null": None,
|
|
"legacy": "2020-01-01 10:00:00",
|
|
"garbage": "yesterday",
|
|
}
|
|
for name, stamp in cases.items():
|
|
with self.subTest(name=name):
|
|
_a, prompt = self.pair(f"edited {name}", category="portrait")
|
|
self.edit_stamp(prompt, stamp)
|
|
draft = self.preview()
|
|
self.assertNotIn(prompt, [t[0] for t in draft.targets])
|
|
self.assertIn(
|
|
(
|
|
f"edited {name}",
|
|
"Kept: edited after it was saved; its empty category or "
|
|
"tags may be intentional",
|
|
),
|
|
self.details(draft),
|
|
)
|
|
self.run_sql(
|
|
"DELETE FROM prompt_pairs WHERE positive_prompt_id=?", prompt
|
|
)
|
|
|
|
def test_first_release_stamps_microseconds_apart_are_never_edited(self):
|
|
_a, prompt = self.pair("caa9649f", category="portrait")
|
|
self.run_sql(
|
|
"UPDATE prompts SET created_at=?, updated_at=? WHERE id=?",
|
|
"2026-10-18T10:00:00.000100+00:00",
|
|
"2026-10-18T10:00:00.000180+00:00",
|
|
prompt,
|
|
)
|
|
self.run_sql(
|
|
"UPDATE prompt_pairs SET created_at=? WHERE positive_prompt_id=?",
|
|
"2026-10-18T10:00:00.000400+00:00",
|
|
prompt,
|
|
)
|
|
self.assertEqual(self.preview().targets, [[prompt, True, False]])
|
|
|
|
def test_edited_beats_conflict(self):
|
|
_a, prompt = self.pair("both", "n1", category="portrait")
|
|
self.pair("both", "n2", category="landscape")
|
|
self.edit_stamp(prompt)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.protected["reasons"], ["1 edited after it was saved"])
|
|
|
|
def test_archived_prompt_is_protected(self):
|
|
_a, prompt = self.pair("archived", category="portrait")
|
|
self.run_sql(
|
|
"INSERT INTO prompt_archive(prompt_id, archived_at) VALUES(?, ?)",
|
|
prompt,
|
|
"2026-10-31T00:00:00+00:00",
|
|
)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [])
|
|
self.assertEqual(draft.protected["reasons"], ["1 archived in the Library"])
|
|
self.assertIn(
|
|
("archived", "Kept: archived in the Library"), self.details(draft)
|
|
)
|
|
|
|
def test_archived_pairs_are_no_source(self):
|
|
pair_id, prompt = self.pair("old pair", "n1", category="portrait")
|
|
self.db.archive_prompt_pair(pair_id)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [])
|
|
self.assertEqual(draft.protected["count"], 0)
|
|
# An archived pair also never disagrees with a live one.
|
|
self.pair("old pair", "n2", category="landscape")
|
|
self.assertEqual(self.preview().targets, [[prompt, True, False]])
|
|
self.assertIn("landscape", self.details(self.preview())[0][1])
|
|
|
|
def test_prompt_not_created_by_its_pair_is_out_of_scope(self):
|
|
prompt = self.save("saved by the node first")
|
|
self.run_sql(
|
|
"UPDATE prompts SET created_at=?, updated_at=? WHERE id=?",
|
|
"2026-01-01T00:00:00+00:00",
|
|
"2026-01-01T00:00:00+00:00",
|
|
prompt,
|
|
)
|
|
self.db.save_prompt_pair("saved by the node first", category="portrait")
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [])
|
|
self.assertEqual(draft.protected["count"], 0)
|
|
|
|
def test_negative_is_never_a_target_and_positive_role_sources_only(self):
|
|
_a, both = self.pair("dual", "shared negative", category="portrait")
|
|
neg = self.sql("SELECT id FROM prompts WHERE text='shared negative'")[0][0]
|
|
# The negative of pair A is the positive of pair B, made later: not
|
|
# Pair-created as a positive, so out of scope.
|
|
self.db.save_prompt_pair("shared negative", category="landscape")
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[both, True, False]])
|
|
self.assertNotIn(neg, [t[0] for t in draft.targets])
|
|
|
|
def test_positive_first_then_negative_is_filled_from_positive_pairs_only(self):
|
|
_a, prompt = self.pair("pos first", category="portrait")
|
|
self.db.save_prompt_pair("another", "pos first", category="landscape")
|
|
draft = self.preview()
|
|
self.assertIn([prompt, True, False], draft.targets)
|
|
self.assertIn("“portrait”", dict(self.details(draft))["pos first"])
|
|
|
|
def test_more_than_twenty_tags_is_protected(self):
|
|
tags = [f"t{n:02d}" for n in range(21)]
|
|
_a, prompt = self.pair("many tags", category="portrait", tags=tags)
|
|
draft = self.preview()
|
|
self.assertEqual(draft.targets, [[prompt, True, False]])
|
|
self.assertEqual(
|
|
draft.protected["reasons"], ["1 whose pair has more than 20 tags"]
|
|
)
|
|
self.assertIn(
|
|
("many tags", "Kept: its pair has more than 20 tags"), self.details(draft)
|
|
)
|
|
|
|
def test_protected_rows_come_first_within_fifty(self):
|
|
self.pair("zz conflict", "n1", category="x")
|
|
self.pair("zz conflict", "n2", category="y")
|
|
for n in range(60):
|
|
self.pair(f"fill {n:02d}", category="portrait")
|
|
draft = self.preview()
|
|
self.assertEqual(len(draft.details), maintenance.MAX_DETAILS)
|
|
self.assertEqual(draft.details[0]["label"], "zz conflict")
|
|
self.assertEqual(draft.details[-1]["label"], "Record")
|
|
self.assertEqual(draft.scope["count"], 60)
|
|
|
|
def test_detail_rows_list_conflicts_before_other_kept_prompts(self):
|
|
_a, archived = self.pair("aa archived", category="portrait")
|
|
self.run_sql(
|
|
"INSERT INTO prompt_archive(prompt_id, archived_at) VALUES(?, ?)",
|
|
archived,
|
|
"2026-10-31T00:00:00+00:00",
|
|
)
|
|
_b, edited = self.pair("bb edited", category="portrait")
|
|
self.edit_stamp(edited)
|
|
self.pair("zz conflict", "n1", category="x")
|
|
self.pair("zz conflict", "n2", category="y")
|
|
draft = self.preview()
|
|
self.assertEqual(
|
|
[label for label, _detail in self.details(draft)],
|
|
["zz conflict", "bb edited", "aa archived"],
|
|
)
|
|
# The reasons keep the precedence order.
|
|
self.assertEqual(
|
|
draft.protected["reasons"],
|
|
[
|
|
"1 archived in the Library",
|
|
"1 edited after it was saved",
|
|
"1 where pairs disagree",
|
|
],
|
|
)
|
|
|
|
def test_same_library_same_fingerprint_and_pair_use_does_not_change_it(self):
|
|
pair_id, _prompt = self.pair("fp", category="portrait", tags=["a"])
|
|
first = self.preview().fingerprint
|
|
self.db.record_prompt_pair_use(pair_id)
|
|
self.db.update_prompt_pair_metadata(pair_id, notes="a note", title="t")
|
|
self.assertEqual(self.preview().fingerprint, first)
|
|
|
|
|
|
class TestPreviewWritesNothing(PairMetadataCase):
|
|
def test_preview_reads_one_read_only_snapshot(self):
|
|
self.pair("cat", category="portrait", tags=["a"])
|
|
before = self.sql("SELECT * FROM prompts ORDER BY id")
|
|
tags = self.sql("SELECT * FROM prompt_tags ORDER BY prompt_id, tag_id")
|
|
connect = maintenance.sqlite3.connect
|
|
opened = []
|
|
|
|
def spy(*args, **kwargs):
|
|
opened.append(args[0])
|
|
return connect(*args, **kwargs)
|
|
|
|
with patch.object(maintenance.sqlite3, "connect", side_effect=spy):
|
|
self.preview()
|
|
self.assertEqual(len(opened), 1)
|
|
self.assertTrue(str(opened[0]).endswith("?mode=ro"))
|
|
self.assertEqual(self.sql("SELECT * FROM prompts ORDER BY id"), before)
|
|
self.assertEqual(
|
|
self.sql("SELECT * FROM prompt_tags ORDER BY prompt_id, tag_id"), tags
|
|
)
|
|
|
|
|
|
class TestApply(PairMetadataCase):
|
|
def snapshot_pairs(self):
|
|
return (
|
|
self.sql("SELECT * FROM prompt_pairs ORDER BY id"),
|
|
self.sql("SELECT * FROM prompt_pair_tags ORDER BY pair_id, tag_id"),
|
|
)
|
|
|
|
def test_apply_fills_only_empty_fields_and_leaves_pairs_alone(self):
|
|
_a, prompt = self.pair("apply me", "neg", category="portrait", tags=["a", "b"])
|
|
_b, partial = self.pair("partial", "n1", category="portrait", tags=["a"])
|
|
self.pair("partial", "n2", category="portrait", tags=["c"])
|
|
neg = self.sql("SELECT id FROM prompts WHERE text='neg'")[0][0]
|
|
pairs = self.snapshot_pairs()
|
|
stamps = self.sql("SELECT id, created_at, updated_at FROM prompts ORDER BY id")
|
|
state = self.sql("SELECT * FROM prompt_library_state ORDER BY prompt_id")
|
|
tag_rows = self.sql("SELECT id, name FROM tags ORDER BY id")
|
|
draft = self.preview()
|
|
result = self.apply(draft)
|
|
self.assertEqual(result["changed"], 2)
|
|
self.assertEqual(
|
|
result["rows"],
|
|
[
|
|
{"prompt_id": prompt, "category": "portrait", "tags": ["a", "b"]},
|
|
{"prompt_id": partial, "category": "portrait"},
|
|
],
|
|
)
|
|
self.assertEqual(result["categories"], 2)
|
|
self.assertEqual(result["tag_sets"], 1)
|
|
self.assertEqual(
|
|
(self.category(prompt), self.tags(prompt)), ("portrait", ["a", "b"])
|
|
)
|
|
self.assertEqual((self.category(partial), self.tags(partial)), ("portrait", []))
|
|
self.assertEqual((self.category(neg), self.tags(neg)), (None, []))
|
|
self.assertEqual(self.snapshot_pairs(), pairs)
|
|
self.assertEqual(
|
|
self.sql("SELECT id, created_at, updated_at FROM prompts ORDER BY id"),
|
|
stamps,
|
|
)
|
|
self.assertEqual(
|
|
self.sql("SELECT * FROM prompt_library_state ORDER BY prompt_id"), state
|
|
)
|
|
self.assertEqual(self.sql("SELECT id, name FROM tags ORDER BY id"), tag_rows)
|
|
|
|
def test_apply_returns_one_kept_row_per_protected_prompt(self):
|
|
_a, conflict = self.pair("conflict", "n1", category="asian")
|
|
self.pair("conflict", "n2", category="Adult")
|
|
_b, edited = self.pair("edited", category="portrait")
|
|
self.edit_stamp(edited)
|
|
_c, both = self.pair(
|
|
"both", "n1", category="x", tags=[f"t{n}" for n in range(21)]
|
|
)
|
|
self.pair("both", "n2", category="y", tags=[f"t{n}" for n in range(21)])
|
|
_d, filled = self.pair("fill", category="portrait")
|
|
draft = self.preview()
|
|
result = self.apply(draft)
|
|
self.assertEqual(
|
|
result["kept"],
|
|
[
|
|
{
|
|
"prompt_id": conflict,
|
|
"reasons": ["pairs disagree on category: Adult, asian"],
|
|
},
|
|
{
|
|
"prompt_id": edited,
|
|
"reasons": [
|
|
"edited after it was saved; its empty category or tags "
|
|
"may be intentional"
|
|
],
|
|
},
|
|
{
|
|
"prompt_id": both,
|
|
"reasons": [
|
|
"pairs disagree on category: x, y",
|
|
"its pair has more than 20 tags",
|
|
],
|
|
},
|
|
],
|
|
)
|
|
self.assertEqual(len(result["kept"]), draft.protected["count"])
|
|
self.assertEqual(self.category(filled), "portrait")
|
|
|
|
def test_kept_rows_are_never_capped(self):
|
|
for n in range(205):
|
|
_p, prompt = self.pair(f"kept {n:03d}", category="portrait")
|
|
self.edit_stamp(prompt)
|
|
result = self.apply(self.preview())
|
|
self.assertEqual(result["changed"], 0)
|
|
self.assertEqual(len(result["kept"]), 205)
|
|
|
|
def test_second_run_finds_nothing_and_a_partial_fill_completes_later(self):
|
|
_a, prompt = self.pair("partial", "n1", category="portrait", tags=["a"])
|
|
p2, _same = self.pair("partial", "n2", category="portrait", tags=["c"])
|
|
self.apply(self.preview())
|
|
again = self.preview()
|
|
self.assertEqual(again.scope["count"], 0)
|
|
self.assertEqual(again.protected["count"], 1) # tags still disagree
|
|
self.db.update_prompt_pair_metadata(p2, tags=["a"])
|
|
self.apply(self.preview())
|
|
self.assertEqual(self.tags(prompt), ["a"])
|
|
self.assertEqual(self.preview().protected["count"], 0)
|
|
|
|
def test_drift_raises_plan_changed_and_writes_nothing(self):
|
|
def pair_category(ids):
|
|
self.db.update_prompt_pair_metadata(ids[0], category="landscape")
|
|
|
|
def prompt_category(ids):
|
|
self.run_sql("UPDATE prompts SET category='mine' WHERE id=?", ids[1])
|
|
|
|
def new_pair(ids):
|
|
self.db.save_prompt_pair("drift", "other neg", category="landscape")
|
|
|
|
def tag_rename(ids):
|
|
tag = self.sql("SELECT id FROM tags WHERE name='a'")[0][0]
|
|
self.run_sql("UPDATE tags SET name='renamed' WHERE id=?", tag)
|
|
|
|
def edited(ids):
|
|
self.edit_stamp(ids[1])
|
|
|
|
for change in (pair_category, prompt_category, new_pair, tag_rename, edited):
|
|
with self.subTest(change=change.__name__):
|
|
ids = self.pair("drift", "n", category="portrait", tags=["a"])
|
|
draft = self.preview()
|
|
change(ids)
|
|
before = self.sql("SELECT id, category FROM prompts ORDER BY id")
|
|
tags = self.sql("SELECT * FROM prompt_tags ORDER BY prompt_id")
|
|
with self.assertRaises(maintenance.PlanChanged):
|
|
self.apply(draft)
|
|
self.assertEqual(
|
|
self.sql("SELECT id, category FROM prompts ORDER BY id"), before
|
|
)
|
|
self.assertEqual(
|
|
self.sql("SELECT * FROM prompt_tags ORDER BY prompt_id"), tags
|
|
)
|
|
self.run_sql("DELETE FROM prompt_pairs")
|
|
self.run_sql("DELETE FROM prompts")
|
|
self.run_sql("DELETE FROM tags")
|
|
|
|
def test_pair_run_between_preview_and_apply_still_applies(self):
|
|
pair_id, prompt = self.pair("busy", category="portrait")
|
|
draft = self.preview()
|
|
self.db.record_prompt_pair_use(pair_id)
|
|
self.apply(draft)
|
|
self.assertEqual(self.category(prompt), "portrait")
|
|
|
|
def test_guard_failure_rolls_back_everything(self):
|
|
_a, first = self.pair("first", category="portrait")
|
|
_b, second = self.pair("second", category="portrait")
|
|
draft = self.preview()
|
|
real = repair._fill_categories
|
|
|
|
def racing(conn, fills):
|
|
# Another writer's value appears inside the transaction.
|
|
conn.execute("UPDATE prompts SET category='x' WHERE id=?", (second,))
|
|
return real(conn, fills)
|
|
|
|
with patch.object(repair, "_fill_categories", side_effect=racing):
|
|
with self.assertRaises(maintenance.PlanChanged):
|
|
self.apply(draft)
|
|
self.assertIsNone(self.category(first))
|
|
self.assertIsNone(self.category(second))
|
|
|
|
def test_more_than_two_hundred_fills_are_all_returned(self):
|
|
for n in range(205):
|
|
self.pair(f"bulk {n:03d}", category="portrait")
|
|
result = self.apply(self.preview())
|
|
self.assertEqual(result["changed"], 205)
|
|
self.assertEqual(len(result["rows"]), 205)
|
|
|
|
def test_batches_bind_few_ids(self):
|
|
for n in range(5):
|
|
self.pair(f"batch {n}", category="portrait", tags=["a"])
|
|
with patch.object(maintenance, "ID_BATCH", 2):
|
|
draft = self.preview()
|
|
result = self.apply(draft)
|
|
self.assertEqual(result["changed"], 5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|