feat(license): wafer license gate, runtime warning, and first-launch wizard

- Gate READ/ERASE/DEBUG behind wafer license check; log hint to load .bin
- waferRuntimeExceeded() warns when onboard runtime > 350s
- LicenseModel: remove-license (soft hide, .bin stays on disk) via
  removed_licenses.txt; re-loading un-hides
- FirstLaunchWizard: 2-step popup (license + save dir) shown on first run
- Per-tab file memory in HomePage (map/graph/data don't bleed state)
- Merge test_device_controller_license into test_device_controller
This commit is contained in:
jack
2026-07-12 20:02:25 -07:00
parent 6003bde84d
commit 67158ed7ab
15 changed files with 699 additions and 54 deletions
+74
View File
@@ -135,6 +135,63 @@ def test_model_load_license_file_url(tmp_path, qapp):
assert model.loadLicenseFile("file://" + src) is True
def test_model_remove_license_keeps_bin_on_disk(tmp_path, qapp):
(tmp_path / "licenses").mkdir()
write_license(tmp_path / "licenses", serial="A00001", level="01")
model = LicenseModel(str(tmp_path))
assert len(model.licenses) == 1
assert model.removeLicense("A00001", "01") is True
assert model.licenses == []
# app-side removal only — the vendor-issued .bin is never deleted
assert (tmp_path / "licenses" / "A00001-01.bin").exists()
# sticky across a fresh scan (not just until the next refresh)
model.refresh()
assert model.licenses == []
# sticky across a real relaunch — brand new instance, no shared state
relaunched = LicenseModel(str(tmp_path))
assert relaunched.licenses == []
def test_model_remove_license_missing_returns_false(tmp_path, qapp):
model = LicenseModel(str(tmp_path))
assert model.removeLicense("Z99999", "00") is False
def test_model_reload_after_remove_unhides(tmp_path, qapp):
src_dir = tmp_path / "dl"
src_dir.mkdir()
src = write_license(src_dir, serial="A00001", level="01")
model = LicenseModel(str(tmp_path / "appdata"))
assert model.loadLicenseFile(src) is True
assert len(model.licenses) == 1
model.removeLicense("A00001", "01")
assert model.licenses == []
# explicit reload of the same file brings it back
assert model.loadLicenseFile(src) is True
assert len(model.licenses) == 1
def test_model_reload_in_place_unhides(tmp_path, qapp):
"""Un-hiding by re-picking the .bin from its own <data_dir>/licenses/ dir —
src and dst resolve to the same file, must not error on the copy step."""
model = LicenseModel(str(tmp_path / "appdata"))
write_license(tmp_path / "appdata" / "licenses", serial="A00001", level="01")
model.refresh()
assert len(model.licenses) == 1
model.removeLicense("A00001", "01")
assert model.licenses == []
in_place = tmp_path / "appdata" / "licenses" / "A00001-01.bin"
assert model.loadLicenseFile(str(in_place)) is True
assert len(model.licenses) == 1
def test_model_load_rejects_invalid(tmp_path, qapp):
src_dir = tmp_path / "dl"
src_dir.mkdir()
@@ -167,6 +224,23 @@ def test_replay_state_none_then_temporary(tmp_path, qapp):
assert model.replayTrialExpired() is False
def test_start_replay_trial_does_not_reset_existing_marker(tmp_path, qapp):
"""Repeat calls must not push the clock forward — the original
touch(exist_ok=True) bumped mtime on every call, which would let anyone
re-trigger the trial start (e.g. an over-limit wafer re-detected) reset
the 30-day window indefinitely."""
model = LicenseModel(str(tmp_path))
assert model.startReplayTrial() is True
first_mtime = model._trial_marker.stat().st_mtime
# Simulate the marker being old (as if most of the trial has elapsed).
old_time = first_mtime - 25 * 86400
os.utime(model._trial_marker, (old_time, old_time))
assert model.startReplayTrial() is True
assert model._trial_marker.stat().st_mtime == old_time
def test_replay_state_permanent_with_level1(tmp_path, qapp):
(tmp_path / "licenses").mkdir()
write_license(tmp_path / "licenses", level="01")