|
|
@@ -226,6 +226,7 @@ class FakeSaddle:
|
|
|
self.zombie_409 = 0 # /start 409s once per unit (then decrements)
|
|
|
self.conflict_running = 0 # while > 0, /start persistently 409s with this id
|
|
|
self.hold_state = False # freeze /status lifecycle transitions
|
|
|
+ self.ssh_public_key = "" # reported on /status (plan-9 contract)
|
|
|
self.started_mono = time.monotonic()
|
|
|
self.freeze_activity = False # hold last_activity_epoch still
|
|
|
|
|
|
@@ -382,7 +383,8 @@ class FakeSaddle:
|
|
|
if self.path == "/status":
|
|
|
response = {"state": self.daemon.state, "summary": self.daemon.summary,
|
|
|
"uptime_seconds": int(time.monotonic() - self.daemon.started_mono),
|
|
|
- "last_activity_epoch": self.daemon.last_activity_epoch}
|
|
|
+ "last_activity_epoch": self.daemon.last_activity_epoch,
|
|
|
+ "ssh_public_key": self.daemon.ssh_public_key}
|
|
|
if self.daemon.resumed_pending:
|
|
|
response["resumed"] = True
|
|
|
self.daemon.resumed_pending = False
|
|
|
@@ -2258,6 +2260,66 @@ def main():
|
|
|
check(wait_task_done(auth, lossless, timeout=60) is not None,
|
|
|
"lossless task completes")
|
|
|
|
|
|
+ print("== R9 B1/B2: git identity + ssh key ==")
|
|
|
+ # The key travels on /status into the saddle row and the page view.
|
|
|
+ fake.ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1teStKey saddle-1@fedora"
|
|
|
+ fake.state = "idle"
|
|
|
+ fake_card = saddle_card("fake-saddle")
|
|
|
+ check(fake_card.get("ssh_public_key", "") == "",
|
|
|
+ "no key shown before the daemon reports one")
|
|
|
+ act(auth, fake_card["refresh"], {})
|
|
|
+ check(wait_for(lambda: "AAAAC3NzaC1lZDI1teStKey" in
|
|
|
+ saddle_card("fake-saddle").get("ssh_public_key", "") or False,
|
|
|
+ timeout=30, what="key lands on the row") is not None,
|
|
|
+ "reported ssh_public_key persists onto the saddle row")
|
|
|
+ conn = db()
|
|
|
+ row_key = conn.execute("select ssh_public_key from saddles where name = 'fake-saddle'"
|
|
|
+ ).fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ check(row_key == fake.ssh_public_key,
|
|
|
+ "the key is stored verbatim in the database")
|
|
|
+
|
|
|
+ # Identity edit flows through the UI into the /start payload.
|
|
|
+ fake_card = saddle_card("fake-saddle")
|
|
|
+ act(auth, fake_card["update_identity"],
|
|
|
+ {"git_name": "clanker-bot", "git_email": "bot@unitatem.net"})
|
|
|
+ fake_card = saddle_card("fake-saddle")
|
|
|
+ check(fake_card.get("git_name") == "clanker-bot"
|
|
|
+ and fake_card.get("git_email") == "bot@unitatem.net",
|
|
|
+ "inline edit saves the git identity")
|
|
|
+
|
|
|
+ pushes_before = len(fake.start_payloads)
|
|
|
+ act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
|
+ "project_id": "1",
|
|
|
+ "title": "Identity task",
|
|
|
+ "priority": "2",
|
|
|
+ })
|
|
|
+ identity_task = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"]
|
|
|
+ if t["title"] == "Identity task"][0])
|
|
|
+ check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == identity_task
|
|
|
+ for p in fake.start_payloads) or False,
|
|
|
+ timeout=60, what="identity task pushed") is not None,
|
|
|
+ "identity task dispatches")
|
|
|
+ payload = [p for p in fake.start_payloads
|
|
|
+ if int(p.get("task", {}).get("id") or 0) == identity_task][-1]
|
|
|
+ identity = payload.get("git_identity") or {}
|
|
|
+ check(identity.get("name") == "clanker-bot"
|
|
|
+ and identity.get("email") == "bot@unitatem.net",
|
|
|
+ "/start carries the saddle's git_identity")
|
|
|
+
|
|
|
+ # Earlier pushes (identity unset) carry no git_identity member.
|
|
|
+ check(all("git_identity" not in p for p in fake.start_payloads[:-1]),
|
|
|
+ "unset identity omits the member (daemon fallback applies)")
|
|
|
+
|
|
|
+ # Clearing the identity omits it from later pushes.
|
|
|
+ fake_card = saddle_card("fake-saddle")
|
|
|
+ act(auth, fake_card["update_identity"], {"git_name": "", "git_email": ""})
|
|
|
+ check(saddle_card("fake-saddle").get("git_name", "") == "",
|
|
|
+ "clearing the identity empties the row")
|
|
|
+ check(wait_task_done(auth, identity_task, timeout=60) is not None,
|
|
|
+ "identity task completes")
|
|
|
+
|
|
|
+
|
|
|
# The daemon vanishing mid-task: three refused polls → offline,
|
|
|
# task requeued with a bot comment, UI reflects.
|
|
|
act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
|
@@ -2302,6 +2364,7 @@ def main():
|
|
|
check(row.get("unreachable") is True,
|
|
|
"queue row shows the unreachable state in the window")
|
|
|
|
|
|
+
|
|
|
finally:
|
|
|
# Exit-code diagnostic: distinguishes a crashed app (negative
|
|
|
# returncode, e.g. -11 SIGSEGV) from a clean shutdown. When
|