87 lines
5.0 KiB
Python
87 lines
5.0 KiB
Python
|
|
"""Quiescence evidence must distinguish process exit from unavailable identity."""
|
||
|
|
import os
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import sqlite3
|
||
|
|
import subprocess
|
||
|
|
from pathlib import Path
|
||
|
|
import unittest
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
SOURCE = Path(__file__).resolve().parents[1] / 'crates/supervisor/src/runner_files.py'
|
||
|
|
|
||
|
|
|
||
|
|
class FileProcessProof(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.boot = '00000000-0000-0000-0000-000000000000:123'
|
||
|
|
self.module = {'process_info': lambda pid: {'start': '456', 'state': 'S'}, 'boot_identity': lambda: self.boot}
|
||
|
|
exec(compile(SOURCE.read_text(), str(SOURCE), 'exec'), self.module)
|
||
|
|
self.reference = {'pid': os.getpid(), 'start': '456', 'boot': self.boot}
|
||
|
|
|
||
|
|
def test_live_reused_and_unavailable_identity(self):
|
||
|
|
finished = self.module['file_process_finished']
|
||
|
|
self.assertFalse(finished(self.reference))
|
||
|
|
self.assertTrue(finished({**self.reference, 'start': '455'}))
|
||
|
|
self.assertIsNone(finished({**self.reference, 'boot': self.boot[:-3] + '999'}))
|
||
|
|
self.assertIsNone(finished({**self.reference, 'boot': 'invalid'}))
|
||
|
|
with patch.object(os, 'stat', side_effect=PermissionError()):
|
||
|
|
self.assertIsNone(finished(self.reference))
|
||
|
|
with patch.object(os, 'stat', side_effect=FileNotFoundError()):
|
||
|
|
self.assertTrue(finished(self.reference))
|
||
|
|
self.module['boot_identity'] = lambda: (_ for _ in ()).throw(FileNotFoundError())
|
||
|
|
self.assertIsNone(finished(self.reference), 'missing boot data is not proof of exit')
|
||
|
|
|
||
|
|
def test_legacy_and_controller_only_evidence(self):
|
||
|
|
quiescent = self.module['file_quiescent']
|
||
|
|
self.assertIsNone(quiescent({}))
|
||
|
|
self.assertFalse(quiescent({'handoff_required': True, 'controller': self.reference}))
|
||
|
|
self.assertFalse(quiescent({'handoff_required': True, 'controller': {**self.reference, 'start': '455'}, 'execution': self.reference}))
|
||
|
|
self.assertTrue(quiescent({'handoff_required': True, 'execution': {**self.reference, 'start': '455'}}))
|
||
|
|
|
||
|
|
def test_controller_exit_refreshes_child_committed_after_first_read(self):
|
||
|
|
identity = {'computer_id': 'fixture', 'bot_id': 'race', 'generation': 1}
|
||
|
|
encoded = self.module['file_identity'](identity)
|
||
|
|
key = hashlib.sha256((encoded + ':operation').encode()).hexdigest()
|
||
|
|
metadata = {'action': 'write', 'path': 'file', 'desiredHash': 'a' * 64,
|
||
|
|
'handoff_required': True, 'controller': {**self.reference, 'start': '455'}}
|
||
|
|
with sqlite3.connect(':memory:') as db:
|
||
|
|
self.module['file_journal_schema'](db)
|
||
|
|
db.execute('INSERT INTO file_operations VALUES (?,?,?,?,NULL,?,?)',
|
||
|
|
(key, encoded, 'digest', 'accepted', json.dumps(metadata), 0))
|
||
|
|
db.commit()
|
||
|
|
def observe_process(pid):
|
||
|
|
# The status SELECT already returned controller-only metadata;
|
||
|
|
# the controller commits its child and dies before /proc is read.
|
||
|
|
db.execute('UPDATE file_operations SET metadata=? WHERE id=?',
|
||
|
|
(json.dumps({**metadata, 'execution': self.reference}), key))
|
||
|
|
db.commit()
|
||
|
|
return {'start': '456', 'state': 'S'}
|
||
|
|
self.module['process_info'] = observe_process
|
||
|
|
fingerprint = subprocess.CompletedProcess([], 0, json.dumps({'path': 'file', 'kind': 'file', 'sha256': 'a' * 64}), '')
|
||
|
|
with patch.object(subprocess, 'run', return_value=fingerprint):
|
||
|
|
result = self.module['file_journal_handle'](db, {
|
||
|
|
'action': 'file_status', 'identity': identity,
|
||
|
|
'input': json.dumps({'operation_id': 'operation', 'readback': True, 'resolve': True})
|
||
|
|
}, '', lambda *_: False)
|
||
|
|
evidence = json.loads(result['stdout'])
|
||
|
|
self.assertEqual(result['status'], 'unknown')
|
||
|
|
self.assertTrue(evidence['desired_state_matches'])
|
||
|
|
self.assertFalse(evidence['helper_quiescent'], 'committed child is still alive')
|
||
|
|
stored = json.loads(db.execute('SELECT observation FROM file_observations').fetchone()[0])
|
||
|
|
self.assertFalse(stored['helper_quiescent'])
|
||
|
|
self.module['process_info'] = lambda pid: {'start': '456', 'state': 'S'}
|
||
|
|
db.execute('UPDATE file_operations SET metadata=? WHERE id=?', (json.dumps({**metadata,
|
||
|
|
'controller': self.reference, 'execution': {**self.reference, 'start': '455'}}), key))
|
||
|
|
db.commit()
|
||
|
|
with patch.object(subprocess, 'run', return_value=fingerprint):
|
||
|
|
result = self.module['file_journal_handle'](db, {
|
||
|
|
'action': 'file_status', 'identity': identity,
|
||
|
|
'input': json.dumps({'operation_id': 'operation', 'readback': True, 'resolve': True})
|
||
|
|
}, '', lambda *_: False)
|
||
|
|
self.assertEqual(result['status'], 'unknown', 'live controller can still commit its original receipt')
|
||
|
|
self.assertFalse(json.loads(result['stdout'])['controller_quiescent'])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
unittest.main()
|