Add validated OTA candidate publishing and fix 1.1.2 dependency failure

This commit is contained in:
2026-09-27 19:00:20 +08:00
parent 98eadc5c8b
commit f80c4d9938
26 changed files with 571 additions and 25 deletions
@@ -0,0 +1,41 @@
{
"note": "Public interoperability fixture only; never use these scalars in real connections.",
"client_scalar": "1",
"server_scalar": "2",
"client_random": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"server_random": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"client_hello": "7b2270726f746f636f6c5f6d616a6f72223a312c2270726f746f636f6c5f6d696e6f72223a302c227075626c69635f6b6579223a224247735830664c684c454a482b4c7a6d35574f6b51504a33413332424c65737a6f5053684f5558596d4d4b57542b4e43347634616635754f352b744b66412b654669764f4d3164724d56374f79375a416144652f5566553d222c2272616e646f6d223a2241414543417751464267634943516f4c4441304f4478415245684d554652595847426b61477877644868383d227d",
"server_hello": "7b2270726f746f636f6c5f6d616a6f72223a312c2270726f746f636f6c5f6d696e6f72223a302c227075626c69635f6b6579223a2242487a796578694e4130392b696c4934417753314773504169576e69642f49624e61594c535078485a706c3442336456454e754f30454170505a72476e3351773237703972655938365949706e6753336e534a346339453d222c2272616e646f6d223a22494345694979516c4a69636f4b536f724c4330754c7a41784d6a4d304e5459334f446b364f7a7739506a383d227d",
"transcript": "9ca718b743dfe549aaba17180599eced0e2bcb680fd7c210e341ae74ccd17b76",
"request": "7b226964223a2231222c226d6574686f64223a2273657373696f6e2e6f70656e222c22706172616d73223a7b22636c69656e745f6e616d65223a22e6b58be8af95e6898be69cba227d7d",
"response": "7b226964223a2231222c226f6b223a747275652c22726573756c74223a7b22616c697665223a747275657d7d",
"client_record": "0000000000000000f2c854e062cd588b6de2066904d362400bf231bdeb046d786fca2a2bb6e1b539d0bd32d225461581655d7dc5066cf89d2d3bea12421e0d6e452f34ba4c39219b3e3efd3826c8461b67b8a74650976db8d47a0edd94e6cf39f526",
"server_record": "0000000000000000fd089bf6a0d3196c601f5dcbefad51b0f130e17cf38d2c76cbb9855a7da88480e53f3e89893dcc35fa175fcfbf01683f575244928462d07d13056a67",
"fragments_mtu23": [
"514d010300000000000000190000006200000000",
"514d010300000000000100190000006200000000",
"514d0103000000000002001900000062f2c854e0",
"514d010300000000000300190000006262cd588b",
"514d01030000000000040019000000626de20669",
"514d010300000000000500190000006204d36240",
"514d01030000000000060019000000620bf231bd",
"514d0103000000000007001900000062eb046d78",
"514d01030000000000080019000000626fca2a2b",
"514d0103000000000009001900000062b6e1b539",
"514d010300000000000a001900000062d0bd32d2",
"514d010300000000000b00190000006225461581",
"514d010300000000000c001900000062655d7dc5",
"514d010300000000000d001900000062066cf89d",
"514d010300000000000e0019000000622d3bea12",
"514d010300000000000f001900000062421e0d6e",
"514d0103000000000010001900000062452f34ba",
"514d01030000000000110019000000624c39219b",
"514d01030000000000120019000000623e3efd38",
"514d010300000000001300190000006226c8461b",
"514d010300000000001400190000006267b8a746",
"514d010300000000001500190000006250976db8",
"514d0103000000000016001900000062d47a0edd",
"514d010300000000001700190000006294e6cf39",
"514d0103000000000018001900000062f526"
]
}
@@ -71,7 +71,10 @@ def test_invalid_hello_and_json():
def test_shared_golden_vector():
path = Path(__file__).resolve().parents[2] / '移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/jvmTest/resources/protocol-v1.json'
path = Path(__file__).resolve().parent / 'fixtures/protocol-v1.json'
mobile_copy = Path(__file__).resolve().parents[2] / '移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/jvmTest/resources/protocol-v1.json'
if mobile_copy.is_file():
assert path.read_bytes() == mobile_copy.read_bytes()
vector = json.loads(path.read_text(encoding='utf-8'))
def raw(key):
return bytes.fromhex(vector[key])
@@ -241,6 +241,8 @@ def release_project(tmp_path, monkeypatch):
source.mkdir()
(source / 'VERSION').write_text('1.0.6\n', encoding='utf-8')
(source / 'FEATURE_UPDATED_AT').write_text('2026-09-07T20:00+08:00\n', encoding='utf-8')
(source / 'requirements.txt').write_text('example==1.0.0\n', encoding='utf-8')
(source / 'requirements-dev.txt').write_text('-r requirements.txt\n', encoding='utf-8')
deps = tmp_path / '发布更新相关/其他依赖'
binary = deps / 'frp/v/frpc'
binary.parent.mkdir(parents=True)
@@ -249,7 +251,9 @@ def release_project(tmp_path, monkeypatch):
(binary.parent/'SHA256SUMS').write_text(f'{digest} frpc\n', encoding='utf-8')
wheels = deps / 'aarch64-py311'
wheels.mkdir()
(wheels/'SHA256SUMS').write_text('fixture', encoding='utf-8')
wheel = wheels / 'example-1.0.0-py3-none-any.whl'
wheel.write_bytes(b'fixture wheel')
(wheels/'SHA256SUMS').write_text(f'{hashlib.sha256(wheel.read_bytes()).hexdigest()} {wheel.name}\n', encoding='ascii')
policy = {'schema_version': 1, 'checkpoints': [{'version': '1.1.0', 'components': {
'frpc': {'version': 'v', 'bundle': 'frp/v', 'sha256': digest}}}]}
(source/'UPGRADE_POLICY.json').write_text(json.dumps(policy), encoding='utf-8')
@@ -258,15 +262,102 @@ def release_project(tmp_path, monkeypatch):
return exporter, source
def test_export_checkpoint_then_patch_without_repeating_binary(tmp_path, monkeypatch):
def validated_ota_report(tmp_path, candidate, source_version, target_version):
artifact = candidate / f'matrix-screen-controller-{target_version}.ota'
report = tmp_path / f'validated-{target_version}.json'
report.write_text(json.dumps({
'schema_version': 1, 'artifact_type': 'ota',
'package_sha256': hashlib.sha256(artifact.read_bytes()).hexdigest(),
'software_version': target_version, 'source_version': source_version,
'restored_version': source_version, 'status': 'success',
'validated_at': '2026-09-27T18:00:00+08:00',
'offline_install_passed': True, 'ota_health_passed': True,
'runtime_lifecycle_passed': True, 'user_data_preserved': True,
'frp_state_preserved': True, 'bluetooth_state_preserved': True,
'transaction_cleanup_passed': True, 'baseline_restored': True,
}), encoding='utf-8')
return report
def publish_fixture_ota(tmp_path, monkeypatch, exporter, current, target, notes):
import sys
candidate = tmp_path / f'candidate-{target}'
args = ['export', 'ota', '--notes', notes, '--candidate-output', str(candidate)]
if target.endswith('.0'):
args.extend(['--version', target])
monkeypatch.setattr(sys, 'argv', args)
assert exporter.main() == 0
report = validated_ota_report(tmp_path, candidate, current, target)
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', notes,
'--validated-candidate', str(candidate),
'--validation-report', str(report)])
assert exporter.main() == 0
return candidate
@pytest.mark.parametrize('failure', ['missing', 'version', 'digest'])
def test_ota_candidate_rejects_incomplete_offline_wheels(tmp_path, monkeypatch, failure):
import sys
exporter, source = release_project(tmp_path, monkeypatch)
wheels = tmp_path / '发布更新相关/其他依赖/aarch64-py311'
if failure == 'missing':
(source / 'requirements-dev.txt').write_text('-r requirements.txt\nparamiko==4.0.0\n', encoding='utf-8')
expected = 'offline wheel missing'
elif failure == 'version':
(source / 'requirements.txt').write_text('example==2.0.0\n', encoding='utf-8')
expected = 'offline wheel version missing'
else:
(wheels / 'example-1.0.0-py3-none-any.whl').write_bytes(b'changed')
expected = 'digest differs'
candidate = tmp_path / 'candidate-1.1.0'
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
'--candidate-output', str(candidate), '--notes', 'test'])
with pytest.raises(ValueError, match=expected):
exporter.main()
assert not candidate.exists()
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
assert json.loads((tmp_path / '发布记录.json').read_text(encoding='utf-8'))['releases'] == []
assert not (tmp_path / '.release-export.lock').exists()
@pytest.mark.parametrize('failure', ['report', 'source', 'artifact'])
def test_ota_candidate_promotion_rejects_changed_evidence(tmp_path, monkeypatch, failure):
import sys
from scripts.promote_ota_release import promote
exporter, source = release_project(tmp_path, monkeypatch)
candidate = tmp_path / 'candidate-1.1.0'
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
'--candidate-output', str(candidate), '--notes', 'test'])
assert exporter.main() == 0
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
assert not (tmp_path / '发布更新相关/OTA数据包/1.1.0').exists()
report = validated_ota_report(tmp_path, candidate, '1.0.6', '1.1.0')
if failure == 'report':
document = json.loads(report.read_text(encoding='utf-8'))
document['package_sha256'] = '0' * 64
report.write_text(json.dumps(document), encoding='utf-8')
expected = 'matching successful OTA'
elif failure == 'source':
(source / 'changed.py').write_text('CHANGED = True\n', encoding='utf-8')
expected = 'software file list differs'
else:
artifact = candidate / 'matrix-screen-controller-1.1.0.ota'
artifact.write_bytes(artifact.read_bytes() + b'tampered')
expected = 'manifest differs'
with pytest.raises(ValueError, match=expected):
promote(source, candidate, report, 'test')
assert (source / 'VERSION').read_text(encoding='utf-8').strip() == '1.0.6'
assert not (tmp_path / '发布更新相关/OTA数据包/1.1.0').exists()
assert json.loads((tmp_path / '发布记录.json').read_text(encoding='utf-8'))['releases'] == []
assert not (tmp_path / '.release-export.lock').exists()
def test_export_checkpoint_then_patch_without_repeating_binary(tmp_path, monkeypatch):
import tarfile
exporter, source = release_project(tmp_path, monkeypatch)
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'test'])
assert exporter.main() == 0
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.0.6', '1.1.0', 'test')
assert (source/'VERSION').read_text(encoding='utf-8').strip() == '1.1.0'
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', 'patch'])
assert exporter.main() == 0
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.1.0', '1.1.1', 'patch')
artifact = next((tmp_path/'发布更新相关/OTA数据包/1.1.1').glob('*.ota'))
with zipfile.ZipFile(artifact) as z, z.open('payload.tar.gz') as f, tarfile.open(fileobj=f, mode='r|gz') as t:
assert not any('system-dependencies' in item.name for item in t)
@@ -282,7 +373,14 @@ def test_failed_export_does_not_consume_version(tmp_path, monkeypatch):
exporter, source = release_project(tmp_path, monkeypatch)
original = (tmp_path/'发布记录.json').read_bytes()
original_version = (source/'VERSION').read_bytes()
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'test'])
candidate = tmp_path / 'candidate-1.1.0'
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0',
'--candidate-output', str(candidate), '--notes', 'test'])
assert exporter.main() == 0
report = validated_ota_report(tmp_path, candidate, '1.0.6', '1.1.0')
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--notes', 'test',
'--validated-candidate', str(candidate),
'--validation-report', str(report)])
def fail(*args):
raise OSError('injected history write failure')
monkeypatch.setattr(exporter, '_atomic_json', fail)
@@ -319,11 +417,9 @@ def test_provisioned_account_does_not_need_sudo_group(host, monkeypatch):
@pytest.mark.parametrize('fail_commit', [False, True])
def test_same_version_repair_promotes_exact_candidate_and_preserves_old_artifact(tmp_path, monkeypatch, fail_commit):
import sys
from scripts.repair_ota_release import promote
exporter, source = release_project(tmp_path, monkeypatch)
monkeypatch.setattr(sys, 'argv', ['export', 'ota', '--version', '1.1.0', '--notes', 'original'])
exporter.main()
publish_fixture_ota(tmp_path, monkeypatch, exporter, '1.0.6', '1.1.0', 'original')
artifact=tmp_path/'发布更新相关/OTA数据包/1.1.0/matrix-screen-controller-1.1.0.ota'
old=artifact.read_bytes();old_history=(tmp_path/'发布记录.json').read_bytes()
(source/'fix.py').write_text('FIXED = True\n',encoding='utf-8')