android2harmony 0.1.2 → 0.1.3
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/agents/self-tester.md +339 -0
- package/dist/index.js +625 -7
- package/dist/index.js.map +4 -4
- package/package.json +37 -29
- package/skills/hmos-incremental-ui-align/README.md +251 -0
- package/skills/hmos-incremental-ui-align/SKILL.md +365 -0
- package/skills/hmos-incremental-ui-align/diff_analysis.md +53 -0
- package/skills/hmos-incremental-ui-align/page_align.md +62 -0
- package/skills/hmos-incremental-ui-align/references/Comparison_Template.md +38 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +648 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +2089 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +1033 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +1183 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +576 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +297 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@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 +395 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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 +903 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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 +1178 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217V1.md +911 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/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 +355 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243//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 +11 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +206 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +373 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +158 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +750 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +1704 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +1012 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +164 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +840 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +856 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@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 +83 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/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 +294 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1407 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/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 +1220 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +47 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2//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 +840 -0
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +74 -0
- package/skills/hmos-incremental-ui-align/references/UI_Analysis_Template.md +34 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2533 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +999 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +343 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +977 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +188 -0
- package/tools/autotest/deps/autotest-agent-0.1.1.tgz +0 -0
- package/tools/autotest/engine/batch-launcher.ts +326 -0
- package/tools/autotest/engine/report-tool.ts +773 -0
- package/tools/autotest/engine/self-test-runner.ts +1024 -0
- package/tools/autotest/engine/testcases-tool.ts +246 -0
- package/tools/autotest/resolve-metadata-tool.ts +143 -0
- package/tools/autotest/runner/logger.ts +45 -0
- package/tools/autotest/runner/process-utils.ts +41 -0
- package/tools/autotest/validate.ts +115 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Android 定时连续采集工具 (TypeScript 版) —— 每隔1秒自动采集一次 view tree + 截图,持续 x 秒。
|
|
4
|
+
*
|
|
5
|
+
* 用于捕获动态UI效果(动画中间态、Toast、瞬态弹窗等)。
|
|
6
|
+
* 零第三方依赖,仅使用 Node 内置模块。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* node page_capture_burst.ts --device adb -o ./output --duration 10
|
|
10
|
+
* node page_capture_burst.ts --device adb -o ./output --duration 5 --serial emulator-5554
|
|
11
|
+
*
|
|
12
|
+
* 等价于原 page_capture_burst.py(functional equivalence,非逐字节一致)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
function sleep(seconds: number): void {
|
|
21
|
+
const end = Date.now() + seconds * 1000;
|
|
22
|
+
const sab = new SharedArrayBuffer(4);
|
|
23
|
+
const ia = new Int32Array(sab);
|
|
24
|
+
while (Date.now() < end) {
|
|
25
|
+
Atomics.wait(ia, 0, 0, Math.min(50, end - Date.now()));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function adb(args: string[], serial: string | null = null, timeout = 15): string {
|
|
30
|
+
const full: string[] = [];
|
|
31
|
+
if (serial) full.push('-s', serial);
|
|
32
|
+
full.push(...args);
|
|
33
|
+
const res = spawnSync('adb', full, {
|
|
34
|
+
timeout: timeout * 1000,
|
|
35
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
38
|
+
if (res.error && (res.error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
|
39
|
+
console.log(` [WARN] adb timeout: ${args.join(' ')}`);
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
const out = (res.stdout as Buffer) ?? Buffer.alloc(0);
|
|
43
|
+
return out.toString('utf-8').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function adbShell(args: string[], serial: string | null = null, timeout = 15): string {
|
|
47
|
+
return adb(['shell', ...args], serial, timeout);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function adbScreenshot(outputDir: string, filename: string, serial: string | null = null): boolean {
|
|
51
|
+
const localPath = path.join(outputDir, filename);
|
|
52
|
+
const args: string[] = [];
|
|
53
|
+
if (serial) args.push('-s', serial);
|
|
54
|
+
args.push('exec-out', 'screencap', '-p');
|
|
55
|
+
const res = spawnSync('adb', args, {
|
|
56
|
+
timeout: 15 * 1000,
|
|
57
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
60
|
+
if (res.error && (res.error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
|
61
|
+
console.log(' [WARN] screenshot timeout');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const stdoutBuf = (res.stdout as Buffer) ?? Buffer.alloc(0);
|
|
65
|
+
if (stdoutBuf.length > 0) {
|
|
66
|
+
fs.writeFileSync(localPath, stdoutBuf);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function adbDumpXml(outputDir: string, filename: string, serial: string | null = null): string {
|
|
73
|
+
const remotePath = '/sdcard/ui_dump.xml';
|
|
74
|
+
adbShell(['rm', '-f', '/sdcard/ui_dump.xml', '&&', 'uiautomator', 'dump', '/sdcard/ui_dump.xml'], serial, 20);
|
|
75
|
+
for (let i = 0; i < 6; i++) {
|
|
76
|
+
const check = adbShell(['ls', remotePath], serial);
|
|
77
|
+
if (check.includes(remotePath)) break;
|
|
78
|
+
sleep(0.2);
|
|
79
|
+
}
|
|
80
|
+
const xmlContent = adbShell(['cat', remotePath], serial, 15);
|
|
81
|
+
if (xmlContent) {
|
|
82
|
+
fs.writeFileSync(path.join(outputDir, filename), xmlContent, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
return xmlContent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CliArgs {
|
|
88
|
+
device: 'adb';
|
|
89
|
+
outputDir: string;
|
|
90
|
+
duration: number;
|
|
91
|
+
serial: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
95
|
+
const args: CliArgs = { device: 'adb', outputDir: '', duration: 10, serial: null };
|
|
96
|
+
for (let i = 0; i < argv.length; i++) {
|
|
97
|
+
const a = argv[i];
|
|
98
|
+
const next = (): string => {
|
|
99
|
+
if (i + 1 >= argv.length) {
|
|
100
|
+
console.error(`Missing value for ${a}`);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
return argv[++i];
|
|
104
|
+
};
|
|
105
|
+
if (a === '--device') {
|
|
106
|
+
const v = next();
|
|
107
|
+
if (v !== 'adb') {
|
|
108
|
+
console.error('--device must be adb (currently adb only)');
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
args.device = v;
|
|
112
|
+
} else if (a === '--output-dir' || a === '-o') {
|
|
113
|
+
args.outputDir = next();
|
|
114
|
+
} else if (a === '--duration' || a === '-d') {
|
|
115
|
+
args.duration = parseInt(next(), 10);
|
|
116
|
+
} else if (a === '--serial') {
|
|
117
|
+
args.serial = next();
|
|
118
|
+
} else if (a === '-h' || a === '--help') {
|
|
119
|
+
printHelp();
|
|
120
|
+
process.exit(0);
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`Unknown argument: ${a}`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!args.outputDir) {
|
|
127
|
+
console.error('error: --output-dir/-o is required');
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
return args;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printHelp(): void {
|
|
134
|
+
console.log(
|
|
135
|
+
[
|
|
136
|
+
'Burst capture: take view tree + screenshot every 1s for N seconds',
|
|
137
|
+
'',
|
|
138
|
+
'Usage: node page_capture_burst.ts --device adb -o <dir> [--duration N] [--serial S]',
|
|
139
|
+
'',
|
|
140
|
+
' --device Device type (currently adb only)',
|
|
141
|
+
' --output-dir,-o Output directory for captured files (required)',
|
|
142
|
+
' --duration,-d Total capture duration in seconds (default: 10)',
|
|
143
|
+
' --serial ADB device serial (optional)',
|
|
144
|
+
].join('\n'),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function main(): number {
|
|
149
|
+
const args = parseArgs(process.argv.slice(2));
|
|
150
|
+
fs.mkdirSync(args.outputDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
console.log(`[Burst Capture] duration=${args.duration}s, output=${args.outputDir}`);
|
|
153
|
+
console.log('[Burst Capture] Starting continuous capture...');
|
|
154
|
+
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
let frame = 0;
|
|
157
|
+
|
|
158
|
+
while (true) {
|
|
159
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
160
|
+
if (elapsed >= args.duration) break;
|
|
161
|
+
|
|
162
|
+
frame += 1;
|
|
163
|
+
const frameStart = Date.now();
|
|
164
|
+
const frameStr = String(frame).padStart(3, '0');
|
|
165
|
+
process.stdout.write(` Frame ${frame} (t=${elapsed.toFixed(1)}s)... `);
|
|
166
|
+
|
|
167
|
+
const xmlOk = !!adbDumpXml(args.outputDir, `view_${frameStr}.xml`, args.serial);
|
|
168
|
+
const shotOk = adbScreenshot(args.outputDir, `screenshot_${frameStr}.png`, args.serial);
|
|
169
|
+
|
|
170
|
+
console.log(xmlOk && shotOk ? 'OK' : 'partial');
|
|
171
|
+
|
|
172
|
+
const frameElapsed = (Date.now() - frameStart) / 1000;
|
|
173
|
+
const sleepTime = Math.max(0, 1.0 - frameElapsed);
|
|
174
|
+
if (sleepTime > 0 && (Date.now() - startTime) / 1000 + sleepTime < args.duration) {
|
|
175
|
+
sleep(sleepTime);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`\n[Burst Capture] Done. ${frame} frames captured to ${args.outputDir}`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const isEntry = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
184
|
+
if (isEntry) {
|
|
185
|
+
process.exit(main());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { adbDumpXml, adbScreenshot, parseArgs };
|
|
Binary file
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch launcher — the detached background process that runs the Node.js
|
|
3
|
+
* `@autotest/agent` engine programmatically via `new AutoTestAgent({...})`.
|
|
4
|
+
*
|
|
5
|
+
* Spawned detached by `self-test-runner.ts` `cmdRun` (which writes `batch.pid` +
|
|
6
|
+
* emits `{status:'RUNNING', pid}` then exits). This process:
|
|
7
|
+
*
|
|
8
|
+
* 1. constructs `new AutoTestAgent({ apiKey, modelName, baseUrl, provider,
|
|
9
|
+
* deviceSn, ip, port, agentMode })` from the `AUTOTEST_*` env vars that
|
|
10
|
+
* `cmdRun` derived from `~/.hometrans/config.json` — so the user's API_KEY
|
|
11
|
+
* / model / device flow in through the constructor, no config.yaml needed;
|
|
12
|
+
* 2. reads the testcases (JSON array or JSONL — same input as the former
|
|
13
|
+
* Python `AutoTest.batch`);
|
|
14
|
+
* 3. runs each case via `agent.runTask(test_steps, { reportDir, taskName })`;
|
|
15
|
+
* 4. writes `task_results.jsonl` + `summary.json` in the SAME schema the
|
|
16
|
+
* Python `AutoTest.batch` produced, so the HomeTrans `status` /
|
|
17
|
+
* `report-tool` contract keeps working unchanged.
|
|
18
|
+
*
|
|
19
|
+
* STDOUT/STDERR are captured to `batch_stdout.log` by the spawner; this process
|
|
20
|
+
* exits 0 on completion regardless of per-case pass/fail (a non-zero exit would
|
|
21
|
+
* be interpreted as a launch failure by `spawnBatch`).
|
|
22
|
+
*/
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { Command } from 'commander';
|
|
28
|
+
import type {
|
|
29
|
+
AutoTestAgent,
|
|
30
|
+
AutoTestAgentConfig,
|
|
31
|
+
RunTaskOptions,
|
|
32
|
+
TaskResult,
|
|
33
|
+
} from '@autotest/agent';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const { AutoTestAgent: AutoTestAgentCtor } = require('@autotest/agent') as {
|
|
37
|
+
AutoTestAgent: new (config: AutoTestAgentConfig) => AutoTestAgent;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const UTF_8 = 'utf-8';
|
|
41
|
+
const CASE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
42
|
+
const DEFAULT_PROMPT_VERSION = 'v2.0';
|
|
43
|
+
|
|
44
|
+
interface CaseRow {
|
|
45
|
+
uuid: string;
|
|
46
|
+
spec: string;
|
|
47
|
+
case_name: string;
|
|
48
|
+
test_steps: string;
|
|
49
|
+
execute_mode?: string;
|
|
50
|
+
[k: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ResultRow {
|
|
54
|
+
exec_index: number;
|
|
55
|
+
uuid: string;
|
|
56
|
+
spec: string;
|
|
57
|
+
case_name: string;
|
|
58
|
+
category: string;
|
|
59
|
+
test_steps: string;
|
|
60
|
+
report_dir: string;
|
|
61
|
+
start_time: string;
|
|
62
|
+
end_time: string;
|
|
63
|
+
duration_seconds: number;
|
|
64
|
+
status: 'PASS' | 'FAIL' | 'UNKNOWN';
|
|
65
|
+
return_code: number;
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface LauncherArgs {
|
|
70
|
+
taskDir: string;
|
|
71
|
+
testcases: string;
|
|
72
|
+
category: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sanitizeFilename(name: string): string {
|
|
76
|
+
return name.replace(/[\\/:*?"<>|]/g, '_');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatTimestamp(date: Date): string {
|
|
80
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
81
|
+
return (
|
|
82
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
83
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readTestCases(filePath: string): CaseRow[] {
|
|
88
|
+
let content = fs.readFileSync(filePath, UTF_8);
|
|
89
|
+
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(content);
|
|
92
|
+
if (Array.isArray(parsed)) return parsed as CaseRow[];
|
|
93
|
+
throw new Error('not an array');
|
|
94
|
+
} catch {
|
|
95
|
+
// Fall back to JSONL.
|
|
96
|
+
const cases: CaseRow[] = [];
|
|
97
|
+
for (const line of content.split('\n')) {
|
|
98
|
+
const trimmed = line.trim();
|
|
99
|
+
if (!trimmed) continue;
|
|
100
|
+
try {
|
|
101
|
+
cases.push(JSON.parse(trimmed) as CaseRow);
|
|
102
|
+
} catch {
|
|
103
|
+
// skip invalid lines
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return cases;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseArgs(argv: string[]): LauncherArgs {
|
|
111
|
+
const program = new Command();
|
|
112
|
+
program.exitOverride();
|
|
113
|
+
program
|
|
114
|
+
.requiredOption('--task-dir <dir>', '任务目录')
|
|
115
|
+
.requiredOption('--testcases <path>', '测试用例文件(JSON 数组或 JSONL)')
|
|
116
|
+
.option('--category <name>', '测试分类名称', 'self_test');
|
|
117
|
+
try {
|
|
118
|
+
program.parse(argv, { from: 'user' });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const e = err as { code?: string };
|
|
121
|
+
if (e.code !== 'commander.helpDisplayed') {
|
|
122
|
+
console.error('用法: node batch-launcher.js --task-dir <dir> --testcases <path> [--category <name>]');
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const opts = program.opts() as LauncherArgs;
|
|
127
|
+
return { taskDir: opts.taskDir, testcases: opts.testcases, category: opts.category };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Construct the agent from AUTOTEST_* env vars (set by `cmdRun` from config.json). */
|
|
131
|
+
function createAgent(): AutoTestAgent {
|
|
132
|
+
const promptVersion = process.env.AUTOTEST_PROMPT_VERSION || DEFAULT_PROMPT_VERSION;
|
|
133
|
+
const envPort = process.env.AUTOTEST_PORT;
|
|
134
|
+
const envMaxSteps = process.env.AUTOTEST_MAX_STEPS;
|
|
135
|
+
const numOrUndef = (raw: string | undefined): number | undefined => {
|
|
136
|
+
if (!raw) return undefined;
|
|
137
|
+
const n = Number(raw);
|
|
138
|
+
return Number.isFinite(n) ? n : undefined;
|
|
139
|
+
};
|
|
140
|
+
// Resolve the bundled config.yaml inside @autotest/agent so the agent loads
|
|
141
|
+
// model/device/agent defaults (base_url, api_key, …) even when AUTOTEST_*
|
|
142
|
+
// env vars are not set (e.g. running batch-launcher standalone without
|
|
143
|
+
// HomeTrans's config.json → AUTOTEST_* mapping). The ConfigManager converts
|
|
144
|
+
// snake_case YAML keys to camelCase before zod validation, so `base_url` in
|
|
145
|
+
// the YAML becomes `baseUrl` as the schema expects.
|
|
146
|
+
const agentMain = require.resolve('@autotest/agent');
|
|
147
|
+
const configPath = path.join(path.dirname(agentMain), 'common', 'config', 'config.yaml');
|
|
148
|
+
const config: AutoTestAgentConfig = {
|
|
149
|
+
configPath,
|
|
150
|
+
apiKey: process.env.AUTOTEST_API_KEY,
|
|
151
|
+
modelName: process.env.AUTOTEST_MODEL,
|
|
152
|
+
baseUrl: process.env.AUTOTEST_BASE_URL,
|
|
153
|
+
provider: process.env.AUTOTEST_PROVIDER,
|
|
154
|
+
deviceSn: process.env.AUTOTEST_DEVICE_SN || undefined,
|
|
155
|
+
ip: process.env.AUTOTEST_IP,
|
|
156
|
+
port: numOrUndef(envPort),
|
|
157
|
+
agentMode: (process.env.AUTOTEST_MODE || undefined) as AutoTestAgentConfig['agentMode'],
|
|
158
|
+
maxSteps: numOrUndef(envMaxSteps),
|
|
159
|
+
// The SingleAgent resolves its prompt version from `model.unified.promptType`
|
|
160
|
+
// (loadPrompt(SINGLE, config.model.unified.promptType)), whose zod default is
|
|
161
|
+
// 'default' → 'vdefault' (not a real version). Inject the real version through
|
|
162
|
+
// configOverrides — the constructor's route for any extra config the user
|
|
163
|
+
// wants to set through `new`.
|
|
164
|
+
configOverrides: { model: { unified: { promptType: promptVersion } } },
|
|
165
|
+
};
|
|
166
|
+
return new AutoTestAgentCtor(config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Run one task with a hard timeout (mirrors batch_runner.runWithTimeout). */
|
|
170
|
+
function runWithTimeout(
|
|
171
|
+
agent: AutoTestAgent,
|
|
172
|
+
task: string,
|
|
173
|
+
options: RunTaskOptions,
|
|
174
|
+
timeoutMs: number,
|
|
175
|
+
): Promise<TaskResult> {
|
|
176
|
+
let timer: NodeJS.Timeout | undefined;
|
|
177
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
178
|
+
timer = setTimeout(
|
|
179
|
+
() => reject(new Error(`Execution timed out (exceeded ${timeoutMs / 1000} seconds)`)),
|
|
180
|
+
timeoutMs,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
// Promise.race attaches handlers to both promises, so a late runTask
|
|
184
|
+
// resolution/rejection after timeout is never "unhandled"; finally clears
|
|
185
|
+
// the timer regardless of which settles first.
|
|
186
|
+
return Promise.race([agent.runTask(task, options), timeout]).finally(() => {
|
|
187
|
+
if (timer) clearTimeout(timer);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function main(): Promise<void> {
|
|
192
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
193
|
+
|
|
194
|
+
const taskDir = path.resolve(opts.taskDir);
|
|
195
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
196
|
+
const jsonlFile = path.join(taskDir, 'task_results.jsonl');
|
|
197
|
+
|
|
198
|
+
const testCases = readTestCases(opts.testcases);
|
|
199
|
+
const total = testCases.length;
|
|
200
|
+
if (total === 0) {
|
|
201
|
+
console.log('No test cases, exiting');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const indexWidth = String(total).length;
|
|
206
|
+
|
|
207
|
+
console.log(`Total test cases to execute: ${total}`);
|
|
208
|
+
console.log(`Test category: ${opts.category}`);
|
|
209
|
+
console.log(`Task directory: ${taskDir}`);
|
|
210
|
+
console.log('='.repeat(60));
|
|
211
|
+
|
|
212
|
+
// promptVersion is injected via the constructor's configOverrides (see
|
|
213
|
+
// createAgent) because AgentAPIImpl reads it from effectiveConfig, not the
|
|
214
|
+
// per-call option.
|
|
215
|
+
const agent = createAgent();
|
|
216
|
+
|
|
217
|
+
let pass = 0;
|
|
218
|
+
let fail = 0;
|
|
219
|
+
let unknown = 0;
|
|
220
|
+
const batchStart = new Date();
|
|
221
|
+
|
|
222
|
+
for (let idx = 1; idx <= total; idx++) {
|
|
223
|
+
const tc = testCases[idx - 1];
|
|
224
|
+
const uuid = String(tc.uuid ?? '');
|
|
225
|
+
const spec = String(tc.spec ?? '');
|
|
226
|
+
const caseName = String(tc.case_name ?? '');
|
|
227
|
+
const testSteps = String(tc.test_steps ?? '');
|
|
228
|
+
const caseMode = String(tc.execute_mode ?? '');
|
|
229
|
+
|
|
230
|
+
const dirname = uuid
|
|
231
|
+
? sanitizeFilename(`${uuid}_${caseName}`)
|
|
232
|
+
: sanitizeFilename(`${String(idx).padStart(indexWidth, '0')}_${caseName}`);
|
|
233
|
+
const reportDir = path.join(taskDir, dirname).replace(/\\/g, '/');
|
|
234
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
235
|
+
|
|
236
|
+
console.log('\n' + '-'.repeat(60));
|
|
237
|
+
console.log(`[${opts.category}][${String(idx).padStart(indexWidth, '0')}/${total}] Execute: ${caseName}`);
|
|
238
|
+
console.log('-'.repeat(60));
|
|
239
|
+
|
|
240
|
+
const start = new Date();
|
|
241
|
+
const runOpts: RunTaskOptions = {
|
|
242
|
+
reportDir,
|
|
243
|
+
taskName: caseName,
|
|
244
|
+
...(caseMode ? { agentMode: caseMode as RunTaskOptions['agentMode'] } : {}),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
let status: 'PASS' | 'FAIL' | 'UNKNOWN' = 'UNKNOWN';
|
|
248
|
+
let reason = '';
|
|
249
|
+
let returnCode = 0;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const taskResult = await runWithTimeout(agent, testSteps, runOpts, CASE_TIMEOUT_MS);
|
|
253
|
+
returnCode = taskResult.status === 'failed' ? 1 : 0;
|
|
254
|
+
if (taskResult.status === 'completed') {
|
|
255
|
+
status = 'PASS';
|
|
256
|
+
pass += 1;
|
|
257
|
+
} else if (taskResult.status === 'failed') {
|
|
258
|
+
status = 'FAIL';
|
|
259
|
+
reason = taskResult.errorMessage ?? 'Execution failed';
|
|
260
|
+
fail += 1;
|
|
261
|
+
} else {
|
|
262
|
+
status = 'UNKNOWN';
|
|
263
|
+
reason = `agent status=${taskResult.status}`;
|
|
264
|
+
unknown += 1;
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
returnCode = -1;
|
|
268
|
+
status = 'FAIL';
|
|
269
|
+
reason = `Execution error: ${e instanceof Error ? e.message : String(e)}`;
|
|
270
|
+
fail += 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const end = new Date();
|
|
274
|
+
const duration = (end.getTime() - start.getTime()) / 1000;
|
|
275
|
+
|
|
276
|
+
const row: ResultRow = {
|
|
277
|
+
exec_index: idx,
|
|
278
|
+
uuid,
|
|
279
|
+
spec,
|
|
280
|
+
case_name: caseName,
|
|
281
|
+
category: opts.category,
|
|
282
|
+
test_steps: testSteps,
|
|
283
|
+
report_dir: reportDir,
|
|
284
|
+
start_time: formatTimestamp(start),
|
|
285
|
+
end_time: formatTimestamp(end),
|
|
286
|
+
duration_seconds: Math.round(duration * 100) / 100,
|
|
287
|
+
status,
|
|
288
|
+
return_code: returnCode,
|
|
289
|
+
reason,
|
|
290
|
+
};
|
|
291
|
+
fs.appendFileSync(jsonlFile, JSON.stringify(row) + '\n', UTF_8);
|
|
292
|
+
|
|
293
|
+
const icon = status === 'PASS' ? 'OK' : status === 'FAIL' ? 'X' : '?';
|
|
294
|
+
console.log(`Result: [${icon}] ${status} | Duration: ${duration.toFixed(2)}s`);
|
|
295
|
+
if (reason && status === 'FAIL') console.log(`Reason: ${reason.slice(0, 100)}`);
|
|
296
|
+
console.log(`Report: ${reportDir}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const summary = {
|
|
300
|
+
total_cases: total,
|
|
301
|
+
pass_count: pass,
|
|
302
|
+
fail_count: fail,
|
|
303
|
+
unknown_count: unknown,
|
|
304
|
+
pass_rate: `${total ? ((pass / total) * 100).toFixed(2) : '0.00'}%`,
|
|
305
|
+
category: opts.category,
|
|
306
|
+
start_time: formatTimestamp(batchStart),
|
|
307
|
+
end_time: formatTimestamp(new Date()),
|
|
308
|
+
task_dir: taskDir.replace(/\\/g, '/'),
|
|
309
|
+
};
|
|
310
|
+
fs.writeFileSync(path.join(taskDir, 'summary.json'), JSON.stringify(summary, null, 4), UTF_8);
|
|
311
|
+
|
|
312
|
+
console.log('\n' + '='.repeat(60));
|
|
313
|
+
console.log('Execution complete!');
|
|
314
|
+
console.log(`Total cases: ${total}`);
|
|
315
|
+
console.log(`Pass: ${pass}`);
|
|
316
|
+
console.log(`Fail: ${fail}`);
|
|
317
|
+
console.log(`Unknown: ${unknown}`);
|
|
318
|
+
console.log(`Pass rate: ${summary.pass_rate}`);
|
|
319
|
+
console.log(`Results file: ${jsonlFile}`);
|
|
320
|
+
console.log('='.repeat(60));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
main().catch((err) => {
|
|
324
|
+
console.error('batch-launcher fatal:', err instanceof Error ? err.stack ?? err.message : String(err));
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|