mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
5a33f8bfc7
* feat(maestro): numeric option fields accept ${VAR} lookup interpolation (#1293)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(maestro): validate resolved numeric strings before coercion
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(maestro): address review feedback on numeric resolution
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(maestro-conformance): preserve unresolved ${VAR} numeric tokens in canonical model
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
104 lines
5.7 KiB
JavaScript
104 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Rebuilds corpus/manifest.json from the flows on disk. `regenerate.mjs` runs
|
|
// this first, so adding a corpus flow is just: drop the .yaml in, add a note
|
|
// below if it needs one, and regenerate.
|
|
//
|
|
// The manifest is the corpus's provenance record: vendored upstream flows carry
|
|
// their upstream repo path and sha256 so a silently-edited flow is detectable;
|
|
// authored flows say why they exist.
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const CORPUS_DIR = path.join(HERE, 'corpus');
|
|
const UPSTREAM_REPO_DIR = 'maestro-test/src/test/resources';
|
|
|
|
const NOTES = {
|
|
'bug-classes/percent-decimal-swipe': 'Bug class 1: decimal percentage rejection at parse.',
|
|
'bug-classes/target-swipe-missing-direction': 'Bug class 2: target swipe requires an explicit direction.',
|
|
'bug-classes/retry-over-cap': 'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.',
|
|
'bug-classes/settle-after-tap': 'Bug class 4: plain tap; settle ordering is a layer-3 differential.',
|
|
'authored/runscript': 'Coverage: runScript file command (no self-contained upstream flow).',
|
|
'authored/runflow-main': 'Coverage: runFlow file include + provenance.',
|
|
'authored/runflow-child': 'Include target for runflow-main (not parsed as a top-level flow).',
|
|
'authored/doubletap': 'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).',
|
|
'authored/scroll-until-visible': 'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).',
|
|
'authored/extended-wait': 'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).',
|
|
'authored/repeat': 'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).',
|
|
'authored/presskey': 'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).',
|
|
'authored/numeric-variable-tap': 'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-doubletap': 'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-swipe': 'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-erase': 'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
|
'authored/numeric-variable-tap-index': 'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.',
|
|
'authored/numeric-variable-wait': 'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.',
|
|
'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.',
|
|
'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).',
|
|
'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.',
|
|
'invalid/duplicate-keys': 'Lenient-guard: duplicate YAML mapping keys.',
|
|
'invalid/unknown-selector-field': 'Lenient-guard: unknown field inside a selector map.',
|
|
'invalid/commands-not-a-list': 'Lenient-guard: command document is not a sequence.',
|
|
};
|
|
|
|
const sha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
|
|
const yamlIn = (dir) =>
|
|
fs.existsSync(path.join(CORPUS_DIR, dir))
|
|
? fs.readdirSync(path.join(CORPUS_DIR, dir)).filter((n) => n.endsWith('.yaml')).sort()
|
|
: [];
|
|
|
|
function entriesFor(dir, kind) {
|
|
return yamlIn(dir).map((name) => {
|
|
const stem = name.slice(0, -'.yaml'.length);
|
|
const id = `${dir}/${stem}`;
|
|
const entry = { id, file: `${dir}/${name}` };
|
|
if (kind === 'upstream') {
|
|
entry.origin = {
|
|
kind: 'upstream',
|
|
repoPath: `${UPSTREAM_REPO_DIR}/${name}`,
|
|
sha256: sha256(path.join(CORPUS_DIR, dir, name)),
|
|
};
|
|
} else {
|
|
entry.origin = { kind: 'authored', note: NOTES[id] ?? '' };
|
|
}
|
|
if (dir === 'bug-classes') entry.bugClass = stem;
|
|
if (id === 'authored/runflow-child') entry.includeTargetOnly = true;
|
|
return entry;
|
|
});
|
|
}
|
|
|
|
export function buildManifest(pin) {
|
|
return {
|
|
description:
|
|
'Corpus of Maestro flows driven through the upstream parser to generate layer-1 fixtures. Upstream flows are vendored verbatim from the pinned commit; sha256 records provenance. Authored flows fill coverage gaps, encode the four #1217 bug classes, and (invalid/) give the never-accept-what-upstream-rejects guard teeth. Generated by build-manifest.mjs — do not hand-edit.',
|
|
upstreamCommit: pin.commit,
|
|
flows: [
|
|
...entriesFor('upstream', 'upstream'),
|
|
...entriesFor('bug-classes', 'authored'),
|
|
...entriesFor('invalid', 'authored'),
|
|
...entriesFor('authored', 'authored'),
|
|
],
|
|
};
|
|
}
|
|
|
|
export function writeManifest(pin) {
|
|
const manifest = buildManifest(pin);
|
|
fs.writeFileSync(
|
|
path.join(CORPUS_DIR, 'manifest.json'),
|
|
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
);
|
|
const missing = manifest.flows.filter((f) => f.origin.kind === 'authored' && !f.origin.note);
|
|
for (const flow of missing) {
|
|
console.warn(`warning: ${flow.id} has no note — add one to NOTES in build-manifest.mjs`);
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const pin = JSON.parse(fs.readFileSync(path.join(HERE, 'pinned-upstream.json'), 'utf8'));
|
|
const manifest = writeManifest(pin);
|
|
console.log(`wrote corpus/manifest.json (${manifest.flows.length} flows)`);
|
|
}
|