| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- from copy import deepcopy
- import hashlib
- import json
- import pytest
- from extensions.main_fan.history_compaction import RecordReader, agent_view, compact_evidence, summarize_point
- def point(system='safety', role='fan_state'):
- return {'node_id': 'node1', 'system': system, 'role': role, 'fan_id': '1号', 'label': '主通1一级', 'unit': '', 'mapping_confirmed': False}
- class Reader:
- def __init__(self, samples):
- self.samples = samples
- def data(self, record):
- return {'samples': self.samples[record['key']]}
- def segment(key='a', start='2026-10-01 00:00:00', end='2026-10-03 00:00:00'):
- return {'key': key, 'status': 'completed', 'node_id': 'node1', 'start_time': start, 'end_time': end}
- def sample(at, value='开', **extras):
- return {'time': at, 'value_label': value, 'status_code': '0', 'status_label': '正常', **extras}
- @pytest.mark.parametrize('stopped_label', ['停', '关'])
- def test_sort_merge_cross_day_and_deduplicate_boundary_without_extending_edges(stopped_label):
- reader = Reader({'a': [sample('2026-10-01 23:59:00'), sample('2026-10-02 00:00:00')],
- 'b': [sample('2026-10-02 00:02:00', stopped_label), sample('2026-10-02 00:00:00'),
- sample('2026-10-02 00:01:00', stopped_label)]})
- result = summarize_point(point(), [segment('b'), segment('a')], reader)
- intervals = result['state_intervals']
- assert len(intervals) == 2 and intervals[0]['state'] == '开' and intervals[1]['state'] == '停'
- assert intervals[0]['observed_from'] == '2026-10-01T23:59:00+08:00'
- assert intervals[-1]['observed_until'] == '2026-10-02T00:02:00+08:00'
- assert result['quality']['duplicate_time_count'] == 1
- assert result['state_changes'][0]['uncertainty_seconds'] == 60
- assert result['state_changes'][0]['machine_switch_confirmed'] is False
- assert result['state_changes'][0]['to_state'] == '停'
- assert reader.samples['b'][0]['value_label'] == stopped_label
- def test_fault_conflicting_timestamp_and_long_gap_break_observed_intervals():
- samples = [sample('2026-10-01 00:00:00'), sample('2026-10-01 00:01:00'),
- sample('2026-10-01 00:02:00', status_label='故障', status_code='2'),
- sample('2026-10-01 00:03:00'), sample('2026-10-01 00:04:00'),
- sample('2026-10-01 00:04:00', '停'), sample('2026-10-01 00:05:00'), sample('2026-10-01 00:30:00')]
- result = summarize_point(point(), [segment()], Reader({'a': samples}))
- assert result['quality']['conflicting_timestamp_count'] == 1 and result['quality']['invalid_value_count'] == 1
- assert result['quality']['gap_count'] == 1 and result['state_changes'] == []
- assert sum(row['state'] == '未知' for row in result['state_intervals']) == 2
- view = agent_view({'points': [result]}, '/workspace/details.json')['points'][0]
- assert len(view['state_periods']) == 1 and view['state_periods'][0]['observed_parts'] == 4
- assert view['unknown_interval_count'] == 2
- def test_state_change_across_unknown_preserves_sampling_uncertainty():
- result = summarize_point(point(), [segment()], Reader({'a': [sample('2026-10-01 01:00:00'),
- sample('2026-10-01 01:01:00', status_label='故障'), sample('2026-10-01 01:05:00', '停')]}))
- change = result['state_changes'][0]
- assert change['after'] == '2026-10-01T01:00:00+08:00' and change['by'] == '2026-10-01T01:05:00+08:00'
- assert change['uncertainty_seconds'] == 300 and change['intervening_unknown_or_gap']
- @pytest.mark.parametrize('role', ['fan_state', 'flow'])
- def test_equipment_zeros_remain_data_and_state_codes_are_not_assumed(role):
- class EquipmentReader:
- def data(self, record):
- return {'payload': {'code': 200, 'data': [{'nodeId': 'node1', 'sbNodeCurve': [
- {'dataTime': '2026-10-01 01:00:00', 'realData': '0'},
- {'dataTime': '2026-10-01 01:01:00', 'realData': '0'}]}]}}
- result = summarize_point(point('equipment', role), [segment()], EquipmentReader())
- assert result['quality']['raw_sample_count'] == 2
- if role == 'fan_state':
- assert len(result['state_intervals']) == 1 and result['state_intervals'][0]['state'] == '原始值0'
- else:
- assert result['values']['zero_count'] == 2 and result['values']['numeric_sample_count'] == 2
- def test_zip_evidence_roundtrip_retry_and_tampered_source(tmp_path):
- resolve = lambda virtual: tmp_path / virtual.removeprefix('/workspace/')
- record = segment(); record['files'] = {}
- for name, value in [('request', {'nodeId': 'node1'}), ('response', {'code': 200, 'data': []}), ('data', {'samples': []})]:
- virtual = '/workspace/daily/a/' + name + '.json'
- local = resolve(virtual); local.parent.mkdir(parents=True, exist_ok=True)
- local.write_text(json.dumps(value)); record['files'][virtual] = hashlib.sha256(local.read_bytes()).hexdigest()
- bad = deepcopy(record)
- raw = resolve('/workspace/daily/a/data.json'); raw.write_text('tampered')
- with pytest.raises(ValueError, match='指纹'):
- compact_evidence([bad], '/workspace/failed.zip', resolve)
- raw.write_text(json.dumps({'samples': []}))
- records, files, cleanup, _ = compact_evidence([record], '/workspace/packed.zip', resolve)
- assert len(files) == 1 and len(cleanup) == 3
- with RecordReader(resolve) as reader:
- assert reader.data(records[0]) == {'samples': []}
- stamp = resolve('/workspace/packed.zip').stat().st_mtime_ns
- again, _, cleanup, _ = compact_evidence(records, '/workspace/packed.zip', resolve)
- assert not cleanup and again == records and resolve('/workspace/packed.zip').stat().st_mtime_ns == stamp
- def test_independent_verifier_reads_compacted_evidence_without_daily_files(tmp_path):
- from scripts.verify_main_fan_batch import read_segment
- resolve = lambda virtual: tmp_path / virtual.lstrip('/')
- record = segment(); record['files'] = {}
- for kind, value in [('request', {}), ('response', {'code': 200}), ('data', {'samples': []})]:
- virtual = '/workspace/staging/' + kind + '.json'
- path = resolve(virtual); path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(json.dumps(value)); record['files'][virtual] = hashlib.sha256(path.read_bytes()).hexdigest()
- records, _, cleanup, _ = compact_evidence([record], '/workspace/packed.zip', resolve)
- for virtual in cleanup:
- resolve(virtual).unlink()
- assert read_segment(tmp_path, records[0]) == {'samples': []}
- def test_noisy_state_overview_is_bounded_and_points_to_complete_details():
- samples = [sample(f'2026-10-01 01:{minute:02d}:00', '开' if minute % 2 else '停') for minute in range(60)]
- result = summarize_point(point(), [segment()], Reader({'a': samples}))
- view = agent_view({'points': [result]}, '/workspace/details.json')['points'][0]
- assert len(result['state_intervals']) == 60 and len(view['state_periods']) == 20
- assert view['state_periods_truncated'] and view['state_change_count'] == 59
|