ep0-copy-gc: read ep0's real {"data": [...]} namespace answer; any other shape aborts (found on the first live dry run, guard held)
gates / gates (push) Successful in 4m31s
gates / gates (push) Successful in 4m31s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
## 2026-10-09 — ep0-copy-gc reads ep0's real namespace answer (found on the first live dry run)
|
||||
|
||||
- `scripts/ep0-copy-gc/felhom-ep0-copy-gc`: `proxmox-backup-client namespace list --output-format json` answers
|
||||
`{"data": [{"ns": ""}, {"ns": "demo-hp"}, …]}`; the job iterated the wrapper object and read `data` as ep0's only
|
||||
namespace. On DooPlex's first dry run (operator present) all four customer namespaces read „absent" and the
|
||||
mass-absence guard stopped it — nothing recorded, nothing deleted. With two or fewer customers it would have started
|
||||
their 7-day clock. Now any shape but `{"data": [{"ns": str}, …]}` aborts as „could not tell". The test fake printed a
|
||||
bare list (a model of the tool, not the tool) and now prints the measured shape; `test_ep0_unknown_shape_aborts`
|
||||
(red-proved: 4 failures with the real shape before the fix).
|
||||
|
||||
## facebook — fb_probe: a deleted post answers (#10) „Object does not exist" (2026-10-08, fixed without a row)
|
||||
|
||||
- The first write run proved a text post gone only on code 100; Meta v26.0 answers a deleted scheduled post with
|
||||
|
||||
@@ -76,10 +76,17 @@ def ep0_namespaces():
|
||||
r = pbc(["namespace", "list", "--repository", EP0_REPO, "--output-format", "json"], EP0_TOKEN_FILE, EP0_FINGERPRINT_FILE)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError("ep0 namespace list failed (rc %d): %s" % (r.returncode, r.stderr.strip()[-200:]))
|
||||
# The real answer is {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} (measured on DooPlex 2026-10-09). Any other
|
||||
# shape is „could not tell": the first version iterated the wrapper object, read „data" as ep0's only namespace,
|
||||
# and was stopped on its first dry run only by the mass-absence guard. Pinned by test_ep0_unknown_shape_aborts and
|
||||
# by the fake, which now prints the real shape.
|
||||
doc = json.loads(r.stdout or "null")
|
||||
items = doc.get("data") if isinstance(doc, dict) else None
|
||||
if not isinstance(items, list) or not all(isinstance(i, dict) and isinstance(i.get("ns"), str) for i in items):
|
||||
raise RuntimeError("ep0 namespace list has an unexpected shape: %s" % (r.stdout or "").strip()[:120])
|
||||
out = set()
|
||||
for item in json.loads(r.stdout or "[]"):
|
||||
name = item.get("ns", "") if isinstance(item, dict) else str(item)
|
||||
top = name.split("/")[0]
|
||||
for item in items:
|
||||
top = item["ns"].split("/")[0]
|
||||
if top:
|
||||
out.add(top)
|
||||
return out
|
||||
|
||||
@@ -24,7 +24,12 @@ with open(os.environ["FAKE_LOG"], "a") as f:
|
||||
if a[:2] == ["namespace", "list"]:
|
||||
if os.environ.get("FAKE_EP0_FAIL"):
|
||||
sys.stderr.write("connection refused\n"); sys.exit(255)
|
||||
print(json.dumps([{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])]))
|
||||
if os.environ.get("FAKE_EP0_RAW"):
|
||||
print(os.environ["FAKE_EP0_RAW"]); sys.exit(0)
|
||||
# the REAL shape, measured on DooPlex 2026-10-09: {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} -- a root entry
|
||||
# and a wrapper object. The first fake printed a bare list, so the tests passed while the job read „data" as
|
||||
# ep0's only namespace on the real tool (stopped live only by the mass-absence guard).
|
||||
print(json.dumps({"data": [{"ns": ""}] + [{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])]}))
|
||||
sys.exit(0)
|
||||
if a[:2] == ["namespace", "delete"]:
|
||||
sys.exit(0)
|
||||
@@ -112,6 +117,14 @@ class EndToEnd(unittest.TestCase):
|
||||
self.assertEqual(r.returncode, 2)
|
||||
self.assertEqual(self.deletes(), [])
|
||||
|
||||
def test_ep0_unknown_shape_aborts(self):
|
||||
# an answer the job cannot read is „could not tell", never „these customers are gone"
|
||||
self.seed("gone-cust", "2026-01-01")
|
||||
for raw in ('{"result": [{"ns": "demo-hp"}]}', '["demo-hp"]', '{"data": "x"}'):
|
||||
r = self.run_gc("--apply", FAKE_EP0_RAW=raw)
|
||||
self.assertEqual(r.returncode, 2, raw)
|
||||
self.assertEqual(self.deletes(), [], raw)
|
||||
|
||||
def test_ep0_unreachable_aborts(self):
|
||||
self.seed("gone-cust", "2026-01-01")
|
||||
r = self.run_gc("--apply", FAKE_EP0_FAIL="1")
|
||||
|
||||
Reference in New Issue
Block a user