import json from urllib.parse import urlencode import uuid from informed_decision.audit import OutboxWorker from informed_decision.lifecycle_http import LifecycleAPI from informed_decision.lifecycle_service import Caller from informed_decision.runtime import Runtime from informed_decision.web import App, SESSION_COOKIE from informed_decision.oidc import ORIGIN from informed_decision.policy import PolicyClient from informed_decision.records import dumps from test_browser_auth import login, signing_key, finish, call from test_deployment_candidate import inputs, module from test_durable_component import receiver from test_lifecycle_http import call as api_call from test_policy_client import PolicyTransport from test_review_runtime import config from test_skeleton import make_memo def test_agent_to_human_collaboration_and_audit_without_configuration_changes(tmp_path, login, receiver): path, configuration = config(tmp_path) configuration['lifecycle_policy'] = {**configuration['policy'], 'package': 'informed-decision.lifecycle'} configuration['lifecycle_delivery'] = True path.write_text(json.dumps(configuration)) original_configuration = path.read_bytes() runtime = Runtime.from_file(path) transport = PolicyTransport() transport.change = lambda data: data['provenance'].update(policy_package='informed-decision.lifecycle') lifecycle = runtime.lifecycle lifecycle.policy = PolicyClient('https://policy.test', lambda: 'synthetic-caller', package='informed-decision.lifecycle', version='v1', package_digest=configuration['policy']['package_digest'], transport=transport) # Match the immutable configured digest in the simulated PDP return. transport.change = lambda data: data['provenance'].update( policy_package='informed-decision.lifecycle', policy_package_digest=configuration['policy']['package_digest']) sid = finish(login) session = login[0].session(sid) from informed_decision.lifecycle import Attribution from informed_decision.disposition import Actor, ActorKind agent = Caller(Attribution(session.tenant.value, Actor('synthetic-agent', ActorKind.AGENT)), session.expires_at, '{"principal_type_source":"authentication-derived"}') def authenticate(token): if token != 'fixture': raise ValueError() return agent api = LifecycleAPI(lifecycle, authenticate) app = App(login[0], runtime.controller, readiness=runtime.pump.ready, lifecycle=lifecycle) cookie = f'{SESSION_COOKIE}={sid}' memo = make_memo(id='agent-posed', approval_id=None, packet=(), highlights=()) status, receipt, _ = api_call(api, '/v1/decisions', method='POST', body={'memo': json.loads(dumps(memo))}) assert status == 200 and receipt['workspace_url'] == ORIGIN + '/decisions/agent-posed?version=1' assert call(app, '/decisions/agent-posed', cookie=cookie)['status'] == 200 def post(action, **values): values.update(csrf=session.csrf, key=str(uuid.uuid4())) return call(app, '/decisions/agent-posed/' + action, 'POST', cookie=cookie, origin=ORIGIN, body=urlencode(values).encode()) assert post('publish', version='1', subjects=session.subject, groups='')['status'] == 303 assert post('comment', version='1', text='Please add the alternative.')['status'] == 303 assert post('revise', version='1', question=memo.question, brief='Alternative added.', summary='Responded to feedback')['status'] == 303 assert api_call(api, '/v1/decisions/agent-posed')[1]['version'] == 2 from dataclasses import replace second = replace(memo, id='second-agent-decision') assert api_call(api, '/v1/decisions', method='POST', body={'memo': json.loads(dumps(second))}, HTTP_IDEMPOTENCY_KEY='second')[0] == 200 assert not runtime.pump.ready() runtime.pump.worker = OutboxWorker(runtime.controller.store, receiver[0]) runtime.pump.tick() assert runtime.pump.ready() assert runtime.controller.store.lifecycle_counts() == {'records': 2, 'events': 5, 'pending': 0} assert path.read_bytes() == original_configuration assert runtime.controller.store.memos_for(memo.binding.principal.id) == [] assert api_call(api, '/v1/decisions/agent-posed/accept', method='POST', body={})[0] == 404