owenrusk.dev

errands

small jobs the runtime does for the studio.

git clone https://owenrusk.dev/errands.git

commit 2cbf7d78b8c80f8dfb26b428750c88975032b1f7
parent d3a252e97cda58a35c4b188a8dfc608eebe0326b
author Owen Rusk <owen@papermothgames.com>
date   2026-03-17 15:05:57 -0500
vault: unlock again, once, when bw has dropped the session
errands/vault.py+11-1
tests/test_vault.py+12-0
diff --git a/errands/vault.py b/errands/vault.py
index f42617a..aa5f78d 100644
--- a/errands/vault.py
+++ b/errands/vault.py
@@ -35,12 +35,22 @@ class Vault:
 
     def item(self, name: str) -> dict:
         self.unlock()
-        found = json.loads(self.bw("list", "items", "--search", name))
+        try:
+            found = json.loads(self.bw("list", "items", "--search", name))
+        except subprocess.CalledProcessError:
+            # bw forgets its sessions when it updates.
+            self.forget()
+            self.unlock()
+            found = json.loads(self.bw("list", "items", "--search", name))
         exact = [item for item in found if item["name"] == name]
         if len(exact) != 1:
             raise LookupError(f"{len(exact)} items called {name}")
         return exact[0]
 
+    def forget(self) -> None:
+        self.session = None
+        (settings.STATE / "bw-session").unlink(missing_ok=True)
+
     def password(self, name: str) -> str:
         return self.item(name)["login"]["password"]
 
diff --git a/tests/test_vault.py b/tests/test_vault.py
index e855872..8f1563a 100644
--- a/tests/test_vault.py
+++ b/tests/test_vault.py
@@ -16,9 +16,13 @@ ITEMS = [
 class FakeBw:
     def __init__(self):
         self.calls = []
+        self.stale = set()
 
     def __call__(self, args, **kwargs):
         self.calls.append(args[1:])
+        session = (kwargs.get("env") or {}).get("BW_SESSION")
+        if session in self.stale:
+            raise subprocess.CalledProcessError(1, args, "", "Vault is locked.")
         if args[1] == "unlock":
             out = "session-key"
         elif args[1:3] == ["list", "items"]:
@@ -64,6 +68,14 @@ class VaultTest(unittest.TestCase):
         self.assertEqual(self.vault.note("wifi"), "office network: see the sticker on the router")
         self.assertEqual(self.vault.note("stillwater api"), "")
 
+    def test_stale_session(self):
+        self.vault.password("stillwater api")
+        self.bw.stale.add("session-key")
+        with self.assertRaises(subprocess.CalledProcessError):
+            # the fake hands out the same key, so the second try fails too and gives up.
+            self.vault.password("stillwater api")
+        self.assertEqual(sum(1 for call in self.bw.calls if call[0] == "unlock"), 2)
+
     def test_missing(self):
         with self.assertRaises(LookupError):
             self.vault.password("nothing like it")