Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
import tempfile
|
|
|
|
|
import threading
|
2026-09-01 23:45:48 +02:00
|
|
|
import sqlite3
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 14:51:23 +02:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from approval_engine.errors import Conflict, Unprocessable
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from approval_engine.store import Engine
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 14:51:23 +02:00
|
|
|
from tests.conftest import FROZEN, approve, binding, validity
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_second_supersession_loses(engine):
|
|
|
|
|
obj = approve(engine)
|
|
|
|
|
first = engine.supersede(obj.id)
|
|
|
|
|
assert engine.get(obj.id).status == "superseded"
|
|
|
|
|
assert first["successor_id"]
|
|
|
|
|
try:
|
|
|
|
|
engine.supersede(obj.id)
|
|
|
|
|
raise AssertionError("second supersession must conflict")
|
|
|
|
|
except Conflict:
|
|
|
|
|
pass
|
|
|
|
|
claim = engine.claim(obj.id)
|
|
|
|
|
assert claim["valid_now"] is False
|
|
|
|
|
assert claim["reason_code"] == "superseded"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_concurrent_supersessions_one_winner():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp) / "a.sqlite"
|
2026-09-01 23:45:48 +02:00
|
|
|
setup = Engine(path, clock=lambda: FROZEN)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
obj = approve(setup)
|
|
|
|
|
setup.close()
|
|
|
|
|
|
|
|
|
|
winners: list[str] = []
|
|
|
|
|
errors: list[str] = []
|
|
|
|
|
barrier = threading.Barrier(2)
|
|
|
|
|
|
|
|
|
|
def race():
|
2026-09-01 23:45:48 +02:00
|
|
|
eng = Engine(path, clock=lambda: FROZEN)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
barrier.wait()
|
|
|
|
|
try:
|
|
|
|
|
result = eng.supersede(obj.id)
|
|
|
|
|
winners.append(result["successor_id"])
|
|
|
|
|
except Conflict as exc:
|
|
|
|
|
errors.append(str(exc))
|
|
|
|
|
finally:
|
|
|
|
|
eng.close()
|
|
|
|
|
|
|
|
|
|
threads = [threading.Thread(target=race) for _ in range(2)]
|
|
|
|
|
for t in threads:
|
|
|
|
|
t.start()
|
|
|
|
|
for t in threads:
|
|
|
|
|
t.join()
|
|
|
|
|
assert len(winners) == 1
|
|
|
|
|
assert len(errors) == 1
|
2026-09-01 23:45:48 +02:00
|
|
|
check = Engine(path, clock=lambda: FROZEN)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
assert check.get(obj.id).status == "superseded"
|
|
|
|
|
check.close()
|
|
|
|
|
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
def test_consume_same_digest_is_idempotent(engine):
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
obj = approve(engine)
|
2026-09-01 23:45:48 +02:00
|
|
|
digest = "sha256:" + "ab" * 32
|
|
|
|
|
first = engine.consume(obj.id, digest, decision_id="decision:first")
|
|
|
|
|
second = engine.consume(obj.id, digest, decision_id="decision:retry")
|
|
|
|
|
assert first["idempotent"] is False
|
|
|
|
|
assert second["idempotent"] is True
|
|
|
|
|
assert second["decision_id"] == "decision:first"
|
|
|
|
|
assert [item["class"] for item in engine.undrained()].count("use") == 1
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
claim = engine.claim(obj.id)
|
|
|
|
|
assert claim["consumed"] is True
|
|
|
|
|
assert claim["valid_now"] is False
|
|
|
|
|
assert claim["reason_code"] == "consumed"
|
2026-09-01 23:45:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_consume_different_digest_conflicts(engine):
|
|
|
|
|
obj = approve(engine)
|
|
|
|
|
engine.consume(obj.id, "sha256:" + "ab" * 32)
|
|
|
|
|
try:
|
|
|
|
|
engine.consume(obj.id, "sha256:" + "cd" * 32)
|
|
|
|
|
raise AssertionError("different request digest must conflict")
|
|
|
|
|
except Conflict:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_concurrent_same_digest_consume_is_one_use_event():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp) / "consume.sqlite"
|
|
|
|
|
setup = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
obj = approve(setup)
|
|
|
|
|
setup.close()
|
|
|
|
|
digest = "sha256:" + "ef" * 32
|
|
|
|
|
results: list[bool] = []
|
|
|
|
|
barrier = threading.Barrier(2)
|
|
|
|
|
|
|
|
|
|
def race():
|
|
|
|
|
eng = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
barrier.wait()
|
|
|
|
|
try:
|
|
|
|
|
results.append(eng.consume(obj.id, digest)["idempotent"])
|
|
|
|
|
finally:
|
|
|
|
|
eng.close()
|
|
|
|
|
|
|
|
|
|
threads = [threading.Thread(target=race) for _ in range(2)]
|
|
|
|
|
for thread in threads:
|
|
|
|
|
thread.start()
|
|
|
|
|
for thread in threads:
|
|
|
|
|
thread.join()
|
|
|
|
|
assert sorted(results) == [False, True]
|
|
|
|
|
check = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
assert [item["class"] for item in check.undrained()].count("use") == 1
|
|
|
|
|
check.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_existing_database_migrates_consumption_columns():
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp) / "legacy.sqlite"
|
|
|
|
|
conn = sqlite3.connect(path)
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""CREATE TABLE approvals (
|
|
|
|
|
id TEXT PRIMARY KEY, status TEXT NOT NULL,
|
|
|
|
|
binding_json TEXT NOT NULL, binding_digest TEXT NOT NULL,
|
|
|
|
|
pdp_digest TEXT, actor TEXT NOT NULL, principal TEXT NOT NULL,
|
|
|
|
|
action TEXT NOT NULL, purpose TEXT NOT NULL,
|
|
|
|
|
target_json TEXT NOT NULL, not_before TEXT NOT NULL,
|
|
|
|
|
expires_at TEXT NOT NULL, required_count INTEGER NOT NULL,
|
|
|
|
|
superseded_by TEXT, created_at TEXT NOT NULL,
|
|
|
|
|
updated_at TEXT NOT NULL
|
|
|
|
|
)"""
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
eng = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
columns = {
|
|
|
|
|
row["name"]
|
|
|
|
|
for row in eng._conn().execute("PRAGMA table_info(approvals)").fetchall()
|
|
|
|
|
}
|
|
|
|
|
assert {"consumed_digest", "consumed_decision_id", "consumed_at"} <= columns
|
|
|
|
|
eng.close()
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 14:51:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pdp_path_requires_a_digest_at_issue(engine):
|
|
|
|
|
"""GH-DEC-2026-008: refuse at issue, not at the protected side effect."""
|
|
|
|
|
with pytest.raises(Unprocessable):
|
|
|
|
|
engine.create(binding(), validity(), pdp_path=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pdp_path_approval_states_itself_on_the_claim(engine):
|
|
|
|
|
obj = engine.create(
|
|
|
|
|
binding(), validity(), pdp_digest="sha256:" + "ab" * 32, pdp_path=True
|
|
|
|
|
)
|
|
|
|
|
engine.add_entry(obj.id, "user:alice")
|
|
|
|
|
claim = engine.claim(obj.id)
|
|
|
|
|
assert claim["binding"]["pdp_path"] is True
|
|
|
|
|
assert claim["binding"]["pdp_digest"] == "sha256:" + "ab" * 32
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_recorded_digest_alone_does_not_declare_the_path(engine):
|
|
|
|
|
"""Intent is declared, never inferred from an incidental digest."""
|
|
|
|
|
obj = engine.create(binding(), validity(), pdp_digest="sha256:" + "cd" * 32)
|
|
|
|
|
claim = engine.claim(obj.id)
|
|
|
|
|
assert claim["binding"]["pdp_digest"] is not None
|
|
|
|
|
assert claim["binding"]["pdp_path"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_successor_inherits_the_pdp_path_declaration(engine):
|
|
|
|
|
obj = engine.create(
|
|
|
|
|
binding(), validity(), pdp_digest="sha256:" + "ef" * 32, pdp_path=True
|
|
|
|
|
)
|
|
|
|
|
engine.add_entry(obj.id, "user:alice")
|
|
|
|
|
result = engine.supersede(obj.id, None)
|
|
|
|
|
successor = engine.get(result["successor_id"])
|
|
|
|
|
assert successor.pdp_path is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_v2_database_migrates_to_v3_preserving_approvals():
|
|
|
|
|
"""Schema v3 (GH-DEC-2026-008) must not disturb approvals issued under v2."""
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp) / "v2.sqlite"
|
|
|
|
|
eng = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
obj = eng.create(binding(), validity(), pdp_digest="sha256:" + "12" * 32)
|
|
|
|
|
eng.add_entry(obj.id, "user:alice")
|
|
|
|
|
# simulate a store written before v3 existed
|
|
|
|
|
eng._conn().execute("ALTER TABLE approvals DROP COLUMN pdp_path")
|
|
|
|
|
eng._conn().execute("PRAGMA user_version=2")
|
|
|
|
|
eng._conn().commit()
|
|
|
|
|
eng.close()
|
|
|
|
|
|
|
|
|
|
upgraded = Engine(path, clock=lambda: FROZEN)
|
|
|
|
|
version = int(upgraded._conn().execute("PRAGMA user_version").fetchone()[0])
|
|
|
|
|
assert version == 3
|
|
|
|
|
survivor = upgraded.get(obj.id)
|
|
|
|
|
assert survivor.status == "approved"
|
|
|
|
|
assert survivor.pdp_digest == "sha256:" + "12" * 32
|
|
|
|
|
# a legacy row never declared the path; intent is not back-filled from
|
|
|
|
|
# a digest that happens to be present
|
|
|
|
|
assert survivor.pdp_path is False
|
|
|
|
|
assert upgraded.claim(obj.id)["binding"]["pdp_path"] is False
|
|
|
|
|
upgraded.close()
|