android2harmony 0.1.3 → 0.1.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/README.md +1 -407
- package/agents/self-tester.md +376 -339
- package/dist/index.js +131 -586
- package/dist/index.js.map +4 -4
- package/package.json +36 -37
- package/skills/a2h-resource-convert/SKILL.md +902 -0
- package/skills/a2h-resource-convert/references/code-vector-icon-rules.md +335 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md +15 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/dependency-analysis-rules.md +24 -2
- package/skills/a2h-resource-convert/references/lottie-conversion-rules.md +219 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/resource-mapping-rules.md +27 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/xml-drawable-to-svg-rules.md +118 -1
- package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +2166 -0
- package/skills/a2h-resource-convert/scripts/code_vector_icons.js +607 -0
- package/skills/a2h-resource-convert/scripts/package.json +3 -0
- package/skills/a2h-resource-convert/scripts/svg_fidelity_check.js +632 -0
- package/skills/a2h-ui-transfer/SKILL.md +420 -0
- package/skills/a2h-ui-transfer/references/conversion-procedure.md +572 -0
- package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +1 -1
- package/skills/a2h-ui-transfer/references/mvvm-v2/_/350/243/205/351/245/260/345/231/250/351/200/237/346/237/245.md +133 -0
- package/skills/{hmos-batch-ui-align/scripts/android_parse_fast.ts → a2h-ui-transfer/scripts/android_parse_fast.js} +397 -283
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +1624 -0
- package/skills/a2h-ui-transfer/scripts/measure_pack.js +1005 -0
- package/skills/a2h-ui-transfer/scripts/package.json +3 -0
- package/skills/hmos-incremental-ui-align/README.md +251 -251
- package/skills/hmos-incremental-ui-align/SKILL.md +364 -364
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +2 -2
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.js +790 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.js +285 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -76
- package/skills/hmos-incremental-ui-align/scripts/page_capture.js +756 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.js +155 -0
- package/skills/hmos-batch-ui-align/SKILL.md +0 -141
- package/skills/hmos-batch-ui-align/references/conversion-procedure.md +0 -217
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +0 -999
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +0 -343
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +0 -977
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +0 -188
- package/skills/hmos-resources-convert/SKILL.md +0 -654
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +0 -10
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +0 -8
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/build-profile.json5 +0 -42
- package/skills/hmos-resources-convert/template/code-linter.json5 +0 -32
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +0 -33
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +0 -23
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +0 -10
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +0 -48
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +0 -23
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +0 -55
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +0 -3
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +0 -5
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +0 -2
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +0 -35
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +0 -16
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +0 -33
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +0 -23
- package/skills/hmos-resources-convert/template/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +0 -28
- package/skills/hmos-resources-convert/template/oh-package.json5 +0 -10
- package/tools/autotest/deps/autotest-agent-0.1.1.tgz +0 -0
- package/tools/autotest/engine/batch-launcher.ts +0 -326
- package/tools/autotest/engine/report-tool.ts +0 -773
- package/tools/autotest/engine/self-test-runner.ts +0 -1024
- package/tools/autotest/engine/testcases-tool.ts +0 -246
- package/tools/autotest/resolve-metadata-tool.ts +0 -143
- package/tools/autotest/runner/logger.ts +0 -45
- package/tools/autotest/runner/process-utils.ts +0 -41
- package/tools/autotest/validate.ts +0 -115
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md" +0 -0
|
@@ -1,1024 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self-Test Runner — launches HarmonyOS on-device self-tests via the Node.js
|
|
3
|
-
* `@autotest/agent` engine (batch_runner), then reports status from output files.
|
|
4
|
-
*
|
|
5
|
-
* run — prepare env (npm install + hdc install), spawn batch detached, return PID.
|
|
6
|
-
* status — report state from output files (summary.json / task_results.jsonl) + native liveness.
|
|
7
|
-
* kill — terminate the background batch.
|
|
8
|
-
*
|
|
9
|
-
* STDOUT carries strict JSON parsed by the caller; all logging goes to stderr /
|
|
10
|
-
* the log file. JSON shapes + exit codes are a frozen contract (see agents/self-tester.md).
|
|
11
|
-
*/
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
|
-
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { Command } from 'commander';
|
|
18
|
-
import { createLogger, type Logger } from '../runner/logger.ts';
|
|
19
|
-
import { killProcessTree, pidAlive } from '../runner/process-utils.ts';
|
|
20
|
-
import {
|
|
21
|
-
type CheckResult,
|
|
22
|
-
checkHap,
|
|
23
|
-
checkInputs,
|
|
24
|
-
checkPre,
|
|
25
|
-
} from '../validate.ts';
|
|
26
|
-
|
|
27
|
-
const HAP_INSTALL_WAIT = 3; // post-install settle, in seconds (reduced from the original 10s).
|
|
28
|
-
const INSTALL_TIMEOUT = 120;
|
|
29
|
-
const DEFAULT_CATEGORY = 'self_test';
|
|
30
|
-
|
|
31
|
-
let logger: Logger = createLogger(null, 'INFO');
|
|
32
|
-
|
|
33
|
-
function timestampForFile(d: Date): string {
|
|
34
|
-
const pad = (n: number) => String(n).padStart(2, '0');
|
|
35
|
-
return (
|
|
36
|
-
`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_` +
|
|
37
|
-
`${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Synchronous sleep (PowerShell on Windows, `sleep` on POSIX). Best-effort. */
|
|
42
|
-
function sleep(sec: number): void {
|
|
43
|
-
if (sec <= 0) return;
|
|
44
|
-
const ms = sec * 1000;
|
|
45
|
-
try {
|
|
46
|
-
if (process.platform === 'win32') {
|
|
47
|
-
spawnSync('powershell.exe', ['-NoProfile', '-Command', `Start-Sleep -Seconds ${sec}`], {
|
|
48
|
-
stdio: 'ignore',
|
|
49
|
-
timeout: ms + 5000,
|
|
50
|
-
windowsHide: true,
|
|
51
|
-
});
|
|
52
|
-
} else {
|
|
53
|
-
spawnSync('sleep', [String(sec)], { stdio: 'ignore', timeout: ms + 5000 });
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
// best effort
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function setupLogging(outputDir: string): void {
|
|
61
|
-
const logPath = path.join(outputDir, `self_test_${timestampForFile(new Date())}.log`);
|
|
62
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
63
|
-
logger = createLogger(fs.createWriteStream(logPath, { encoding: 'utf-8' }), 'DEBUG');
|
|
64
|
-
logger.info(`日志文件: ${logPath}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---- external tool discovery ----------------------------------------------
|
|
68
|
-
|
|
69
|
-
/** Find `name` on PATH via `where` (Windows) or `which` (POSIX). Null on failure. */
|
|
70
|
-
function findOnPath(name: string): string | null {
|
|
71
|
-
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
72
|
-
try {
|
|
73
|
-
const out = execFileSync(finder, [name], {
|
|
74
|
-
encoding: 'utf-8',
|
|
75
|
-
windowsHide: true,
|
|
76
|
-
timeout: 5000,
|
|
77
|
-
});
|
|
78
|
-
const first = out.split(/\r?\n/).find((l) => l.trim());
|
|
79
|
-
return first ? first.trim() : null;
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getHdc(): string {
|
|
86
|
-
const found = findOnPath('hdc');
|
|
87
|
-
if (!found) {
|
|
88
|
-
logger.error('hdc 未找到');
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
return found;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Absolute path to the compiled `batch-launcher.js` — the detached background
|
|
96
|
-
* process that drives `new AutoTestAgent({...})` programmatically. Sits next to
|
|
97
|
-
* this module under `dist/test-tools/autotest/` (and `src/...` in dev mode via
|
|
98
|
-
* `HOMETRANS_AUTOTEST_DIR`).
|
|
99
|
-
*/
|
|
100
|
-
function batchLauncherPath(): string {
|
|
101
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
102
|
-
const p = path.join(here, 'batch-launcher.ts');
|
|
103
|
-
if (!fs.existsSync(p)) {
|
|
104
|
-
logger.error(
|
|
105
|
-
`未找到 batch-launcher: ${p}\n` +
|
|
106
|
-
'请先在 HomeTrans 根目录执行 `npm run build` 重新构建 dist/。',
|
|
107
|
-
);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
return p;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Model params injected via stdin by the a2h plugin tool (which resolved them
|
|
115
|
-
* via `deps.resolveModelParams`). `provider` is not surfaced by that contract,
|
|
116
|
-
* so it defaults to `openai` (OpenAI-compatible — works with any baseURL the
|
|
117
|
-
* `@ai-sdk/openai` client can target). No `~/.hometrans/config.json` involved.
|
|
118
|
-
*/
|
|
119
|
-
interface ModelConfig {
|
|
120
|
-
apiKey: string;
|
|
121
|
-
modelName: string;
|
|
122
|
-
baseURL: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Optional device/agent overrides passed through from CLI args. */
|
|
126
|
-
interface DeviceOpts {
|
|
127
|
-
deviceSn?: string;
|
|
128
|
-
ip?: string;
|
|
129
|
-
port?: string;
|
|
130
|
-
agentMode?: string;
|
|
131
|
-
maxSteps?: string;
|
|
132
|
-
promptVersion?: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Read `{ apiKey, modelName, baseURL }` JSON from stdin (fd 0). The plugin
|
|
137
|
-
* tool writes the blob and closes the write end; `readFileSync(0)` returns
|
|
138
|
-
* once EOF is seen. Throws on missing/invalid/empty apiKey.
|
|
139
|
-
*/
|
|
140
|
-
function readModelFromStdin(): ModelConfig {
|
|
141
|
-
let raw = '';
|
|
142
|
-
try {
|
|
143
|
-
raw = fs.readFileSync(0, 'utf-8');
|
|
144
|
-
} catch (e) {
|
|
145
|
-
throw new Error(`--model-stdin: failed to read stdin: ${(e as Error).message}`);
|
|
146
|
-
}
|
|
147
|
-
let parsed: unknown;
|
|
148
|
-
try {
|
|
149
|
-
parsed = JSON.parse(raw);
|
|
150
|
-
} catch (e) {
|
|
151
|
-
throw new Error(`--model-stdin: stdin is not valid JSON: ${(e as Error).message}`);
|
|
152
|
-
}
|
|
153
|
-
const m = (parsed ?? {}) as Partial<ModelConfig>;
|
|
154
|
-
if (!m.apiKey) {
|
|
155
|
-
throw new Error('--model-stdin: stdin JSON missing apiKey');
|
|
156
|
-
}
|
|
157
|
-
return { apiKey: m.apiKey, modelName: m.modelName ?? '', baseURL: m.baseURL ?? '' };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Bridge the stdin-injected model params + CLI device/agent options into the
|
|
162
|
-
* `AUTOTEST_*` env vars the batch launcher reads — these become the
|
|
163
|
-
* `new AutoTestAgent({ apiKey, modelName, … })` constructor args, so the
|
|
164
|
-
* key/model/device flow straight into the agent constructor with no
|
|
165
|
-
* config.yaml file and no `~/.hometrans/config.json`.
|
|
166
|
-
*/
|
|
167
|
-
function agentEnvFromModel(model: ModelConfig, dev: DeviceOpts, base: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
168
|
-
const env: NodeJS.ProcessEnv = { ...base };
|
|
169
|
-
env.AUTOTEST_API_KEY = model.apiKey;
|
|
170
|
-
if (model.modelName) env.AUTOTEST_MODEL = model.modelName;
|
|
171
|
-
if (model.baseURL) env.AUTOTEST_BASE_URL = model.baseURL;
|
|
172
|
-
// resolveModelParams returns no provider; OpenAI-compatible default.
|
|
173
|
-
env.AUTOTEST_PROVIDER = 'openai';
|
|
174
|
-
if (dev.deviceSn) env.AUTOTEST_DEVICE_SN = dev.deviceSn;
|
|
175
|
-
if (dev.ip) env.AUTOTEST_IP = dev.ip;
|
|
176
|
-
if (dev.port) env.AUTOTEST_PORT = dev.port;
|
|
177
|
-
env.AUTOTEST_MODE = dev.agentMode || 'single';
|
|
178
|
-
if (dev.maxSteps) env.AUTOTEST_MAX_STEPS = dev.maxSteps;
|
|
179
|
-
if (dev.promptVersion) env.AUTOTEST_PROMPT_VERSION = dev.promptVersion;
|
|
180
|
-
return env;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ---- sync subprocess runner ------------------------------------------------
|
|
184
|
-
|
|
185
|
-
interface RunCmdResult {
|
|
186
|
-
returncode: number;
|
|
187
|
-
stdout: string;
|
|
188
|
-
stderr: string;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Synchronous subprocess runner with logging. On non-zero exit logs
|
|
193
|
-
* stdout/stderr; if `check` (default true), exits with the failing code.
|
|
194
|
-
*/
|
|
195
|
-
function runCmd(
|
|
196
|
-
cmd: string[],
|
|
197
|
-
opts: { cwd?: string; check?: boolean; timeout?: number; env?: NodeJS.ProcessEnv } = {},
|
|
198
|
-
): RunCmdResult {
|
|
199
|
-
const { cwd, check = true, timeout, env } = opts;
|
|
200
|
-
const cmdStr = cmd.join(' ');
|
|
201
|
-
logger.info(`>>> ${cmdStr}`);
|
|
202
|
-
const result = spawnSync(cmd[0], cmd.slice(1), {
|
|
203
|
-
cwd,
|
|
204
|
-
env,
|
|
205
|
-
encoding: 'utf-8',
|
|
206
|
-
timeout: timeout ? timeout * 1000 : undefined,
|
|
207
|
-
maxBuffer: 1024 * 1024 * 1024,
|
|
208
|
-
windowsHide: true,
|
|
209
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
210
|
-
});
|
|
211
|
-
if (result.error) {
|
|
212
|
-
logger.error(`命令异常: ${result.error.message}`);
|
|
213
|
-
throw result.error;
|
|
214
|
-
}
|
|
215
|
-
const status = result.status ?? 0;
|
|
216
|
-
const stdout = result.stdout ?? '';
|
|
217
|
-
const stderr = result.stderr ?? '';
|
|
218
|
-
if (status !== 0) {
|
|
219
|
-
logger.warning(`退出码=${status}: ${cmdStr}`);
|
|
220
|
-
if (stdout) logger.warning(` stdout: ${stdout.trim()}`);
|
|
221
|
-
if (stderr) logger.warning(` stderr: ${stderr.trim()}`);
|
|
222
|
-
if (check) process.exit(status);
|
|
223
|
-
} else {
|
|
224
|
-
logger.debug(`成功 (rc=0): ${cmdStr}`);
|
|
225
|
-
}
|
|
226
|
-
return { returncode: status, stdout, stderr };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---- testcases JSONL normalizer -------------------------------------------
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Normalize `--testcases` into a JSONL path. A JSON array (legacy
|
|
233
|
-
* `testcases.json`) is converted to JSONL at `<taskSubdir>/testcases.jsonl`,
|
|
234
|
-
* filling missing `uuid` (md5 of case_name or `case_<idx>`, first 8 hex) and
|
|
235
|
-
* `spec` (""). Already-JSONL input is validated line-by-line and passed through.
|
|
236
|
-
*/
|
|
237
|
-
export function prepareTestcasesJsonl(inputPath: string, taskSubdir: string): string {
|
|
238
|
-
if (!fs.existsSync(inputPath)) {
|
|
239
|
-
logger.error(`测试用例文件不存在: ${inputPath}`);
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
let text = fs.readFileSync(inputPath, 'utf-8');
|
|
243
|
-
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1); // strip BOM
|
|
244
|
-
const stripped = text.trimStart();
|
|
245
|
-
if (!stripped) {
|
|
246
|
-
logger.error(`测试用例文件为空: ${inputPath}`);
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (stripped[0] === '[') {
|
|
251
|
-
let arr: unknown;
|
|
252
|
-
try {
|
|
253
|
-
arr = JSON.parse(text);
|
|
254
|
-
} catch (e) {
|
|
255
|
-
logger.error(`测试用例 JSON 数组解析失败 ${inputPath}: ${e instanceof Error ? e.message : e}`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
if (!Array.isArray(arr)) {
|
|
259
|
-
logger.error(`测试用例文件根不是数组: ${inputPath}`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
const outPath = path.join(taskSubdir, 'testcases.jsonl');
|
|
263
|
-
const lines = arr.map((entry, idx) => {
|
|
264
|
-
const i = idx + 1;
|
|
265
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
266
|
-
logger.error(`用例 #${i} 不是 JSON 对象: ${JSON.stringify(entry)}`);
|
|
267
|
-
process.exit(1);
|
|
268
|
-
}
|
|
269
|
-
const c = entry as Record<string, unknown>;
|
|
270
|
-
if (!c.uuid) {
|
|
271
|
-
const seed = typeof c.case_name === 'string' && c.case_name ? c.case_name : `case_${i}`;
|
|
272
|
-
c.uuid = crypto.createHash('md5').update(seed, 'utf-8').digest('hex').slice(0, 8);
|
|
273
|
-
}
|
|
274
|
-
if (!('spec' in c)) c.spec = '';
|
|
275
|
-
return JSON.stringify(c);
|
|
276
|
-
});
|
|
277
|
-
fs.writeFileSync(outPath, lines.join('\n') + '\n', 'utf-8');
|
|
278
|
-
logger.info(`已将 JSON 数组转换为 JSONL: ${outPath}(${arr.length} 条用例)`);
|
|
279
|
-
return outPath;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
283
|
-
lines.forEach((line, i) => {
|
|
284
|
-
try {
|
|
285
|
-
JSON.parse(line);
|
|
286
|
-
} catch (e) {
|
|
287
|
-
logger.error(`JSONL 第 ${i + 1} 行解析失败 ${inputPath}: ${e instanceof Error ? e.message : e}`);
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
logger.info(`测试用例文件已为 JSONL,原样透传: ${inputPath}(${lines.length} 条用例)`);
|
|
292
|
-
return inputPath;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ---- HAP install -----------------------------------------------------------
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Install every `.hap`/`.hsp` from a comma-separated `--hap` arg (each entry a
|
|
299
|
-
* file or a directory) in one `hdc install -r` transaction. Real apps ship an
|
|
300
|
-
* entry HAP plus HSPs, so at least one `.hap` is required. Success is judged by
|
|
301
|
-
* hdc's stdout heuristic — hdc can return 0 on failure.
|
|
302
|
-
*/
|
|
303
|
-
function installHaps(hapArg: string, hdc: string): void {
|
|
304
|
-
const entries = hapArg
|
|
305
|
-
.split(',')
|
|
306
|
-
.map((s) => s.trim())
|
|
307
|
-
.filter((s) => s);
|
|
308
|
-
if (entries.length === 0) {
|
|
309
|
-
logger.error('hap-path 为空');
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
|
-
const pkgs: string[] = [];
|
|
313
|
-
for (const e of entries) {
|
|
314
|
-
if (!fs.existsSync(e)) {
|
|
315
|
-
logger.error(`hap 路径不存在: ${e}`);
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
318
|
-
const stat = fs.statSync(e);
|
|
319
|
-
const lower = e.toLowerCase();
|
|
320
|
-
if (stat.isDirectory()) {
|
|
321
|
-
const found = fs
|
|
322
|
-
.readdirSync(e)
|
|
323
|
-
.filter((f) => f.toLowerCase().endsWith('.hap') || f.toLowerCase().endsWith('.hsp'))
|
|
324
|
-
.sort()
|
|
325
|
-
.map((f) => path.join(e, f));
|
|
326
|
-
if (found.length === 0) {
|
|
327
|
-
logger.error(`目录内无 .hap/.hsp 包: ${e}`);
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
330
|
-
pkgs.push(...found);
|
|
331
|
-
} else if (stat.isFile() && (lower.endsWith('.hap') || lower.endsWith('.hsp'))) {
|
|
332
|
-
pkgs.push(e);
|
|
333
|
-
} else {
|
|
334
|
-
logger.error(`既不是目录也不是 .hap/.hsp 文件: ${e}`);
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Resolve to native absolute paths (backslashes on Windows) AND dedup in one
|
|
339
|
-
// pass. Resolving is required, not cosmetic: hdc on Windows mis-parses a
|
|
340
|
-
// forward-slash *drive-absolute* path (e.g. `C:/a/b.hap`) as relative and
|
|
341
|
-
// prepends its own cwd (`C:\cwd\C:/a/b.hap` → invalid), silently skipping the
|
|
342
|
-
// install after Step 2a already uninstalled the app. `path.resolve` yields the
|
|
343
|
-
// native `C:\a\b.hap` form hdc accepts. On POSIX it's a harmless no-op for
|
|
344
|
-
// already-absolute inputs. Callers assemble --hap from multiple sources
|
|
345
|
-
// (package-set dir, comma-list, build-fixer's hsp/ copy) that can overlap, so
|
|
346
|
-
// dedup on the same resolved key.
|
|
347
|
-
const seen = new Set<string>();
|
|
348
|
-
const deduped: string[] = [];
|
|
349
|
-
for (const p of pkgs) {
|
|
350
|
-
const resolved = path.resolve(p);
|
|
351
|
-
if (seen.has(resolved)) continue;
|
|
352
|
-
seen.add(resolved);
|
|
353
|
-
deduped.push(resolved);
|
|
354
|
-
}
|
|
355
|
-
if (!deduped.some((p) => p.toLowerCase().endsWith('.hap'))) {
|
|
356
|
-
logger.error(`缺少 entry HAP(至少需要一个 .hap): ${hapArg}`);
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// hdc install -r accepts multiple package paths in one transaction.
|
|
361
|
-
logger.info(`安装 ${deduped.length} 个包: ${deduped.map((p) => path.basename(p)).join(', ')}`);
|
|
362
|
-
const result = runCmd([hdc, 'install', '-r', ...deduped], {
|
|
363
|
-
timeout: INSTALL_TIMEOUT,
|
|
364
|
-
check: false,
|
|
365
|
-
});
|
|
366
|
-
const combined = `${(result.stdout || '').trim()}\n${(result.stderr || '').trim()}`;
|
|
367
|
-
const combinedLower = combined.toLowerCase();
|
|
368
|
-
// msg:error is case-sensitive; the rest match on lowercased output.
|
|
369
|
-
const hasError =
|
|
370
|
-
combined.includes('msg:error') ||
|
|
371
|
-
combinedLower.includes('failed to install') ||
|
|
372
|
-
(combined.includes('code:') && combinedLower.includes('error:'));
|
|
373
|
-
const hasSuccess = combinedLower.includes('successfully');
|
|
374
|
-
if (result.returncode === 0 && !hasError && hasSuccess) {
|
|
375
|
-
logger.info('安装成功');
|
|
376
|
-
sleep(HAP_INSTALL_WAIT);
|
|
377
|
-
} else if (result.returncode === 0 && !hasError) {
|
|
378
|
-
logger.warning(`安装结果不确定(rc=0 但无 success 消息): ${combined}`);
|
|
379
|
-
sleep(HAP_INSTALL_WAIT);
|
|
380
|
-
} else {
|
|
381
|
-
logger.error(`安装失败: rc=${result.returncode}, output=${combined}`);
|
|
382
|
-
process.exit(1);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ---- run subcommand --------------------------------------------------------
|
|
387
|
-
|
|
388
|
-
export interface RunArgs {
|
|
389
|
-
testcases: string;
|
|
390
|
-
hap: string;
|
|
391
|
-
bundleName: string;
|
|
392
|
-
taskDir: string;
|
|
393
|
-
outputDir?: string;
|
|
394
|
-
category: string;
|
|
395
|
-
modelStdin?: boolean;
|
|
396
|
-
deviceSn?: string;
|
|
397
|
-
ip?: string;
|
|
398
|
-
port?: string;
|
|
399
|
-
agentMode?: string;
|
|
400
|
-
maxSteps?: string;
|
|
401
|
-
promptVersion?: string;
|
|
402
|
-
timeout?: number | 'auto';
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Per-case outer poll budget when `--timeout auto` is used: mirrors the
|
|
406
|
-
// batch-launcher hard cap CASE_TIMEOUT_MS=10min + 20% margin = 720s/case. The
|
|
407
|
-
// CLI already parses `--testcases`, so it derives the total itself instead of
|
|
408
|
-
// making the caller compute `caseCount * 720` in a (shell-specific) wrapper.
|
|
409
|
-
const PER_CASE_TIMEOUT_SEC = 720;
|
|
410
|
-
|
|
411
|
-
export interface SpawnBatchOpts {
|
|
412
|
-
cmd: string[];
|
|
413
|
-
cwd: string;
|
|
414
|
-
env: NodeJS.ProcessEnv;
|
|
415
|
-
stdoutLog: string;
|
|
416
|
-
pidFile: string;
|
|
417
|
-
taskDir: string;
|
|
418
|
-
taskSubdir: string;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Spawn the batch-launcher detached and write pidFile. Resolves with the PID
|
|
423
|
-
* on the spawn event, rejects with an Error on the spawn-error event (ENOENT,
|
|
424
|
-
* EACCES, …). Caller is responsible for emitting JSON (RUNNING or FAILED) —
|
|
425
|
-
* this function is silent on stdout so the same code path can serve both the
|
|
426
|
-
* backward-compat "emit RUNNING immediately" mode and the `--timeout`
|
|
427
|
-
* "suppress RUNNING, poll until terminal" mode. `spawnFn` is injectable for tests.
|
|
428
|
-
*/
|
|
429
|
-
export function spawnBatch(opts: SpawnBatchOpts, spawnFn: typeof spawn = spawn): Promise<number> {
|
|
430
|
-
return new Promise((resolve, reject) => {
|
|
431
|
-
const { cmd, cwd, env, stdoutLog, pidFile, taskDir, taskSubdir } = opts;
|
|
432
|
-
const logFd = fs.openSync(stdoutLog, 'w');
|
|
433
|
-
let settled = false;
|
|
434
|
-
const fail = (error: string): void => {
|
|
435
|
-
if (settled) return;
|
|
436
|
-
settled = true;
|
|
437
|
-
try { fs.closeSync(logFd); } catch { /* ignore */ }
|
|
438
|
-
try { fs.rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
439
|
-
logger.error(`后台 batch 进程启动失败: ${error}`);
|
|
440
|
-
reject(new Error(error));
|
|
441
|
-
};
|
|
442
|
-
const proc = spawnFn(cmd[0], cmd.slice(1), {
|
|
443
|
-
cwd,
|
|
444
|
-
stdio: ['ignore', logFd, logFd],
|
|
445
|
-
env,
|
|
446
|
-
detached: true,
|
|
447
|
-
windowsHide: true,
|
|
448
|
-
});
|
|
449
|
-
// spawn() reports failure asynchronously via the 'error' event and success
|
|
450
|
-
// via the 'spawn' event. proc.pid is set synchronously even for some
|
|
451
|
-
// failures, so emitting RUNNING off the pid could hand the caller a
|
|
452
|
-
// live-looking but dead PID. Gate both outcomes on the events (with a
|
|
453
|
-
// `settled` latch) so exactly one of resolve/reject fires.
|
|
454
|
-
proc.on('error', (e: Error) => fail(e.message));
|
|
455
|
-
proc.on('spawn', () => {
|
|
456
|
-
if (settled) return;
|
|
457
|
-
settled = true;
|
|
458
|
-
fs.writeFileSync(pidFile, String(proc.pid), 'utf-8');
|
|
459
|
-
proc.unref();
|
|
460
|
-
logger.info(`后台进程 PID=${proc.pid}`);
|
|
461
|
-
resolve(proc.pid);
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Poll `probeStatus` every 60 seconds until a terminal status is reached
|
|
468
|
-
* (COMPLETED / CRASHED / NOT_STARTED) or `timeoutSec` elapses. On timeout,
|
|
469
|
-
* auto-kill the batch tree via `killProcessTree` and return a `TIMEOUT`
|
|
470
|
-
* result with exit code 5 (new). The first probe happens immediately (no
|
|
471
|
-
* leading sleep) so a fast-finished batch is detected without a 60s delay.
|
|
472
|
-
*
|
|
473
|
-
* Returns `{ json, exitCode }` — caller emits `JSON.stringify(json)` to stdout
|
|
474
|
-
* and `process.exit(exitCode)`.
|
|
475
|
-
*
|
|
476
|
-
* Exit codes: COMPLETED=0, CRASHED=3, NOT_STARTED=4, TIMEOUT=5.
|
|
477
|
-
*/
|
|
478
|
-
export function pollUntilTerminal(
|
|
479
|
-
taskDir: string,
|
|
480
|
-
outputDir: string,
|
|
481
|
-
timeoutSec: number,
|
|
482
|
-
): { json: Record<string, unknown>; exitCode: number } {
|
|
483
|
-
const start = Date.now();
|
|
484
|
-
|
|
485
|
-
while (true) {
|
|
486
|
-
const r = probeStatus(taskDir, outputDir);
|
|
487
|
-
|
|
488
|
-
if (r.status === 'COMPLETED') {
|
|
489
|
-
return {
|
|
490
|
-
json: {
|
|
491
|
-
status: 'COMPLETED',
|
|
492
|
-
pid: r.pid,
|
|
493
|
-
task_subdir: r.taskSubdir,
|
|
494
|
-
cases_done: r.casesDone,
|
|
495
|
-
pass_count: r.passCount ?? 0,
|
|
496
|
-
fail_count: r.failCount ?? 0,
|
|
497
|
-
unknown_count: r.unknownCount ?? 0,
|
|
498
|
-
pass_rate: r.passRate ?? 0,
|
|
499
|
-
log_tail: r.logTail,
|
|
500
|
-
},
|
|
501
|
-
exitCode: 0,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (r.status === 'CRASHED') {
|
|
506
|
-
return {
|
|
507
|
-
json: {
|
|
508
|
-
status: 'CRASHED',
|
|
509
|
-
pid: r.pid,
|
|
510
|
-
task_subdir: r.taskSubdir,
|
|
511
|
-
cases_done: r.casesDone,
|
|
512
|
-
last_case: r.lastCase,
|
|
513
|
-
log_tail: r.logTail,
|
|
514
|
-
},
|
|
515
|
-
exitCode: 3,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (r.status === 'NOT_STARTED') {
|
|
520
|
-
return {
|
|
521
|
-
json: { status: 'NOT_STARTED' },
|
|
522
|
-
exitCode: 4,
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// r.status === 'RUNNING' — check timeout before sleeping
|
|
527
|
-
const elapsed = (Date.now() - start) / 1000;
|
|
528
|
-
if (elapsed >= timeoutSec) {
|
|
529
|
-
// Auto-kill the batch tree, then return TIMEOUT.
|
|
530
|
-
if (r.pid != null) {
|
|
531
|
-
try {
|
|
532
|
-
killProcessTree(r.pid);
|
|
533
|
-
sleep(2);
|
|
534
|
-
} catch {
|
|
535
|
-
// best effort
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return {
|
|
539
|
-
json: {
|
|
540
|
-
status: 'TIMEOUT',
|
|
541
|
-
pid: r.pid,
|
|
542
|
-
task_subdir: r.taskSubdir,
|
|
543
|
-
cases_done: r.casesDone,
|
|
544
|
-
last_case: r.lastCase,
|
|
545
|
-
log_tail: r.logTail,
|
|
546
|
-
},
|
|
547
|
-
exitCode: 5,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Still RUNNING and within budget — sleep 60s then probe again.
|
|
552
|
-
sleep(60);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/** `run` subcommand: prepare environment, then spawn the batch-launcher detached. */
|
|
557
|
-
export async function cmdRun(args: RunArgs): Promise<void> {
|
|
558
|
-
let taskDir: string;
|
|
559
|
-
let outputDir: string;
|
|
560
|
-
let taskSubdir: string;
|
|
561
|
-
let pidFile: string;
|
|
562
|
-
let stdoutLog: string;
|
|
563
|
-
let batchEnv: NodeJS.ProcessEnv;
|
|
564
|
-
let cmd: string[];
|
|
565
|
-
let effectiveTimeout: number | undefined;
|
|
566
|
-
try {
|
|
567
|
-
taskDir = path.resolve(args.taskDir);
|
|
568
|
-
fs.mkdirSync(taskDir, { recursive: true });
|
|
569
|
-
outputDir = args.outputDir ? path.resolve(args.outputDir) : taskDir;
|
|
570
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
571
|
-
|
|
572
|
-
setupLogging(outputDir);
|
|
573
|
-
logger.info(`Node: ${process.execPath}`);
|
|
574
|
-
logger.info(
|
|
575
|
-
`参数: testcases=${args.testcases}, hap=${args.hap}, bundle=${args.bundleName}, ` +
|
|
576
|
-
`task_dir=${taskDir}, category=${args.category}, model_stdin=${args.modelStdin ? 'yes' : 'no'}` +
|
|
577
|
-
(args.deviceSn ? `, device_sn=${args.deviceSn}` : '') +
|
|
578
|
-
(args.agentMode ? `, agent_mode=${args.agentMode}` : '') +
|
|
579
|
-
(args.timeout === 'auto' ? `, timeout=auto` : args.timeout ? `, timeout=${args.timeout}s` : ''),
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
// Per-run task subdir; status subcommand globs `task_*` to find the latest.
|
|
583
|
-
taskSubdir = path.join(taskDir, `task_${timestampForFile(new Date())}`);
|
|
584
|
-
fs.mkdirSync(taskSubdir, { recursive: true });
|
|
585
|
-
|
|
586
|
-
pidFile = path.join(taskDir, 'batch.pid');
|
|
587
|
-
stdoutLog = path.join(taskDir, 'batch_stdout.log');
|
|
588
|
-
|
|
589
|
-
// Kill any previous run's batch still recorded alive in the pid file.
|
|
590
|
-
if (fs.existsSync(pidFile)) {
|
|
591
|
-
try {
|
|
592
|
-
const oldPid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
593
|
-
if (Number.isFinite(oldPid) && pidAlive(oldPid)) {
|
|
594
|
-
logger.warning(`杀掉上次残留进程 PID=${oldPid}`);
|
|
595
|
-
killProcessTree(oldPid);
|
|
596
|
-
sleep(2);
|
|
597
|
-
}
|
|
598
|
-
} catch {
|
|
599
|
-
// best effort
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// a2h: model params come from stdin (--model-stdin, piped by the plugin
|
|
604
|
-
// tool which resolved them via deps.resolveModelParams). No
|
|
605
|
-
// ~/.hometrans/config.json and no HOMETRANS_MODEL_* env fallback.
|
|
606
|
-
if (!args.modelStdin) {
|
|
607
|
-
throw new Error(
|
|
608
|
-
'a2h selftest run requires --model-stdin: the plugin tool pipes ' +
|
|
609
|
-
'{apiKey, modelName, baseURL} via stdin. No config-file fallback.',
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
const model = readModelFromStdin();
|
|
613
|
-
logger.info('已从 stdin 读取模型参数(apiKey/model/baseURL 将通过 new AutoTestAgent 构造器传入)');
|
|
614
|
-
const devOpts: DeviceOpts = {
|
|
615
|
-
deviceSn: args.deviceSn,
|
|
616
|
-
ip: args.ip,
|
|
617
|
-
port: args.port,
|
|
618
|
-
agentMode: args.agentMode,
|
|
619
|
-
maxSteps: args.maxSteps,
|
|
620
|
-
promptVersion: args.promptVersion,
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
const hdc = getHdc();
|
|
624
|
-
|
|
625
|
-
logger.info(`=== Step 2a: uninstall ${args.bundleName} ===`);
|
|
626
|
-
runCmd([hdc, 'uninstall', args.bundleName], { timeout: INSTALL_TIMEOUT, check: false });
|
|
627
|
-
|
|
628
|
-
logger.info(`=== Step 2b: install ${args.hap} ===`);
|
|
629
|
-
installHaps(args.hap, hdc);
|
|
630
|
-
|
|
631
|
-
const testcasesJsonl = prepareTestcasesJsonl(path.resolve(args.testcases), taskSubdir);
|
|
632
|
-
|
|
633
|
-
// Resolve `--timeout auto` now that the testcases are parsed: total budget =
|
|
634
|
-
// caseCount × 720s (min 1 case). A numeric --timeout passes through; omitted
|
|
635
|
-
// stays undefined → backward-compat immediate-RUNNING mode below.
|
|
636
|
-
if (args.timeout === 'auto') {
|
|
637
|
-
const caseCount = fs.readFileSync(testcasesJsonl, 'utf-8').split(/\r?\n/).filter((l) => l.trim()).length;
|
|
638
|
-
effectiveTimeout = Math.max(caseCount, 1) * PER_CASE_TIMEOUT_SEC;
|
|
639
|
-
logger.info(`--timeout auto: ${caseCount} 条用例 × ${PER_CASE_TIMEOUT_SEC}s = ${effectiveTimeout}s`);
|
|
640
|
-
} else {
|
|
641
|
-
effectiveTimeout = args.timeout;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Node engine: `node <batch-launcher.js>` constructs `new AutoTestAgent(...)`
|
|
645
|
-
// from AUTOTEST_* env and runs the testcases batch in-process. cwd=taskSubdir
|
|
646
|
-
// keeps the per-case report dirs and task_results.jsonl inside the subdir.
|
|
647
|
-
cmd = [
|
|
648
|
-
process.execPath, batchLauncherPath(),
|
|
649
|
-
'--task-dir', taskSubdir,
|
|
650
|
-
'--testcases', testcasesJsonl,
|
|
651
|
-
'--category', args.category,
|
|
652
|
-
];
|
|
653
|
-
batchEnv = agentEnvFromModel(model, devOpts, process.env);
|
|
654
|
-
} catch (e) {
|
|
655
|
-
const error = e instanceof Error ? e.message : String(e);
|
|
656
|
-
logger.error(`异常:\n${e instanceof Error ? e.stack ?? error : String(e)}`);
|
|
657
|
-
console.log(JSON.stringify({ status: 'FAILED', error }));
|
|
658
|
-
process.exit(1);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
logger.info('=== Step 3: 后台启动 AutoTest (Node @autotest/agent, new AutoTestAgent) ===');
|
|
662
|
-
|
|
663
|
-
// No --timeout: backward-compat mode — emit RUNNING JSON immediately on successful spawn.
|
|
664
|
-
try {
|
|
665
|
-
// ponytail: the non-null assertions are safe because the prep try/catch above
|
|
666
|
-
// either assigned all these locals or process.exit(1) — no path reaches here
|
|
667
|
-
// with an unassigned variable. If the catch ever stops exiting, drop the `!`.
|
|
668
|
-
const pid = await spawnBatch({ cmd: cmd!, cwd: taskSubdir!, env: batchEnv!, stdoutLog, pidFile, taskDir: taskDir!, taskSubdir: taskSubdir! });
|
|
669
|
-
if (effectiveTimeout) {
|
|
670
|
-
const r = pollUntilTerminal(taskDir!, outputDir!, effectiveTimeout);
|
|
671
|
-
console.log(JSON.stringify(r.json));
|
|
672
|
-
process.exit(r.exitCode);
|
|
673
|
-
} else {
|
|
674
|
-
console.log(
|
|
675
|
-
JSON.stringify({
|
|
676
|
-
status: 'RUNNING',
|
|
677
|
-
pid,
|
|
678
|
-
pid_file: pidFile,
|
|
679
|
-
stdout_log: stdoutLog,
|
|
680
|
-
task_dir: taskDir,
|
|
681
|
-
task_subdir: taskSubdir,
|
|
682
|
-
}),
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
} catch (e) {
|
|
686
|
-
const error = e instanceof Error ? e.message : String(e);
|
|
687
|
-
console.log(JSON.stringify({ status: 'FAILED', error }));
|
|
688
|
-
process.exit(1);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
export interface ProbeResult {
|
|
693
|
-
status: 'NOT_STARTED' | 'COMPLETED' | 'RUNNING' | 'CRASHED';
|
|
694
|
-
pid: number | null;
|
|
695
|
-
taskSubdir: string | null;
|
|
696
|
-
casesDone: number;
|
|
697
|
-
lastCase: string;
|
|
698
|
-
logTail: string;
|
|
699
|
-
// COMPLETED-only (undefined for other statuses):
|
|
700
|
-
passCount?: number;
|
|
701
|
-
failCount?: number;
|
|
702
|
-
unknownCount?: number;
|
|
703
|
-
passRate?: string | number;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Read batch state from on-disk artifacts (no JSON emission, no exit).
|
|
708
|
-
* Same logic as `cmdStatus` but returns a structured `ProbeResult` so callers
|
|
709
|
-
* (`cmdStatus` for one-shot, `pollUntilTerminal` for the `run --timeout` loop)
|
|
710
|
-
* can share one source of truth.
|
|
711
|
-
*/
|
|
712
|
-
export function probeStatus(taskDir: string, outputDir: string): ProbeResult {
|
|
713
|
-
const pidFile = path.join(taskDir, 'batch.pid');
|
|
714
|
-
if (!fs.existsSync(pidFile)) {
|
|
715
|
-
return {
|
|
716
|
-
status: 'NOT_STARTED',
|
|
717
|
-
pid: null,
|
|
718
|
-
taskSubdir: null,
|
|
719
|
-
casesDone: 0,
|
|
720
|
-
lastCase: '',
|
|
721
|
-
logTail: '',
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
726
|
-
const taskSubdir = findTaskSubdir(taskDir);
|
|
727
|
-
|
|
728
|
-
let casesDone = 0;
|
|
729
|
-
let lastCase = '';
|
|
730
|
-
const jsonlFile = taskSubdir ? path.join(taskSubdir, 'task_results.jsonl') : null;
|
|
731
|
-
if (jsonlFile && fs.existsSync(jsonlFile)) {
|
|
732
|
-
const text = fs.readFileSync(jsonlFile, 'utf-8').trim();
|
|
733
|
-
const lines = text ? text.split(/\r?\n/) : [];
|
|
734
|
-
casesDone = lines.length;
|
|
735
|
-
if (lines.length) {
|
|
736
|
-
try {
|
|
737
|
-
const last = JSON.parse(lines[lines.length - 1]) as { case_name?: string };
|
|
738
|
-
if (typeof last.case_name === 'string') lastCase = last.case_name;
|
|
739
|
-
} catch {
|
|
740
|
-
// ignore
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
let logTail = '';
|
|
746
|
-
const logFile = findLatestLog(outputDir);
|
|
747
|
-
if (logFile && fs.existsSync(logFile)) {
|
|
748
|
-
const text = fs.readFileSync(logFile, 'utf-8').trim();
|
|
749
|
-
logTail = (text ? text.split(/\r?\n/) : []).slice(-5).join('\n');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const summaryFile = taskSubdir ? path.join(taskSubdir, 'summary.json') : null;
|
|
753
|
-
if (summaryFile && fs.existsSync(summaryFile)) {
|
|
754
|
-
let summary: Record<string, unknown> = {};
|
|
755
|
-
try {
|
|
756
|
-
summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
|
|
757
|
-
} catch {
|
|
758
|
-
// ignore
|
|
759
|
-
}
|
|
760
|
-
const num = (v: unknown, fallback: number) => (typeof v === 'number' ? v : fallback);
|
|
761
|
-
// ponytail: cast `pass_rate` (unknown from JSON) to the documented schema
|
|
762
|
-
// (string like "30.00%" or absent). `num(...)` would zero a string rate, so
|
|
763
|
-
// we keep the `?? 0` fallback verbatim from the original cmdStatus.
|
|
764
|
-
return {
|
|
765
|
-
status: 'COMPLETED',
|
|
766
|
-
pid,
|
|
767
|
-
taskSubdir,
|
|
768
|
-
casesDone: num(summary.total_cases, casesDone),
|
|
769
|
-
lastCase,
|
|
770
|
-
passCount: num(summary.pass_count, num(summary.success_count, 0)),
|
|
771
|
-
failCount: num(summary.fail_count, 0),
|
|
772
|
-
unknownCount: num(summary.unknown_count, 0),
|
|
773
|
-
passRate: (summary.pass_rate as string | number | undefined) ?? 0,
|
|
774
|
-
logTail,
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const alive = pidAlive(pid);
|
|
779
|
-
return {
|
|
780
|
-
status: alive ? 'RUNNING' : 'CRASHED',
|
|
781
|
-
pid,
|
|
782
|
-
taskSubdir,
|
|
783
|
-
casesDone,
|
|
784
|
-
lastCase,
|
|
785
|
-
logTail,
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ---- status subcommand -----------------------------------------------------
|
|
790
|
-
|
|
791
|
-
export interface StatusArgs {
|
|
792
|
-
taskDir: string;
|
|
793
|
-
outputDir?: string;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/** Latest `task_*` subdir in `taskDir` (name-sorted desc), or null. */
|
|
797
|
-
export function findTaskSubdir(taskDir: string): string | null {
|
|
798
|
-
if (!fs.existsSync(taskDir)) return null;
|
|
799
|
-
let entries: fs.Dirent[];
|
|
800
|
-
try {
|
|
801
|
-
entries = fs.readdirSync(taskDir, { withFileTypes: true });
|
|
802
|
-
} catch {
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
const candidates = entries
|
|
806
|
-
.filter((e) => e.isDirectory() && e.name.startsWith('task_'))
|
|
807
|
-
.map((e) => e.name)
|
|
808
|
-
.sort()
|
|
809
|
-
.reverse();
|
|
810
|
-
return candidates.length ? path.join(taskDir, candidates[0]) : null;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/** Latest `self_test_*.log` (desc), falling back to `batch_stdout.log`, or null. */
|
|
814
|
-
export function findLatestLog(outputDir: string): string | null {
|
|
815
|
-
if (!fs.existsSync(outputDir)) return null;
|
|
816
|
-
let entries: fs.Dirent[];
|
|
817
|
-
try {
|
|
818
|
-
entries = fs.readdirSync(outputDir, { withFileTypes: true });
|
|
819
|
-
} catch {
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
const selfTest = entries
|
|
823
|
-
.filter((e) => e.isFile() && e.name.startsWith('self_test_') && e.name.endsWith('.log'))
|
|
824
|
-
.map((e) => e.name)
|
|
825
|
-
.sort()
|
|
826
|
-
.reverse();
|
|
827
|
-
const batchLog = entries
|
|
828
|
-
.filter((e) => e.isFile() && e.name === 'batch_stdout.log')
|
|
829
|
-
.map((e) => e.name);
|
|
830
|
-
const ordered = [...selfTest, ...batchLog];
|
|
831
|
-
return ordered.length ? path.join(outputDir, ordered[0]) : null;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* `status` subcommand: report state from output files.
|
|
836
|
-
* summary.json present → COMPLETED (exit 0)
|
|
837
|
-
* else pid alive → RUNNING (exit 2)
|
|
838
|
-
* else → CRASHED (exit 3)
|
|
839
|
-
* No pid file at all → NOT_STARTED (exit 4).
|
|
840
|
-
*/
|
|
841
|
-
export function cmdStatus(args: StatusArgs): void {
|
|
842
|
-
const taskDir = path.resolve(args.taskDir);
|
|
843
|
-
const outputDir = args.outputDir ? path.resolve(args.outputDir) : taskDir;
|
|
844
|
-
|
|
845
|
-
const r = probeStatus(taskDir, outputDir);
|
|
846
|
-
|
|
847
|
-
if (r.status === 'NOT_STARTED') {
|
|
848
|
-
console.log(JSON.stringify({ status: 'NOT_STARTED' }));
|
|
849
|
-
process.exit(4);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (r.status === 'COMPLETED') {
|
|
853
|
-
console.log(
|
|
854
|
-
JSON.stringify({
|
|
855
|
-
status: 'COMPLETED',
|
|
856
|
-
pid: r.pid,
|
|
857
|
-
task_subdir: r.taskSubdir,
|
|
858
|
-
cases_done: r.casesDone,
|
|
859
|
-
pass_count: r.passCount ?? 0,
|
|
860
|
-
fail_count: r.failCount ?? 0,
|
|
861
|
-
unknown_count: r.unknownCount ?? 0,
|
|
862
|
-
pass_rate: r.passRate ?? 0,
|
|
863
|
-
log_tail: r.logTail,
|
|
864
|
-
}),
|
|
865
|
-
);
|
|
866
|
-
process.exit(0);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// RUNNING or CRASHED
|
|
870
|
-
console.log(
|
|
871
|
-
JSON.stringify({
|
|
872
|
-
status: r.status,
|
|
873
|
-
pid: r.pid,
|
|
874
|
-
cases_done: r.casesDone,
|
|
875
|
-
last_case: r.lastCase,
|
|
876
|
-
task_subdir: r.taskSubdir,
|
|
877
|
-
log_tail: r.logTail,
|
|
878
|
-
}),
|
|
879
|
-
);
|
|
880
|
-
process.exit(r.status === 'RUNNING' ? 2 : 3);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// ---- kill subcommand -------------------------------------------------------
|
|
884
|
-
|
|
885
|
-
export interface KillArgs {
|
|
886
|
-
taskDir: string;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/** `kill` subcommand: terminate the batch-launcher background process by pid. */
|
|
890
|
-
export function cmdKill(args: KillArgs): void {
|
|
891
|
-
const taskDir = path.resolve(args.taskDir);
|
|
892
|
-
const pidFile = path.join(taskDir, 'batch.pid');
|
|
893
|
-
if (!fs.existsSync(pidFile)) {
|
|
894
|
-
console.log(JSON.stringify({ status: 'NO_PID_FILE', killed: false }));
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
let pid: number;
|
|
898
|
-
try {
|
|
899
|
-
pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
900
|
-
} catch {
|
|
901
|
-
console.log(JSON.stringify({ status: 'NO_PID_FILE', killed: false }));
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (!pidAlive(pid)) {
|
|
905
|
-
console.log(JSON.stringify({ status: 'ALREADY_DEAD', pid, killed: false }));
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
killProcessTree(pid);
|
|
909
|
-
console.log(JSON.stringify({ status: 'KILLED', pid, killed: true }));
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// ---- CLI entry -------------------------------------------------------------
|
|
913
|
-
|
|
914
|
-
/** Wire up `run` / `status` / `kill` subcommands via commander. */
|
|
915
|
-
export function main(): void {
|
|
916
|
-
const program = new Command();
|
|
917
|
-
program.name('self-test-runner').description('自测执行器 - 统一的进程管理和测试执行');
|
|
918
|
-
|
|
919
|
-
program
|
|
920
|
-
.command('run')
|
|
921
|
-
.description('准备环境并执行用例')
|
|
922
|
-
.requiredOption('--testcases <path>', '测试用例文件,JSON 数组或 JSONL')
|
|
923
|
-
.requiredOption('--hap <path>', '签名包:单个 .hap/.hsp 文件、目录,或逗号分隔的多个文件/目录')
|
|
924
|
-
.requiredOption('--bundle-name <name>', '包名')
|
|
925
|
-
.option('--task-dir <dir>', '任务目录(内部会创建 task_<timestamp> 子目录)', 'task')
|
|
926
|
-
.option('--output-dir <dir>', '日志目录')
|
|
927
|
-
.option('--category <name>', '测试分类名称,传给 batch-launcher', DEFAULT_CATEGORY)
|
|
928
|
-
.option('--model-stdin', '从 stdin 读取 {apiKey, modelName, baseURL}(a2h 插件工具注入;无 config 回退)')
|
|
929
|
-
.option('--device-sn <sn>', '设备序列号(传给 AutoTestAgent.deviceSn)')
|
|
930
|
-
.option('--ip <ip>', '设备 IP(传给 AutoTestAgent.ip)')
|
|
931
|
-
.option('--port <port>', '设备端口(传给 AutoTestAgent.port)')
|
|
932
|
-
.option('--agent-mode <mode>', 'agent 模式(默认 single)', 'single')
|
|
933
|
-
.option('--max-steps <n>', '单用例最大步数')
|
|
934
|
-
.option('--prompt-version <ver>', 'prompt 版本(v1.0/v2.0/v3.0,默认 v2.0)', 'v2.0')
|
|
935
|
-
.option('--timeout <sec>', '阻塞直到终态或超时(秒),或 `auto` = 按用例数 × 720s 自动推导。不传则立即返回 RUNNING JSON。超时后自动 kill batch 并返回 TIMEOUT JSON(exit 5)', v => {
|
|
936
|
-
if (v === 'auto') return 'auto' as const;
|
|
937
|
-
const n = Number(v);
|
|
938
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
939
|
-
throw new Error(`--timeout 必须是正整数或 'auto',收到: ${v}`);
|
|
940
|
-
}
|
|
941
|
-
return n;
|
|
942
|
-
})
|
|
943
|
-
.action(async (opts: RunArgs) => {
|
|
944
|
-
await cmdRun({
|
|
945
|
-
testcases: opts.testcases,
|
|
946
|
-
hap: opts.hap,
|
|
947
|
-
bundleName: opts.bundleName,
|
|
948
|
-
taskDir: opts.taskDir,
|
|
949
|
-
outputDir: opts.outputDir,
|
|
950
|
-
category: opts.category,
|
|
951
|
-
modelStdin: opts.modelStdin,
|
|
952
|
-
deviceSn: opts.deviceSn,
|
|
953
|
-
ip: opts.ip,
|
|
954
|
-
port: opts.port,
|
|
955
|
-
agentMode: opts.agentMode,
|
|
956
|
-
maxSteps: opts.maxSteps,
|
|
957
|
-
promptVersion: opts.promptVersion,
|
|
958
|
-
timeout: opts.timeout,
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
program
|
|
963
|
-
.command('status')
|
|
964
|
-
.description('查询执行状态')
|
|
965
|
-
.requiredOption('--task-dir <dir>')
|
|
966
|
-
.option('--output-dir <dir>')
|
|
967
|
-
.action((opts: StatusArgs) => {
|
|
968
|
-
cmdStatus({ taskDir: opts.taskDir, outputDir: opts.outputDir });
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
program
|
|
972
|
-
.command('kill')
|
|
973
|
-
.description('终止进程')
|
|
974
|
-
.requiredOption('--task-dir <dir>')
|
|
975
|
-
.action((opts: KillArgs) => {
|
|
976
|
-
cmdKill({ taskDir: opts.taskDir });
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
// ---- Validation gates (replace the self-tester.md `uv run python -c` lines) --
|
|
980
|
-
// Each prints its message to stdout on pass, or stderr + exit(1) on fail —
|
|
981
|
-
// matching the prompt's `… && echo OK` expectation. These are NOT part of the
|
|
982
|
-
// run/status/kill stdout-JSON contract; they are plain pass/fail checks.
|
|
983
|
-
const gate = (fn: () => CheckResult): void => {
|
|
984
|
-
const r = fn();
|
|
985
|
-
if (r.ok) {
|
|
986
|
-
console.log(r.message);
|
|
987
|
-
} else {
|
|
988
|
-
console.error(r.message);
|
|
989
|
-
process.exit(1);
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
program
|
|
994
|
-
.command('check-hap')
|
|
995
|
-
.description('校验 hap_path:各项存在且至少含一个入口 .hap')
|
|
996
|
-
.requiredOption('--hap <path>', '签名包:文件、目录,或逗号分隔的多个文件/目录')
|
|
997
|
-
.action((opts: { hap: string }) => gate(() => checkHap(opts.hap)));
|
|
998
|
-
|
|
999
|
-
program
|
|
1000
|
-
.command('check-inputs')
|
|
1001
|
-
.description('校验 testcases.json(非空数组)与 app-metadata.json(必需字段)可解析')
|
|
1002
|
-
.requiredOption('--testcases <path>')
|
|
1003
|
-
.requiredOption('--metadata <path>')
|
|
1004
|
-
.action((opts: { testcases: string; metadata: string }) =>
|
|
1005
|
-
gate(() => checkInputs(opts.testcases, opts.metadata)),
|
|
1006
|
-
);
|
|
1007
|
-
|
|
1008
|
-
program
|
|
1009
|
-
.command('check-pre')
|
|
1010
|
-
.description('校验 testcases.json 中 [PRE] 用例从索引 0 起连续')
|
|
1011
|
-
.requiredOption('--testcases <path>')
|
|
1012
|
-
.action((opts: { testcases: string }) => gate(() => checkPre(opts.testcases)));
|
|
1013
|
-
|
|
1014
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
1015
|
-
console.error(err instanceof Error ? err.message : err);
|
|
1016
|
-
process.exit(1);
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1021
|
-
const isMainEntry = !!process.argv[1] && path.resolve(process.argv[1]) === __filename;
|
|
1022
|
-
if (isMainEntry) {
|
|
1023
|
-
main();
|
|
1024
|
-
}
|