@riddledc/riddle-proof 0.5.33 → 0.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-A5AWVY5A.js → chunk-4YCWZVBN.js} +20 -2
- package/dist/{chunk-CHKYSZLU.js → chunk-7R6ZQE3X.js} +5 -5
- package/dist/{chunk-TMMKRKY5.js → chunk-J2MERROF.js} +1 -0
- package/dist/{chunk-F6VFKS7K.js → chunk-MQ2BHHLX.js} +2 -2
- package/dist/{chunk-7ZJAUEUN.js → chunk-OASB3CYU.js} +6 -1
- package/dist/chunk-ODORKNSO.js +121 -0
- package/dist/engine-harness.cjs +26 -2
- package/dist/engine-harness.js +4 -4
- package/dist/index.cjs +154 -4
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +24 -8
- package/dist/openclaw.cjs +5 -0
- package/dist/openclaw.js +2 -2
- package/dist/proof-run-core.cjs +20 -2
- package/dist/proof-run-core.d.cts +16 -0
- package/dist/proof-run-core.d.ts +16 -0
- package/dist/proof-run-core.js +1 -1
- package/dist/proof-run-engine.cjs +20 -2
- package/dist/proof-run-engine.d.cts +24 -0
- package/dist/proof-run-engine.d.ts +24 -0
- package/dist/proof-run-engine.js +1 -1
- package/dist/proof-session.cjs +151 -0
- package/dist/proof-session.d.cts +37 -0
- package/dist/proof-session.d.ts +37 -0
- package/dist/proof-session.js +18 -0
- package/dist/result.cjs +1 -0
- package/dist/result.js +1 -1
- package/dist/runner.cjs +6 -0
- package/dist/runner.js +3 -3
- package/dist/state.cjs +5 -0
- package/dist/state.js +2 -2
- package/dist/types.d.cts +67 -1
- package/dist/types.d.ts +67 -1
- package/package.json +7 -2
- package/runtime/lib/preflight.py +42 -2
- package/runtime/lib/util.py +237 -0
- package/runtime/lib/verify.py +55 -2
- package/runtime/tests/recon_verify_smoke.py +120 -1
package/runtime/lib/util.py
CHANGED
|
@@ -20,6 +20,8 @@ CAPTURE_DIAGNOSTIC_VERSION = 'riddle-proof.capture-diagnostic.v1'
|
|
|
20
20
|
DEBUG_STRING_LIMIT = 2000
|
|
21
21
|
CAPTURE_HINT_CACHE_VERSION = 'riddle-proof.capture-hints.v1'
|
|
22
22
|
CAPTURE_HINT_CACHE_LIMIT = 12
|
|
23
|
+
PROOF_SESSION_VERSION = 'riddle-proof.visual-session.v1'
|
|
24
|
+
PROOF_SESSION_FINGERPRINT_VERSION = 'riddle-proof.visual-session.fingerprint.v1'
|
|
23
25
|
SENSITIVE_KEY_FRAGMENTS = (
|
|
24
26
|
'authorization',
|
|
25
27
|
'apikey',
|
|
@@ -306,6 +308,241 @@ def compact_debug_value(value, limit=DEBUG_STRING_LIMIT):
|
|
|
306
308
|
return value
|
|
307
309
|
|
|
308
310
|
|
|
311
|
+
def stable_json(value):
|
|
312
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def sha256_text(value):
|
|
316
|
+
text = str(value or '').strip()
|
|
317
|
+
if not text:
|
|
318
|
+
return ''
|
|
319
|
+
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def parse_optional_json(value, field_name):
|
|
323
|
+
if value in (None, ''):
|
|
324
|
+
return None
|
|
325
|
+
if isinstance(value, (dict, list)):
|
|
326
|
+
return value
|
|
327
|
+
if not isinstance(value, str):
|
|
328
|
+
raise SystemExit(field_name + ' must be JSON.')
|
|
329
|
+
text = value.strip()
|
|
330
|
+
if not text:
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
return json.loads(text)
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
raise SystemExit(field_name + ' is not valid JSON: ' + str(exc))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def load_proof_session_source(value):
|
|
339
|
+
source = str(value or '').strip()
|
|
340
|
+
if not source:
|
|
341
|
+
return None
|
|
342
|
+
try:
|
|
343
|
+
if source.startswith('{'):
|
|
344
|
+
session = json.loads(source)
|
|
345
|
+
elif source.startswith(('http://', 'https://')):
|
|
346
|
+
with urlopen(source, timeout=20) as response:
|
|
347
|
+
session = json.loads(response.read(512 * 1024).decode('utf-8'))
|
|
348
|
+
else:
|
|
349
|
+
with open(os.path.abspath(os.path.expanduser(source))) as f:
|
|
350
|
+
session = json.load(f)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
raise SystemExit('resume_session could not be loaded: ' + str(exc))
|
|
353
|
+
|
|
354
|
+
if not isinstance(session, dict):
|
|
355
|
+
raise SystemExit('resume_session must be a JSON object.')
|
|
356
|
+
if session.get('version') != PROOF_SESSION_VERSION:
|
|
357
|
+
raise SystemExit('resume_session has unsupported version: ' + str(session.get('version') or ''))
|
|
358
|
+
if not session.get('session_id'):
|
|
359
|
+
raise SystemExit('resume_session is missing session_id.')
|
|
360
|
+
if not session.get('fingerprint'):
|
|
361
|
+
raise SystemExit('resume_session is missing fingerprint.')
|
|
362
|
+
return session
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def apply_proof_session_defaults(state, session):
|
|
366
|
+
if not isinstance(session, dict):
|
|
367
|
+
return []
|
|
368
|
+
applied = []
|
|
369
|
+
route = session.get('route') if isinstance(session.get('route'), dict) else {}
|
|
370
|
+
capture = session.get('capture') if isinstance(session.get('capture'), dict) else {}
|
|
371
|
+
target_image = session.get('target_image') if isinstance(session.get('target_image'), dict) else {}
|
|
372
|
+
|
|
373
|
+
defaults = [
|
|
374
|
+
('server_path', route.get('path'), 'proof_session'),
|
|
375
|
+
('wait_for_selector', capture.get('wait_for_selector'), 'proof_session'),
|
|
376
|
+
('proof_plan', capture.get('proof_plan'), 'proof_session'),
|
|
377
|
+
('capture_script', capture.get('capture_script'), 'proof_session'),
|
|
378
|
+
('reference', session.get('reference'), 'proof_session'),
|
|
379
|
+
('verification_mode', session.get('verification_mode'), 'proof_session'),
|
|
380
|
+
('target_image_url', target_image.get('url'), 'proof_session'),
|
|
381
|
+
('target_image_hash', target_image.get('hash'), 'proof_session'),
|
|
382
|
+
]
|
|
383
|
+
for key, value, source in defaults:
|
|
384
|
+
text = str(value or '').strip()
|
|
385
|
+
if text and not str(state.get(key) or '').strip():
|
|
386
|
+
state[key] = text
|
|
387
|
+
applied.append(key)
|
|
388
|
+
if key == 'server_path':
|
|
389
|
+
state['server_path_source'] = source
|
|
390
|
+
|
|
391
|
+
if state.get('viewport_matrix_json') in (None, '') and session.get('viewport_matrix') is not None:
|
|
392
|
+
state['viewport_matrix_json'] = json.dumps(session.get('viewport_matrix'))
|
|
393
|
+
applied.append('viewport_matrix_json')
|
|
394
|
+
if state.get('deterministic_setup_json') in (None, '') and session.get('deterministic_setup') is not None:
|
|
395
|
+
state['deterministic_setup_json'] = json.dumps(session.get('deterministic_setup'))
|
|
396
|
+
applied.append('deterministic_setup_json')
|
|
397
|
+
if state.get('assertions_json') in (None, '') and session.get('assertions') is not None:
|
|
398
|
+
state['assertions_json'] = json.dumps(session.get('assertions'))
|
|
399
|
+
applied.append('assertions_json')
|
|
400
|
+
return applied
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def visual_session_fingerprint_basis(state, route=''):
|
|
404
|
+
route_value = str(route or state.get('server_path') or '').strip()
|
|
405
|
+
basis = {
|
|
406
|
+
'version': PROOF_SESSION_FINGERPRINT_VERSION,
|
|
407
|
+
'repo': str(state.get('repo') or '').strip() or None,
|
|
408
|
+
'route': route_value or None,
|
|
409
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip() or None,
|
|
410
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip() or None,
|
|
411
|
+
'verification_mode': str(state.get('verification_mode') or '').strip().lower() or None,
|
|
412
|
+
'target_image_url': str(state.get('target_image_url') or '').strip() or None,
|
|
413
|
+
'target_image_hash': str(state.get('target_image_hash') or '').strip() or None,
|
|
414
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
415
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
416
|
+
'assertions': state.get('parsed_assertions'),
|
|
417
|
+
'capture_script_hash': sha256_text(state.get('capture_script')),
|
|
418
|
+
}
|
|
419
|
+
return {key: value for key, value in basis.items() if value is not None}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def visual_session_fingerprint_from_basis(basis):
|
|
423
|
+
return hashlib.sha256(stable_json(basis).encode('utf-8')).hexdigest()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def visual_session_fingerprint(state, route=''):
|
|
427
|
+
return visual_session_fingerprint_from_basis(visual_session_fingerprint_basis(state, route=route))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def proof_session_mismatches(state, session, route=''):
|
|
431
|
+
if not isinstance(session, dict):
|
|
432
|
+
return []
|
|
433
|
+
expected = session.get('fingerprint_basis') if isinstance(session.get('fingerprint_basis'), dict) else {}
|
|
434
|
+
actual = visual_session_fingerprint_basis(state, route=route)
|
|
435
|
+
keys = sorted(set(expected.keys()) | set(actual.keys()))
|
|
436
|
+
mismatches = []
|
|
437
|
+
for key in keys:
|
|
438
|
+
if stable_json(expected.get(key)) != stable_json(actual.get(key)):
|
|
439
|
+
mismatches.append({
|
|
440
|
+
'key': key,
|
|
441
|
+
'expected': expected.get(key),
|
|
442
|
+
'actual': actual.get(key),
|
|
443
|
+
})
|
|
444
|
+
return mismatches
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def validate_proof_session_resume(state, route=''):
|
|
448
|
+
session = state.get('parent_proof_session')
|
|
449
|
+
if not isinstance(session, dict):
|
|
450
|
+
return {'status': 'not_requested'}
|
|
451
|
+
mismatches = proof_session_mismatches(state, session, route=route)
|
|
452
|
+
if mismatches:
|
|
453
|
+
state['proof_session_resume'] = {
|
|
454
|
+
'status': 'fingerprint_mismatch',
|
|
455
|
+
'parent_session_id': session.get('session_id'),
|
|
456
|
+
'parent_fingerprint': session.get('fingerprint'),
|
|
457
|
+
'mismatches': mismatches,
|
|
458
|
+
}
|
|
459
|
+
raise SystemExit(
|
|
460
|
+
'resume_session fingerprint mismatch: ' +
|
|
461
|
+
', '.join(item['key'] for item in mismatches[:8])
|
|
462
|
+
)
|
|
463
|
+
state['proof_session_resume'] = {
|
|
464
|
+
'status': 'accepted',
|
|
465
|
+
'parent_session_id': session.get('session_id'),
|
|
466
|
+
'parent_fingerprint': session.get('fingerprint'),
|
|
467
|
+
'fingerprint': visual_session_fingerprint(state, route=route),
|
|
468
|
+
}
|
|
469
|
+
return state['proof_session_resume']
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def capture_proof_session_seed(state, route=''):
|
|
473
|
+
parent = state.get('parent_proof_session') if isinstance(state.get('parent_proof_session'), dict) else {}
|
|
474
|
+
basis = visual_session_fingerprint_basis(state, route=route)
|
|
475
|
+
return {
|
|
476
|
+
'version': PROOF_SESSION_VERSION,
|
|
477
|
+
'session_kind': 'capture-seed',
|
|
478
|
+
'run_id': str(state.get('run_id') or '').strip(),
|
|
479
|
+
'parent_session_id': parent.get('session_id') or None,
|
|
480
|
+
'parent_fingerprint': parent.get('fingerprint') or None,
|
|
481
|
+
'fingerprint': visual_session_fingerprint_from_basis(basis),
|
|
482
|
+
'fingerprint_basis': basis,
|
|
483
|
+
'repo': str(state.get('repo') or '').strip(),
|
|
484
|
+
'route': {'path': str(route or state.get('server_path') or '').strip()},
|
|
485
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip(),
|
|
486
|
+
'verification_mode': str(state.get('verification_mode') or '').strip(),
|
|
487
|
+
'target_image': {
|
|
488
|
+
'url': str(state.get('target_image_url') or '').strip(),
|
|
489
|
+
'hash': str(state.get('target_image_hash') or '').strip(),
|
|
490
|
+
},
|
|
491
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
492
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
493
|
+
'capture': {
|
|
494
|
+
'proof_plan': str(state.get('proof_plan') or '').strip(),
|
|
495
|
+
'capture_script': str(state.get('capture_script') or '').strip(),
|
|
496
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip(),
|
|
497
|
+
},
|
|
498
|
+
'assertions': state.get('parsed_assertions'),
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def proof_session_output_url(payload):
|
|
503
|
+
item = capture_output_item(payload, 'proof-session.json') or capture_output_item(payload, 'proof-session')
|
|
504
|
+
return (item or {}).get('url', '') if item else ''
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def build_visual_proof_session(state, route='', observed_after_path='', artifacts=None, evidence=None, status=''):
|
|
508
|
+
parent = state.get('parent_proof_session') if isinstance(state.get('parent_proof_session'), dict) else {}
|
|
509
|
+
basis = visual_session_fingerprint_basis(state, route=route)
|
|
510
|
+
session_id = 'rps_' + time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()) + '_' + hashlib.sha1(os.urandom(16)).hexdigest()[:8]
|
|
511
|
+
return {
|
|
512
|
+
'version': PROOF_SESSION_VERSION,
|
|
513
|
+
'session_id': session_id,
|
|
514
|
+
'run_id': str(state.get('run_id') or '').strip(),
|
|
515
|
+
'parent_session_id': parent.get('session_id') or None,
|
|
516
|
+
'parent_fingerprint': parent.get('fingerprint') or None,
|
|
517
|
+
'created_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
518
|
+
'fingerprint': visual_session_fingerprint_from_basis(basis),
|
|
519
|
+
'fingerprint_basis': basis,
|
|
520
|
+
'repo': str(state.get('repo') or '').strip(),
|
|
521
|
+
'branch': str(state.get('branch') or state.get('target_branch') or '').strip(),
|
|
522
|
+
'route': {
|
|
523
|
+
'path': str(route or state.get('server_path') or '').strip(),
|
|
524
|
+
'observed_after_path': str(observed_after_path or '').strip(),
|
|
525
|
+
},
|
|
526
|
+
'reference': str(state.get('requested_reference') or state.get('reference') or '').strip(),
|
|
527
|
+
'verification_mode': str(state.get('verification_mode') or '').strip(),
|
|
528
|
+
'target_image': {
|
|
529
|
+
'url': str(state.get('target_image_url') or '').strip(),
|
|
530
|
+
'hash': str(state.get('target_image_hash') or '').strip(),
|
|
531
|
+
},
|
|
532
|
+
'viewport_matrix': state.get('viewport_matrix'),
|
|
533
|
+
'deterministic_setup': state.get('deterministic_setup'),
|
|
534
|
+
'capture': {
|
|
535
|
+
'proof_plan': str(state.get('proof_plan') or '').strip(),
|
|
536
|
+
'capture_script': str(state.get('capture_script') or '').strip(),
|
|
537
|
+
'wait_for_selector': str(state.get('wait_for_selector') or '').strip(),
|
|
538
|
+
},
|
|
539
|
+
'assertions': state.get('parsed_assertions'),
|
|
540
|
+
'artifacts': artifacts or {},
|
|
541
|
+
'evidence': evidence or {},
|
|
542
|
+
'status': status,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
309
546
|
def redact_for_diagnostics(value):
|
|
310
547
|
if isinstance(value, dict):
|
|
311
548
|
redacted = {}
|
package/runtime/lib/verify.py
CHANGED
|
@@ -13,6 +13,8 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
13
13
|
from util import (
|
|
14
14
|
append_capture_diagnostic,
|
|
15
15
|
apply_auth_context,
|
|
16
|
+
build_visual_proof_session,
|
|
17
|
+
capture_proof_session_seed,
|
|
16
18
|
capture_static_preview,
|
|
17
19
|
enrich_capture_payload,
|
|
18
20
|
has_auth_context,
|
|
@@ -24,6 +26,7 @@ from util import (
|
|
|
24
26
|
run_project_build,
|
|
25
27
|
save_state,
|
|
26
28
|
should_use_static_preview,
|
|
29
|
+
proof_session_output_url,
|
|
27
30
|
summarize_capture_artifacts,
|
|
28
31
|
)
|
|
29
32
|
import subprocess as sp
|
|
@@ -215,7 +218,7 @@ def abort_capture_failure(state, results, expected_path, message, raw_payload):
|
|
|
215
218
|
raise SystemExit(summary)
|
|
216
219
|
|
|
217
220
|
|
|
218
|
-
def build_probe_capture_script(base_script='', verification_mode='proof'):
|
|
221
|
+
def build_probe_capture_script(base_script='', verification_mode='proof', proof_session_seed=None):
|
|
219
222
|
pieces = []
|
|
220
223
|
script = (base_script or '').strip()
|
|
221
224
|
pieces.append('let __riddleProofCaptureScriptError = null;')
|
|
@@ -280,6 +283,12 @@ def build_probe_capture_script(base_script='', verification_mode='proof'):
|
|
|
280
283
|
])
|
|
281
284
|
if auto_screenshot_for_mode(verification_mode) and not capture_script_saves_screenshot(script):
|
|
282
285
|
pieces.append("await saveScreenshot('after-proof');")
|
|
286
|
+
if isinstance(proof_session_seed, dict):
|
|
287
|
+
pieces.append(
|
|
288
|
+
'try { if (typeof saveJson === "function") await saveJson("proof-session", ' +
|
|
289
|
+
json.dumps(proof_session_seed) +
|
|
290
|
+
'); } catch {}'
|
|
291
|
+
)
|
|
283
292
|
pieces.append('if (__riddleProofCaptureScriptError) throw __riddleProofCaptureScriptError;')
|
|
284
293
|
pieces.append('return { pageState, proofEvidence: __riddleProofEvidenceValue };')
|
|
285
294
|
return ' '.join(pieces)
|
|
@@ -1056,6 +1065,36 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1056
1065
|
semantic_context,
|
|
1057
1066
|
[],
|
|
1058
1067
|
)
|
|
1068
|
+
observed_after_path = ((semantic_context.get('route') or {}).get('after_observed_path')) or ''
|
|
1069
|
+
proof_session_artifact_url = proof_session_output_url(after_payload)
|
|
1070
|
+
outputs = []
|
|
1071
|
+
for item in after_payload.get('outputs') or []:
|
|
1072
|
+
if not isinstance(item, dict):
|
|
1073
|
+
continue
|
|
1074
|
+
outputs.append({
|
|
1075
|
+
'name': str(item.get('name') or ''),
|
|
1076
|
+
'url': str(item.get('url') or ''),
|
|
1077
|
+
'type': str(item.get('type') or item.get('kind') or ''),
|
|
1078
|
+
})
|
|
1079
|
+
proof_session = build_visual_proof_session(
|
|
1080
|
+
state,
|
|
1081
|
+
route=expected_path,
|
|
1082
|
+
observed_after_path=observed_after_path,
|
|
1083
|
+
artifacts={
|
|
1084
|
+
'before': state.get('before_cdn') or '',
|
|
1085
|
+
'prod': state.get('prod_cdn') or '',
|
|
1086
|
+
'after': state.get('after_cdn') or '',
|
|
1087
|
+
'session': proof_session_artifact_url,
|
|
1088
|
+
'outputs': outputs,
|
|
1089
|
+
},
|
|
1090
|
+
evidence={
|
|
1091
|
+
'visual_delta': visual_delta,
|
|
1092
|
+
'semantic_context': semantic_context,
|
|
1093
|
+
'artifact_contract': artifact_contract,
|
|
1094
|
+
'artifact_usage': artifact_usage,
|
|
1095
|
+
},
|
|
1096
|
+
status='evidence_captured' if after_observation.get('valid') else 'capture_incomplete',
|
|
1097
|
+
)
|
|
1059
1098
|
return {
|
|
1060
1099
|
'verification_mode': normalized_verification_mode(state.get('verification_mode')),
|
|
1061
1100
|
'reference': state.get('requested_reference') or state.get('reference', 'both'),
|
|
@@ -1078,6 +1117,7 @@ def build_evidence_bundle(state, results, after_payload, after_observation, requ
|
|
|
1078
1117
|
'proof_evidence_sample': compact_value(proof_evidence) if proof_evidence is not None else '',
|
|
1079
1118
|
'success_criteria': (state.get('success_criteria') or '').strip(),
|
|
1080
1119
|
'assertions': state.get('parsed_assertions') or None,
|
|
1120
|
+
'proof_session': proof_session,
|
|
1081
1121
|
}
|
|
1082
1122
|
|
|
1083
1123
|
|
|
@@ -1193,7 +1233,8 @@ expected_path = (
|
|
|
1193
1233
|
or '/'
|
|
1194
1234
|
)
|
|
1195
1235
|
verification_mode = normalized_verification_mode(s.get('verification_mode'))
|
|
1196
|
-
|
|
1236
|
+
proof_session_seed = capture_proof_session_seed(s, expected_path)
|
|
1237
|
+
probe_capture_script = build_probe_capture_script(capture_script, verification_mode, proof_session_seed)
|
|
1197
1238
|
results = {
|
|
1198
1239
|
'baseline': {
|
|
1199
1240
|
'reference': reference,
|
|
@@ -1402,6 +1443,18 @@ if reference in ('prod', 'both') and prod_url:
|
|
|
1402
1443
|
|
|
1403
1444
|
evidence_bundle = build_evidence_bundle(s, results, after_payload, after_observation, required_baseline_present, expected_path)
|
|
1404
1445
|
s['evidence_bundle'] = evidence_bundle
|
|
1446
|
+
s['proof_session'] = evidence_bundle.get('proof_session') or {}
|
|
1447
|
+
s['proof_session_fingerprint'] = (s.get('proof_session') or {}).get('fingerprint') or ''
|
|
1448
|
+
s['proof_session_artifact_url'] = ((s.get('proof_session') or {}).get('artifacts') or {}).get('session') or ''
|
|
1449
|
+
if s.get('proof_session'):
|
|
1450
|
+
summary_lines.append(
|
|
1451
|
+
'Proof session: ' +
|
|
1452
|
+
str((s.get('proof_session') or {}).get('session_id') or '') +
|
|
1453
|
+
' ' +
|
|
1454
|
+
str((s.get('proof_session') or {}).get('fingerprint') or '')[:12]
|
|
1455
|
+
)
|
|
1456
|
+
if s.get('proof_session_artifact_url'):
|
|
1457
|
+
summary_lines.append('Proof session artifact: ' + s['proof_session_artifact_url'])
|
|
1405
1458
|
visual_delta = ((evidence_bundle.get('after') or {}).get('visual_delta') or {})
|
|
1406
1459
|
if visual_delta.get('status') != 'not_applicable':
|
|
1407
1460
|
summary_lines.append('Visual delta gate: ' + compact_value(visual_delta, limit=700))
|
|
@@ -204,10 +204,13 @@ class FakeRiddle:
|
|
|
204
204
|
],
|
|
205
205
|
}
|
|
206
206
|
if 'after-proof' in script:
|
|
207
|
+
outputs = [{'name': 'after.png', 'url': 'https://cdn.example.com/after.png'}]
|
|
208
|
+
if 'proof-session' in script:
|
|
209
|
+
outputs.append({'name': 'proof-session.json', 'url': 'https://cdn.example.com/proof-session.json'})
|
|
207
210
|
return {
|
|
208
211
|
'ok': True,
|
|
209
212
|
'screenshots': [{'url': 'https://cdn.example.com/after.png'}],
|
|
210
|
-
'outputs':
|
|
213
|
+
'outputs': outputs,
|
|
211
214
|
'console': state_console({
|
|
212
215
|
'bodyTextLength': 180,
|
|
213
216
|
'visibleTextSample': 'Pricing CTA Buy Now',
|
|
@@ -637,6 +640,105 @@ def run_preflight_records_prod_reference_skip_reason():
|
|
|
637
640
|
shutil.rmtree(tempdir, ignore_errors=True)
|
|
638
641
|
|
|
639
642
|
|
|
643
|
+
def run_preflight_resumes_visual_proof_session():
|
|
644
|
+
tempdir = Path(tempfile.mkdtemp(prefix='riddle-proof-preflight-session-'))
|
|
645
|
+
args_path = tempdir / 'args.json'
|
|
646
|
+
state_path = tempdir / 'state.json'
|
|
647
|
+
repo_dir = tempdir / 'repo'
|
|
648
|
+
try:
|
|
649
|
+
make_project(repo_dir, "export const routes = [{ path: '/pricing', element: <Pricing /> }];\n")
|
|
650
|
+
util = load_module('util_proof_session_builder', UTIL_PATH)
|
|
651
|
+
parent_session = util.build_visual_proof_session({
|
|
652
|
+
'repo': 'example/repo',
|
|
653
|
+
'server_path': '/pricing',
|
|
654
|
+
'reference': 'before',
|
|
655
|
+
'verification_mode': 'visual',
|
|
656
|
+
'target_image_url': 'https://cdn.example.com/spec.png',
|
|
657
|
+
'target_image_hash': 'sha256:spec',
|
|
658
|
+
'viewport_matrix': [
|
|
659
|
+
{'name': 'mobile', 'width': 390, 'height': 844},
|
|
660
|
+
{'name': 'desktop', 'width': 1280, 'height': 900},
|
|
661
|
+
],
|
|
662
|
+
'deterministic_setup': {'seed': 'pricing-visual-v1'},
|
|
663
|
+
'parsed_assertions': [{'kind': 'text', 'contains': 'Buy Now'}],
|
|
664
|
+
'proof_plan': 'Capture pricing route and compare CTA visual state.',
|
|
665
|
+
'capture_script': "await page.waitForSelector('[data-testid=pricing-cta]'); await saveScreenshot('after-proof');",
|
|
666
|
+
'wait_for_selector': '[data-testid=pricing-cta]',
|
|
667
|
+
}, route='/pricing', observed_after_path='/pricing', status='evidence_captured')
|
|
668
|
+
|
|
669
|
+
args_path.write_text(json.dumps({
|
|
670
|
+
'repo': 'example/repo',
|
|
671
|
+
'repo_dir': str(repo_dir),
|
|
672
|
+
'mode': 'static',
|
|
673
|
+
'change_request': 'Continue the pricing visual iteration',
|
|
674
|
+
'commit_message': 'Continue the pricing visual iteration',
|
|
675
|
+
'build_command': BUILD_SCRIPT,
|
|
676
|
+
'build_output': 'build',
|
|
677
|
+
'allow_static_preview_fallback': True,
|
|
678
|
+
'resume_session': json.dumps(parent_session),
|
|
679
|
+
}, indent=2))
|
|
680
|
+
with temporary_env(
|
|
681
|
+
RIDDLE_PROOF_ARGS_FILE=str(args_path),
|
|
682
|
+
RIDDLE_PROOF_STATE_FILE=str(state_path),
|
|
683
|
+
):
|
|
684
|
+
sys.modules.pop('util', None)
|
|
685
|
+
load_module('preflight_resume_session', PREFLIGHT_PATH)
|
|
686
|
+
after_preflight = json.loads(state_path.read_text())
|
|
687
|
+
assert after_preflight['proof_session_resume']['status'] == 'accepted'
|
|
688
|
+
assert after_preflight['proof_session_resume']['applied_fields']
|
|
689
|
+
assert after_preflight['parent_proof_session']['session_id'] == parent_session['session_id']
|
|
690
|
+
assert after_preflight['server_path'] == '/pricing'
|
|
691
|
+
assert after_preflight['server_path_source'] == 'proof_session'
|
|
692
|
+
assert after_preflight['reference'] == 'before'
|
|
693
|
+
assert after_preflight['verification_mode'] == 'visual'
|
|
694
|
+
assert after_preflight['target_image_url'] == 'https://cdn.example.com/spec.png'
|
|
695
|
+
assert after_preflight['target_image_hash'] == 'sha256:spec'
|
|
696
|
+
assert after_preflight['viewport_matrix'][0]['name'] == 'mobile'
|
|
697
|
+
assert after_preflight['deterministic_setup']['seed'] == 'pricing-visual-v1'
|
|
698
|
+
assert after_preflight['parsed_assertions'][0]['contains'] == 'Buy Now'
|
|
699
|
+
assert after_preflight['proof_plan_status'] == 'ready'
|
|
700
|
+
assert after_preflight['author_status'] == 'ready'
|
|
701
|
+
|
|
702
|
+
mismatch_args_path = tempdir / 'mismatch-args.json'
|
|
703
|
+
mismatch_state_path = tempdir / 'mismatch-state.json'
|
|
704
|
+
mismatch_args_path.write_text(json.dumps({
|
|
705
|
+
'repo': 'example/repo',
|
|
706
|
+
'repo_dir': str(repo_dir),
|
|
707
|
+
'mode': 'static',
|
|
708
|
+
'reference': 'before',
|
|
709
|
+
'verification_mode': 'visual',
|
|
710
|
+
'change_request': 'Continue the pricing visual iteration',
|
|
711
|
+
'commit_message': 'Continue the pricing visual iteration',
|
|
712
|
+
'build_command': BUILD_SCRIPT,
|
|
713
|
+
'build_output': 'build',
|
|
714
|
+
'allow_static_preview_fallback': True,
|
|
715
|
+
'server_path': '/wrong',
|
|
716
|
+
'resume_session': json.dumps(parent_session),
|
|
717
|
+
}, indent=2))
|
|
718
|
+
with temporary_env(
|
|
719
|
+
RIDDLE_PROOF_ARGS_FILE=str(mismatch_args_path),
|
|
720
|
+
RIDDLE_PROOF_STATE_FILE=str(mismatch_state_path),
|
|
721
|
+
):
|
|
722
|
+
try:
|
|
723
|
+
sys.modules.pop('util', None)
|
|
724
|
+
load_module('preflight_resume_session_mismatch', PREFLIGHT_PATH)
|
|
725
|
+
except SystemExit as exc:
|
|
726
|
+
assert 'fingerprint mismatch' in str(exc), exc
|
|
727
|
+
else:
|
|
728
|
+
raise AssertionError('route mismatch should reject resumed proof session')
|
|
729
|
+
mismatch_state = json.loads(mismatch_state_path.read_text())
|
|
730
|
+
assert mismatch_state['proof_session_resume']['status'] == 'fingerprint_mismatch'
|
|
731
|
+
assert any(item['key'] == 'route' for item in mismatch_state['proof_session_resume']['mismatches'])
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
'ok': True,
|
|
735
|
+
'parent_session_id': parent_session['session_id'],
|
|
736
|
+
'fingerprint': after_preflight['proof_session_resume']['fingerprint'],
|
|
737
|
+
}
|
|
738
|
+
finally:
|
|
739
|
+
shutil.rmtree(tempdir, ignore_errors=True)
|
|
740
|
+
|
|
741
|
+
|
|
640
742
|
def base_state(tempdir: Path, *, reference='before', prod_url=''):
|
|
641
743
|
before_dir = tempdir / 'before'
|
|
642
744
|
after_dir = tempdir / 'after'
|
|
@@ -1157,6 +1259,7 @@ def run_verify_requests_supervisor_assessment():
|
|
|
1157
1259
|
try:
|
|
1158
1260
|
state = base_state(tempdir, reference='both', prod_url='https://prod.example.com/pricing')
|
|
1159
1261
|
state.update({
|
|
1262
|
+
'repo': 'example/repo',
|
|
1160
1263
|
'recon_status': 'ready_for_proof_plan',
|
|
1161
1264
|
'author_status': 'ready',
|
|
1162
1265
|
'proof_plan_status': 'ready',
|
|
@@ -1223,6 +1326,21 @@ def run_verify_requests_supervisor_assessment():
|
|
|
1223
1326
|
assert after_details['observed_path_raw'] == '/s/pv-after/pricing', after_details
|
|
1224
1327
|
assert 'Buy Now' in after_details['visible_text_sample'], after_details
|
|
1225
1328
|
assert after_details['buttons'] == ['Buy Now'], after_details
|
|
1329
|
+
proof_session = after_verify['proof_session']
|
|
1330
|
+
assert proof_session['version'] == 'riddle-proof.visual-session.v1'
|
|
1331
|
+
assert proof_session['repo'] == 'example/repo'
|
|
1332
|
+
assert proof_session['route']['path'] == '/pricing'
|
|
1333
|
+
assert proof_session['route']['observed_after_path'] == '/pricing'
|
|
1334
|
+
assert proof_session['artifacts']['before'] == 'https://cdn.example.com/before.png'
|
|
1335
|
+
assert proof_session['artifacts']['prod'] == 'https://cdn.example.com/prod.png'
|
|
1336
|
+
assert proof_session['artifacts']['after'] == 'https://cdn.example.com/after.png'
|
|
1337
|
+
assert proof_session['artifacts']['session'] == 'https://cdn.example.com/proof-session.json'
|
|
1338
|
+
assert proof_session['capture']['wait_for_selector'] == '[data-testid=pricing-cta]'
|
|
1339
|
+
assert proof_session['fingerprint_basis']['route'] == '/pricing'
|
|
1340
|
+
assert after_verify['proof_session_fingerprint'] == proof_session['fingerprint']
|
|
1341
|
+
assert after_verify['proof_session_artifact_url'] == 'https://cdn.example.com/proof-session.json'
|
|
1342
|
+
assert after_verify['evidence_bundle']['proof_session']['fingerprint'] == proof_session['fingerprint']
|
|
1343
|
+
assert after_verify['proof_assessment_request']['evidence_bundle']['proof_session']['fingerprint'] == proof_session['fingerprint']
|
|
1226
1344
|
runtime_events = after_verify.get('runtime_events') or []
|
|
1227
1345
|
assert any(event.get('kind') == 'workflow.phase.started' and event.get('step') == 'verify' and event.get('phase') == 'build' for event in runtime_events)
|
|
1228
1346
|
assert any(event.get('kind') == 'workflow.phase.finished' and event.get('step') == 'verify' and event.get('phase') == 'build' for event in runtime_events)
|
|
@@ -1866,6 +1984,7 @@ def run_ship_resolves_real_pr_branch():
|
|
|
1866
1984
|
if __name__ == '__main__':
|
|
1867
1985
|
payload = {
|
|
1868
1986
|
'preflight_reference_skip_reason': run_preflight_records_prod_reference_skip_reason(),
|
|
1987
|
+
'preflight_resume_visual_proof_session': run_preflight_resumes_visual_proof_session(),
|
|
1869
1988
|
'capture_artifact_enrichment': run_capture_artifact_enrichment(),
|
|
1870
1989
|
'capture_diagnostics_redaction': run_capture_diagnostics_redact_sensitive_values(),
|
|
1871
1990
|
'apply_auth_context': run_apply_auth_context_passes_supported_auth_payloads(),
|