Files
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00

92 lines
3.7 KiB
Python

import hashlib
import io
import tempfile
import unittest
import urllib.error
from unittest.mock import patch
from pathlib import Path
try:
import contracts.product_lock as product_lock
except ModuleNotFoundError:
import product_lock
class ProductLockTests(unittest.TestCase):
def test_verify_digests_checks_all_assets(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
asset = root / "msime.db"
asset.write_bytes(b"database")
digest = hashlib.sha256(b"database").hexdigest()
product_lock.verify_digests(root, {"msime.db": digest})
asset.write_bytes(b"changed")
with self.assertRaises(ValueError):
product_lock.verify_digests(root, {"msime.db": digest})
def test_verify_digests_rejects_path_traversal(self):
with tempfile.TemporaryDirectory() as temporary:
with self.assertRaises(ValueError):
product_lock.verify_digests(Path(temporary), {"../outside": "0" * 64})
def test_manifest_provenance_is_bound_to_lock(self):
calls = []
def verify_product(directory, profile, required_files):
calls.append((Path(directory), profile, required_files))
return {"source": {"repository": "example/repo", "commit": "abc", "dirty": False}}
manifest = product_lock.verify_manifest_provenance(Path("fixture"), "dictionary-manifest.json", verify_product,
{"msime.db"}, "example/repo", "abc")
self.assertEqual(manifest["source"]["commit"], "abc")
self.assertEqual(calls[0][1:], ("desktop", {"msime.db"}))
def test_published_checksums_ignores_malformed_lines(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "SHA256SUMS.txt"
path.write_text("a" * 64 + " msime.db\nnot a checksum\n", encoding="utf-8")
self.assertEqual(product_lock.published_checksums(path), {"msime.db": "a" * 64})
def test_published_checksums_rejects_duplicates(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "SHA256SUMS.txt"
path.write_text("a" * 64 + " msime.db\n" + "b" * 64 + " msime.db\n", encoding="utf-8")
with self.assertRaises(ValueError):
product_lock.published_checksums(path)
def test_resolve_tag_commit_rejects_non_dictionary_tag_before_git(self):
with patch.object(product_lock.subprocess, "check_output") as check_output:
with self.assertRaises(ValueError):
product_lock.resolve_tag_commit("https://example.invalid/repo.git", "latest")
check_output.assert_not_called()
def test_download_retries_and_replaces_atomically(self):
attempts = 0
class Response(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
def open_url(_url, timeout):
nonlocal attempts
self.assertEqual(timeout, 7)
attempts += 1
if attempts == 1:
raise urllib.error.URLError("temporary")
return Response(b"new")
with tempfile.TemporaryDirectory() as temporary:
target = Path(temporary) / "asset"
target.write_bytes(b"old")
with patch.object(product_lock.urllib.request, "urlopen", side_effect=open_url):
product_lock.download_with_retries("https://example.invalid/asset", target, attempts=2, timeout=7)
self.assertEqual(target.read_bytes(), b"new")
self.assertEqual(attempts, 2)
if __name__ == "__main__":
unittest.main()