Drafts rebase onto the live settings; the review speaks in words
A draft started before someone else's Apply used to carry that older config and would have put the newer changes back. Now every read of the draft replays only the draft's own changes onto the live settings (base config from history), so the review lists exactly what you changed. Values read in words (windows as days and hours), long ones fold open with was/now, commands are no longer red, and warnings the live config already had are not repeated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+28
-2
@@ -88,9 +88,13 @@ class TestDraftAndPlan(unittest.TestCase):
|
||||
hd2["checks"][0]["optional"] = False
|
||||
after["site"]["editing_code"] = "new words"
|
||||
after["hosts"]["lighting"]["notes"] = "keep it Public"
|
||||
texts = [ap.words_for(p, a, b, self.cfg, after)["text"] for p, a, b in ap.changed_paths(self.cfg, after)]
|
||||
words = [ap.words_for(p, a, b, self.cfg, after) for p, a, b in ap.changed_paths(self.cfg, after)]
|
||||
texts = [w["text"] for w in words]
|
||||
self.assertIn("key HD2 › check 1 › optional: yes → no", texts)
|
||||
self.assertIn("computer Lighting PC › notes: Keep the network profile on Public: rtpMIDI's Bonjour rules only allow Public. → keep it Public", texts)
|
||||
self.assertIn("computer Lighting PC › notes changed", texts) # long values fold open
|
||||
note = next(w for w in words if w["text"] == "computer Lighting PC › notes changed")
|
||||
self.assertEqual((note["before"][:8], note["after"]), ("Keep the", "keep it Public"))
|
||||
self.assertEqual(ap.plain_value([{"days": ["sun"], "start": "06:30", "end": "13:30"}, {"days": ["wed"], "start": "16:00", "end": "21:00"}]), "Sun 6:30 am – 1:30 pm; Wed 4 pm – 9 pm")
|
||||
self.assertTrue(any(t == "site › editing code changed" for t in texts), texts) # a secret: never its value
|
||||
more = json.loads(json.dumps(self.cfg))
|
||||
midi = next(t for t in more["tiles"] if t["id"] == "midi")
|
||||
@@ -100,6 +104,28 @@ class TestDraftAndPlan(unittest.TestCase):
|
||||
self.assertTrue(texts[0].startswith("key MIDI › check %d added: " % (len(midi["checks"]))), texts)
|
||||
|
||||
|
||||
class TestRebase(unittest.TestCase):
|
||||
def test_set_path_and_rebase(self):
|
||||
base = json.loads(json.dumps(CONFIG))
|
||||
live = json.loads(json.dumps(base)); live["debounce_seconds"] = 7 # moved on since the draft started
|
||||
live["fixes"]["vista"]["label"] = "start Vista (new words)"
|
||||
draft = {"config": json.loads(json.dumps(base))}
|
||||
draft["config"]["auto_fix"]["windows"].append({"days": ["wed"], "start": "16:00", "end": "21:00"}) # the draft's own change
|
||||
next(t for t in draft["config"]["tiles"] if t["id"] == "hd2")["checks"][0]["optional"] = False
|
||||
draft["config"]["hosts"]["lighting"]["notes"] = "mine"
|
||||
merged, changes = ap.rebase(draft, live, base)
|
||||
self.assertEqual(merged["debounce_seconds"], 7) # the live change survives
|
||||
self.assertEqual(merged["fixes"]["vista"]["label"], "start Vista (new words)")
|
||||
self.assertEqual(len(merged["auto_fix"]["windows"]), 2) # and the draft's own
|
||||
self.assertFalse(next(t for t in merged["tiles"] if t["id"] == "hd2")["checks"][0]["optional"])
|
||||
self.assertEqual(merged["hosts"]["lighting"]["notes"], "mine")
|
||||
self.assertEqual(len(changes), 3)
|
||||
cfg = json.loads(json.dumps(base))
|
||||
ap.set_path(cfg, "tiles[hd2]", None); self.assertFalse(any(t["id"] == "hd2" for t in cfg["tiles"]))
|
||||
ap.set_path(cfg, "tiles[hd9]", {"id": "hd9", "label": "HD9"}); self.assertTrue(any(t["id"] == "hd9" for t in cfg["tiles"]))
|
||||
ap.set_path(cfg, "hosts.newpc.ssh", "a@b"); self.assertEqual(cfg["hosts"]["newpc"]["ssh"], "a@b")
|
||||
|
||||
|
||||
class TestPipeline(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.data = tempfile.mkdtemp(prefix="sp-pipe-")
|
||||
|
||||
@@ -268,6 +268,29 @@ class TestServer(unittest.TestCase):
|
||||
self.assertNotIn("HyperDeck_1", gone[0]["candidates"]) # used by HD1
|
||||
self.put("/api/draft", {}, method="DELETE")
|
||||
|
||||
def test_a_draft_is_rebased_when_the_live_settings_move_on(self):
|
||||
from test_apply import FakeLaunchctl
|
||||
self.site.attempts.clear()
|
||||
fake = FakeLaunchctl(self.data); self.site.applier.run, self.site.applier.sleep = fake, lambda s: None
|
||||
live = self.site.load()
|
||||
mine = json.loads(json.dumps(live)); mine["hosts"]["lighting"]["notes"] = "sam's note"
|
||||
self.put("/api/draft", {"config": mine}, who="sam") # sam's draft
|
||||
d = self.site.draft.read()
|
||||
self.site.draft.clear()
|
||||
other = json.loads(json.dumps(live)); other["debounce_seconds"] = (live["debounce_seconds"] or 3) + 1 # chris applies something else
|
||||
self.put("/api/draft", {"config": other}, who="chris")
|
||||
self.put("/api/apply", {"note": "chris", "emergency": {"word": "LIVE", "reason": "test"}}, method="POST", who="chris")
|
||||
self.site.applier.thread.join(30)
|
||||
sp.write_json(self.site.draft.path, d) # sam's draft is back, based on the old settings
|
||||
st, raw = get(self.port, "/api/draft")
|
||||
r = json.loads(raw)
|
||||
self.assertFalse(r["stale"])
|
||||
self.assertEqual(r["draft"]["config"]["hosts"]["lighting"]["notes"], "sam's note") # sam's own change
|
||||
self.assertEqual(r["draft"]["config"]["debounce_seconds"], other["debounce_seconds"]) # chris's change kept
|
||||
st, v = self.put("/api/validate", {})
|
||||
self.assertEqual(len(v["changes"]), 1); self.assertTrue(v["changes"][0]["text"].startswith("computer Lighting PC › notes"))
|
||||
self.put("/api/draft", {}, method="DELETE")
|
||||
|
||||
def test_probe_is_refused_while_a_fix_runs(self):
|
||||
json.dump({"fix-x": {"key": "hd2"}}, open(os.path.join(self.data, "fixes-running.json"), "w"))
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user