android2harmony 0.1.1 → 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.
Files changed (58) hide show
  1. package/agents/scripts/hmos-precheck/index.mjs +0 -8
  2. package/agents/self-tester.md +339 -0
  3. package/dist/index.js +635 -77
  4. package/dist/index.js.map +4 -4
  5. package/package.json +37 -29
  6. package/skills/hmos-incremental-ui-align/README.md +251 -0
  7. package/skills/hmos-incremental-ui-align/SKILL.md +365 -0
  8. package/skills/hmos-incremental-ui-align/diff_analysis.md +53 -0
  9. package/skills/hmos-incremental-ui-align/page_align.md +62 -0
  10. package/skills/hmos-incremental-ui-align/references/Comparison_Template.md +38 -0
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +74 -0
  40. package/skills/hmos-incremental-ui-align/references/UI_Analysis_Template.md +34 -0
  41. package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2533 -0
  42. package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
  43. package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
  44. package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +999 -0
  45. package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +343 -0
  46. package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -0
  47. package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +977 -0
  48. package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +188 -0
  49. package/skills/hmos-spec-generate/SKILL.md +1 -1
  50. package/tools/autotest/deps/autotest-agent-0.1.1.tgz +0 -0
  51. package/tools/autotest/engine/batch-launcher.ts +326 -0
  52. package/tools/autotest/engine/report-tool.ts +773 -0
  53. package/tools/autotest/engine/self-test-runner.ts +1024 -0
  54. package/tools/autotest/engine/testcases-tool.ts +246 -0
  55. package/tools/autotest/resolve-metadata-tool.ts +143 -0
  56. package/tools/autotest/runner/logger.ts +45 -0
  57. package/tools/autotest/runner/process-utils.ts +41 -0
  58. package/tools/autotest/validate.ts +115 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Generate or validate testcases.json for the Self-Testing Agent.
3
+ *
4
+ * TS port of `tools/test-tools/autotest/testcases_tool.py`.
5
+ *
6
+ * Subcommands:
7
+ * generate <input-json> <output-json> [--validate]
8
+ * Rewrite app_name → bundle_name and compose `test_steps` strictly
9
+ * ("动作:..." / "预期结果:..."). Optionally re-validate the output.
10
+ *
11
+ * validate <testcases-json>
12
+ * Check a `testcases.json` (array of cases) against the schema in
13
+ * `testcases_schema.md`. Exit 0 = PASSED, exit 1 = FAILED.
14
+ *
15
+ * The forbidden-strings and section-header rules live in this file as the
16
+ * single source of truth — generate enforces them at write time, validate
17
+ * re-checks them on existing files.
18
+ */
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { Command } from 'commander';
23
+
24
+ const FORBIDDEN_STRINGS = [
25
+ '前置条件',
26
+ 'Spec:',
27
+ 'Scenario:',
28
+ 'ability_name:',
29
+ 'module_name:',
30
+ ] as const;
31
+
32
+ const SECTION_HEADER_RE = /【(.+?)】/;
33
+
34
+ const REQUIRED_LINES = ['动作:', '预期结果:'] as const;
35
+
36
+ const TEST_STEPS_TEMPLATE = '动作:{actions}\n预期结果:{expected_results}';
37
+
38
+ /** Input case shape from the extractor. */
39
+ interface InputCase {
40
+ case_name: string;
41
+ actions: string;
42
+ expected_results: string;
43
+ }
44
+
45
+ /** Input document: extractor output consumed by `generate`. */
46
+ interface InputData {
47
+ bundle_name: string;
48
+ app_name: string;
49
+ cases: InputCase[];
50
+ }
51
+
52
+ /** Output case shape: what we write to testcases.json. */
53
+ interface OutputCase {
54
+ case_name: string;
55
+ bundle_name: string;
56
+ app_name: string;
57
+ test_steps: string;
58
+ }
59
+
60
+ /** Safety net: replace remaining app_name references with bundle_name. */
61
+ function rewriteAppName(text: string, appName: string, bundleName: string): string {
62
+ if (appName && text.includes(appName)) {
63
+ return text.replaceAll(appName, bundleName);
64
+ }
65
+ return text;
66
+ }
67
+
68
+ /** Compose a compliant test_steps string with only 动作 and 预期结果. */
69
+ function composeTestSteps(
70
+ appName: string,
71
+ bundleName: string,
72
+ actions: string,
73
+ expectedResults: string,
74
+ ): string {
75
+ const rewrittenActions = rewriteAppName(actions, appName, bundleName);
76
+ const rewrittenExpected = rewriteAppName(expectedResults, appName, bundleName);
77
+ // Use function replacements so '$' sequences ($&, $$, $1, ...) in the action /
78
+ // expected text are inserted literally — String.replace with a STRING argument
79
+ // would interpret them, diverging from Python str.format().
80
+ let testSteps = TEST_STEPS_TEMPLATE.replace('{actions}', () => rewrittenActions).replace(
81
+ '{expected_results}',
82
+ () => rewrittenExpected,
83
+ );
84
+ // Final safety: rewrite once more in case app_name only became visible after composition.
85
+ testSteps = rewriteAppName(testSteps, appName, bundleName);
86
+
87
+ for (const forbidden of FORBIDDEN_STRINGS) {
88
+ if (testSteps.includes(forbidden)) {
89
+ throw new Error(
90
+ `FATAL: forbidden string '${forbidden}' found in generated test_steps. ` +
91
+ `This is a bug in testcases-tool.`,
92
+ );
93
+ }
94
+ }
95
+ const sectionMatch = SECTION_HEADER_RE.exec(testSteps);
96
+ if (sectionMatch) {
97
+ throw new Error(
98
+ `FATAL: unexpected section header 【${sectionMatch[1]}】 in generated test_steps. ` +
99
+ `test_steps should only contain 动作 and 预期结果.`,
100
+ );
101
+ }
102
+ return testSteps;
103
+ }
104
+
105
+ /** Generate compliant testcases array from input data. */
106
+ export function generate(inputData: InputData): OutputCase[] {
107
+ const { bundle_name: bundleName, app_name: appName, cases } = inputData;
108
+ // Fail loudly on malformed extractor input instead of silently writing
109
+ // `undefined`/dropped keys (Python indexed with [] and raised KeyError).
110
+ const missingTop = (['bundle_name', 'app_name', 'cases'] as const).filter(
111
+ (k) => inputData[k] === undefined,
112
+ );
113
+ if (missingTop.length > 0) {
114
+ throw new Error(`FATAL: input is missing required key(s): ${missingTop.join(', ')}.`);
115
+ }
116
+ const result: OutputCase[] = [];
117
+ for (const [i, c] of cases.entries()) {
118
+ const missingCase = (['case_name', 'actions', 'expected_results'] as const).filter(
119
+ (k) => c[k] === undefined,
120
+ );
121
+ if (missingCase.length > 0) {
122
+ throw new Error(
123
+ `FATAL: case ${i} (${c.case_name ?? '<no case_name>'}) is missing required key(s): ` +
124
+ `${missingCase.join(', ')}.`,
125
+ );
126
+ }
127
+ const testSteps = composeTestSteps(appName, bundleName, c.actions, c.expected_results);
128
+ result.push({
129
+ case_name: c.case_name,
130
+ bundle_name: bundleName,
131
+ app_name: appName,
132
+ test_steps: testSteps,
133
+ });
134
+ }
135
+ return result;
136
+ }
137
+
138
+ /** Return a list of validation error messages. Empty list means PASSED. */
139
+ export function validateTestcases(filepath: string): string[] {
140
+ const raw = fs.readFileSync(filepath, 'utf-8');
141
+ const cases: unknown = JSON.parse(raw);
142
+
143
+ if (!Array.isArray(cases)) return ['Root element is not a JSON array'];
144
+
145
+ const errors: string[] = [];
146
+ const globalSectionRe = new RegExp(SECTION_HEADER_RE.source, 'g');
147
+ cases.forEach((c: any, i: number) => {
148
+ const ts: string = c?.test_steps ?? '';
149
+ const name: string = c?.case_name ?? `case_${i}`;
150
+ const appName: string = c?.app_name ?? '';
151
+ const bundleName: string = c?.bundle_name ?? '';
152
+ const prefix = `Case ${i + 1} [${name}]`;
153
+
154
+ for (const required of REQUIRED_LINES) {
155
+ if (!ts.includes(required)) errors.push(`${prefix}: MISSING ${required} line`);
156
+ }
157
+
158
+ for (const forbidden of FORBIDDEN_STRINGS) {
159
+ if (ts.includes(forbidden)) {
160
+ errors.push(`${prefix}: CONTAINS ${forbidden} (must be excluded)`);
161
+ }
162
+ }
163
+
164
+ let m: RegExpExecArray | null;
165
+ while ((m = globalSectionRe.exec(ts)) !== null) {
166
+ errors.push(
167
+ `${prefix}: CONTAINS unexpected section header 【${m[1]}】 ` +
168
+ `(test_steps should only contain 动作 and 预期结果)`,
169
+ );
170
+ }
171
+
172
+ if (appName && bundleName && appName !== bundleName && ts.includes(appName)) {
173
+ errors.push(
174
+ `${prefix}: test_steps contains app_name '${appName}' instead of bundle_name ` +
175
+ `'${bundleName}'. All app name references in test_steps must use bundle_name.`,
176
+ );
177
+ }
178
+ });
179
+ return errors;
180
+ }
181
+
182
+ function cmdGenerate(input: string, output: string, doValidate: boolean): void {
183
+ const inputData = JSON.parse(fs.readFileSync(input, 'utf-8')) as InputData;
184
+ const testcases = generate(inputData);
185
+ fs.writeFileSync(output, JSON.stringify(testcases, null, 2), 'utf-8');
186
+ console.log(`Generated ${testcases.length} test cases -> ${output}`);
187
+
188
+ if (doValidate) {
189
+ const errors = validateTestcases(output);
190
+ if (errors.length > 0) {
191
+ console.log('VALIDATION FAILED:');
192
+ for (const e of errors) console.log(` [FAIL] ${e}`);
193
+ process.exit(1);
194
+ }
195
+ console.log(`VALIDATION PASSED: all ${testcases.length} cases conform to the schema`);
196
+ }
197
+ }
198
+
199
+ function cmdValidate(filepath: string): void {
200
+ const errors = validateTestcases(filepath);
201
+ if (errors.length > 0) {
202
+ console.log('VALIDATION FAILED:');
203
+ for (const e of errors) console.log(` [FAIL] ${e}`);
204
+ process.exit(1);
205
+ }
206
+ const cases = JSON.parse(fs.readFileSync(filepath, 'utf-8')) as unknown[];
207
+ console.log(`VALIDATION PASSED: all ${cases.length} cases have correct test_steps format`);
208
+ }
209
+
210
+ /** CLI entry — wires up commander with `generate` and `validate` subcommands. */
211
+ export function main(): void {
212
+ const program = new Command();
213
+ program.description(
214
+ 'Generate or validate testcases.json for the Self-Testing Agent.',
215
+ );
216
+
217
+ program
218
+ .command('generate')
219
+ .description('Generate compliant testcases.json from an extracted input')
220
+ .argument('<input>', 'Path to the extracted input JSON')
221
+ .argument('<output>', 'Path to write the compliant testcases.json')
222
+ .option('--validate', 'After writing, re-validate the output against the schema')
223
+ .action((input: string, output: string, opts: { validate?: boolean }) => {
224
+ cmdGenerate(input, output, !!opts.validate);
225
+ });
226
+
227
+ program
228
+ .command('validate')
229
+ .description('Validate an existing testcases.json against the schema')
230
+ .argument('<path>', 'Path to the testcases.json to validate')
231
+ .action((filepath: string) => {
232
+ cmdValidate(filepath);
233
+ });
234
+
235
+ program.parseAsync(process.argv).catch((err) => {
236
+ console.error(err instanceof Error ? err.message : err);
237
+ process.exit(1);
238
+ });
239
+ }
240
+
241
+ const __filename = fileURLToPath(import.meta.url);
242
+ const isMainEntry =
243
+ !!process.argv[1] && path.resolve(process.argv[1]) === __filename;
244
+ if (isMainEntry) {
245
+ main();
246
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Resolve App Metadata — discover `bundle_name` / `app_name` from a HarmonyOS
3
+ * project and write `app-metadata.json`.
4
+ *
5
+ * Subcommand:
6
+ * resolve-metadata --project-dir <dir> --output <path>
7
+ * Reads `<project-dir>/AppScope/app.json5` (JSON5 via jsonc-parser),
8
+ * extracts `app.bundleName` and `app.label` (resolving `$string:xxx`
9
+ * references from the base-qualifier string.json), writes
10
+ * `{bundle_name, app_name, project_root}` to `--output`, and emits the
11
+ * same JSON to stdout.
12
+ *
13
+ * Exit codes: 0 = success (JSON emitted + file written), 1 = any failure
14
+ * (invalid project root, JSON5 parse failure, missing bundleName, write
15
+ * failure). Errors go to stderr; stdout carries exactly one JSON line on
16
+ * success. This is the deterministic replacement for the LLM-driven discovery
17
+ * that previously lived in the self-tester agent prompt (S2/S3).
18
+ */
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { Command } from 'commander';
23
+ import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser';
24
+
25
+ /**
26
+ * Resolve a `$string:<key>` reference from `AppScope/resources/base/element/string.json`
27
+ * (the HarmonyOS base-qualifier string resource file). Returns the value, or null
28
+ * when the file is missing / unparseable / the key is absent. The caller falls back
29
+ * to the literal `$string:xxx` text on null — app_name is a display name, not
30
+ * load-bearing, so a missing resource must not fail the run.
31
+ */
32
+ function resolveStringResource(projectRoot: string, key: string): string | null {
33
+ const stringJson = path.join(
34
+ projectRoot, 'AppScope', 'resources', 'base', 'element', 'string.json',
35
+ );
36
+ if (!fs.existsSync(stringJson)) return null;
37
+ let doc: unknown;
38
+ try {
39
+ doc = parseJsonc(fs.readFileSync(stringJson, 'utf-8'));
40
+ } catch {
41
+ return null;
42
+ }
43
+ const arr = (doc as { string?: unknown })?.string;
44
+ if (!Array.isArray(arr)) return null;
45
+ const entry = arr.find((e) => (e as { name?: unknown })?.name === key);
46
+ const value = (entry as { value?: unknown } | undefined)?.value;
47
+ return typeof value === 'string' && value ? value : null;
48
+ }
49
+
50
+ export interface ResolveMetadataArgs {
51
+ projectDir: string;
52
+ output: string;
53
+ }
54
+
55
+ export function cmdResolveMetadata(args: ResolveMetadataArgs): void {
56
+ const projectRoot = path.resolve(args.projectDir);
57
+
58
+ const appJson5 = path.join(projectRoot, 'AppScope', 'app.json5');
59
+ if (!fs.existsSync(appJson5)) {
60
+ console.error(`not a HarmonyOS project root (no AppScope/app.json5): ${projectRoot}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ let doc: unknown;
65
+ const errs: ParseError[] = [];
66
+ try {
67
+ doc = parseJsonc(fs.readFileSync(appJson5, 'utf-8'), errs, { allowTrailingComma: true });
68
+ } catch (e) {
69
+ console.error(`failed to parse ${appJson5}: ${e instanceof Error ? e.message : String(e)}`);
70
+ process.exit(1);
71
+ }
72
+ if (errs.length > 0) {
73
+ console.error(
74
+ `failed to parse ${appJson5}: ${errs.map((e) => printParseErrorCode(e.error)).join('; ')}`,
75
+ );
76
+ process.exit(1);
77
+ }
78
+
79
+ const app = (doc as { app?: Record<string, unknown> })?.app;
80
+ const bundleName = app?.bundleName;
81
+ if (typeof bundleName !== 'string' || !bundleName) {
82
+ console.error(`missing app.bundleName (string) in ${appJson5}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ const label = app?.label;
87
+ let appName: string;
88
+ if (typeof label === 'string' && label.startsWith('$string:')) {
89
+ const key = label.slice('$string:'.length);
90
+ const resolved = resolveStringResource(projectRoot, key);
91
+ if (resolved) {
92
+ appName = resolved;
93
+ } else {
94
+ console.error(
95
+ `warning: string resource '${key}' not found in AppScope/resources/base/element/string.json — ` +
96
+ `using literal '${label}' as app_name`,
97
+ );
98
+ appName = label;
99
+ }
100
+ } else if (typeof label === 'string' && label) {
101
+ appName = label;
102
+ } else {
103
+ // ponytail: label missing/empty — bundleName is a valid non-empty string, reuse it.
104
+ // app_name is display-only; failing the whole run over it would be over-strict.
105
+ console.error(`warning: missing app.label in ${appJson5} — using bundleName as app_name`);
106
+ appName = bundleName;
107
+ }
108
+
109
+ const meta = { bundle_name: bundleName, app_name: appName, project_root: projectRoot };
110
+
111
+ try {
112
+ fs.mkdirSync(path.dirname(path.resolve(args.output)), { recursive: true });
113
+ fs.writeFileSync(path.resolve(args.output), JSON.stringify(meta) + '\n', 'utf-8');
114
+ } catch (e) {
115
+ console.error(`failed to write ${args.output}: ${e instanceof Error ? e.message : String(e)}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ console.log(JSON.stringify(meta));
120
+ }
121
+
122
+ /** CLI entry — wires up the `resolve-metadata` subcommand. */
123
+ export function main(): void {
124
+ const program = new Command();
125
+ program
126
+ .description('从 HarmonyOS 工程解析 bundle_name/app_name 并写 app-metadata.json')
127
+ .requiredOption('--project-dir <dir>', 'HarmonyOS 工程根目录(含 AppScope/app.json5)')
128
+ .requiredOption('--output <path>', 'app-metadata.json 输出路径')
129
+ .action((opts: ResolveMetadataArgs) => {
130
+ cmdResolveMetadata({ projectDir: opts.projectDir, output: opts.output });
131
+ });
132
+
133
+ program.parseAsync(process.argv).catch((err) => {
134
+ console.error(err instanceof Error ? err.message : err);
135
+ process.exit(1);
136
+ });
137
+ }
138
+
139
+ const __filename = fileURLToPath(import.meta.url);
140
+ const isMainEntry = !!process.argv[1] && path.resolve(process.argv[1]) === __filename;
141
+ if (isMainEntry) {
142
+ main();
143
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal logger for the self-test runner. Writes `[timestamp] LEVEL msg` to an
3
+ * optional file stream and to stderr — never stdout, which carries the strict
4
+ * JSON the orchestrator parses.
5
+ */
6
+ import fs from 'node:fs';
7
+
8
+ /** Format a Date as `YYYY-MM-DD HH:MM:SS`. Shared with self-test-runner's status log. */
9
+ export function formatLogTimestamp(d: Date): string {
10
+ const pad = (n: number) => String(n).padStart(2, '0');
11
+ return (
12
+ `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
13
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
14
+ );
15
+ }
16
+
17
+ export interface Logger {
18
+ info(msg: string): void;
19
+ warning(msg: string): void;
20
+ error(msg: string): void;
21
+ debug(msg: string): void;
22
+ }
23
+
24
+ type Level = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
25
+ const LEVEL_RANK: Record<Level, number> = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 };
26
+
27
+ /** Logger writing to `stream` (if non-null) and stderr; drops messages below `minLevel`. */
28
+ export function createLogger(
29
+ stream: fs.WriteStream | null,
30
+ minLevel: 'DEBUG' | 'INFO' = 'DEBUG',
31
+ ): Logger {
32
+ const rank = LEVEL_RANK[minLevel];
33
+ const write = (level: Level, msg: string): void => {
34
+ if (LEVEL_RANK[level] < rank) return;
35
+ const line = `[${formatLogTimestamp(new Date())}] ${level} ${msg}`;
36
+ if (stream) stream.write(line + '\n');
37
+ process.stderr.write(line + '\n');
38
+ };
39
+ return {
40
+ info: (m) => write('INFO', m),
41
+ warning: (m) => write('WARNING', m),
42
+ error: (m) => write('ERROR', m),
43
+ debug: (m) => write('DEBUG', m),
44
+ };
45
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Process helpers for the self-test runner: native liveness + tree-kill.
3
+ */
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ const IS_WIN = process.platform === 'win32';
7
+
8
+ /** Whether `pid` is alive, via `kill(pid, 0)`: ESRCH = dead, EPERM = alive. */
9
+ export function pidAlive(pid: number): boolean {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ } catch (e) {
14
+ return (e as NodeJS.ErrnoException).code === 'EPERM';
15
+ }
16
+ }
17
+
18
+ /** Kill `pid` + descendants. Windows: `taskkill /F /T`; POSIX: SIGKILL the group then the pid. Best-effort. */
19
+ export function killProcessTree(pid: number): void {
20
+ if (IS_WIN) {
21
+ try {
22
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], {
23
+ timeout: 10000,
24
+ windowsHide: true,
25
+ stdio: 'ignore',
26
+ });
27
+ } catch {
28
+ // best effort
29
+ }
30
+ return;
31
+ }
32
+ try {
33
+ process.kill(-pid, 'SIGKILL');
34
+ } catch {
35
+ try {
36
+ process.kill(pid, 'SIGKILL');
37
+ } catch {
38
+ // best effort
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Self-test input validation gates — TS ports of the `uv run python -c "..."`
3
+ * one-liners that previously lived inline in `agents/self-tester.md`.
4
+ *
5
+ * Folding them here removes a cold Python+uv start per check and gives the
6
+ * logic a single, typed, testable home. Each function is pure and returns
7
+ * `{ ok, message }`; the CLI layer prints `message` to stdout on success or
8
+ * stderr+exit(1) on failure, matching the prompt's `… && echo OK`
9
+ * expectation. No stdout-JSON contract applies to these gates (unlike
10
+ * run/status/kill) — they are simple human-readable pass/fail checks.
11
+ *
12
+ * a2h edition: the config-dependent gates (`checkConfig`, `resolveDir`) were
13
+ * dropped — a2h has no `~/.hometrans/config.json`; model params are injected
14
+ * by the plugin tool via stdin. Only the pure file-shape checks remain.
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ export interface CheckResult {
20
+ ok: boolean;
21
+ message: string;
22
+ }
23
+
24
+ /**
25
+ * `hap_path` is a comma-separated list of files/dirs — each entry a
26
+ * `.hap`/`.hsp` file or a directory. Every entry must exist and, across all
27
+ * entries, at least one `.hap` (the entry package) must be present.
28
+ */
29
+ export function checkHap(hapArg: string): CheckResult {
30
+ const parts = hapArg
31
+ .split(',')
32
+ .map((s) => s.trim())
33
+ .filter(Boolean);
34
+ if (parts.length === 0) return { ok: false, message: 'hap_path empty' };
35
+
36
+ for (const p of parts) {
37
+ if (!fs.existsSync(p)) return { ok: false, message: `path not found: ${p}` };
38
+ }
39
+
40
+ const haps: string[] = [];
41
+ for (const p of parts) {
42
+ const names = fs.statSync(p).isDirectory() ? fs.readdirSync(p) : [path.basename(p)];
43
+ for (const n of names) {
44
+ if (n.toLowerCase().endsWith('.hap')) haps.push(n);
45
+ }
46
+ }
47
+ if (haps.length === 0) {
48
+ return { ok: false, message: 'no entry .hap found across hap_path entries' };
49
+ }
50
+ return { ok: true, message: 'OK' };
51
+ }
52
+
53
+ /**
54
+ * `setup=false` precondition: both JSONs must parse, `testcases.json` must be a
55
+ * non-empty array, and `app-metadata.json` must carry the required keys.
56
+ */
57
+ export function checkInputs(testcasesPath: string, metadataPath: string): CheckResult {
58
+ let t: unknown;
59
+ try {
60
+ t = JSON.parse(fs.readFileSync(testcasesPath, 'utf-8'));
61
+ } catch (e) {
62
+ return { ok: false, message: `testcases.json failed to parse: ${(e as Error).message}` };
63
+ }
64
+ if (!Array.isArray(t) || t.length === 0) {
65
+ return { ok: false, message: 'testcases.json empty or not a list' };
66
+ }
67
+
68
+ let m: unknown;
69
+ try {
70
+ m = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
71
+ } catch (e) {
72
+ return { ok: false, message: `app-metadata.json failed to parse: ${(e as Error).message}` };
73
+ }
74
+ // A JSON primitive/array has no keys → treat as missing fields (avoids the
75
+ // `in` operator throwing a TypeError on a non-object).
76
+ if (typeof m !== 'object' || m === null || Array.isArray(m)) {
77
+ return { ok: false, message: 'app-metadata.json missing required fields' };
78
+ }
79
+ const mo = m as Record<string, unknown>;
80
+ for (const k of ['bundle_name', 'app_name', 'project_root']) {
81
+ if (!(k in mo)) return { ok: false, message: 'app-metadata.json missing required fields' };
82
+ }
83
+ return { ok: true, message: 'OK' };
84
+ }
85
+
86
+ /**
87
+ * `[PRE]` cases must occupy a contiguous block starting at index 0 in
88
+ * `testcases.json`. Run only when pre-cases were extracted.
89
+ */
90
+ export function checkPre(testcasesPath: string): CheckResult {
91
+ let d: unknown;
92
+ try {
93
+ d = JSON.parse(fs.readFileSync(testcasesPath, 'utf-8'));
94
+ } catch (e) {
95
+ return { ok: false, message: `testcases.json failed to parse: ${(e as Error).message}` };
96
+ }
97
+ if (!Array.isArray(d)) return { ok: false, message: 'testcases.json not a list' };
98
+
99
+ const pre: number[] = [];
100
+ for (let i = 0; i < d.length; i++) {
101
+ const name = (d[i] as { case_name?: unknown })?.case_name;
102
+ if (typeof name !== 'string') {
103
+ return { ok: false, message: `testcases.json entry at index ${i} missing string case_name` };
104
+ }
105
+ if (name.startsWith('[PRE] ')) pre.push(i);
106
+ }
107
+ const contiguous = pre.every((v, idx) => v === idx);
108
+ if (!contiguous) {
109
+ return {
110
+ ok: false,
111
+ message: `[PRE] entries must be contiguous from index 0, got ${JSON.stringify(pre)}`,
112
+ };
113
+ }
114
+ return { ok: true, message: `OK: ${pre.length} [PRE] entries at front` };
115
+ }