Block isolation breaches and bind frozen replay to scheduled steps
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e76f-be98-7ae3-965d-e0b31290a4c4
This commit is contained in:
parent
a8bb787d12
commit
9cdade2116
10 changed files with 229 additions and 14 deletions
110
tests/test_isolation_and_replay.py
Normal file
110
tests/test_isolation_and_replay.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""Framework isolation and frozen schedule identity are acceptance boundaries."""
|
||||
from copy import deepcopy
|
||||
from dataclasses import replace
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from scenarios.alice_bob_carol import build
|
||||
from testdriver import Runner, Verdict
|
||||
from testdriver.actions import SemanticAction
|
||||
from testdriver.classification import classify
|
||||
from testdriver.crystallization import CrystallizedDriver, Trajectory, assess_stability
|
||||
from testdriver.drivers import CompositeDriver
|
||||
from testdriver.scenario import Step
|
||||
|
||||
|
||||
@pytest.mark.parametrize('leak_at', [None, 0, 1, 2])
|
||||
def test_isolation_breach_stops_run_and_cannot_be_accepted_or_frozen(leak_at):
|
||||
world, driver, observer, asset, oracle = build()
|
||||
calls = []
|
||||
|
||||
def leak():
|
||||
world.cast['bob'].remember('overheard', world.cast['alice'].canary)
|
||||
|
||||
class LeakingDriver:
|
||||
def realize(self, actor, action):
|
||||
result = driver.realize(actor, action)
|
||||
calls.append(action.name)
|
||||
if len(calls) - 1 == leak_at:
|
||||
leak()
|
||||
return result
|
||||
|
||||
if leak_at is None:
|
||||
leak()
|
||||
result = Runner(world, LeakingDriver(), observer, oracle).run(asset)
|
||||
assert result.verdict is Verdict.INCONCLUSIVE
|
||||
assert len(calls) == (0 if leak_at is None else leak_at + 1)
|
||||
assert any(j.verdict is Verdict.INCONCLUSIVE for j in result.judgments)
|
||||
pack = json.loads(result.evidence.to_json())
|
||||
assert any(o['data']['violations'] for o in pack['observations']
|
||||
if o['kind'] == 'actor_isolation')
|
||||
assert not classify(pack, pack).safe_to_accept
|
||||
assert not assess_stability([pack, deepcopy(pack), deepcopy(pack)]).stable
|
||||
|
||||
|
||||
@pytest.mark.parametrize('damage', ['violation', 'missing', 'stratum', 'duplicate', 'malformed'])
|
||||
def test_passing_product_verdicts_cannot_hide_invalid_isolation_evidence(damage):
|
||||
packs = []
|
||||
for _ in range(3):
|
||||
world, driver, observer, asset, oracle = build()
|
||||
pack = json.loads(Runner(world, driver, observer, oracle).run(asset).evidence.to_json())
|
||||
guard = [o for o in pack['observations'] if o['kind'] == 'actor_isolation'][-1]
|
||||
if damage == 'violation':
|
||||
guard['data']['violations'] = ['actor leaked']
|
||||
elif damage == 'missing':
|
||||
pack['observations'].remove(guard)
|
||||
elif damage == 'stratum':
|
||||
guard['stratum'] = 'S1'
|
||||
elif damage == 'duplicate':
|
||||
pack['observations'].append(deepcopy(guard))
|
||||
else:
|
||||
guard['data'].pop('violations')
|
||||
assert all(v['verdict'] == 'PASS' for v in pack['verdicts'])
|
||||
assert not classify(pack, pack).safe_to_accept
|
||||
packs.append(pack)
|
||||
assert not assess_stability(packs).stable
|
||||
|
||||
|
||||
def trajectories():
|
||||
return [Trajectory('a', 'grant_access', 'browser', '/resources/A/grant', ('subject_id',)),
|
||||
Trajectory('b', 'grant_access', 'browser', '/resources/B/grant', ('subject_id',))]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('composite', [False, True])
|
||||
def test_runner_replays_repeated_actions_by_step_and_can_reuse_driver(composite):
|
||||
world, _, observer, asset, oracle = build()
|
||||
calls = []
|
||||
|
||||
class Session:
|
||||
def post_form(self, path, fields):
|
||||
calls.append((path, fields))
|
||||
return 200, ''
|
||||
|
||||
driver = CrystallizedDriver(trajectories(), lambda actor: Session())
|
||||
if composite:
|
||||
driver = CompositeDriver({'browser': driver}, 'browser')
|
||||
asset.scenario = replace(asset.scenario, steps=tuple(
|
||||
Step(step_id, 'alice', SemanticAction('grant_access', {'subject_id': subject},
|
||||
frozenset({'browser'})))
|
||||
for step_id, subject in [('a', 'bob'), ('b', 'carol')]
|
||||
))
|
||||
for _ in range(2):
|
||||
Runner(world, driver, observer, oracle).run(asset)
|
||||
assert calls == [('/resources/A/grant', {'subject_id': 'bob'}),
|
||||
('/resources/B/grant', {'subject_id': 'carol'})] * 2
|
||||
|
||||
|
||||
def test_ambiguous_unknown_and_mismatched_frozen_actions_do_not_send_requests():
|
||||
world, _, _, _, _ = build()
|
||||
|
||||
def unexpected_session(actor):
|
||||
pytest.fail('invalid frozen dispatch must not open a session')
|
||||
|
||||
driver = CrystallizedDriver(trajectories(), unexpected_session)
|
||||
action = SemanticAction('grant_access', {}, frozenset({'browser'}))
|
||||
assert driver.realize(world.cast['alice'], action).raised
|
||||
assert driver.realize_step(world.cast['alice'], 'unknown', action).raised
|
||||
assert driver.realize_step(world.cast['alice'], 'a', replace(action, name='revoke_access')).raised
|
||||
with pytest.raises(ValueError, match='duplicate'):
|
||||
CrystallizedDriver([trajectories()[0]] * 2, unexpected_session)
|
||||
Loading…
Add table
Add a link
Reference in a new issue