fix(chv-muse-r12fix-4): detect_checkout 接进 CDP broker(P10)
R12 真机:detect_checkout 在 chv-guest/、host.py、 executor.py 里零引用,结账页无任何拦截。按 P10 gap §4.1 接线:broker 加载 cloud/chv/checkout_detect.py(同目录 优先,仓库路径兜底;缺失时降级关闭检测、写警告,绝不 crash-loop);snapshot() 每次重算 verdict(aria 行拼伪节点, 与 _form_risk 同形),open() 导航前后两道门——落点是结账 页时返回 form_submit ask 并暂停,上一页是结账页时下一次 导航先审批;ask 批过后同 URL 不再重复 ask(批准即放行, 与 form_risk 同口径);verdict 进 snapshot()/status() 回包。 build.sh:checkout_detect.py 条件拷入 /usr/local/bin/(0644, P3/P6 同款只警告口径,M4 不硬要求 cloud/), extra_contents 与两处 lockfile 登记补齐。钱包三腿不在本轮 范围,未动。
This commit is contained in:
1 parent
1da0d86159
commit
0ebcdfb2a7
7 files changed
+258
-8
No files matched your search
@@ -232,6 +232,16 @@ if [ -f "$REPO_DIR/cloud/chv/privsep.py" ] && [ -f "$REPO_DIR/cloud/chv/safety.p
|
|||||||
else
|
else
|
||||||
echo "警告:Muse P6 云端叶子模块不在仓库里(cloud/chv/privsep.py 与 safety.py),镜像暂不含 worker 叶子模块" >&2
|
echo "警告:Muse P6 云端叶子模块不在仓库里(cloud/chv/privsep.py 与 safety.py),镜像暂不含 worker 叶子模块" >&2
|
||||||
fi
|
fi
|
||||||
|
# P10 云端检测模块(checkout_detect.py)只警告、不失败——
|
||||||
|
# 与 P3/P6 同款口径(M4 既定 build.sh 不硬要求 cloud/);缺失时
|
||||||
|
# broker 的结账检测降级关闭(写警告、不 crash-loop),但完整
|
||||||
|
# 镜像必须含它(extra_contents 与 lockfile 均有登记)。
|
||||||
|
P10_CHECKOUT_PRESENT=0
|
||||||
|
if [ -f "$REPO_DIR/cloud/chv/checkout_detect.py" ]; then
|
||||||
|
P10_CHECKOUT_PRESENT=1
|
||||||
|
else
|
||||||
|
echo "警告:Muse P10 结账检测模块不在仓库里(cloud/chv/checkout_detect.py),镜像暂不含 checkout_detect" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$DRY_RUN" = "0" ]; then
|
if [ "$DRY_RUN" = "0" ]; then
|
||||||
if [ ! -e /dev/kvm ]; then
|
if [ ! -e /dev/kvm ]; then
|
||||||
@@ -412,6 +422,15 @@ if [ "$P6_LIBS_PRESENT" = "1" ]; then
|
|||||||
--copy-in "$SCRIPT_DIR/authd_init_stub.py:/usr/local/lib/chv-gitea/cloud/chv/"
|
--copy-in "$SCRIPT_DIR/authd_init_stub.py:/usr/local/lib/chv-gitea/cloud/chv/"
|
||||||
--run-command 'mv /usr/local/lib/chv-gitea/cloud/chv/authd_init_stub.py /usr/local/lib/chv-gitea/cloud/chv/__init__.py')
|
--run-command 'mv /usr/local/lib/chv-gitea/cloud/chv/authd_init_stub.py /usr/local/lib/chv-gitea/cloud/chv/__init__.py')
|
||||||
fi
|
fi
|
||||||
|
# P10 交接(Muse P10 的结账检测模块,round-12 修复轮项 4):
|
||||||
|
# 文件在仓库里存在时拷入 /usr/local/bin/ 供 chv-cdp-broker 加载
|
||||||
|
#(broker 先找同目录,再找仓库 cloud/chv/;0644 即可,broker
|
||||||
|
# 以 chv-browser 身份读);不存在时只警告、不失败(与 P3/P6
|
||||||
|
# 同款口径,broker 降级关闭检测、不 crash-loop)。
|
||||||
|
if [ "$P10_CHECKOUT_PRESENT" = "1" ]; then
|
||||||
|
VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/checkout_detect.py:/usr/local/bin/"
|
||||||
|
--run-command 'chmod 0644 /usr/local/bin/checkout_detect.py')
|
||||||
|
fi
|
||||||
# P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入
|
# P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入
|
||||||
# 镜像并建 vsock-notify.service(自动启动,round-11 真机修复:
|
# 镜像并建 vsock-notify.service(自动启动,round-11 真机修复:
|
||||||
# 此前只拷文件、不建服务,vsock 通道从未自动启动;服务形状按
|
# 此前只拷文件、不建服务,vsock 通道从未自动启动;服务形状按
|
||||||
@@ -488,6 +507,7 @@ else
|
|||||||
EXTRA_CONTENTS=""
|
EXTRA_CONTENTS=""
|
||||||
[ "$P3_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="cloud/chv/authd.py,cloud/chv/sentinel.py,cloud/chv/errors.py,cloud/chv/__init__.py"
|
[ "$P3_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="cloud/chv/authd.py,cloud/chv/sentinel.py,cloud/chv/errors.py,cloud/chv/__init__.py"
|
||||||
[ "$P6_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}cloud/chv/privsep.py,cloud/chv/safety.py"
|
[ "$P6_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}cloud/chv/privsep.py,cloud/chv/safety.py"
|
||||||
|
[ "$P10_CHECKOUT_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}cloud/chv/checkout_detect.py"
|
||||||
[ "$VSOCK_NOTIFY_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}vsock_notify"
|
[ "$VSOCK_NOTIFY_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}vsock_notify"
|
||||||
python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" "$EXTRA_CONTENTS"
|
python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" "$EXTRA_CONTENTS"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ DECLARED_SOURCES = {
|
|||||||
"chv-sync-worker-client": "repo_commit",
|
"chv-sync-worker-client": "repo_commit",
|
||||||
"chv-gitea-worker.service": "repo_commit",
|
"chv-gitea-worker.service": "repo_commit",
|
||||||
"cloud/chv/privsep.py": "repo_commit",
|
"cloud/chv/privsep.py": "repo_commit",
|
||||||
|
"cloud/chv/checkout_detect.py": "repo_commit",
|
||||||
"cloud/chv/safety.py": "repo_commit",
|
"cloud/chv/safety.py": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+123
-6
@@ -8,7 +8,8 @@ socket 说话,绝不直连浏览器、绝不见 CDP 端点。
|
|||||||
|
|
||||||
操作(Unix socket /run/chv/cdp-broker.sock,JSON 行):
|
操作(Unix socket /run/chv/cdp-broker.sock,JSON 行):
|
||||||
open {url} -> 导航并返回 {title, url, a11y, truncated,
|
open {url} -> 导航并返回 {title, url, a11y, truncated,
|
||||||
untrusted_input, injection_flags, form_risk}
|
untrusted_input, injection_flags, form_risk,
|
||||||
|
checkout}
|
||||||
snapshot {} -> 当前页同上
|
snapshot {} -> 当前页同上
|
||||||
screenshot {} -> 截图落盘 /run/chv/shots/<uuid>.png,返回 {path}
|
screenshot {} -> 截图落盘 /run/chv/shots/<uuid>.png,返回 {path}
|
||||||
close {} -> 关当前页(浏览器进程继续跑)
|
close {} -> 关当前页(浏览器进程继续跑)
|
||||||
@@ -26,8 +27,11 @@ socket 说话,绝不直连浏览器、绝不见 CDP 端点。
|
|||||||
导航前 validate_url + 静态拦截名单;快照恒带
|
导航前 validate_url + 静态拦截名单;快照恒带
|
||||||
untrusted_input=true(外部页面文本永远是数据、不是指令),
|
untrusted_input=true(外部页面文本永远是数据、不是指令),
|
||||||
命中注入启发式带 injection_flags;高风险表单在页时 open
|
命中注入启发式带 injection_flags;高风险表单在页时 open
|
||||||
导航即返回 ask(入队、执行停止);观测到导航 POST 即自动
|
导航即返回 ask(入队、执行停止);结账页命中时 open 导航
|
||||||
暂停 + ask;下载命中可执行扩展名即取消 + 暂停 + ask。
|
同样返回 ask 并暂停(round-12 修复轮项 4,P10 接线,
|
||||||
|
detect_checkout 在快照时重算、导航前后两道门);观测到
|
||||||
|
导航 POST 即自动暂停 + ask;下载命中可执行扩展名即取消
|
||||||
|
+ 暂停 + ask。
|
||||||
所有回包与日志过 sanitize_error;响应里绝无 ws 端点、
|
所有回包与日志过 sanitize_error;响应里绝无 ws 端点、
|
||||||
DevTools 地址、原始 HTML。
|
DevTools 地址、原始 HTML。
|
||||||
|
|
||||||
@@ -56,6 +60,7 @@ MAX_REQUEST_BYTES = 65536
|
|||||||
NAV_TIMEOUT_MS = 30000
|
NAV_TIMEOUT_MS = 30000
|
||||||
|
|
||||||
_GUARD = None
|
_GUARD = None
|
||||||
|
_CHECKOUT = None
|
||||||
|
|
||||||
|
|
||||||
def _load_guard():
|
def _load_guard():
|
||||||
@@ -75,6 +80,40 @@ def _load_guard():
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def _load_checkout():
|
||||||
|
"""加载结账检测模块 checkout_detect(纯函数模块)。
|
||||||
|
|
||||||
|
运行时安装在 /usr/local/bin/checkout_detect.py;测试时在
|
||||||
|
仓库 cloud/chv/checkout_detect.py。找到即只加载一次;两处
|
||||||
|
都找不到时返回 None(降级:结账检测关闭,broker 继续跑并
|
||||||
|
写警告——不因缺一个检测模块把浏览器整路 crash-loop,但完
|
||||||
|
整镜像必须含它,build.sh 有登记、BUILDINFO 有登记)。
|
||||||
|
"""
|
||||||
|
global _CHECKOUT
|
||||||
|
if _CHECKOUT is not None:
|
||||||
|
return _CHECKOUT
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidates = [
|
||||||
|
os.path.join(here, "checkout_detect.py"),
|
||||||
|
os.path.join(os.path.dirname(here), "cloud", "chv",
|
||||||
|
"checkout_detect.py"),
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"checkout_detect", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
_CHECKOUT = module
|
||||||
|
return module
|
||||||
|
sys.stderr.write(
|
||||||
|
"chv-cdp-broker: 警告:checkout_detect 未找到(%s),"
|
||||||
|
"结账检测关闭\n" % candidates)
|
||||||
|
_CHECKOUT = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _load_playwright():
|
def _load_playwright():
|
||||||
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。
|
"""懒加载 playwright;缺失时报明错(不静默假装有浏览器)。
|
||||||
返回调用后的上下文管理器(sync_playwright()),不是函数本身——
|
返回调用后的上下文管理器(sync_playwright()),不是函数本身——
|
||||||
@@ -102,6 +141,15 @@ class CdpBroker:
|
|||||||
blocklist_path=BLOCKLIST_PATH,
|
blocklist_path=BLOCKLIST_PATH,
|
||||||
ask_queue_dir=ASK_QUEUE_DIR, shots_dir=SHOTS_DIR):
|
ask_queue_dir=ASK_QUEUE_DIR, shots_dir=SHOTS_DIR):
|
||||||
self._guard = _load_guard()
|
self._guard = _load_guard()
|
||||||
|
# round-12 修复轮(项 4,P10):结账检测接线。模块缺失
|
||||||
|
# 时 _checkout_mod 为 None,检测降级关闭(见 _load_checkout)。
|
||||||
|
self._checkout_mod = _load_checkout()
|
||||||
|
self._checkout = {"is_checkout": False, "confidence": "low",
|
||||||
|
"reasons": [], "form_risk": None,
|
||||||
|
"amount_text": None}
|
||||||
|
# 本次会话里已批过 ask 的结账页 URL:批过之后同一页
|
||||||
|
# 再 open 不再重复 ask(与 form_risk“批准即放行”同口径)。
|
||||||
|
self._checkout_approved = set()
|
||||||
self._factory = playwright_factory or _load_playwright
|
self._factory = playwright_factory or _load_playwright
|
||||||
self._blocklist_path = blocklist_path
|
self._blocklist_path = blocklist_path
|
||||||
self._ask_queue_dir = ask_queue_dir
|
self._ask_queue_dir = ask_queue_dir
|
||||||
@@ -176,6 +224,39 @@ class CdpBroker:
|
|||||||
return {"ok": False, "ask": True, "ask_id": ask_id,
|
return {"ok": False, "ask": True, "ask_id": ask_id,
|
||||||
"error": "需审批:%s" % detail}
|
"error": "需审批:%s" % detail}
|
||||||
|
|
||||||
|
def _checkout_verdict(self, url, aria):
|
||||||
|
"""结账检测:URL 信号 + ARIA 快照行拼成的伪节点。
|
||||||
|
|
||||||
|
broker 只有 aria_snapshot() 字符串,没有节点树;每行
|
||||||
|
快照文本作一个节点(name=行文本),与 detect_checkout
|
||||||
|
的节点口径一致(_node_text 拼 role/name/label/type)。
|
||||||
|
模块缺失时返回“非结账页”(降级,见 _load_checkout)。
|
||||||
|
"""
|
||||||
|
if self._checkout_mod is None:
|
||||||
|
return {"is_checkout": False, "confidence": "low",
|
||||||
|
"reasons": [], "form_risk": None,
|
||||||
|
"amount_text": None}
|
||||||
|
nodes = [{"role": "", "name": line, "label": "", "type": ""}
|
||||||
|
for line in (aria or "").splitlines()
|
||||||
|
if line.strip()]
|
||||||
|
verdict = self._checkout_mod.detect_checkout(url, nodes)
|
||||||
|
return {"is_checkout": verdict.is_checkout,
|
||||||
|
"confidence": verdict.confidence,
|
||||||
|
"reasons": list(verdict.reasons),
|
||||||
|
"form_risk": verdict.form_risk,
|
||||||
|
"amount_text": verdict.amount_text}
|
||||||
|
|
||||||
|
def _checkout_ask(self, url, pause):
|
||||||
|
"""结账页 ask:与 P4 的 form_submit 同种(P10 gap §4.1
|
||||||
|
对齐),入队并按需暂停,返回 ask 响应。"""
|
||||||
|
reasons = ",".join(self._checkout.get("reasons") or [])
|
||||||
|
ask_id = self._guard.enqueue_ask(
|
||||||
|
self._ask_queue_dir, "form_submit", url,
|
||||||
|
"结账页命中(%s),购买前须审批" % reasons)
|
||||||
|
if pause:
|
||||||
|
self._paused = True
|
||||||
|
return self._ask_resp(ask_id, "结账页命中,购买前须审批")
|
||||||
|
|
||||||
def open(self, url):
|
def open(self, url):
|
||||||
if self._paused:
|
if self._paused:
|
||||||
return self._paused_resp()
|
return self._paused_resp()
|
||||||
@@ -193,6 +274,11 @@ class CdpBroker:
|
|||||||
"高风险表单在页(%s),导航须审批"
|
"高风险表单在页(%s),导航须审批"
|
||||||
% self._form_risk["level"])
|
% self._form_risk["level"])
|
||||||
return self._ask_resp(ask_id, "高风险表单在页,导航须审批")
|
return self._ask_resp(ask_id, "高风险表单在页,导航须审批")
|
||||||
|
# round-12 修复轮(项 4,P10):导航前门——上一页已是
|
||||||
|
# 结账页时,下一次导航须先审批(与 _form_risk 门同形)。
|
||||||
|
if (self._checkout.get("is_checkout")
|
||||||
|
and url not in self._checkout_approved):
|
||||||
|
return self._checkout_ask(url, pause=False)
|
||||||
try:
|
try:
|
||||||
self._ensure_browser()
|
self._ensure_browser()
|
||||||
self._new_page()
|
self._new_page()
|
||||||
@@ -202,7 +288,13 @@ class CdpBroker:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False,
|
return {"ok": False,
|
||||||
"error": self._guard.sanitize_error(e)}
|
"error": self._guard.sanitize_error(e)}
|
||||||
return self.snapshot()
|
result = self.snapshot()
|
||||||
|
# 导航后门——落点本身是结账页时,返回 ask 并暂停
|
||||||
|
#(P10 gap §4.1:open 导航返回 ask 并暂停)。
|
||||||
|
if (result.get("ok") and self._checkout.get("is_checkout")
|
||||||
|
and url not in self._checkout_approved):
|
||||||
|
return self._checkout_ask(url, pause=True)
|
||||||
|
return result
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
if self._paused:
|
if self._paused:
|
||||||
@@ -226,11 +318,15 @@ class CdpBroker:
|
|||||||
aria, truncated = self._guard.truncate_aria(aria or "")
|
aria, truncated = self._guard.truncate_aria(aria or "")
|
||||||
flags = self._guard.detect_injection(aria)
|
flags = self._guard.detect_injection(aria)
|
||||||
self._form_risk = self._guard.detect_form_risk(aria)
|
self._form_risk = self._guard.detect_form_risk(aria)
|
||||||
|
# round-12 修复轮(项 4,P10):每次快照重算结账 verdict,
|
||||||
|
# 与 _form_risk 同形(快照缓存,open 的两道门读它)。
|
||||||
|
self._checkout = self._checkout_verdict(url, aria)
|
||||||
self._current_url = url
|
self._current_url = url
|
||||||
return {"ok": True, "title": title, "url": url,
|
return {"ok": True, "title": title, "url": url,
|
||||||
"a11y": aria, "truncated": truncated,
|
"a11y": aria, "truncated": truncated,
|
||||||
"untrusted_input": True, "injection_flags": flags,
|
"untrusted_input": True, "injection_flags": flags,
|
||||||
"form_risk": self._form_risk}
|
"form_risk": self._form_risk,
|
||||||
|
"checkout": self._checkout}
|
||||||
|
|
||||||
def screenshot(self):
|
def screenshot(self):
|
||||||
if self._paused:
|
if self._paused:
|
||||||
@@ -255,6 +351,9 @@ class CdpBroker:
|
|||||||
pass
|
pass
|
||||||
self._page = None
|
self._page = None
|
||||||
self._form_risk = {"level": "none", "fields": []}
|
self._form_risk = {"level": "none", "fields": []}
|
||||||
|
self._checkout = {"is_checkout": False, "confidence": "low",
|
||||||
|
"reasons": [], "form_risk": None,
|
||||||
|
"amount_text": None}
|
||||||
self._current_url = ""
|
self._current_url = ""
|
||||||
return {"ok": True, "closed": True}
|
return {"ok": True, "closed": True}
|
||||||
|
|
||||||
@@ -272,13 +371,22 @@ class CdpBroker:
|
|||||||
def status(self):
|
def status(self):
|
||||||
return {"ok": True, "paused": self._paused,
|
return {"ok": True, "paused": self._paused,
|
||||||
"url": self._current_url,
|
"url": self._current_url,
|
||||||
"form_risk": self._form_risk}
|
"form_risk": self._form_risk,
|
||||||
|
"checkout": self._checkout}
|
||||||
|
|
||||||
def list_asks(self):
|
def list_asks(self):
|
||||||
return {"ok": True,
|
return {"ok": True,
|
||||||
"asks": self._guard.list_asks(self._ask_queue_dir)}
|
"asks": self._guard.list_asks(self._ask_queue_dir)}
|
||||||
|
|
||||||
def resolve_ask(self, ask_id, decision):
|
def resolve_ask(self, ask_id, decision):
|
||||||
|
# 批之前先读回 ask 记录:批后它不再是 pending,
|
||||||
|
# list_asks 读不到——结账 ask 批过后要把 URL 记进
|
||||||
|
# _checkout_approved(同页再 open 不再重复 ask)。
|
||||||
|
record = None
|
||||||
|
for ask in self._guard.list_asks(self._ask_queue_dir):
|
||||||
|
if ask.get("ask_id") == ask_id:
|
||||||
|
record = ask
|
||||||
|
break
|
||||||
ok, err = self._guard.resolve_ask(
|
ok, err = self._guard.resolve_ask(
|
||||||
self._ask_queue_dir, ask_id, decision)
|
self._ask_queue_dir, ask_id, decision)
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -288,6 +396,15 @@ class CdpBroker:
|
|||||||
# 批准即放行这次操作:清掉快照缓存的表单风险(下一次
|
# 批准即放行这次操作:清掉快照缓存的表单风险(下一次
|
||||||
# snapshot 会按新页面重算,不会丢检测)。
|
# snapshot 会按新页面重算,不会丢检测)。
|
||||||
self._form_risk = {"level": "none", "fields": []}
|
self._form_risk = {"level": "none", "fields": []}
|
||||||
|
# round-12 修复轮(项 4,P10):结账 ask 批过后,
|
||||||
|
# 同 URL 再 open 不重复 ask;_checkout 本身也清掉,
|
||||||
|
# 下一次 snapshot 按新页面重算。
|
||||||
|
if record is not None and str(
|
||||||
|
record.get("detail", "")).startswith("结账页命中"):
|
||||||
|
self._checkout_approved.add(record.get("url"))
|
||||||
|
self._checkout = {"is_checkout": False,
|
||||||
|
"confidence": "low", "reasons": [],
|
||||||
|
"form_risk": None, "amount_text": None}
|
||||||
else:
|
else:
|
||||||
self.close()
|
self.close()
|
||||||
self._paused = False
|
self._paused = False
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ DECLARED_SOURCES = {
|
|||||||
"chv-sync-worker-client": "repo_commit",
|
"chv-sync-worker-client": "repo_commit",
|
||||||
"chv-gitea-worker.service": "repo_commit",
|
"chv-gitea-worker.service": "repo_commit",
|
||||||
"cloud/chv/privsep.py": "repo_commit",
|
"cloud/chv/privsep.py": "repo_commit",
|
||||||
|
"cloud/chv/checkout_detect.py": "repo_commit",
|
||||||
"cloud/chv/safety.py": "repo_commit",
|
"cloud/chv/safety.py": "repo_commit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
- 中英文特征;节点为空;页面金额只作展示信号。
|
- 中英文特征;节点为空;页面金额只作展示信号。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -129,5 +132,43 @@ class CheckoutNodesTest(unittest.TestCase):
|
|||||||
self.assertIsNone(verdict.amount_text)
|
self.assertIsNone(verdict.amount_text)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildInfoContentsTest(unittest.TestCase):
|
||||||
|
"""round-12 修复轮(项 4,P10):checkout_detect 是条件拷入
|
||||||
|
件——存在时经 extra_contents 登记进 BUILDINFO contents
|
||||||
|
(不在 BASE_CONTENTS 里,缺失的构建绝不谎称已进镜像,
|
||||||
|
与 P3 叶子模块同款口径)。"""
|
||||||
|
|
||||||
|
def test_extra_contents_include_checkout_detect(self):
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
path = root / "chv-guest" / "buildinfo.py"
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"chv_guest_p10_buildinfo", str(path))
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
self.assertNotIn("checkout_detect",
|
||||||
|
module.BASE_CONTENTS)
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
out = os.path.join(tmp, "out")
|
||||||
|
os.makedirs(out)
|
||||||
|
for name, content in (("disk.raw", b"D"),
|
||||||
|
("vmlinux", b"K")):
|
||||||
|
with open(os.path.join(out, name), "wb") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
for name, lines in (
|
||||||
|
("guest-versions-dpkg.txt", ["git 9.9.9-9ubuntu9"]),
|
||||||
|
("guest-versions-pip.txt", ["playwright==9.9.9"]),
|
||||||
|
("guest-versions-gt.txt", ["gt version 1.0.0"]),
|
||||||
|
("guest-versions-chromium.txt",
|
||||||
|
["chromium_headless_shell-1248"])):
|
||||||
|
with open(os.path.join(out, name), "w") as fh:
|
||||||
|
fh.write("\n".join(lines) + "\n")
|
||||||
|
info = module.build(
|
||||||
|
out, "chv-guest-24.04-0.164.0-alpha.9-abc1234", "24.04",
|
||||||
|
"0.164.0-alpha.9", "abc1234", "0" * 64, True, "1248",
|
||||||
|
["cloud/chv/checkout_detect.py"])
|
||||||
|
self.assertIn("cloud/chv/checkout_detect.py",
|
||||||
|
info["contents"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -645,6 +645,12 @@ class BuildDryRunTest(unittest.TestCase):
|
|||||||
dest = os.path.join(repo, rel)
|
dest = os.path.join(repo, rel)
|
||||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
shutil.copy2(ROOT / rel, dest)
|
shutil.copy2(ROOT / rel, dest)
|
||||||
|
# round-12 修复轮(项 4,P10):checkout_detect 是 cloud/
|
||||||
|
# 下的条件拷入件,夹具须一并拷入才走拷入分支。
|
||||||
|
rel = "cloud/chv/checkout_detect.py"
|
||||||
|
dest = os.path.join(repo, rel)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
shutil.copy2(ROOT / rel, dest)
|
||||||
if with_vsock:
|
if with_vsock:
|
||||||
rel = "cloud/chv/guest/vsock_notify.py"
|
rel = "cloud/chv/guest/vsock_notify.py"
|
||||||
dest = os.path.join(repo, rel)
|
dest = os.path.join(repo, rel)
|
||||||
@@ -709,7 +715,10 @@ class BuildDryRunTest(unittest.TestCase):
|
|||||||
# 三件进镜像,干跑输出须含拷入与 enable。
|
# 三件进镜像,干跑输出须含拷入与 enable。
|
||||||
"chv-gitea-worker",
|
"chv-gitea-worker",
|
||||||
"chv-gitea-worker.service",
|
"chv-gitea-worker.service",
|
||||||
"systemctl enable chv-gitea-worker"):
|
"systemctl enable chv-gitea-worker",
|
||||||
|
# round-12 修复轮(项 4,P10):结账检测
|
||||||
|
# 模块进镜像,干跑输出须含条件拷入行。
|
||||||
|
"checkout_detect"):
|
||||||
self.assertIn(needle, out, needle)
|
self.assertIn(needle, out, needle)
|
||||||
# P1 文件不在本分支:干跑只警告、不失败。
|
# P1 文件不在本分支:干跑只警告、不失败。
|
||||||
self.assertIn("vsock_notify.py 不在仓库里", out)
|
self.assertIn("vsock_notify.py 不在仓库里", out)
|
||||||
|
|||||||
@@ -462,6 +462,58 @@ class CdpBrokerTest(unittest.TestCase):
|
|||||||
third = broker.open("https://example.com/other")
|
third = broker.open("https://example.com/other")
|
||||||
self.assertTrue(third["ok"], third)
|
self.assertTrue(third["ok"], third)
|
||||||
|
|
||||||
|
def test_checkout_open_returns_ask_and_pauses(self):
|
||||||
|
# round-12 修复轮(项 4,P10):落点是结账页时,open
|
||||||
|
# 导航返回 ask 并暂停(P10 gap §4.1)。
|
||||||
|
aria = ("- generic [ref=e1]:\n"
|
||||||
|
" - heading \"Checkout\" [ref=e2]\n"
|
||||||
|
" - button \"Pay now\" [ref=e3]\n"
|
||||||
|
" - text \"Total: $49.99\" [ref=e4]")
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
broker, page, _f = _make_broker(tmp, aria=aria)
|
||||||
|
first = broker.open("https://shop.example.com/checkout")
|
||||||
|
self.assertFalse(first["ok"])
|
||||||
|
self.assertTrue(first.get("ask"))
|
||||||
|
self.assertIn("ask_id", first)
|
||||||
|
self.assertEqual(len(page.goto_calls), 1)
|
||||||
|
self.assertTrue(broker.status()["paused"])
|
||||||
|
asks = broker.list_asks()["asks"]
|
||||||
|
self.assertEqual(len(asks), 1)
|
||||||
|
self.assertEqual(asks[0]["kind"], "form_submit")
|
||||||
|
self.assertIn("结账页命中", asks[0]["detail"])
|
||||||
|
|
||||||
|
def test_checkout_allowed_then_open_ok(self):
|
||||||
|
# 批过 ask 后同 URL 再 open 不再重复 ask(与 form_risk
|
||||||
|
#“批准即放行”同口径);但随后导航到别的 URL 时,上一页
|
||||||
|
# 仍是结账页,导航前门再次拦下。
|
||||||
|
aria = ("- generic [ref=e1]:\n"
|
||||||
|
" - heading \"Checkout\" [ref=e2]\n"
|
||||||
|
" - button \"Pay now\" [ref=e3]\n"
|
||||||
|
" - text \"Total: $49.99\" [ref=e4]")
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
broker, page, _f = _make_broker(tmp, aria=aria)
|
||||||
|
first = broker.open("https://shop.example.com/checkout")
|
||||||
|
self.assertTrue(first.get("ask"))
|
||||||
|
asks = broker.list_asks()["asks"]
|
||||||
|
ok = broker.resolve_ask(asks[0]["ask_id"], "allowed")
|
||||||
|
self.assertTrue(ok["ok"])
|
||||||
|
second = broker.open("https://shop.example.com/checkout")
|
||||||
|
self.assertTrue(second["ok"], second)
|
||||||
|
self.assertTrue(second["checkout"]["is_checkout"])
|
||||||
|
self.assertEqual(second["checkout"]["confidence"], "high")
|
||||||
|
self.assertFalse(broker.status()["paused"])
|
||||||
|
third = broker.open("https://example.com/other")
|
||||||
|
self.assertFalse(third["ok"])
|
||||||
|
self.assertTrue(third.get("ask"))
|
||||||
|
self.assertEqual(len(page.goto_calls), 2)
|
||||||
|
|
||||||
|
def test_non_checkout_open_ok(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
broker, _page, _f = _make_broker(tmp)
|
||||||
|
resp = broker.open("https://example.com")
|
||||||
|
self.assertTrue(resp["ok"], resp)
|
||||||
|
self.assertFalse(resp["checkout"]["is_checkout"])
|
||||||
|
|
||||||
def test_post_navigation_request_auto_pauses(self):
|
def test_post_navigation_request_auto_pauses(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
broker, page, _f = _make_broker(tmp)
|
broker, page, _f = _make_broker(tmp)
|
||||||
@@ -820,6 +872,12 @@ class BuildDryRunTest(unittest.TestCase):
|
|||||||
dest = os.path.join(repo, rel)
|
dest = os.path.join(repo, rel)
|
||||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
shutil.copy2(ROOT / rel, dest)
|
shutil.copy2(ROOT / rel, dest)
|
||||||
|
# round-12 修复轮(项 4,P10):checkout_detect 是
|
||||||
|
# cloud/ 下的条件拷入件,夹具须一并拷入才走拷入分支。
|
||||||
|
rel = "cloud/chv/checkout_detect.py"
|
||||||
|
dest = os.path.join(repo, rel)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
shutil.copy2(ROOT / rel, dest)
|
||||||
codex = os.path.join(bindir, "codex")
|
codex = os.path.join(bindir, "codex")
|
||||||
with open(codex, "w") as fh:
|
with open(codex, "w") as fh:
|
||||||
fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n")
|
fh.write("#!/bin/sh\necho 'codex-cli 0.164.0-alpha.9'\n")
|
||||||
@@ -862,7 +920,10 @@ class BuildDryRunTest(unittest.TestCase):
|
|||||||
# 三件进镜像,干跑输出须含拷入与 enable。
|
# 三件进镜像,干跑输出须含拷入与 enable。
|
||||||
"chv-gitea-worker",
|
"chv-gitea-worker",
|
||||||
"chv-gitea-worker.service",
|
"chv-gitea-worker.service",
|
||||||
"systemctl enable chv-gitea-worker"):
|
"systemctl enable chv-gitea-worker",
|
||||||
|
# round-12 修复轮(项 4,P10):结账检测
|
||||||
|
# 模块进镜像,干跑输出须含条件拷入行。
|
||||||
|
"checkout_detect"):
|
||||||
self.assertIn(needle, out, needle)
|
self.assertIn(needle, out, needle)
|
||||||
# P1 文件不在本分支:干跑只警告、不失败。
|
# P1 文件不在本分支:干跑只警告、不失败。
|
||||||
self.assertIn("vsock_notify.py 不在仓库里", out)
|
self.assertIn("vsock_notify.py 不在仓库里", out)
|
||||||
|
|||||||
Reference in new issue
Block a user