Add audited operator approval for one entity repair attempt
This commit is contained in:
1 parent
4cbba4c1e4
commit
61889740e6
14 files changed
+296
-17
No files matched your search
@@ -33,6 +33,19 @@ try {
|
||||
await until(()=>evaluate("document.readyState === 'complete' && typeof render === 'function'"));
|
||||
await evaluate("document.getElementById('username').value='preview';document.getElementById('password').value='ui-fixture-password';document.getElementById('login-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));");
|
||||
await until(()=>evaluate("document.getElementById('token-count').textContent==='35.300'"));
|
||||
await until(()=>evaluate('operationState?.operator === true'));
|
||||
const repairControl=await evaluate(`(()=>{
|
||||
const root=document.createElement('div');
|
||||
addRetryControl({state:'BLOCKED',reason:'ENTITY_RESOLUTION_INVALID_RESPONSES',revision:2,current_revision:2,key:'fixture',package_generation:43},root);
|
||||
const input=root.querySelector('textarea'),button=root.querySelector('button');
|
||||
if(!input||!button)return false;
|
||||
const original=changeOperation;let sent;
|
||||
changeOperation=(data)=>{sent=data;};
|
||||
try{input.value='Installed technical correction';button.click();
|
||||
return button.textContent==='Einen Reparaturversuch freigeben'&&sent?.action==='repair'&&sent.package_generation===43&&sent.reason==='Installed technical correction';
|
||||
}finally{changeOperation=original;}
|
||||
})()`);
|
||||
if(!repairControl)throw new Error('Scoped operator repair control failed');
|
||||
if(await evaluate("document.cookie.includes('vba_session')"))throw new Error('Session cookie is readable by JavaScript');
|
||||
await send('Page.reload');
|
||||
await until(()=>evaluate("session===0&&token==='session'&&document.getElementById('token-count').textContent==='35.300'"));
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
from test_case_builder import case_setup
|
||||
from test_case_interpreter import built_case
|
||||
from test_entity_resolution import Model,service_for,run
|
||||
from viano_bug_analysis.api import APIConfig,Project,Client
|
||||
from viano_bug_analysis.api.app import create_app
|
||||
from viano_bug_analysis.api.cases import case_key
|
||||
from viano_bug_analysis.lifecycle import CheckpointStore
|
||||
from viano_bug_analysis.lifecycle.checkpoints import checkpoint_chain
|
||||
from viano_bug_analysis.package import PackageReader
|
||||
from viano_bug_analysis.worker import AnalysisWorker,WorkerConfig,WorkerError
|
||||
from viano_bug_analysis.worker.repair import approve_entity_repair
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def project_manifest(manifest):
|
||||
manifest['project']={'id':'p','type':'wow','profile':'vcore'}
|
||||
|
||||
|
||||
def exhaust(store):
|
||||
model=Model();model.invalid_plan=3;service=service_for(store)
|
||||
for _ in range(3):assert run(store,model,service).status=='REJECTED'
|
||||
assert run(store,model,service).status=='BLOCKED'
|
||||
return model,service
|
||||
|
||||
|
||||
def test_approval_keeps_revision_artifacts_and_only_allows_one_attempt(built_case,tmp_path):
|
||||
store=built_case[0];model,service=exhaust(store)
|
||||
with PackageReader(store.latest('case-A','stable-package')) as reader:
|
||||
manifest=reader.manifest;before={n:reader.read_bytes(n) for n in reader.files if n!='manifest.json'}
|
||||
path=approve_entity_repair(store,'case-A','stable-package',actor='alice',reason='Updated plan validation guidance',expected_generation=manifest['package_generation'])
|
||||
with PackageReader(path) as reader:
|
||||
assert reader.manifest['analysis_revision']==manifest['analysis_revision']
|
||||
assert {k:v for k,v in reader.manifest['artifacts'].items() if k!='lifecycle_checkpoint'}=={k:v for k,v in manifest['artifacts'].items() if k!='lifecycle_checkpoint'}
|
||||
assert reader.manifest['state']=='INTERPRETING_CASE'
|
||||
for name,raw in before.items():assert reader.read_bytes(name)==raw
|
||||
details=checkpoint_chain(reader)[0][1]['details']
|
||||
assert details['repair']['attempt']==4 and details['actor']=='alice'
|
||||
with pytest.raises(WorkerError):
|
||||
approve_entity_repair(store,'case-A','stable-package',actor='bob',reason='duplicate',expected_generation=manifest['package_generation'])
|
||||
# Another invalid response exhausts this approval; no new attempt is automatic.
|
||||
model.invalid_plan=1
|
||||
assert run(store,model,service).status=='REJECTED'
|
||||
assert run(store,model,service).status=='BLOCKED'
|
||||
assert run(store,model,service).status=='BLOCKED'
|
||||
assert len(model.requests)==4
|
||||
with PackageReader(store.latest('case-A','stable-package')) as reader:generation=reader.manifest['package_generation']
|
||||
approve_entity_repair(store,'case-A','stable-package',actor='alice',reason='Second explicit correction',expected_generation=generation)
|
||||
result=run(store,model,service)
|
||||
assert result.status=='COMPLETED'
|
||||
recipient=CheckpointStore(type(store.workspace)(tmp_path/'portable'))
|
||||
recipient.intake(result.package)
|
||||
calls=len(model.requests)
|
||||
assert run(recipient,model).reused and len(model.requests)==calls
|
||||
|
||||
|
||||
def test_web_approval_is_scoped_csrf_protected_and_audited(built_case,monkeypatch):
|
||||
store=built_case[0];model,service=exhaust(store)
|
||||
monkeypatch.setenv('MACHINE_TOKEN','m_'+'x'*40)
|
||||
config=APIConfig(store.workspace.root,(Project('p','wow','vcore'),),
|
||||
(Client('capture','MACHINE_TOKEN',('p',)),))
|
||||
app=create_app(config);web=TestClient(app)
|
||||
app.state.sessions.users.manage('create_user','alice','example-password')
|
||||
login=web.post('/v1/auth/login',json={'username':'alice','password':'example-password'})
|
||||
headers={'X-CSRF-Token':login.json()['csrf']}
|
||||
with PackageReader(store.latest('case-A','stable-package')) as reader:generation=reader.manifest['package_generation']
|
||||
request={'action':'repair','key':case_key('case-A','stable-package'),'package_generation':generation,'reason':'Installed technical correction'}
|
||||
assert web.post('/v1/operations',json=request).status_code==403
|
||||
machine=TestClient(app)
|
||||
assert machine.post('/v1/operations',headers={'Authorization':'Bearer m_'+'x'*40},json=request).status_code==403
|
||||
assert web.post('/v1/operations',headers=headers,json={**request,'reason':' '}).status_code==422
|
||||
assert web.post('/v1/operations',headers=headers,json={**request,'package_generation':generation-1}).status_code==409
|
||||
reply=web.post('/v1/operations',headers=headers,json=request)
|
||||
assert reply.status_code==200 and reply.json()['status']=='QUEUED'
|
||||
assert web.post('/v1/operations',headers=headers,json=request).json()['command_id']==reply.json()['command_id']
|
||||
worker=AnalysisWorker(WorkerConfig(config,min_free_bytes=1))
|
||||
worker._commands()
|
||||
assert not worker.control.read()['commands']
|
||||
assert worker.control.read()['events'][-1]['action']=='repair'
|
||||
assert worker.control.read()['events'][-1]['status']=='APPLIED'
|
||||
assert run(store,model,service).status=='COMPLETED'
|
||||
|
||||
|
||||
def test_assessment_approval_keeps_plan_and_database_captures(built_case):
|
||||
store=built_case[0];model=Model();model.invalid_assessment=3;service=service_for(store)
|
||||
for _ in range(3):assert run(store,model,service).status=='REJECTED'
|
||||
assert run(store,model,service).status=='BLOCKED'
|
||||
with PackageReader(store.latest('case-A','stable-package')) as reader:
|
||||
generation=reader.manifest['package_generation']
|
||||
plan=reader.read_bytes('analysis/interpretations/0001/entity-resolution/plan.json')
|
||||
search=reader.read_bytes('analysis/interpretations/0001/entity-resolution/search.json')
|
||||
approve_entity_repair(store,'case-A','stable-package',actor='alice',reason='Precise assessment repair installed',expected_generation=generation)
|
||||
result=run(store,model,service)
|
||||
assert result.status=='COMPLETED' and len(service.database.calls)==1
|
||||
with PackageReader(result.package) as reader:
|
||||
assert reader.read_bytes('analysis/interpretations/0001/entity-resolution/plan.json')==plan
|
||||
assert reader.read_bytes('analysis/interpretations/0001/entity-resolution/search.json')==search
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reason',['AWAITING_TEST_FEEDBACK','RESEARCH_UNAVAILABLE','INVALID_LLM_RESPONSES'])
|
||||
def test_other_blocks_cannot_receive_an_entity_allowance(built_case,reason):
|
||||
store=built_case[0]
|
||||
# The protected human-feedback state is rejected before its resume semantics matter.
|
||||
store.advance('case-A','stable-package','BLOCKED',step_id='other-block',reason=reason,
|
||||
retry_not_before='2099-01-01T00:00:00+00:00')
|
||||
with PackageReader(store.latest('case-A','stable-package')) as reader:generation=reader.manifest['package_generation']
|
||||
with pytest.raises(WorkerError):
|
||||
approve_entity_repair(store,'case-A','stable-package',actor='alice',reason='Do not bypass',expected_generation=generation)
|
||||
Reference in new issue
Block a user