@hmharness/domain-harmony 0.1.0

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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @hmharness/domain-harmony - ondevicetest (minimal honest version)
3
+ * The ROADMAP gap "onDeviceTest 设备测试": a scripted assertion loop over
4
+ * the REAL device instead of a hypothetical unit-test framework. What we
5
+ * can actually verify on-device today, deterministically:
6
+ * 1. the hap installs
7
+ * 2. the ability launches
8
+ * 3. the app's own lifecycle log marker appears in hilog within N seconds
9
+ * 4. (cleanup) uninstalls
10
+ * That is exactly the loop our e2e script runs by hand - productized as a
11
+ * tool with a pass/fail verdict per step. Richer device-side test runners
12
+ * (aa test / ArkTSTDD) can slot in behind the same verdict shape later.
13
+ */
14
+ import { execFile } from 'node:child_process';
15
+ import { access } from 'node:fs/promises';
16
+ import { join, resolve } from 'node:path';
17
+ import { promisify } from 'node:util';
18
+ const execCb = promisify(execFile);
19
+ async function exists(p) {
20
+ try {
21
+ await access(p);
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ export async function runDeviceTest(o) {
29
+ const pre = o.target ? ['-t', o.target] : [];
30
+ const run = o.runImpl ?? (async (args, timeout) => {
31
+ try {
32
+ const { stdout, stderr } = await execCb(o.hdc, [...pre, ...args], { timeout, windowsHide: true });
33
+ return { ok: true, out: ((stdout || '') + (stderr ? '\n' + stderr : '')).trim() };
34
+ }
35
+ catch (err) {
36
+ const e = err;
37
+ return { ok: false, out: [e.stdout, e.stderr, e.message].filter(Boolean).join('\n').slice(0, 600) };
38
+ }
39
+ });
40
+ const steps = [];
41
+ // 1. install
42
+ let r = await run(['install', '-r', o.hap], 120_000);
43
+ if (!r.ok || /fail/i.test(r.out))
44
+ r = await run(['app', 'install', '-r', o.hap], 120_000);
45
+ steps.push({ step: 'install', pass: r.ok && /success/i.test(r.out), detail: r.out.slice(0, 200) });
46
+ if (!steps[0].pass)
47
+ return steps;
48
+ // 2. launch
49
+ r = await run(['shell', 'aa', 'start', '-a', o.ability, '-b', o.bundle], 30_000);
50
+ steps.push({ step: 'launch', pass: r.ok && /successfully/i.test(r.out), detail: r.out.slice(0, 200) });
51
+ // 3. lifecycle marker in hilog (poll briefly - logs flush asynchronously)
52
+ let sawMarker = false;
53
+ let tail = '';
54
+ const wait = o.waitMs ?? 6000;
55
+ const t0 = Date.now();
56
+ while (!sawMarker && Date.now() - t0 < wait) {
57
+ await new Promise((res) => setTimeout(res, 1000));
58
+ const g = await run(['shell', 'hilog', '-x'], 30_000);
59
+ tail = g.out;
60
+ sawMarker = tail.includes(o.expectLog);
61
+ }
62
+ steps.push({ step: `log-marker "${o.expectLog}"`, pass: sawMarker, detail: sawMarker ? `found in hilog within ${Date.now() - t0}ms` : `NOT found in ${(wait / 1000).toFixed(0)}s of hilog tail` });
63
+ // 4. cleanup uninstall
64
+ r = await run(['uninstall', o.bundle], 60_000);
65
+ if (!r.ok || /unknown command/i.test(r.out))
66
+ r = await run(['app', 'uninstall', o.bundle], 60_000);
67
+ steps.push({ step: 'uninstall(cleanup)', pass: r.ok && /success/i.test(r.out), detail: r.out.slice(0, 120) });
68
+ return steps;
69
+ }
70
+ export const harmonyDeviceTest = {
71
+ name: 'harmony_device_test',
72
+ description: 'On-device smoke test of a .hap against a connected device/emulator: install -> launch ability -> assert the app lifecycle marker appears in hilog (polls up to 6s) -> cleanup uninstall. Returns a per-step pass/fail verdict table. This is the honest minimal device test: real install, real launch, real log evidence.',
73
+ parameters: {
74
+ type: 'object',
75
+ properties: {
76
+ hap: { type: 'string', description: 'signed .hap path (default: newest -signed under the project)' },
77
+ bundle: { type: 'string', description: 'bundle name (default: read from AppScope/app.json5)' },
78
+ ability: { type: 'string', description: 'ability to launch (default: EntryAbility)' },
79
+ expect_log: { type: 'string', description: 'log marker to assert (default: "EntryAbility onCreate")' },
80
+ target: { type: 'string', description: 'device target id from harmony_devices' },
81
+ },
82
+ required: [],
83
+ },
84
+ needsApproval: () => true, // installs+launches+uninstalls on the device
85
+ async execute(args, ctx) {
86
+ // resolve hdc
87
+ const deveco = process.env.HM_DEVECO_HOME ?? 'C:\\DevEco-Studio';
88
+ let hdc = 'hdc';
89
+ try {
90
+ await execCb(hdc, ['--version'], { timeout: 8000, windowsHide: true });
91
+ }
92
+ catch {
93
+ const cand = join(deveco, 'sdk', 'default', 'openharmony', 'toolchains', 'hdc.exe');
94
+ if (await exists(cand))
95
+ hdc = cand;
96
+ else
97
+ return { output: 'hdc not found.', isError: true };
98
+ }
99
+ // project root walk
100
+ let root = ctx.cwd;
101
+ for (;;) {
102
+ if (await exists(join(root, 'build-profile.json5')))
103
+ break;
104
+ const parent = resolve(root, '..');
105
+ if (parent === root)
106
+ break;
107
+ root = parent;
108
+ }
109
+ // hap: explicit or newest -signed
110
+ let hap = typeof args.hap === 'string' && args.hap ? resolve(ctx.cwd, args.hap) : '';
111
+ if (!hap) {
112
+ const { readdir, stat } = await import('node:fs/promises');
113
+ let best = '';
114
+ let bestMtime = 0;
115
+ const stack = [root];
116
+ while (stack.length) {
117
+ const d = stack.pop();
118
+ let entries;
119
+ try {
120
+ entries = await readdir(d, { withFileTypes: true });
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ for (const e of entries) {
126
+ const p = join(d, e.name);
127
+ if (e.isDirectory()) {
128
+ if (!['node_modules', 'oh_modules', '.hvigor', '.preview', 'src'].includes(e.name))
129
+ stack.push(p);
130
+ }
131
+ else if (e.name.endsWith('-signed.hap')) {
132
+ const m = (await stat(p)).mtimeMs;
133
+ if (m > bestMtime) {
134
+ bestMtime = m;
135
+ best = p;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ hap = best;
141
+ if (!hap)
142
+ return { output: 'No signed .hap found. harmony_build then harmony_sign first (device tests need a signed hap).', isError: true };
143
+ }
144
+ if (!(await exists(hap)))
145
+ return { output: `No such file: ${hap}`, isError: true };
146
+ // bundle from app.json5 (via the schema module's lenient parser)
147
+ let bundle = typeof args.bundle === 'string' && args.bundle ? args.bundle : '';
148
+ if (!bundle) {
149
+ const { parseJson5 } = await import("./schema.js");
150
+ try {
151
+ const app = parseJson5(await (await import('node:fs/promises')).readFile(join(root, 'AppScope', 'app.json5'), 'utf8'));
152
+ bundle = app.app?.bundleName ?? '';
153
+ }
154
+ catch { /* schema check reports */ }
155
+ }
156
+ if (!bundle)
157
+ return { output: 'Could not determine bundle name - pass `bundle` explicitly.', isError: true };
158
+ const steps = await runDeviceTest({
159
+ hdc,
160
+ target: typeof args.target === 'string' ? args.target : undefined,
161
+ hap,
162
+ bundle,
163
+ ability: typeof args.ability === 'string' && args.ability ? args.ability : 'EntryAbility',
164
+ expectLog: typeof args.expect_log === 'string' && args.expect_log ? args.expect_log : 'EntryAbility onCreate',
165
+ });
166
+ const allPass = steps.every((s) => s.pass);
167
+ const lines = [
168
+ `device test: ${hap} (bundle ${bundle})`,
169
+ ...steps.map((s) => ` ${s.pass ? 'PASS' : 'FAIL'} ${s.step}${s.detail && !s.pass ? ` - ${s.detail}` : ''}`),
170
+ allPass ? 'ALL STEPS PASS - the app really installed, launched and logged on device.' : 'FAILED steps above - each names what broke.',
171
+ ];
172
+ return { output: lines.join('\n'), isError: !allPass };
173
+ },
174
+ };
@@ -0,0 +1,22 @@
1
+ import type { Tool } from '@hmharness/kernel';
2
+ export interface ProjectProfile {
3
+ root: string;
4
+ bundleName: string;
5
+ sdkVersion: string;
6
+ modules: Array<{
7
+ name: string;
8
+ type: string;
9
+ pages: string[];
10
+ abilities: string[];
11
+ deviceTypes: string[];
12
+ }>;
13
+ hapDependencies: string[];
14
+ resourceCounts: {
15
+ strings: number;
16
+ media: number;
17
+ };
18
+ sourceFiles: number;
19
+ configIssues: number;
20
+ }
21
+ export declare function profileProject(root: string): Promise<ProjectProfile>;
22
+ export declare const harmonyProjectProfile: Tool;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @hmharness/domain-harmony - profile (project picture, quality-trio minimal)
3
+ * A one-call inventory of a HarmonyOS project: modules, pages, abilities,
4
+ * har/hap dependency edges, resource counts, and config health (schema
5
+ * issues inline). Answer "what is this project" without reading 20 files -
6
+ * the base layer the old line's quality trio (profile / regression /
7
+ * scoring) starts from.
8
+ */
9
+ import { readdir, readFile, stat } from 'node:fs/promises';
10
+ import { join, resolve } from 'node:path';
11
+ import { parseJson5, checkProjectSchemas } from "./schema.js";
12
+ async function exists(p) {
13
+ try {
14
+ await stat(p);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function countFiles(dir, ext, maxDepth = 6) {
22
+ let n = 0;
23
+ const stack = [[dir, 0]];
24
+ while (stack.length) {
25
+ const [d, dep] = stack.pop();
26
+ if (dep > maxDepth)
27
+ continue;
28
+ let entries;
29
+ try {
30
+ entries = await readdir(d, { withFileTypes: true });
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ for (const e of entries) {
36
+ if (e.name.startsWith('.') || ['node_modules', 'oh_modules', 'build', '.hvigor'].includes(e.name))
37
+ continue;
38
+ const p = join(d, e.name);
39
+ if (e.isDirectory())
40
+ stack.push([p, dep + 1]);
41
+ else if (e.name.endsWith(ext))
42
+ n++;
43
+ }
44
+ }
45
+ return n;
46
+ }
47
+ export async function profileProject(root) {
48
+ const r = resolve(root);
49
+ // app scope
50
+ let bundleName = '';
51
+ try {
52
+ const app = parseJson5(await readFile(join(r, 'AppScope', 'app.json5'), 'utf8'));
53
+ bundleName = app.app?.bundleName ?? '';
54
+ }
55
+ catch { /* no AppScope */ }
56
+ // root build-profile for sdk
57
+ let sdk = '';
58
+ try {
59
+ const bp = parseJson5(await readFile(join(r, 'build-profile.json5'), 'utf8'));
60
+ sdk = bp.app?.products?.[0]?.compatibleSdkVersion ?? '';
61
+ }
62
+ catch { /* absent */ }
63
+ // modules
64
+ const modules = [];
65
+ let entries = [];
66
+ try {
67
+ entries = (await readdir(r, { withFileTypes: true })).filter((d) => d.isDirectory() && !d.name.startsWith('.') && !['AppScope', 'hvigor', 'build', 'oh_modules', 'node_modules', '.hvigor'].includes(d.name)).map((d) => ({ name: d.name }));
68
+ }
69
+ catch { /* none */ }
70
+ for (const { name } of entries) {
71
+ const mp = join(r, name, 'src', 'main', 'module.json5');
72
+ if (!(await exists(mp)))
73
+ continue;
74
+ try {
75
+ const m = parseJson5(await readFile(mp, 'utf8'));
76
+ // pages list from the profile json
77
+ let pages = [];
78
+ try {
79
+ const pp = parseJson5(await readFile(join(r, name, 'src', 'main', 'resources', 'base', 'profile', 'main_pages.json'), 'utf8'));
80
+ pages = pp.src ?? [];
81
+ }
82
+ catch { /* no pages (har) */ }
83
+ modules.push({
84
+ name,
85
+ type: m.module?.type ?? '',
86
+ pages,
87
+ abilities: (m.module?.abilities ?? []).map((a) => a.name),
88
+ deviceTypes: m.module?.deviceTypes ?? [],
89
+ });
90
+ }
91
+ catch { /* unparseable module.json5 - schema check will report it */ }
92
+ }
93
+ // entry's har deps
94
+ let hapDependencies = [];
95
+ try {
96
+ const pkg = parseJson5(await readFile(join(r, 'entry', 'oh-package.json5'), 'utf8'));
97
+ hapDependencies = Object.entries(pkg.dependencies ?? {}).map(([k, v]) => `${k}@${v}`);
98
+ }
99
+ catch { /* none */ }
100
+ const schema = await checkProjectSchemas(r);
101
+ return {
102
+ root: r,
103
+ bundleName,
104
+ sdkVersion: sdk,
105
+ modules,
106
+ hapDependencies,
107
+ resourceCounts: {
108
+ strings: await countFiles(join(r, 'AppScope', 'resources'), '.json') + await countFiles(join(r, 'entry', 'src', 'main', 'resources'), '.json'),
109
+ media: await countFiles(join(r, 'AppScope', 'resources'), '.png') + await countFiles(join(r, 'entry', 'src', 'main', 'resources'), '.png'),
110
+ },
111
+ sourceFiles: await countFiles(r, '.ets'),
112
+ configIssues: schema.issues.length,
113
+ };
114
+ }
115
+ export const harmonyProjectProfile = {
116
+ name: 'harmony_project_profile',
117
+ description: 'One-call project inventory: modules (type/pages/abilities/deviceTypes), har dependencies, resource + source counts, bundle/SDK, and config health (issue count from schema check). The "what is this project" answer without reading files one by one - use before planning changes or estimating scope.',
118
+ parameters: {
119
+ type: 'object',
120
+ properties: {
121
+ path: { type: 'string', description: 'project root (default: auto-detect from cwd)' },
122
+ },
123
+ required: [],
124
+ },
125
+ needsApproval: () => false,
126
+ async execute(args, ctx) {
127
+ let root = ctx.cwd;
128
+ if (typeof args.path === 'string' && args.path)
129
+ root = resolve(ctx.cwd, args.path);
130
+ // walk up to the project root marker
131
+ let dir = root;
132
+ for (;;) {
133
+ if (await exists(join(dir, 'build-profile.json5'))) {
134
+ root = dir;
135
+ break;
136
+ }
137
+ const parent = resolve(dir, '..');
138
+ if (parent === dir)
139
+ break;
140
+ dir = parent;
141
+ }
142
+ try {
143
+ const p = await profileProject(root);
144
+ const lines = [
145
+ `project: ${p.root}`,
146
+ `bundle: ${p.bundleName || '(unknown)'} · SDK: ${p.sdkVersion || '(not set)'}`,
147
+ `modules (${p.modules.length}):`,
148
+ ...p.modules.map((m) => ` - ${m.name} [${m.type}] pages:${m.pages.length} abilities:${m.abilities.join(',') || '-'} devices:${m.deviceTypes.join(',') || '-'}`),
149
+ `entry deps: ${p.hapDependencies.join(', ') || '(none)'}`,
150
+ `sources: ${p.sourceFiles} .ets · resources: ${p.resourceCounts.strings} json / ${p.resourceCounts.media} png`,
151
+ `config health: ${p.configIssues === 0 ? 'OK' : `${p.configIssues} issue(s) - run harmony_schema_check for details`}`,
152
+ ];
153
+ return { output: lines.join('\n') };
154
+ }
155
+ catch (err) {
156
+ return { output: `profile failed: ${String(err).slice(0, 160)}`, isError: true };
157
+ }
158
+ },
159
+ };
@@ -0,0 +1,22 @@
1
+ import type { Tool } from '@hmharness/kernel';
2
+ /** Solid-color RGBA PNG (used for app/start icons - content is cosmetic). */
3
+ export declare function solidPng(size: number, rgba: [number, number, number, number]): Buffer;
4
+ export declare function sdkVersion(): string;
5
+ export interface ScaffoldModule {
6
+ name: string;
7
+ type?: 'feature' | 'har';
8
+ }
9
+ export interface ScaffoldOptions {
10
+ name?: string;
11
+ bundleId?: string;
12
+ /** Extra pages beyond Index (PascalCase identifiers). */
13
+ pages?: string[];
14
+ /** Extra modules: feature HAPs or har libraries. */
15
+ modules?: ScaffoldModule[];
16
+ }
17
+ export declare function scaffoldProject(dir: string, opts?: ScaffoldOptions): Promise<{
18
+ root: string;
19
+ bundleId: string;
20
+ files: number;
21
+ }>;
22
+ export declare const harmonyProjectCreate: Tool;