oil-ui-pro 0.15.4 → 0.22.2(上游 ui.oiloil.org 正式包)+ 记 P0-98 跨盘 rename 坑

- oil-ui-pro 升到 0.22.2:references 13→28 份(新增 16、删 imagery.md)、assets 增 cards/(22 组)与 fonts/、SKILL.md 顶层五步结构未改名。
- 升级路径:官方 `npx github:oil-oil/oil-cli update` 稳定失败(error=replace、报「检查目录权限」)。
  真身为 EXDEV —— 解压在 os.tmpdir()(C 盘)、技能目录在 E 盘,Windows 上 rename 不能跨盘;
  CLI 的 catch 把非 CliError 原生异常一律兜底成 replaceFailed,真错误一个字不吐。
  做法=借其 prepare() 只做下载+校验,再 cp 到同盘 stage、rename 交换;官方命令复验 up_to_date。
- 回归:product-planning 按名引用的 visual-review.md/tools.md/design-direction.md/style-explorer.md 全部健在;命名闸门 2061|0|27。
- session-mechanism/references/pitfalls.md 新增 P0-98(含通则:更新器把原生错误兜底成指错方向的人话)。
This commit is contained in:
admin committed 2026-10-14 10:21:46 +08:00
1 parent 053a6a6723
commit d9445b5888
126 files changed
+10345 -1401

No files matched your search

+84 -29
View File
@@ -13,19 +13,25 @@ SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "check_update.py"
LAUNCHER = SCRIPT.with_suffix(".sh")
SH = shutil.which("sh")
# 假的 npx:收到 update --path <目录> 时把那里的版本号改成 99.0.0,并记下调用
FAKE_NPX = """#!/bin/sh
echo "$@" >> "$FAKE_NPX_LOG"
[ -n "$FAKE_NPX_STDERR" ] && printf '%s\\n' "$FAKE_NPX_STDERR" >&2
[ -n "$FAKE_NPX_JSON" ] && printf '%s\\n' "$FAKE_NPX_JSON"
[ -n "$FAKE_NPX_FAIL" ] && exit 1
while [ $# -gt 0 ]; do
if [ "$1" = "--path" ]; then sed -i.bak 's/version: "0.10.0"/version: "99.0.0"/' "$2/SKILL.md"; fi
shift
done
# 配置来自测试文件,不能依赖被安全环境白名单过滤的假环境变量。
FAKE_NPX = f"""#!{sys.executable}
import json, os, sys
from pathlib import Path
c = json.loads(Path(__file__).with_name('settings.json').read_text())
with Path(c['log']).open('a') as log: log.write(' '.join(sys.argv[1:])+'\\n')
Path(c['env_log']).write_text(json.dumps({{k: k in os.environ for k in
['AWS_SECRET_ACCESS_KEY','OPENAI_API_KEY','NODE_OPTIONS','NPM_TOKEN','OIL_TOKEN','CI']}}))
if c.get('stderr'): print(c['stderr'], file=sys.stderr)
if c.get('json'): print(c['json'])
if c.get('fail'): sys.exit(1)
if '--path' in sys.argv:
skill = Path(sys.argv[sys.argv.index('--path')+1])/'SKILL.md'
skill.write_text(skill.read_text().replace('version: "0.10.0"','version: "99.0.0"'))
"""
FAKE_NODE = """#!/bin/sh
printf '%s\\n' "${FAKE_NODE_VERSION:-v20.0.0}"
FAKE_NODE = f"""#!{sys.executable}
import json
from pathlib import Path
print(json.loads(Path(__file__).with_name('settings.json').read_text()).get('node_version') or 'v20.0.0')
"""
@@ -88,7 +94,12 @@ class CheckUpdateTest(unittest.TestCase):
config.mkdir(parents=True, exist_ok=True)
(config / "config.json").write_text(json.dumps({"token": "oil_test", "email": "[email protected]"}), encoding="utf-8")
def run_check(self, api=None, launcher=False, **extra):
def run_check(self, api=None, launcher=False, update=True, **extra):
(self.bin / 'settings.json').write_text(json.dumps({
'log': str(self.npx_log), 'env_log': str(self.tmp / 'env.json'),
'stderr': extra.get('FAKE_NPX_STDERR'), 'json': extra.get('FAKE_NPX_JSON'),
'fail': extra.get('FAKE_NPX_FAIL'), 'node_version': extra.get('FAKE_NODE_VERSION'),
}), encoding='utf-8')
env = {k: v for k, v in os.environ.items() if not k.startswith("OIL_")}
env.update(HOME=str(self.tmp / "home"), APPDATA=str(self.tmp / "config"), LOCALAPPDATA=str(self.tmp / "state"),
XDG_STATE_HOME=str(self.tmp / "state"), XDG_CONFIG_HOME=str(self.tmp / "config"),
@@ -96,9 +107,10 @@ class CheckUpdateTest(unittest.TestCase):
LANG="zh_CN.UTF-8", LC_ALL="", LC_MESSAGES="",
OIL_API=api or f"http://127.0.0.1:{self.server.server_port}")
env.update(extra)
self.last_env = env
script = self.skill / "scripts" / ("check_update.sh" if launcher else "check_update.py")
result = subprocess.run([SH if launcher else sys.executable, str(script)],
capture_output=True, text=True, env=env, timeout=30)
result = subprocess.run([SH if launcher else sys.executable, str(script), *(['--update'] if update else [])],
capture_output=True, text=True, env=env, timeout=30, encoding="utf-8")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
return result.stdout
@@ -119,7 +131,7 @@ class CheckUpdateTest(unittest.TestCase):
def test_logged_in_paid_skill_updates_itself(self):
self.login()
out = self.run_check()
self.assertIn("Oil UI Pro 已自动更新到 99.0.0(原来是 0.10.0):动效改成三处基本动效。", out)
self.assertIn("Oil UI Pro 已自动更新到 99.0.0(原来是 0.10.0)。", out)
self.assertIn(f"update oil-ui-pro --path {self.skill}", self.npx_calls()[0])
self.assertIn('version: "99.0.0"', (self.skill / "SKILL.md").read_text(encoding="utf-8"))
self.assertEqual(self.run_check(), "")
@@ -128,10 +140,45 @@ class CheckUpdateTest(unittest.TestCase):
self.set_latest("99.0.0", free=True)
self.assertIn("已自动更新到 99.0.0", self.run_check())
def test_default_check_never_executes_the_updater_or_changes_the_skill(self):
self.set_latest("99.0.0", free=True)
before = (self.skill / 'SKILL.md').read_bytes()
out = self.run_check(update=False)
self.assertIn('有新版本 99.0.0', out)
self.assertIn('--update', out)
self.assertEqual(self.npx_calls(), [])
self.assertEqual((self.skill / 'SKILL.md').read_bytes(), before)
self.assertEqual(self.run_check(update=False), '')
def test_explicit_update_pins_cli_and_filters_unrelated_secrets(self):
self.set_latest('99.0.0', free=True)
self.assertIn('已自动更新', self.run_check(
AWS_SECRET_ACCESS_KEY='dummy-only-aws', OPENAI_API_KEY='dummy-only-openai',
NODE_OPTIONS='--require /nonexistent-oil-test.js', NPM_TOKEN='dummy-only-npm',
OIL_TOKEN='dummy-only-oil'))
self.assertRegex(self.npx_calls()[0], r'-y github:oil-oil/oil-cli#[0-9a-f]{40} update')
seen = json.loads((self.tmp / 'env.json').read_text(encoding="utf-8"))
for key in ('AWS_SECRET_ACCESS_KEY','OPENAI_API_KEY','NODE_OPTIONS','NPM_TOKEN'):
self.assertFalse(seen[key], key)
self.assertTrue(seen['OIL_TOKEN'])
self.assertTrue(seen['CI'])
def test_remote_release_notes_are_not_printed_or_cached(self):
marker = 'REMOTE_INSTRUCTION_DO_NOT_FOLLOW'
Versions.payload['skills']['oil-ui-pro']['history'][0]['notes'] = marker
self.assertNotIn(marker, self.run_check(update=False))
cache = self.tmp / 'state/oil/oil-ui-pro-update.json'
data = json.loads(cache.read_text(encoding="utf-8"))
self.assertNotIn('notes', data)
data['notes'] = marker
cache.write_text(json.dumps(data), encoding="utf-8")
self.assertNotIn(marker, self.run_check(update=False))
self.assertNotIn('notes', json.loads(cache.read_text(encoding="utf-8")))
def test_paid_skill_without_login_only_notifies_once_a_day(self):
out = self.run_check()
self.assertIn("Oil UI Pro 有新版本 99.0.0(当前 0.10.0):动效改成三处基本动效。", out)
self.assertIn("npx github:oil-oil/oil-cli update oil-ui-pro", out)
self.assertIn("Oil UI Pro 有新版本 99.0.0(当前 0.10.0)。", out)
self.assertIn("--update", out)
self.assertEqual(self.npx_calls(), [])
self.assertEqual(self.run_check(), "")
self.assertEqual(Versions.hits, 1)
@@ -160,13 +207,14 @@ class CheckUpdateTest(unittest.TestCase):
out = self.run_check(LANG=language, FAKE_NPX_FAIL="1", FAKE_NPX_JSON=json.dumps({
"ok": False, "command": "update", "error": error, "message": "DO NOT ECHO THIS",
"new_field": "forward-compatible"}))
self.assertIn(f'npx github:oil-oil/oil-cli update oil-ui-pro --path "{self.skill}"', out)
self.assertIn(str(self.skill / 'scripts/check_update.py'), out)
self.assertIn('--update', out)
self.assertNotIn("DO NOT ECHO THIS", out)
if error == "unauthorized":
self.assertIn("npx github:oil-oil/oil-cli login", out)
self.assertRegex(out, r"npx github:oil-oil/oil-cli#[0-9a-f]{40} login")
self.assertIn("Authorization has expired" if language.startswith("en") else "授权已失效", out)
else:
self.assertIn("https://ui.oiloil.org/pro/", out)
self.assertIn("https://ui.oiloil.org/en/pro/" if language.startswith("en") else "https://ui.oiloil.org/pro/", out)
self.assertIn("no purchase record" if language.startswith("en") else "没有对应的购买记录", out)
_, state = self.install_state()
self.assertEqual(state["auto_failed_reason"], error)
@@ -197,7 +245,7 @@ class CheckUpdateTest(unittest.TestCase):
target.rename(hidden)
out = self.run_check(PATH=str(self.bin))
self.assertIn("自动更新需要 Node.js 18 以上", out)
self.assertIn(f'--path "{self.skill}"', out)
self.assertIn(str(self.skill / 'scripts/check_update.py'), out)
self.assert_no_attempt()
hidden.rename(target)
out = self.run_check(FAKE_NODE_VERSION="v16.20.0")
@@ -279,22 +327,29 @@ class CheckUpdateTest(unittest.TestCase):
self.assertEqual(Versions.hits, 2)
def test_manual_command_targets_absolute_path_and_handles_shell_characters(self):
self.login()
self.skill.rename(self.skill.parent / 'custom space $HOME `false` "quote"')
self.skill = self.skill.parent / 'custom space $HOME `false` "quote"'
out = self.run_check(OIL_NO_AUTO_UPDATE="1")
command = out.split("在终端运行 ", 1)[1].rsplit(" 即可更新。", 1)[0]
# 实际复制执行提示;参数只能指向该副本,不能展开变量或命令替换。
result = subprocess.run([SH, "-c", command], env={**os.environ, "HOME": str(self.tmp / "home"),
"PATH": f"{self.bin}{os.pathsep}{os.environ['PATH']}",
"FAKE_NPX_LOG": str(self.npx_log)}, capture_output=True, text=True)
env = {k: v for k, v in self.last_env.items() if k != 'OIL_NO_AUTO_UPDATE'}
result = subprocess.run([SH, "-c", command], env=env, capture_output=True, text=True, encoding="utf-8")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(str(self.skill), self.npx_calls()[0])
self.assertIn('version: "99.0.0"', (self.skill / "SKILL.md").read_text(encoding="utf-8"))
def test_non_chinese_locales_get_english(self):
for language in ("ja_JP.UTF-8", "hi_IN.UTF-8", ""):
with self.subTest(language=language):
shutil.rmtree(self.tmp / "state", ignore_errors=True)
self.assertIn("is available", self.run_check(LANG=language, LC_ALL="", LC_MESSAGES="", OIL_NO_AUTO_UPDATE="1"))
def test_english_success_and_manual_notice(self):
out = self.run_check(LANG="en_US.UTF-8", OIL_NO_AUTO_UPDATE="1")
self.assertIn("Oil UI Pro 99.0.0 is available (current: 0.10.0)", out)
self.assertIn("To update, run npx github:oil-oil/oil-cli", out)
self.assertIn("To update, run", out)
self.assertIn("--update", out)
self.login()
self.assertIn("updated automatically to 99.0.0", self.run_check(LANG="en_US.UTF-8"))
@@ -314,13 +369,13 @@ class CheckUpdateTest(unittest.TestCase):
def test_missing_python_warns_once_and_recovery_resets_the_reminder(self):
path = self.shell_path()
self.assertEqual(self.run_check(launcher=True, PATH=path), "自动更新需要 Python 3,本次没有检查更新。\n")
self.assertEqual(self.run_check(launcher=True, PATH=path), "版本检查需要 Python 3,本次没有检查更新。\n")
self.assertEqual(self.run_check(launcher=True, PATH=path), "OIL_UPDATE_CHECK_SKIPPED: missing_python\n")
self.assertEqual(Versions.hits, 0)
(self.bin / "python").symlink_to(sys.executable)
self.assertIn("有新版本", self.run_check(launcher=True, PATH=path, OIL_NO_AUTO_UPDATE="1"))
(self.bin / "python").unlink()
self.assertIn("Automatic updates need Python 3", self.run_check(launcher=True, PATH=path, LANG="en_US.UTF-8"))
self.assertIn("Version checks need Python 3", self.run_check(launcher=True, PATH=path, LANG="en_US.UTF-8"))
def test_python2_is_not_used_and_disabled_or_development_launcher_is_silent(self):
(self.bin / "python").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
@@ -330,7 +385,7 @@ class CheckUpdateTest(unittest.TestCase):
(self.skill / ".git").mkdir()
self.assertEqual(self.run_check(launcher=True, PATH=path), "")
(self.skill / ".git").rmdir()
self.assertIn("自动更新需要 Python 3", self.run_check(launcher=True, PATH=path))
self.assertIn("版本检查需要 Python 3", self.run_check(launcher=True, PATH=path))
def test_silent_when_up_to_date_offline_disabled_or_in_a_checkout(self):
self.set_latest("0.0.1", free=False)