用户反馈的四件事一次做完:
1. 给快照加保护(单份上锁,自动清理一律跳过)
2. 说明「手动快照受保护不被清理」,并讲清它与上锁的区别
3. 快照备注事后可改(此前只能在创建时填,存下来就只能看不能改)
4. 列表从「配置快照与回滚」卡片搬进「紧急救援通道」,回滚入口不再弹残缺弹窗
protected 与 keep_manual 是两件事,必须分开:
- keep_manual 是全局策略,按 tag.startswith('auto') 猜来源
- protected 是用户对这一份的显式意图,跨策略生效
判据只能加在删除分支上,混进 auto 算法的话,受保护的自动快照
会开始占 keep_count 配额,把别的自动快照挤掉 —— 用户设「保留 5 份」
却只剩 4 份,且没有任何提示。
上锁不等于删不掉:删受保护的那份时后端回 needs_force,前端弹二次确认
后带 force 重试。审计里标注「受保护,已强制删除」,否则事后只看得到
「删了快照」,看不出删的是用户特意保下来的那份。
meta 用 .tmp + os.replace 原子写 —— meta 是 prune 唯一的判据来源,
写到一半被打断会留下解析不了的残文件,之后那份快照既不会被清理,
也上不了锁(读不到就当没保护)。
备注消毒:控制字符一律换成空格而不是删掉。删掉会把「甲 BEL 乙」
变成「甲乙」,两个词粘成一坨,而用户看到的备注莫名其妙。
真机验收(40 项全绿)当场抓到两个本地回归漏掉的真 bug:
- 备注消毒把制表符删掉而不是换空格:'a\\nb\\tc' 变成 'ab c'
本地判据只写「不含换行和制表符」,恰好放过 —— 「没有危险字符」
和「内容没被偷偷改掉」是两件事
- web 层无条件写 'protected': b.get('protected'),
payload 里这个键永远在(值可能是 None),helper 的
`if 'protected' in p` 恒成立,「不传就取反」从 HTTP 路上彻底失效
两个都补了反向验证(注入后必须观察到红)。
顺带修掉一个既存缺陷:t-107.py 里的 js_code_only 一直在吃 app.js
的真代码 —— 用 re.sub 块注释优先、再剥行注释,而 app.js 8222 行有一句
讲「怎么防止 CSS 注释被提前闭合」的行注释,注释里写着块注释的起止符
字面量,于是被当成真注释开头一路吃到文件尾。
证据:块注释起始 169 次而结束 170 次,数量不等就是配对错位一格。
HEAD 版同样中招(163/164),所以不是本轮引入。
换成 _dev/jsstrip.py 的词法扫描版(正确处理模板串的插值、正则字面量、
prev 记 token 尾词而不是一个字符),并加 node 侧 V8 交叉验证。
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""把两个 node 侧的前端检查器纳入 python 回归。
|
||
|
||
为什么要有这么一个壳,而不是直接改 run-static.py 去调 node:
|
||
|
||
1. run-static.py 的收集规则是 `glob('t-*.py')` + EXCL 黑名单。
|
||
让 node 检查器**以 t-*.py 的身份出现**,就不用给 run-static.py
|
||
加第二套收集逻辑(那是两条会各自漂移的路径 —— 以后再加一个
|
||
node 检查器时,十有八九只记得改其中一处)。
|
||
|
||
2. `devtools/check-jsstrip.js` 需要一份「剥完注释的 app.js」当输入,
|
||
而那份产物只能由 python 生成(node 自己跑 python 在 Windows 上
|
||
EBUSY,spawnSync 会被信号杀掉、rc=null)。所以顺序必须是
|
||
**先 python 备料,再 node 验收** —— 这个顺序约束也只有放在
|
||
python 侧才表达得自然。
|
||
|
||
用法:
|
||
python _dev/t-109-node-front.py
|
||
python _dev/run-static.py t-109 # 跟全量回归一起跑
|
||
"""
|
||
import glob
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = os.path.dirname(HERE)
|
||
|
||
NODE = os.environ.get('NODE_BIN') or shutil.which('node') or 'node'
|
||
|
||
# 要串行跑的 node 检查器。**顺序有意义**:jsstrip 的产物是
|
||
# check-jsstrip.js 的输入,所以它虽然只是「备料」,也放在同一串里,
|
||
# 免得有人在只跑单个检查器时得到「产物不存在」的假红。
|
||
NODE_CHECKS = [
|
||
('check-jsstrip.js', '剥注释器(词法扫描版)的 V8 交叉验证'),
|
||
('check-snap-render.js', '快照列表内嵌渲染 + 备注/上锁/二次确认交互'),
|
||
]
|
||
|
||
|
||
def run(cmd, what):
|
||
try:
|
||
p = subprocess.run(cmd, capture_output=True, text=True,
|
||
encoding='utf-8', errors='replace', timeout=180)
|
||
return p.returncode, (p.stdout or '') + (p.stderr or '')
|
||
except FileNotFoundError:
|
||
# 没装 node 不是代码的错。别让一个环境问题把整轮回归判红。
|
||
return 127, '%s:找不到可执行文件 %r\n' % (what, cmd[0])
|
||
except subprocess.TimeoutExpired:
|
||
return 124, '%s:超时(180s)\n' % what
|
||
|
||
|
||
def main():
|
||
ok_all, bad_all = 0, []
|
||
|
||
# ---- 步骤 1:跑 jsstrip.py 生成 .jsstrip-appjs.out ----
|
||
# 这一步自己会跑一遍 44 项 selftest,产物是副产品。
|
||
rc, out = run([sys.executable, os.path.join(HERE, 'jsstrip.py')],
|
||
'jsstrip.py')
|
||
m = re.findall(r'通过 (\d+) 项', out)
|
||
if rc == 0:
|
||
print('[OK] jsstrip.py 自检%s 项(并写出 .jsstrip-appjs.out)'
|
||
% ((' ' + m[-1]) if m else ''))
|
||
else:
|
||
print('[NG] jsstrip.py 自检失败 rc=%s' % rc)
|
||
for line in out.splitlines():
|
||
if line.strip().startswith('·') or 'Traceback' in line:
|
||
print(' ' + line.rstrip())
|
||
bad_all.append('jsstrip.py')
|
||
|
||
# ---- 步骤 2:逐个跑 node 检查器 ----
|
||
for name, desc in NODE_CHECKS:
|
||
f = os.path.join(ROOT, 'devtools', name)
|
||
if not os.path.isfile(f):
|
||
print('[NG] %-24s 文件不存在:devtools/%s' % (name, name))
|
||
bad_all.append(name)
|
||
continue
|
||
rc, out = run([NODE, f], name)
|
||
m = re.findall(r'通过 (\d+) 项', out)
|
||
n = int(m[-1]) if m else 0
|
||
if rc == 0:
|
||
ok_all += n
|
||
print('[OK] %-24s %s(%d 项)' % (name, desc, n))
|
||
else:
|
||
print('[NG] %-24s %s rc=%s' % (name, desc, rc))
|
||
for line in out.splitlines():
|
||
if '[NG]' in line or line.strip().startswith('·') \
|
||
or 'Error' in line:
|
||
print(' ' + line.rstrip())
|
||
bad_all.append(name)
|
||
|
||
print()
|
||
print('=' * 60)
|
||
print('通过 %d 项,失败 %d 项' % (ok_all, len(bad_all)))
|
||
for b in bad_all:
|
||
print(' · %s' % b)
|
||
print('=' * 60)
|
||
return 1 if bad_all else 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main()) |