deveco_hmigbot 0.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smoke.mjs — 波级装机冒烟(可证版)。最小成本运行时验证:崩溃与空壳两类最大杀伤,脚本先裁,异常才升级模型。
|
|
4
|
+
*
|
|
5
|
+
* 判定阶梯(全部文本判定,不依赖多模态;截图只落盘不裁决):
|
|
6
|
+
* ⓪ 前置可证(环境类,exit 3,调用方记阻塞不派修):设备在;hap 在且不早于源码;`hdc install` 输出含成功
|
|
7
|
+
* (hdc 装机失败退出码仍为 0,不看输出就是拿旧包/别的包当结果)
|
|
8
|
+
* ① faultlog 硬判:冒烟期间新增 jscrash/cppcrash → violation(exit 10)
|
|
9
|
+
* ② 直达可证(case,exit 2):起后前台 bundle 必须是目标(`aa dump -l`);着陆页名取 Index 钩子打的
|
|
10
|
+
* hilog(tag hmigSmoke,`didShow <name>`)必须等于 route——不等即 misland;曾着陆又弹回首页/navBar 记 bounced;
|
|
11
|
+
* 曾着陆后被应用自身推到新页(登录墙/引导页)记 forwarded(立案但非接线问题,模板不派修);
|
|
12
|
+
* 工程缺钩子(EntryAbility 透传 / Index 着陆日志,均由 wire-routes 生成)记 nohook,不给绿。
|
|
13
|
+
* ③ 控件树软判(case,exit 2):只数着陆 NavDestination 子树(整窗口约 40 个系统节点,整窗口计数骨架页也恒过);
|
|
14
|
+
* 子树节点 < 阈值 → hollow(着陆页仍是骨架占位记 scaffold);--expect 文本缺失同级
|
|
15
|
+
* ④ 截图落盘(--out):供多模态模型/人工增强判读,本脚本不据此裁决
|
|
16
|
+
*
|
|
17
|
+
* 登录墙/空态页口径:崩溃判定不受登录态影响(未登录直达即崩=真缺陷,照抓);渲染出登录墙/空态=有节点不误报;
|
|
18
|
+
* 确因无数据被判 hollow 的属 case 软立案——模型裁决"需登录/空态合理"即 waive;登录后业务纵深归 4b(测试账号)。
|
|
19
|
+
*
|
|
20
|
+
* 用法:smoke.mjs --project <鸿蒙工程根> [--bundle <包名>] [--routes A,B,C] [--device <hdc key>]
|
|
21
|
+
* [--expect "RouteA:文本1|文本2,RouteB:文本"] [--nodes-min 10] [--out spec/smoke] [--json <报告>]
|
|
22
|
+
* 退出码:0 全绿 / 2 立案(misland·nohook·bounced·forwarded·scaffold·hollow·expect-miss·nodump·not-foreground)/ 3 环境阻塞(无设备·无 hap·hap 过期·装机失败)/ 10 崩溃硬违规
|
|
23
|
+
*/
|
|
24
|
+
import { execSync } from 'node:child_process';
|
|
25
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { join, resolve, dirname } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { probeVerdict } from './verdict.mjs';
|
|
29
|
+
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
if (argv.includes('--help')) { console.log(readFileSync(new URL(import.meta.url), 'utf8').split('\n').filter(l => /^ \* /.test(l)).map(l => l.slice(3)).join('\n')); process.exit(0); }
|
|
32
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
|
|
33
|
+
const PROJECT = argOf('--project', null);
|
|
34
|
+
if (!PROJECT) { console.error('usage: smoke.mjs --project <root> [--bundle] [--routes] [--expect] [--out] [--json]'); process.exit(3); }
|
|
35
|
+
const OUT = argOf('--out', 'spec/smoke');
|
|
36
|
+
const NODES_MIN = Number(argOf('--nodes-min', '10'));
|
|
37
|
+
const ROUTES = (argOf('--routes', '') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
38
|
+
const EXPECT = new Map((argOf('--expect', '') || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
39
|
+
.map(kv => { const i = kv.indexOf(':'); return [kv.slice(0, i), kv.slice(i + 1).split('|').filter(Boolean)]; }));
|
|
40
|
+
|
|
41
|
+
// 多设备:hdc 连着多个目标时每条命令都要 -t <connect-key>(否则报 need connect-key)。HMIG_HDC_TARGET 指定,否则取列表首个。
|
|
42
|
+
const hdcTargets = () => { try { return execSync('hdc list targets', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 }).split('\n').map(s => s.trim()).filter(s => s && !/\[Empty\]/i.test(s)); } catch { return []; } };
|
|
43
|
+
// 设备选择:--device <key> > HMIG_HDC_TARGET > 多目标取首个(Windows 的 cmd/PowerShell 没有 `VAR=x cmd` 前缀写法,故补 --device)
|
|
44
|
+
const hdcKey = (argOf('--device', '') || process.env.HMIG_HDC_TARGET || '').trim() || (() => { const t = hdcTargets(); return t.length > 1 ? t[0] : ''; })();
|
|
45
|
+
const sh = (cmd, opt) => execSync(hdcKey && /^hdc\s(?!list\s+targets)/.test(cmd) ? cmd.replace(/^hdc\s/, `hdc -t ${hdcKey} `) : cmd,
|
|
46
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, ...opt }).trim();
|
|
47
|
+
const shOk = (cmd) => { try { return sh(cmd); } catch { return null; } };
|
|
48
|
+
const sleep = (ms) => execSync(process.platform === 'win32' ? `ping -n ${Math.ceil(ms / 1000) + 1} 127.0.0.1 > NUL` : `sleep ${ms / 1000}`);
|
|
49
|
+
const envBlock = (msg) => { console.error(`[smoke] 环境阻塞(exit 3):${msg}`); process.exit(3); };
|
|
50
|
+
|
|
51
|
+
// 设备与 hap(无 target 先经 align-sdk --device-only 自动拉起模拟器,拉不起才跳过)
|
|
52
|
+
const hasTarget = () => { const t = shOk('hdc list targets') || ''; return !!t && !/\[Empty\]/i.test(t); };
|
|
53
|
+
if (!hasTarget()) {
|
|
54
|
+
const ensure = fileURLToPath(new URL('./align-sdk.mjs', import.meta.url));
|
|
55
|
+
try { execSync(`node "${ensure}" --device-only`, { stdio: 'inherit', timeout: 240000 }); } catch { /* exit 3 走跳过 */ }
|
|
56
|
+
}
|
|
57
|
+
if (!hasTarget()) envBlock('无设备/模拟器(自动拉起未成)');
|
|
58
|
+
let bundle = argOf('--bundle', null);
|
|
59
|
+
if (!bundle) {
|
|
60
|
+
const appPath = resolve(PROJECT, 'AppScope/app.json5');
|
|
61
|
+
const app = existsSync(appPath) ? readFileSync(appPath, 'utf8') : '';
|
|
62
|
+
const m = app.match(/"bundleName"\s*:\s*"([^"]+)"/); bundle = m ? m[1] : null;
|
|
63
|
+
}
|
|
64
|
+
if (!bundle) envBlock('解析不到 bundleName,--bundle 显式给');
|
|
65
|
+
// 入口模块目录从 build-profile 派生(modules[].srcPath 中 module.json5 type=entry 者),不写死 entry
|
|
66
|
+
function entryModule() {
|
|
67
|
+
try {
|
|
68
|
+
const bp = readFileSync(bpPath, 'utf8').replace(/\/\/.*$/gm, '');
|
|
69
|
+
for (const m of bp.matchAll(/["']?srcPath["']?\s*:\s*["']([^"']+)["']/g)) {
|
|
70
|
+
const mj = resolve(PROJECT, m[1], 'src/main/module.json5');
|
|
71
|
+
if (existsSync(mj) && /["']?type["']?\s*:\s*["']entry["']/.test(readFileSync(mj, 'utf8'))) return m[1].replace(/^\.\//, '');
|
|
72
|
+
}
|
|
73
|
+
} catch { /* 回退默认 */ }
|
|
74
|
+
return 'entry';
|
|
75
|
+
}
|
|
76
|
+
const bpPath = resolve(PROJECT, 'build-profile.json5');
|
|
77
|
+
const MOD = entryModule();
|
|
78
|
+
const hapDir = resolve(PROJECT, MOD, 'build/default/outputs/default');
|
|
79
|
+
const hap = existsSync(hapDir) ? readdirSync(hapDir).filter(f => f.endsWith('.hap'))
|
|
80
|
+
.map(f => ({ f, t: statSync(join(hapDir, f)).mtimeMs, size: statSync(join(hapDir, f)).size })).sort((a, b) => b.t - a.t)[0] : null;
|
|
81
|
+
if (!hap) envBlock('无 hap 产物(先构建)');
|
|
82
|
+
// hap 新鲜度:源码任何一处比 hap 新 = 装的是旧包,冒烟结论无效(实测三波"绿"全在测骨架包)
|
|
83
|
+
function newestSrc(dir) {
|
|
84
|
+
let t = 0;
|
|
85
|
+
const stack = [dir];
|
|
86
|
+
while (stack.length) {
|
|
87
|
+
const d = stack.pop();
|
|
88
|
+
let es; try { es = readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
89
|
+
for (const e of es) {
|
|
90
|
+
if (e.name === 'build' || e.name === 'oh_modules' || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
91
|
+
const p = join(d, e.name);
|
|
92
|
+
if (e.isDirectory()) stack.push(p);
|
|
93
|
+
else if (/\.(ets|ts|json5?)$/.test(e.name)) { try { t = Math.max(t, statSync(p).mtimeMs); } catch { /* 忽略 */ } }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return t;
|
|
97
|
+
}
|
|
98
|
+
const srcNewest = newestSrc(resolve(PROJECT, MOD, 'src/main'));
|
|
99
|
+
if (srcNewest > hap.t + 1000) envBlock(`hap 早于源码(hap ${new Date(hap.t).toISOString()} < 源 ${new Date(srcNewest).toISOString()})——先构建再冒烟`);
|
|
100
|
+
|
|
101
|
+
// API 一致性预检:工程 compatibleSdkVersion ≤ 设备 apiversion 才能装——不符时前置报因,
|
|
102
|
+
// 免得落成 install 阶段的天书报错(分发环境高频坑)
|
|
103
|
+
if (existsSync(bpPath)) {
|
|
104
|
+
const mm = readFileSync(bpPath, 'utf8').match(/"compatibleSdkVersion"\s*:\s*"?(?:[\d.]+\((\d+)\)|(\d+))"?/);
|
|
105
|
+
const compat = mm ? Number(mm[1] || mm[2]) : null;
|
|
106
|
+
const devApi = Number((shOk('hdc shell param get const.ohos.apiversion') || '').trim()) || null;
|
|
107
|
+
if (compat && devApi && devApi < compat) {
|
|
108
|
+
envBlock(`API 不匹配:设备 apiversion ${devApi} < 工程 compatibleSdkVersion ${compat}——` +
|
|
109
|
+
`换高版本镜像(Emulator -imageList / -install -osVersion 对应版)或降工程 compatibleSdkVersion 后重试`);
|
|
110
|
+
}
|
|
111
|
+
if (compat && devApi) console.log(`[smoke] API 匹配:设备 ${devApi} ≥ 工程 compatible ${compat}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 钩子在场判定(wire-routes 生成物):透传缺 = 直达不可能;着陆日志缺 = 直达不可证
|
|
115
|
+
const etsRoot = resolve(PROJECT, MOD, 'src/main/ets');
|
|
116
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
117
|
+
const hasPassthrough = /smokeRoute/.test(readIf(join(etsRoot, 'entryability', 'EntryAbility.ets')));
|
|
118
|
+
const hasLandingLog = /hmigSmoke/.test(readIf(join(etsRoot, 'pages', 'Index.ets')));
|
|
119
|
+
if (ROUTES.length && !(hasPassthrough && hasLandingLog)) {
|
|
120
|
+
console.log(`[smoke] ▲ 直达钩子不全(EntryAbility 透传:${hasPassthrough ? '有' : '缺'} · Index 着陆日志:${hasLandingLog ? '有' : '缺'})——路由探针将记 nohook;重跑 wire-routes 补钩子`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
mkdirSync(resolve(OUT), { recursive: true });
|
|
124
|
+
const report = { bundle, hap: { file: hap.f, size: hap.size, mtime: new Date(hap.t).toISOString() }, hooks: { passthrough: hasPassthrough, landingLog: hasLandingLog }, routes: [], crash: null };
|
|
125
|
+
const FAULT_DIR = '/data/log/faultlog/faultlogger/';
|
|
126
|
+
const faultBase = new Set((shOk(`hdc shell "ls ${FAULT_DIR}"`) || '').split(/\s+/).filter(Boolean));
|
|
127
|
+
|
|
128
|
+
// 装机(看输出不看退出码)+ 冷启
|
|
129
|
+
console.log(`[smoke] install ${hap.f} (${Math.round(hap.size / 1024)}KB, ${report.hap.mtime}) → ${bundle}`);
|
|
130
|
+
const installOk = (out) => out !== null && /success/i.test(out) && !/\[fail\]|failed|error/i.test(out);
|
|
131
|
+
let inst = shOk(`hdc install -r "${join(hapDir, hap.f)}"`);
|
|
132
|
+
// 签名不一致(code 9568332):设备上残留上一轮不同签名的同包名包,覆盖装必失败且旧包被当结果。
|
|
133
|
+
// 被测应用是迁移产物,卸旧重装是唯一解,自动做并明说;其他失败原因不动设备。
|
|
134
|
+
if (!installOk(inst) && /9568332|sign info inconsistent|signature/i.test(String(inst || ''))) {
|
|
135
|
+
console.log(`[smoke] 设备上同包名旧包签名不一致(上一轮残留)→ 卸载重装`);
|
|
136
|
+
shOk(`hdc uninstall ${bundle}`);
|
|
137
|
+
inst = shOk(`hdc install "${join(hapDir, hap.f)}"`);
|
|
138
|
+
}
|
|
139
|
+
if (!installOk(inst)) envBlock(`装机失败:${String(inst || '(无输出)').trim().slice(-300)}`);
|
|
140
|
+
sh(`hdc shell aa force-stop ${bundle}`);
|
|
141
|
+
// 着陆页是否仍是 wire 骨架占位(判据:route_map 的 pageSourceFile 含占位标记)——2a 冷启落骨架页属预期(只看崩溃与着陆),
|
|
142
|
+
// 路由探针落骨架页=该片没实现(立案)。骨架残留的终审归 check-routes(integration case / final violation)。
|
|
143
|
+
const rmPath = resolve(PROJECT, MOD, 'src/main/resources/base/profile/route_map.json');
|
|
144
|
+
const scaffoldRoutes = new Set();
|
|
145
|
+
if (existsSync(rmPath)) {
|
|
146
|
+
try {
|
|
147
|
+
for (const e of (JSON.parse(readFileSync(rmPath, 'utf8')).routerMap || [])) {
|
|
148
|
+
const pf = resolve(PROJECT, MOD, String(e.pageSourceFile || ''));
|
|
149
|
+
if (existsSync(pf) && /TODO\(切片\): 页面内容/.test(readFileSync(pf, 'utf8'))) scaffoldRoutes.add(e.name);
|
|
150
|
+
}
|
|
151
|
+
} catch { /* 坏 JSON 由构建报错 */ }
|
|
152
|
+
}
|
|
153
|
+
// 首页路由名:Index 壳冷启 push 的 Routes 常量 → Router.ets 里查其字面值(都是 wire-routes 生成的冻结件)。
|
|
154
|
+
// 用途:区分"弹回首页"(bounced,接线/参数问题)与"被应用转发到新页"(forwarded,登录墙等分派链);取不到时 verdict 保守按弹回。
|
|
155
|
+
let launcher = null;
|
|
156
|
+
try {
|
|
157
|
+
const ets = resolve(PROJECT, MOD, 'src/main/ets');
|
|
158
|
+
const c = (readFileSync(resolve(ets, 'pages/Index.ets'), 'utf8').match(/AppRouter\.push\(Routes\.([A-Z0-9_]+)/) || [])[1];
|
|
159
|
+
const m = c && readFileSync(resolve(ets, 'common/Router.ets'), 'utf8').match(new RegExp(`\\b${c}\\s*=\\s*'([^']+)'`));
|
|
160
|
+
launcher = m ? m[1] : null;
|
|
161
|
+
} catch { launcher = null; }
|
|
162
|
+
report.launcher = launcher;
|
|
163
|
+
|
|
164
|
+
function dumpTree() {
|
|
165
|
+
const p = shOk('hdc shell uitest dumpLayout') || ''; // 输出含落盘路径 …/layout_xxx.json
|
|
166
|
+
const m = p.match(/\/\S+\.json/);
|
|
167
|
+
if (!m) return null;
|
|
168
|
+
const local = resolve(OUT, 'layout.json');
|
|
169
|
+
if (shOk(`hdc file recv ${m[0]} "${local}"`) === null) return null;
|
|
170
|
+
try { return JSON.parse(readFileSync(local, 'utf8')); } catch { return null; }
|
|
171
|
+
}
|
|
172
|
+
function countNodes(n) { if (!n || typeof n !== 'object') return { c: 0, texts: [] }; let c = 1; let texts = [];
|
|
173
|
+
const t = n.attributes && (n.attributes.text || n.attributes.title); if (t && String(t).trim()) texts.push(String(t).trim());
|
|
174
|
+
for (const k of ['children']) for (const ch of n[k] || []) { const r = countNodes(ch); c += r.c; texts = texts.concat(r.texts); }
|
|
175
|
+
return { c, texts }; }
|
|
176
|
+
// 着陆子树:栈顶页 = 树中最后一个可见 NavDestination(Navigation 把栈内页按序挂在 NavigationContent 下)
|
|
177
|
+
function landingSubtree(tree) {
|
|
178
|
+
let last = null, lastVisible = null;
|
|
179
|
+
(function w(n) {
|
|
180
|
+
if (!n || typeof n !== 'object') return;
|
|
181
|
+
const a = n.attributes || {};
|
|
182
|
+
if (a.type === 'NavDestination') { last = n; if (a.visible !== 'false') lastVisible = n; }
|
|
183
|
+
for (const ch of n.children || []) w(ch);
|
|
184
|
+
})(tree);
|
|
185
|
+
return lastVisible || last;
|
|
186
|
+
}
|
|
187
|
+
function foreground() {
|
|
188
|
+
const d = shOk('hdc shell aa dump -l') || '';
|
|
189
|
+
return d.split(/Mission ID #/).some(b => b.includes(`bundle name [${bundle}]`) && /state #FOREGROUND/.test(b));
|
|
190
|
+
}
|
|
191
|
+
const landingLines = () => (shOk('hdc shell "hilog -x -T hmigSmoke"') || '').split('\n').filter(l => /didShow/.test(l));
|
|
192
|
+
|
|
193
|
+
function probe(route) {
|
|
194
|
+
const label = route || '(cold-start)';
|
|
195
|
+
sh(`hdc shell aa force-stop ${bundle}`);
|
|
196
|
+
const before = new Set(landingLines());
|
|
197
|
+
sh(`hdc shell aa start -a EntryAbility -b ${bundle}${route ? ` --ps smokeRoute ${route}` : ''}`);
|
|
198
|
+
sleep(4000);
|
|
199
|
+
const fg = foreground();
|
|
200
|
+
const fresh = landingLines().filter(l => !before.has(l));
|
|
201
|
+
const shown = fresh.map(l => (l.match(/didShow\s+(\S+)/) || [])[1]).filter(Boolean);
|
|
202
|
+
const landed = shown.length ? shown[shown.length - 1] : null;
|
|
203
|
+
const tree = dumpTree();
|
|
204
|
+
const whole = tree ? countNodes(tree).c : -1;
|
|
205
|
+
const sub = tree ? landingSubtree(tree) : null;
|
|
206
|
+
const { c, texts } = sub ? countNodes(sub) : (tree ? countNodes(tree) : { c: -1, texts: [] });
|
|
207
|
+
const shot = join(OUT, `smoke-${route || 'home'}.png`.replace(/[^\w.-]/g, '_')); // 只清洗文件名,不动目录(Windows 盘符/反斜杠)
|
|
208
|
+
shOk(`hdc shell uitest screenCap -p /data/local/tmp/smk.png`); shOk(`hdc file recv /data/local/tmp/smk.png "${resolve(shot)}"`);
|
|
209
|
+
const miss = (EXPECT.get(route || '') || []).filter(t => !texts.some(x => x.includes(t)));
|
|
210
|
+
// 判定逻辑在 verdict.mjs(纯函数,测试覆盖各分支)
|
|
211
|
+
const { verdict, bounced, forwarded, failing, why } = probeVerdict({ route, landed, shown, foreground: fg, hasPassthrough, hasLandingLog, nodes: c, nodesMin: NODES_MIN, scaffoldRoutes, missing: miss, launcher });
|
|
212
|
+
report.routes.push({ route: label, foreground: fg, landed, nodes: c, windowNodes: whole, textCount: texts.length, missing: miss, shot, verdict, failing, ...(bounced ? { bounced: true, shown } : {}), ...(forwarded ? { forwarded: true, shown } : {}) });
|
|
213
|
+
console.log(` ${failing ? '▲' : '✓'} ${label}: 着陆 ${landed || '-'} · 页面节点 ${c}(窗口 ${whole})${miss.length ? ` 缺文本[${miss.join(',')}]` : ''}${why ? ` · ${why}` : ''} → ${shot}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
probe(null);
|
|
217
|
+
for (const r of ROUTES) probe(r);
|
|
218
|
+
|
|
219
|
+
// faultlog 复查
|
|
220
|
+
const faultNow = (shOk(`hdc shell "ls -t ${FAULT_DIR}"`) || '').split(/\s+/).filter(Boolean);
|
|
221
|
+
const fresh = faultNow.filter(f => !faultBase.has(f) && new RegExp(bundle.replace(/\./g, '\\.')).test(f));
|
|
222
|
+
if (fresh.length) {
|
|
223
|
+
const stack = (shOk(`hdc shell "cat ${FAULT_DIR}${fresh[0]}"`) || '').split('\n').slice(0, 40).join('\n');
|
|
224
|
+
report.crash = { file: fresh[0], stackHead: stack };
|
|
225
|
+
console.log(`✗ 冒烟期间崩溃:${fresh[0]}\n${stack.split('\n').filter(l => /Error|at |Reason/.test(l)).slice(0, 8).join('\n')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const jsonOut = argOf('--json', null);
|
|
229
|
+
if (jsonOut) { mkdirSync(dirname(resolve(jsonOut)), { recursive: true }); writeFileSync(resolve(jsonOut), JSON.stringify(report, null, 2)); }
|
|
230
|
+
const bad = report.routes.filter(r => r.failing);
|
|
231
|
+
if (report.crash) process.exit(10);
|
|
232
|
+
if (bad.length) {
|
|
233
|
+
const tally = {}; for (const r of bad) tally[r.verdict] = (tally[r.verdict] || 0) + 1;
|
|
234
|
+
console.log(`[smoke] ${bad.length} 页未过(${Object.entries(tally).map(([k, v]) => `${k}×${v}`).join(' ')}):misland/nohook=接线问题(透传·钩子·route_map 注册),bounced=推入即弹回(参数判型/pop 防御),scaffold=片未实现(only 续跑该片),forwarded=应用自身转发链(登录墙/引导页,不派修,4b 走查),hollow=页面空壳或空态(case,回模型裁决),nodump=控件树没取到(查 uitest/hdc 后重跑)`);
|
|
235
|
+
process.exit(2);
|
|
236
|
+
}
|
|
237
|
+
console.log('[smoke] 全绿(装机成功·前台·着陆核对·页面子树均过)');
|
|
238
|
+
process.exit(0);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 冒烟探针判定(纯函数):smoke.mjs 与测试共用——探针本身要设备,判定逻辑抽出来才能无设备覆盖各分支。
|
|
2
|
+
// 输入全是探针已取到的事实(前台、着陆序列、节点数、期望文本缺失),输出裁决与人读原因。
|
|
3
|
+
export function probeVerdict({ route, landed, shown = [], foreground, hasPassthrough, hasLandingLog, nodes, nodesMin, scaffoldRoutes = new Set(), missing = [], launcher = null }) {
|
|
4
|
+
// 曾着陆目标页、随后又出现别的着陆记录:两种成因要分开——
|
|
5
|
+
// 弹回(bounced):落回首页/navBar 或目标之前已出现过的页 = 推入后 pop 回栈底(典型:参数判型失败即 back() 的防御逻辑;实证编辑页永远打不开)
|
|
6
|
+
// 转发(forwarded):目标之后才首次出现、且不是首页/navBar 的新页 = 应用自身的分派链(登录墙/引导页),不是接线问题
|
|
7
|
+
// (实证:Main→PgLogin 登录分派被判 misland 派修 9 分钟,修复代理只做了豁免)。首页未知时不认转发,一律按弹回(保守)。
|
|
8
|
+
const visited = !!route && landed !== route && shown.includes(route);
|
|
9
|
+
const forwarded = visited && !!launcher && landed !== launcher && landed !== 'navBar' && shown.indexOf(landed) > shown.indexOf(route);
|
|
10
|
+
const bounced = visited && !forwarded;
|
|
11
|
+
let verdict = 'ok';
|
|
12
|
+
if (!foreground) verdict = 'not-foreground';
|
|
13
|
+
else if (route && !(hasPassthrough && hasLandingLog)) verdict = 'nohook';
|
|
14
|
+
else if (route && landed !== route) verdict = bounced ? 'bounced' : forwarded ? 'forwarded' : 'misland';
|
|
15
|
+
if (verdict === 'ok' && nodes >= 0 && nodes < nodesMin) verdict = (landed && scaffoldRoutes.has(landed)) ? 'scaffold' : 'hollow';
|
|
16
|
+
// 控件树没拿到(dumpLayout/recv 失败)不能当绿:没有证据的页记 nodump(立案),否则空壳页会假绿
|
|
17
|
+
if (verdict === 'ok' && nodes < 0) verdict = 'nodump';
|
|
18
|
+
if (missing.length) verdict = verdict === 'ok' ? 'expect-miss' : verdict + '+expect-miss';
|
|
19
|
+
// 冷启落骨架页(2a 首屏冒烟)属预期,不计失败;路由探针落骨架页=该片没实现,计失败
|
|
20
|
+
const failing = verdict !== 'ok' && !(verdict === 'scaffold' && !route);
|
|
21
|
+
const base = verdict.replace(/\+expect-miss$/, '');
|
|
22
|
+
const why = base === 'bounced' ? `曾着陆 ${route} 随后自动弹回到 ${landed || '(无)'}(多为参数判型失败即 back()/pop 的防御逻辑——参数经序列化后 instanceof 恒 false)`
|
|
23
|
+
: base === 'forwarded' ? `着陆 ${route} 后被应用自身转发到 ${landed}(登录墙/引导/分派页),非接线问题——不派修,4b 沿该转发链走查`
|
|
24
|
+
: base === 'misland' ? `着陆 ${landed || '(无着陆日志)'} ≠ ${route}`
|
|
25
|
+
: base === 'not-foreground' ? '目标 bundle 不在前台'
|
|
26
|
+
: base === 'nohook' ? '缺直达钩子'
|
|
27
|
+
: base === 'scaffold' ? (route ? '仍是骨架占位页(片未实现)' : '骨架占位页(2a 预期)')
|
|
28
|
+
: base === 'nodump' ? '控件树未取到(uitest dumpLayout/recv 失败),无页面证据'
|
|
29
|
+
: '';
|
|
30
|
+
return { verdict, bounced, forwarded, failing, why };
|
|
31
|
+
}
|