@hmharness/domain-harmony 0.6.4 → 0.6.6
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/dist/uiregress.d.ts +26 -1
- package/dist/uiregress.js +85 -14
- package/package.json +2 -2
package/dist/uiregress.d.ts
CHANGED
|
@@ -13,15 +13,40 @@ export interface UiRegressionResult {
|
|
|
13
13
|
saw: string | null;
|
|
14
14
|
description: string;
|
|
15
15
|
screenshot: string | null;
|
|
16
|
+
/** true when NO provider actually looked at the image (all blind/failed).
|
|
17
|
+
* The verdict is then about the vision chain, NOT about the app: never
|
|
18
|
+
* render it as a product FAIL - a tool that reports FAIL when its own
|
|
19
|
+
* eyes are shut is worse than no tool. */
|
|
20
|
+
visionUnavailable?: boolean;
|
|
21
|
+
/** per-provider errors collected while walking the vision chain */
|
|
22
|
+
visionErrors?: string[];
|
|
16
23
|
}
|
|
24
|
+
/** Injectable vision call (tests substitute it; production uses chatVision). */
|
|
25
|
+
export type VisionCall = (provider: ProviderConfig, prompt: string, imageDataUrl: string) => Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Walk the vision chain until a provider truly LOOKS at the image. A reply
|
|
28
|
+
* that is a refusal ("I can't view the image" - what a text-only provider
|
|
29
|
+
* returns with HTTP 200) is treated as a provider failure and the next
|
|
30
|
+
* candidate is tried. Returns the first real description, or the collected
|
|
31
|
+
* errors when every provider was blind/dead.
|
|
32
|
+
*/
|
|
33
|
+
export declare function describeWithChain(chain: ProviderConfig[], prompt: string, imageDataUrl: string, call?: VisionCall): Promise<{
|
|
34
|
+
described: string | null;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}>;
|
|
37
|
+
/** Keyword assertion on a genuine description (any hit wins). */
|
|
38
|
+
export declare function assertKeywords(described: string, expect: string[]): string | null;
|
|
17
39
|
/** Capture the device screen via hdc; returns the local file path. */
|
|
18
40
|
export declare function captureDeviceScreen(hdc: string, target: string | undefined, outDir: string): Promise<string>;
|
|
19
41
|
/** One regression case through the real device + vision chain. */
|
|
20
42
|
export declare function runUiRegression(opts: {
|
|
21
43
|
hdc: string;
|
|
22
44
|
target?: string;
|
|
23
|
-
vision
|
|
45
|
+
/** one provider, or the ordered chain (vision + visionFallbacks) */
|
|
46
|
+
vision: ProviderConfig | ProviderConfig[];
|
|
24
47
|
cases: UiRegressionCase[];
|
|
25
48
|
outDir: string;
|
|
49
|
+
/** test seam: substitute the multimodal call */
|
|
50
|
+
visionCall?: VisionCall;
|
|
26
51
|
}): Promise<UiRegressionResult[]>;
|
|
27
52
|
export declare const harmonyUiRegression: Tool;
|
package/dist/uiregress.js
CHANGED
|
@@ -19,8 +19,36 @@ import { execFile } from 'node:child_process';
|
|
|
19
19
|
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { promisify } from 'node:util';
|
|
22
|
-
import { chatVision } from '@hmharness/kernel';
|
|
22
|
+
import { chatVision, foundNothing, isVisionRefusal } from '@hmharness/kernel';
|
|
23
23
|
const execCb = promisify(execFile);
|
|
24
|
+
/**
|
|
25
|
+
* Walk the vision chain until a provider truly LOOKS at the image. A reply
|
|
26
|
+
* that is a refusal ("I can't view the image" - what a text-only provider
|
|
27
|
+
* returns with HTTP 200) is treated as a provider failure and the next
|
|
28
|
+
* candidate is tried. Returns the first real description, or the collected
|
|
29
|
+
* errors when every provider was blind/dead.
|
|
30
|
+
*/
|
|
31
|
+
export async function describeWithChain(chain, prompt, imageDataUrl, call = chatVision) {
|
|
32
|
+
const errors = [];
|
|
33
|
+
for (const provider of chain) {
|
|
34
|
+
try {
|
|
35
|
+
const text = await call(provider, prompt, imageDataUrl);
|
|
36
|
+
if (isVisionRefusal(text)) {
|
|
37
|
+
errors.push(`${provider.model}: answered without seeing the image ("${text.trim().slice(0, 80)}")`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
return { described: text, errors };
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
errors.push(`${provider.model}: ${String(err).slice(0, 120)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { described: null, errors };
|
|
47
|
+
}
|
|
48
|
+
/** Keyword assertion on a genuine description (any hit wins). */
|
|
49
|
+
export function assertKeywords(described, expect) {
|
|
50
|
+
return expect.find((k) => described.toLowerCase().includes(k.toLowerCase())) ?? null;
|
|
51
|
+
}
|
|
24
52
|
/** Capture the device screen via hdc; returns the local file path. */
|
|
25
53
|
export async function captureDeviceScreen(hdc, target, outDir) {
|
|
26
54
|
await mkdir(outDir, { recursive: true });
|
|
@@ -45,6 +73,7 @@ export async function captureDeviceScreen(hdc, target, outDir) {
|
|
|
45
73
|
}
|
|
46
74
|
/** One regression case through the real device + vision chain. */
|
|
47
75
|
export async function runUiRegression(opts) {
|
|
76
|
+
const chain = Array.isArray(opts.vision) ? opts.vision : [opts.vision];
|
|
48
77
|
const results = [];
|
|
49
78
|
for (const c of opts.cases) {
|
|
50
79
|
const pre = opts.target ? ['-t', opts.target] : [];
|
|
@@ -66,17 +95,38 @@ export async function runUiRegression(opts) {
|
|
|
66
95
|
results.push({ case: c.name, pass: false, saw: null, description: 'screenshot failed: ' + String(err).slice(0, 120), screenshot: null });
|
|
67
96
|
continue;
|
|
68
97
|
}
|
|
69
|
-
// vision describe
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
// vision describe (through the chain; refusals do not count as seeing)
|
|
99
|
+
const b64 = (await readFile(shot)).toString('base64');
|
|
100
|
+
const { described, errors } = await describeWithChain(chain, 'Describe this device screen briefly. Then on the last line output exactly: FOUND: <the most prominent UI text you can read>.', `data:image/jpeg;base64,${b64}`, opts.visionCall);
|
|
101
|
+
if (described === null) {
|
|
102
|
+
// EVERY provider failed to look: this says nothing about the app.
|
|
103
|
+
results.push({
|
|
104
|
+
case: c.name,
|
|
105
|
+
pass: false,
|
|
106
|
+
saw: null,
|
|
107
|
+
visionUnavailable: true,
|
|
108
|
+
visionErrors: errors,
|
|
109
|
+
description: `VISION PROVIDER FAILURE (no UI verdict): none of the ${chain.length} configured vision provider(s) could see the image - ${errors.join('; ')}`,
|
|
110
|
+
screenshot: shot,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
76
113
|
}
|
|
77
|
-
|
|
78
|
-
|
|
114
|
+
const text = described.trim();
|
|
115
|
+
const saw = assertKeywords(text, c.expect);
|
|
116
|
+
if (!saw && foundNothing(text)) {
|
|
117
|
+
// the model itself says it read no text: no evidence either way
|
|
118
|
+
results.push({
|
|
119
|
+
case: c.name,
|
|
120
|
+
pass: false,
|
|
121
|
+
saw: null,
|
|
122
|
+
visionUnavailable: true,
|
|
123
|
+
visionErrors: [...errors, `${chain[0].model}: reported no readable text on screen ("${text.slice(0, 120)}")`],
|
|
124
|
+
description: `NO VERDICT - the vision model reported no readable text on screen (blank screen, or a provider that cannot see): ${text.slice(0, 240)}`,
|
|
125
|
+
screenshot: shot,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
79
128
|
}
|
|
129
|
+
results.push({ case: c.name, pass: Boolean(saw), saw, description: text.slice(0, 400), screenshot: shot, visionErrors: errors });
|
|
80
130
|
}
|
|
81
131
|
return results;
|
|
82
132
|
}
|
|
@@ -99,18 +149,20 @@ export const harmonyUiRegression = {
|
|
|
99
149
|
const expect = Array.isArray(args.expect) ? args.expect.map(String).filter(Boolean) : [];
|
|
100
150
|
if (!bundle || expect.length === 0)
|
|
101
151
|
return { output: 'bundle and non-empty expect[] required', isError: true };
|
|
102
|
-
// vision provider from config (kernel routing)
|
|
103
|
-
|
|
152
|
+
// vision provider from config (kernel routing): the full chain, so a
|
|
153
|
+
// blind or dead provider is retried rather than graded as a UI verdict
|
|
154
|
+
const { loadConfig, visionProviderChain } = await import('@hmharness/kernel');
|
|
104
155
|
const cfg = await loadConfig();
|
|
105
156
|
let vision;
|
|
106
157
|
try {
|
|
107
|
-
vision =
|
|
108
|
-
if (!vision.apiKey)
|
|
158
|
+
vision = visionProviderChain(cfg);
|
|
159
|
+
if (vision.length === 0 || !vision[0].apiKey)
|
|
109
160
|
throw new Error('no key');
|
|
110
161
|
}
|
|
111
162
|
catch {
|
|
112
163
|
return { output: 'No vision provider configured (vision block or providers+routing.vision) - harmony_ui_regression needs one.', isError: true };
|
|
113
164
|
}
|
|
165
|
+
const chainNames = vision.map((p) => `${p.model}${p.supportsVision === false ? ' (marked text-only!)' : ''}`);
|
|
114
166
|
// hdc
|
|
115
167
|
const deveco = process.env.HM_DEVECO_HOME ?? 'C:\\DevEco-Studio';
|
|
116
168
|
let hdc = 'hdc';
|
|
@@ -136,12 +188,31 @@ export const harmonyUiRegression = {
|
|
|
136
188
|
outDir,
|
|
137
189
|
});
|
|
138
190
|
const r = results[0];
|
|
191
|
+
if (r.visionUnavailable) {
|
|
192
|
+
// The machine could not look at the screen. Report that loudly and do
|
|
193
|
+
// NOT dress it up as a product verdict.
|
|
194
|
+
return {
|
|
195
|
+
output: [
|
|
196
|
+
`UI regression: ${r.case}`,
|
|
197
|
+
`NO VERDICT - vision provider failure, not a UI result`,
|
|
198
|
+
`vision chain tried (${chainNames.join(' -> ')}):`,
|
|
199
|
+
...(r.visionErrors ?? []).map((e) => ` - ${e}`),
|
|
200
|
+
...(r.screenshot ? [`screenshot: ${r.screenshot}`] : []),
|
|
201
|
+
'Fix: point routing.vision at a model that accepts images, or mark the text-only one with "supportsVision": false so it is skipped.',
|
|
202
|
+
'Ground truth without vision: hdc shell uitest dumpLayout -p /data/local/tmp/layout.json (the view tree carries every node text).',
|
|
203
|
+
].join('\n'),
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
139
207
|
const lines = [
|
|
140
208
|
`UI regression: ${r.case}`,
|
|
141
209
|
r.saw ? `PASS - saw "${r.saw}" on screen` : `FAIL - none of [${expect.join(', ')}] visible`,
|
|
142
210
|
...(r.screenshot ? [`screenshot: ${r.screenshot}`] : []),
|
|
143
211
|
`vision said: ${r.description}`,
|
|
144
212
|
];
|
|
213
|
+
if (r.visionErrors?.length) {
|
|
214
|
+
lines.push(`degraded chain (earlier provider(s) did not see the image): ${r.visionErrors.join('; ')}`);
|
|
215
|
+
}
|
|
145
216
|
return { output: lines.join('\n'), isError: !r.pass };
|
|
146
217
|
},
|
|
147
218
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/domain-harmony",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "hmharness HarmonyOS domain core: device, toolchain, build, and project lifecycle capabilities. HarmonyOS is the framework's native domain, not an add-on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/kernel": "0.6.
|
|
18
|
+
"@hmharness/kernel": "0.6.6"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|