446 lines
30 KiB
Python
446 lines
30 KiB
Python
"""Independent frozen archive and packaged-server checks; never runs Cargo or GUI."""
|
|
from pathlib import Path, PurePosixPath
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
import stat
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import time
|
|
import tomllib
|
|
import traceback
|
|
import urllib.request
|
|
import uuid
|
|
import zipfile
|
|
|
|
OUT = Path(__file__).resolve().parent
|
|
ROOT = OUT.parents[1]
|
|
PREP = ROOT / '.local/contextual-prerelease-20260911'
|
|
NAME = 'ArcadiaStudio-0.2.0-preview.contextual.20260911-x86_64-pc-windows-gnullvm'
|
|
ZIP = PREP / 'packages' / (NAME + '.zip')
|
|
EXTRACT = OUT / 'extracted'
|
|
PACKAGE = EXTRACT / NAME
|
|
SOURCE = PREP / 'source'
|
|
FROZEN = json.loads((OUT / 'frozen-inputs.json').read_text(encoding='utf-8'))
|
|
SOURCE_COMMIT = FROZEN['source_commit']
|
|
BASE_COMMIT = 'b2c3e54001fc19a05423bcd5808ceea78d13ff59'
|
|
EXPECTED_ZIP = FROZEN['zip_sha256'].lower()
|
|
BINARIES = {
|
|
'arcadia-studio.exe': '5657bab5c841939b66f901efe6c0ffb2d7cbbe8cc5f111a0ca3de8073d3846ce',
|
|
'arcadia-server.exe': 'efeeb89c0d2e0f93df4472cb3e9f48b2b722a37ae1043c90a5a56f2e845fe665',
|
|
'libunwind.dll': 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4',
|
|
}
|
|
RESULT = {'checks': [], 'scope': sys.argv[1], 'started_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
|
|
|
|
|
|
def check(condition, label):
|
|
if not condition:
|
|
raise AssertionError(label)
|
|
RESULT['checks'].append(label)
|
|
|
|
|
|
def sha(data):
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def read_json(path):
|
|
return json.loads(path.read_text(encoding='utf-8-sig'))
|
|
|
|
|
|
def write_json(path, value):
|
|
with path.open('x', encoding='utf-8', newline='\n') as f:
|
|
json.dump(value, f, indent=2, ensure_ascii=False)
|
|
f.write('\n')
|
|
|
|
|
|
def file_map(directory):
|
|
result = {}
|
|
for p in directory.rglob('*'):
|
|
check(not p.is_symlink() and not (getattr(p.lstat(), 'st_file_attributes', 0) & 0x400), 'ordinary path: ' + p.relative_to(directory).as_posix())
|
|
if p.is_file():
|
|
result[p.relative_to(directory).as_posix()] = sha(p.read_bytes())
|
|
return result
|
|
|
|
|
|
def safe_name(name):
|
|
p = PurePosixPath(name)
|
|
return not p.is_absolute() and p.as_posix() == name and '\\' not in name and ':' not in name and all(c not in ('', '.', '..') and not c.endswith(('.', ' ')) and not re.match(r'(?i)^(con|nul|prn|aux|com[1-9]|lpt[1-9])(\.|$)', c) for c in p.parts)
|
|
|
|
|
|
def git_blobs(ref, paths):
|
|
raw = subprocess.run(['git', 'cat-file', '--batch'], input=''.join(ref + ':' + p + '\n' for p in paths).encode(), cwd=ROOT, capture_output=True, check=True).stdout
|
|
result, cursor = {}, 0
|
|
for p in paths:
|
|
end = raw.index(b'\n', cursor)
|
|
header = raw[cursor:end].split()
|
|
check(len(header) == 3 and header[1] == b'blob', 'Git blob exists: ' + ref[:7] + ':' + p)
|
|
size = int(header[2])
|
|
result[p] = raw[end + 1:end + 1 + size]
|
|
cursor = end + 2 + size
|
|
check(cursor == len(raw), 'Git batch framing complete: ' + ref[:7])
|
|
return result
|
|
|
|
|
|
def artifact():
|
|
check(not EXTRACT.exists(), 'clean extraction destination absent')
|
|
check(ZIP.stat().st_size == FROZEN['zip_bytes'] and sha(ZIP.read_bytes()) == EXPECTED_ZIP, 'frozen ZIP size and SHA256')
|
|
with zipfile.ZipFile(ZIP) as archive:
|
|
infos = archive.infolist()
|
|
names = [i.filename for i in infos]
|
|
check(0 < len(infos) == FROZEN['zip_members'], 'measured ZIP members equal frozen producer inventory')
|
|
check(len({n.casefold() for n in names}) == len(names), 'no duplicate or case-colliding ZIP names')
|
|
check(all(safe_name(n) and n.startswith(NAME + '/') for n in names), 'one safe ZIP root without traversal or Windows special paths')
|
|
check(all(not i.is_dir() and stat.S_IFMT(i.external_attr >> 16) in (0, stat.S_IFREG) for i in infos), 'ordinary file-only ZIP entries')
|
|
check(all(i.date_time == (2000, 1, 1, 0, 0, 0) and i.flag_bits & 1 == 0 for i in infos), 'fixed timestamps and no encrypted entries')
|
|
check(archive.testzip() is None, 'all ZIP CRCs')
|
|
EXTRACT.mkdir()
|
|
archive.extractall(EXTRACT)
|
|
archive_hashes = {i.filename[len(NAME) + 1:]: sha(archive.read(i)) for i in infos}
|
|
actual = file_map(PACKAGE)
|
|
check(actual == archive_hashes, 'complete extracted file set and bytes equal ZIP')
|
|
sums = {}
|
|
for line in (PACKAGE / 'SHA256SUMS.txt').read_text().splitlines():
|
|
match = re.fullmatch(r'([0-9a-f]{64}) (.+)', line)
|
|
check(match is not None and safe_name(match[2]) and match[2] not in sums, 'well-formed unique checksum row')
|
|
sums[match[2]] = match[1]
|
|
check(sums == {p: h for p, h in actual.items() if p != 'SHA256SUMS.txt'}, 'checksum inventory covers every other file exactly')
|
|
package = read_json(PACKAGE / 'package.json')
|
|
inputs = package['input_sha256']
|
|
generated = {'SHA256SUMS.txt', 'START-HERE.txt', 'THIRD-PARTY-NOTICES.txt', 'dependency-notices.json', 'documentation-check.json', 'package.json'}
|
|
check(set(actual) - set(inputs) == generated and all(actual[p] == h.lower() for p, h in inputs.items()), 'complete package input hashes plus exactly six generated files')
|
|
check(package['version'] == '0.2.0-preview.contextual.20260911' and package['target'] == 'x86_64-pc-windows-gnullvm', 'version and target')
|
|
check(all(actual[p] == h for p, h in BINARIES.items()), 'exact GUI, server and runtime hashes')
|
|
for p in BINARIES:
|
|
data = (PACKAGE / p).read_bytes()
|
|
offset = struct.unpack_from('<I', data, 0x3c)[0]
|
|
check(data[:2] == b'MZ' and data[offset:offset + 4] == b'PE\0\0' and struct.unpack_from('<H', data, offset + 4)[0] == 0x8664, 'ordinary PE x86_64: ' + p)
|
|
check({p for p in actual if '/' not in p} == {'arcadia-server.exe', 'arcadia-studio.exe', 'libunwind.dll', 'LICENSE', 'README.md'} | generated, 'only declared portable top-level files')
|
|
check({p.split('/')[0] for p in actual if '/' in p} == {'deployment', 'docs', 'examples', 'licenses'}, 'no harness, private profile, source checkout or credential-directory payload')
|
|
for p in actual:
|
|
if p.startswith(('docs/', 'examples/', 'deployment/')) or p == 'LICENSE':
|
|
check((SOURCE / p).is_file() and sha((SOURCE / p).read_bytes()) == actual[p], 'public source document/example bytes: ' + p)
|
|
RESULT['archive'] = {'path': str(ZIP), 'sha256': EXPECTED_ZIP, 'bytes': ZIP.stat().st_size, 'file_count': len(actual), 'files': actual}
|
|
RESULT['package_claims'] = {k: package[k] for k in ('version', 'target', 'acceptance', 'build', 'build_evidence')}
|
|
check(actual['README.md'] == sha((SOURCE / 'scripts/package-readme.md').read_bytes()), 'packaged README equals its explicit public source input')
|
|
provenance()
|
|
notices(actual)
|
|
check(file_map(PACKAGE) == actual, 'artifact verification leaves extracted package unchanged')
|
|
check(sha(ZIP.read_bytes()) == EXPECTED_ZIP, 'artifact verification leaves frozen ZIP unchanged')
|
|
|
|
|
|
def local_source_map(directory):
|
|
result = {}
|
|
for p in directory.rglob('*'):
|
|
rel = p.relative_to(directory)
|
|
check(not p.is_symlink() and not (getattr(p.lstat(), 'st_file_attributes', 0) & 0x400), 'ordinary source input: ' + str(p))
|
|
if p.is_file() and rel.parts[0] != 'target' and p.name != '.cargo-ok':
|
|
result[rel.as_posix()] = sha(p.read_bytes())
|
|
return result
|
|
|
|
|
|
def provenance():
|
|
gui_path = ROOT / '.local/contextual-source-layout-20260911/gui-verified/manifest.json'
|
|
input_path = gui_path.parent / '../inputs4.json'
|
|
check(sha(gui_path.read_bytes()) == '4beb64b955aecf5ae3bfb606316ec1fc9d17d288ed85023515148bb1890f034f', 'original GUI5657 manifest identity')
|
|
check(sha(input_path.read_bytes()) == '669724858967d83ab7da5f78bfafb6065e2bcab3bad0e68d0a74feb86c7195e7', 'original GUI5657 raw input manifest identity')
|
|
gui = read_json(gui_path)
|
|
inputs = read_json(input_path)
|
|
expected = {i['path'].replace('\\', '/'): i['sha256'].lower() for i in inputs['inputs']}
|
|
check(len(expected) == len(inputs['inputs']) == 238, 'measured 238 unique original GUI inputs')
|
|
check({i['file']: i['sha256'].lower() for i in gui['artifacts']} == {k: BINARIES[k] for k in ('arcadia-studio.exe', 'libunwind.dll')}, 'GUI record binds shipped GUI and runtime')
|
|
derived = git_blobs(SOURCE_COMMIT, list(expected))
|
|
base = git_blobs(BASE_COMMIT, list(expected))
|
|
differences = []
|
|
mixed = 'crates/oa-desktop/src/behavior/creation.rs'
|
|
wanted = {'docs/openapi.json'} | {
|
|
'crates/oa-desktop/src/' + p for p in (
|
|
'behavior/creation.rs', 'behavior/scenario/workspace.rs',
|
|
'behavior/topology/contextual.rs', 'guide.rs',
|
|
'semantic_review/artifacts_view.rs', 'semantic_review/attachments_view.rs',
|
|
'semantic_review/keyed_rows.rs', 'semantic_review/keyed_tests.rs',
|
|
'semantic_review/keyed.rs', 'semantic_review/ordered.rs')
|
|
}
|
|
for p, h in expected.items():
|
|
current = (SOURCE / p).read_bytes()
|
|
check(sha(derived[p]) == h == sha(current), 'original GUI input equals reconstructed Git blob and checkout: ' + p)
|
|
if sha(base[p]) != h:
|
|
check(base[p].replace(b'\r\n', b'\n') == current.replace(b'\r\n', b'\n'), 'restoration changes only newline bytes: ' + p)
|
|
uniform = base[p].replace(b'\r\n', b'\n').replace(b'\n', b'\r\n')
|
|
if p == mixed:
|
|
crlf = current.count(b'\r\n')
|
|
bare_lf = current.count(b'\n') - crlf
|
|
check(crlf > 0 and bare_lf > 0 and sha(uniform) != h, 'creation.rs retains exact mixed newlines; uniform conversion is insufficient')
|
|
else:
|
|
check(sha(uniform) == h, 'exact uniform CRLF restoration: ' + p)
|
|
differences.append(p)
|
|
check(set(differences) == wanted and len(differences) == 11, 'exact eleven documented newline restorations')
|
|
delta = subprocess.run(['git', 'diff', '--name-only', BASE_COMMIT, SOURCE_COMMIT], cwd=ROOT, check=True, capture_output=True).stdout.decode().splitlines()
|
|
check(set(delta) == wanted | {'.gitattributes'}, 'reconstruction changes only eleven exact inputs and preservation attributes')
|
|
semantic_delta = subprocess.run(['git', 'diff', '--name-only', '--ignore-space-at-eol', BASE_COMMIT, SOURCE_COMMIT], cwd=ROOT, check=True, capture_output=True).stdout.decode().splitlines()
|
|
check(semantic_delta == ['.gitattributes'], 'no non-newline source change in reconstruction')
|
|
|
|
origin_path = ROOT / 'harness/experiments/textual-authoring-20260911-local/provenance.json'
|
|
origin = read_json(origin_path)
|
|
check(origin['exe_sha256'].lower() == BINARIES['arcadia-server.exe'], 'original independent server record binds unchanged EFEE')
|
|
tar_path = ROOT / '.local/linux-qa/runtime/source7-20260907T082753190288Z.tar.gz'
|
|
check(sha(tar_path.read_bytes()) == 'cbb371a93a562e1e641d7629fe351e016d63e66fc9112499b3fed6d5cd171f78', 'original server source tar identity')
|
|
server_expected = {i['path']: i['snapshot_sha256'].lower() for i in origin['source_comparisons']}
|
|
check(len(server_expected) == len(origin['source_comparisons']) == 120, 'measured 120 unique originally recorded server source files')
|
|
server_base = git_blobs(BASE_COMMIT, list(server_expected))
|
|
tar_manifest = read_json(tar_path.with_suffix('').with_suffix('.tar.manifest.json'))
|
|
with tarfile.open(tar_path) as tar:
|
|
members = {m.name.removeprefix('./'): m for m in tar.getmembers() if m.isfile()}
|
|
for p, h in server_expected.items():
|
|
check(sha(tar.extractfile(members[p]).read()) == h == sha(server_base[p]) == sha((SOURCE / p).read_bytes()), 'original server tar equals base Git and reconstructed input: ' + p)
|
|
for crate in ('oa-core', 'oa-server'):
|
|
prefix = 'crates/' + crate + '/'
|
|
expected_crate = {p[len(prefix):]: h for p, h in server_expected.items() if p.startswith(prefix)}
|
|
check(local_source_map(SOURCE / 'crates' / crate) == expected_crate, 'complete unchanged server input closure: ' + crate)
|
|
shared = [p for p in members if p in ('Cargo.toml', 'Cargo.lock', 'docs/openapi.json') or p.startswith('build/')]
|
|
old_shared = {p: tar.extractfile(members[p]).read() for p in shared}
|
|
for p, raw in old_shared.items():
|
|
check(sha(raw) == tar_manifest['files'][p].lower(), 'original server shared input matches original tar manifest: ' + p)
|
|
if p not in ('Cargo.toml', 'Cargo.lock'):
|
|
check(raw == (SOURCE / p).read_bytes(), 'unchanged actual shared server build bytes: ' + p)
|
|
|
|
old_manifest = tomllib.loads(old_shared['Cargo.toml'].decode())
|
|
new_manifest = tomllib.loads((SOURCE / 'Cargo.toml').read_text())
|
|
old_members = old_manifest['workspace']['members']
|
|
check(new_manifest['workspace']['members'] == old_members + ['crates/oa-cli'], 'only explicit CLI workspace member added since original server build')
|
|
new_manifest['workspace']['members'] = old_members
|
|
check(new_manifest == old_manifest, 'workspace dependency, resolver and profile semantics remain unchanged')
|
|
old_lock = tomllib.loads(old_shared['Cargo.lock'].decode())
|
|
new_lock = tomllib.loads((SOURCE / 'Cargo.lock').read_text())
|
|
old_packages = {(p['name'], p['version'], p.get('source')): p for p in old_lock.pop('package')}
|
|
new_packages = {(p['name'], p['version'], p.get('source')): p for p in new_lock.pop('package')}
|
|
cli = new_packages.pop(('oa-cli', '0.2.0', None))
|
|
check(cli == {'name': 'oa-cli', 'version': '0.2.0', 'dependencies': ['oa-core', 'serde', 'serde_json']}, 'one explicit local CLI lock entry, no new feature request')
|
|
check(old_lock == new_lock and old_packages == new_packages, 'all original lock identities/dependencies/checksums unchanged')
|
|
cli_manifest = tomllib.loads((SOURCE / 'crates/oa-cli/Cargo.toml').read_text())
|
|
check(cli_manifest['dependencies'] == {'oa-core': {'path': '../oa-core'}, 'serde': {'workspace': True}, 'serde_json': {'workspace': True}}, 'CLI uses existing core/serde requests')
|
|
check(not set(cli_manifest).intersection({'features', 'build-dependencies', 'target'}), 'CLI adds no feature/build/target request')
|
|
evidence = read_json(PACKAGE / 'package.json')['build_evidence']['evidence']
|
|
check(evidence == read_json(PREP / 'build-evidence.json'), 'packaged build evidence equals the producer frozen evidence bytes semantically')
|
|
check(evidence['binary_sha256'] == {k: v for k, v in BINARIES.items() if k.endswith('.exe')}, 'build evidence binds both exact shipped executables')
|
|
check(SOURCE_COMMIT in evidence['source_identity'] and BASE_COMMIT in evidence['source_identity'], 'evidence distinguishes reconstruction from release base')
|
|
check('not' in evidence['source_identity'].lower() and 'original build commit' in evidence['source_identity'].lower(), 'reconstruction is not claimed as original build commit')
|
|
check('server' in evidence['profile'].lower() and 'september 7' in evidence['profile'].lower(), 'server retains its separate historical build provenance')
|
|
check('neither a new server build nor release-profile/reproducible-build acceptance is claimed' in evidence['profile'].lower(), 'development/reproducibility limitation retained')
|
|
preserved_originals = {
|
|
'evidence/gui-build-status.json': '.local/contextual-source-layout-20260911/gui1-status.json',
|
|
'evidence/gui-stage-manifest.json': '.local/contextual-source-layout-20260911/gui-verified/manifest.json',
|
|
'evidence/gui-inputs4.json': '.local/contextual-source-layout-20260911/inputs4.json',
|
|
'evidence/gui-full679-result.json': 'harness/experiments/contextual-source-layout-full-20260911/full-result.json',
|
|
'evidence/server-original-gates.json': '.local/source7-post-routing-gates-status.json',
|
|
'evidence/server-original-binary-hashes.json': '.local/native-qa/source7-post-routing-binary-hashes.json',
|
|
'evidence/server-prior-input-comparisons.json': '.local/authoring-prerelease-20260911/inventory.json',
|
|
}
|
|
referred = set()
|
|
for binary, references in evidence['per_binary_provenance'].items():
|
|
check(binary in BINARIES and binary.endswith('.exe'), 'per-binary history names a shipped executable')
|
|
for value in references.values():
|
|
if value.startswith('evidence/'):
|
|
check(value in preserved_originals, 'known independently identified historical evidence path: ' + value)
|
|
check((PREP / value).read_bytes() == (ROOT / preserved_originals[value]).read_bytes(), 'preserved per-binary evidence equals original bytes: ' + value)
|
|
referred.add(value)
|
|
check(referred == set(preserved_originals), 'both original binary histories remain complete and separate')
|
|
check('not files inside the distributable ZIP' in evidence['provenance_reference_base'], 'external provenance paths are not advertised as bundled files')
|
|
RESULT['provenance'] = {
|
|
'gui_manifest_sha256': sha(gui_path.read_bytes()), 'gui_inputs_sha256': sha(input_path.read_bytes()),
|
|
'gui_inputs': len(expected), 'historical_gui_input_record': {k: v for k, v in inputs.items() if k != 'inputs'},
|
|
'source_commit': SOURCE_COMMIT, 'base_commit': BASE_COMMIT, 'eol_restorations': differences,
|
|
'mixed_newline_input': mixed, 'server_source_files': len(server_expected),
|
|
'server_origin_sha256': sha(origin_path.read_bytes()), 'shared_input_files': shared,
|
|
'original_lock_packages': len(old_packages), 'additional_local_lock_package': cli,
|
|
'original_server_lock_sha256': sha(old_shared['Cargo.lock']),
|
|
'current_gui_lock_sha256': sha((SOURCE / 'Cargo.lock').read_bytes()),
|
|
'tar_sha256': sha(tar_path.read_bytes())
|
|
}
|
|
|
|
|
|
def notices(actual):
|
|
notice = read_json(PACKAGE / 'dependency-notices.json')
|
|
check(notice['root_packages'] == ['oa-desktop', 'oa-server'], 'exact desktop/server notice roots')
|
|
previous_path = ROOT / '.local/authoring-prerelease-review-20260911/extracted/ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm/dependency-notices.json'
|
|
check(sha(previous_path.read_bytes()) == '98fab15c1a2bb3aa727c6d52d5c69435b15efa56b6668ed717e7a04cf86f9b52', 'previous accepted notice inventory identity')
|
|
previous = read_json(previous_path)
|
|
key = lambda p: (p['name'], p['version'], p['source'])
|
|
old = {key(p): p for p in previous['packages']}
|
|
current = {key(p): p for p in notice['packages']}
|
|
check(len(current) == len(notice['packages']) and set(current) == set(old), 'measured notice package identities remain the accepted desktop/server graph')
|
|
for identity, p in current.items():
|
|
if p['source'] is not None:
|
|
check(p == old[identity], 'unchanged registry identity/features/source inventory/notices: ' + p['name'])
|
|
check(notice['rust'] == previous['rust'] and notice['runtime'] == previous['runtime'], 'unchanged exact Rust/runtime inventory and notice provenance')
|
|
check(notice['cargo_lock_sha256'].lower() == sha((SOURCE / 'Cargo.lock').read_bytes()), 'notice lock equals reconstructed current GUI lock, distinct from historical server lock')
|
|
for p, h in notice['workspace_build_inputs_sha256'].items():
|
|
check(sha((SOURCE / p).read_bytes()) == h, 'notice common build input: ' + p)
|
|
lock = tomllib.loads((SOURCE / 'Cargo.lock').read_text())
|
|
locked = {(p['name'], p['version'], p.get('source')): p for p in lock['package']}
|
|
referenced = set()
|
|
def refs(items):
|
|
for i in items:
|
|
p = i['path']
|
|
check(safe_name(p) and p.startswith('licenses/') and actual.get(p) == i['sha256'].lower(), 'bundled exact notice reference: ' + p)
|
|
referenced.add(p)
|
|
fonts = []
|
|
for p in notice['packages']:
|
|
key = (p['name'], p['version'], p['source'])
|
|
check(key in locked and p['registry_checksum'] == locked[key].get('checksum'), 'notice identity/checksum occurs in historical lock: ' + p['name'] + '@' + p['version'])
|
|
check(len(p['notices']) > 0, 'package has notice text: ' + p['name'])
|
|
refs(p['notices'])
|
|
for f in p['embedded_fonts']:
|
|
check(len(f['notices']) > 0, 'embedded font notice present: ' + p['name'] + '/' + f['path'])
|
|
refs(f['notices'])
|
|
fonts.append({'package': p['name'], 'path': f['path'], 'sha256': f['sha256'], 'license': f['license']})
|
|
if p['source'] is None:
|
|
directory = SOURCE / 'crates' / p['name']
|
|
files = local_source_map(directory)
|
|
check(files == p['local_source_sha256'], 'complete ordinary local-crate inventory: ' + p['name'])
|
|
refs(notice['rust']['notices'])
|
|
refs(notice['runtime']['notices'])
|
|
check(notice['runtime']['sha256'] == BINARIES['libunwind.dll'], 'runtime notice is bound to shipped DLL')
|
|
library_dir = ROOT / '.local/toolchain/rustup/toolchains/stable-x86_64-pc-windows-gnullvm/lib/rustlib/x86_64-pc-windows-gnullvm/lib'
|
|
check(set(notice['rust']['target_libraries_sha256']) == set(previous['rust']['target_libraries_sha256']), 'measured compiler target-library file set unchanged')
|
|
for name, h in notice['rust']['target_libraries_sha256'].items():
|
|
check(sha((library_dir / name).read_bytes()) == h, 'retained compiler target-library bytes: ' + name)
|
|
RESULT['notices'] = {'packages': len(notice['packages']), 'roots': notice['root_packages'], 'lock_sha256': notice['cargo_lock_sha256'], 'notice_reference_files': len(referenced), 'additional_license_files': sorted({p for p in actual if p.startswith('licenses/')} - referenced), 'embedded_fonts': fonts, 'rust_version': notice['rust']['version'], 'runtime_provenance': notice['runtime']['provenance'], 'scope': notice['scope']}
|
|
|
|
|
|
def server():
|
|
prior = read_json(OUT / 'artifact-result.json')
|
|
check(prior['status'] == 'PASS' and file_map(PACKAGE) == prior['archive']['files'], 'previously verified extracted package is unchanged')
|
|
runtime = OUT / 'server-run'
|
|
runtime.mkdir()
|
|
data_dir = runtime / 'data'
|
|
temp = runtime / 'temp'
|
|
temp.mkdir()
|
|
windows = Path(os.environ['SystemRoot'])
|
|
env = {'SystemRoot': str(windows), 'WINDIR': str(windows), 'PATH': str(windows / 'System32') + ';' + str(windows), 'TEMP': str(temp), 'TMP': str(temp)}
|
|
exe = PACKAGE / 'arcadia-server.exe'
|
|
RESULT['environment'] = env
|
|
RESULT['runtime_package_file_count'] = len(prior['archive']['files'])
|
|
RESULT['processes'] = []
|
|
for arg in ('--version', '--help'):
|
|
output = subprocess.run([str(exe), arg], cwd=PACKAGE, env=env, capture_output=True, timeout=15, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
(runtime / (arg[2:] + '.stdout')).write_bytes(output.stdout)
|
|
(runtime / (arg[2:] + '.stderr')).write_bytes(output.stderr)
|
|
check(output.returncode == 0 and not output.stderr, arg + ' succeeds under clean runtime PATH')
|
|
if arg == '--version':
|
|
check(output.stdout.decode().strip() == 'arcadia-server 0.2.0', 'server reports underlying application version 0.2.0')
|
|
check(not data_dir.exists(), 'help/version do not create explicit test data directory')
|
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
request_number = 0
|
|
def request(port, method, path, payload=None):
|
|
nonlocal request_number
|
|
request_number += 1
|
|
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode()
|
|
req = urllib.request.Request(f'http://127.0.0.1:{port}' + path, data=body, method=method, headers={} if body is None else {'Content-Type': 'application/json'})
|
|
with opener.open(req, timeout=10) as response:
|
|
raw = response.read()
|
|
meta = {'method': method, 'path': path, 'status': response.status, 'headers': dict(response.headers), 'body_sha256': sha(raw)}
|
|
prefix = runtime / f'http-{request_number:02d}'
|
|
prefix.with_suffix('.body').write_bytes(raw)
|
|
write_json(prefix.with_suffix('.json'), meta)
|
|
return meta, raw
|
|
def start(index):
|
|
with socket.socket() as s:
|
|
s.bind(('127.0.0.1', 0))
|
|
port = s.getsockname()[1]
|
|
out = (runtime / f'server-{index}.stdout').open('xb')
|
|
err = (runtime / f'server-{index}.stderr').open('xb')
|
|
args = [str(exe), '--bind', f'127.0.0.1:{port}', '--data-dir', str(data_dir)]
|
|
process = subprocess.Popen(args, cwd=PACKAGE, env=env, stdout=out, stderr=err, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
out.close()
|
|
err.close()
|
|
RESULT['processes'].append({'pid': process.pid, 'port': port, 'arguments': args})
|
|
deadline = time.monotonic() + 15
|
|
while True:
|
|
if process.poll() is not None:
|
|
raise AssertionError('packaged server exited before listening')
|
|
try:
|
|
with socket.create_connection(('127.0.0.1', port), timeout=.2):
|
|
break
|
|
except OSError:
|
|
if time.monotonic() > deadline:
|
|
process.terminate()
|
|
process.wait(timeout=5)
|
|
raise AssertionError('packaged server did not listen within 15s')
|
|
time.sleep(.05)
|
|
return process, port
|
|
def stop(process):
|
|
process.terminate()
|
|
code = process.wait(timeout=10)
|
|
RESULT['processes'][-1]['exit_after_explicit_terminate'] = code
|
|
process = None
|
|
try:
|
|
process, port = start(1)
|
|
for path in ('/health', '/health/ready'):
|
|
meta, raw = request(port, 'GET', path)
|
|
check(meta['status'] == 200 and json.loads(raw) == {'status': 'ok'}, path + ' contract')
|
|
meta, raw = request(port, 'GET', '/api/v1/openapi.json')
|
|
check(meta['status'] == 200 and raw == (PACKAGE / 'docs/openapi.json').read_bytes(), 'served OpenAPI equals complete packaged source bytes')
|
|
name = 'Independent contextual package persistence — 救助 20260911'
|
|
meta, raw = request(port, 'POST', '/api/v1/projects', {'name': name})
|
|
created = json.loads(raw)
|
|
check(meta['status'] == 201, 'one create returns 201')
|
|
resource = str(uuid.UUID(created['resource_id']))
|
|
model_id = str(uuid.UUID(created['model_id']))
|
|
expected_summary = {'resource_id': resource, 'revision': 1, 'model_id': model_id, 'schema_version': 7, 'name': name, 'elements': 0, 'relations': 0, 'migration': None}
|
|
check(created == expected_summary and resource != model_id, 'complete create summary with distinct fresh resource/model IDs')
|
|
etag = f'"p-{resource}-r1"'
|
|
headers = {k.lower(): v for k, v in meta['headers'].items()}
|
|
path = '/api/v1/projects/' + resource
|
|
check(headers.get('etag') == etag and headers.get('location') == path, 'create ETag and resource Location')
|
|
meta, raw = request(port, 'GET', path)
|
|
stored = json.loads(raw)
|
|
model = {'schema_version': 7, 'id': model_id, 'alias': 'mission', 'name': name, 'description': '', 'elements': {}, 'relations': {}, 'diagrams': {}, 'artifact_links': {}, 'traces': {}}
|
|
source = f'arcadia 7;\nproject mission {json.dumps(name, ensure_ascii=False)} id "{model_id}" {{\n description "";\n}}\n'
|
|
expected = {'resource_id': resource, 'revision': 1, 'model': model, 'source_project': {'entry': 'project.arcadia', 'files': {'project.arcadia': source}}}
|
|
check(meta['status'] == 200 and stored == expected, 'GET equals independently predicted complete Project and SourceBundle')
|
|
check({k.lower(): v for k, v in meta['headers'].items()}.get('etag') == etag, 'GET ETag equals created revision')
|
|
disk = data_dir / (resource + '.json')
|
|
disk_bytes = disk.read_bytes()
|
|
check(json.loads(disk_bytes) == expected, 'durable disk envelope equals complete expected GET')
|
|
check({p.name for p in data_dir.iterdir()} == {'.arcadia.lock', disk.name}, 'private store contains only one project and lock')
|
|
stop(process)
|
|
process = None
|
|
process, port = start(2)
|
|
meta, raw = request(port, 'GET', '/health/ready')
|
|
check(meta['status'] == 200 and json.loads(raw) == {'status': 'ok'}, 'restarted packaged server ready')
|
|
meta, raw = request(port, 'GET', path)
|
|
check(meta['status'] == 200 and json.loads(raw) == expected, 'restart retains exact complete Project and SourceBundle')
|
|
check({k.lower(): v for k, v in meta['headers'].items()}.get('etag') == etag and disk.read_bytes() == disk_bytes, 'restart retains revision, ETag and every durable envelope byte')
|
|
RESULT['persistence'] = {'resource_id': resource, 'model_id': model_id, 'revision': 1, 'etag': etag, 'disk_sha256': sha(disk_bytes), 'expected': expected, 'stop_kind': 'explicit process termination after acknowledged durable create; not a graceful-drain claim'}
|
|
finally:
|
|
if process is not None and process.poll() is None:
|
|
stop(process)
|
|
check(file_map(PACKAGE) == prior['archive']['files'], 'server execution leaves complete extracted package unchanged')
|
|
check(sha(ZIP.read_bytes()) == EXPECTED_ZIP, 'frozen ZIP unchanged after independent runtime checks')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
result_path = OUT / (sys.argv[1] + '-result.json')
|
|
if result_path.exists():
|
|
raise SystemExit('Refusing to overwrite prior evidence: ' + str(result_path))
|
|
try:
|
|
{'artifact': artifact, 'server': server}[sys.argv[1]]()
|
|
RESULT['status'] = 'PASS'
|
|
except Exception as error:
|
|
RESULT['status'] = 'FAIL'
|
|
RESULT['error'] = repr(error)
|
|
RESULT['traceback'] = traceback.format_exc()
|
|
RESULT['finished_utc'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
|
RESULT['script_sha256'] = sha(Path(__file__).read_bytes())
|
|
RESULT['frozen_inputs_sha256'] = sha((OUT / 'frozen-inputs.json').read_bytes())
|
|
write_json(result_path, RESULT)
|
|
print(json.dumps({k: RESULT[k] for k in ('scope', 'status', 'started_utc', 'finished_utc')}, indent=2))
|
|
print('checks:', len(RESULT['checks']))
|
|
if RESULT['status'] == 'FAIL':
|
|
print(RESULT['traceback'])
|
|
sys.exit(1)
|