@deeeed/metamask-harness 0.58.0 → 0.60.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.
- package/CHANGELOG.md +21 -0
- package/README.md +48 -0
- package/adapters/extension/build-handoff.cjs +4 -0
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/live.sh +26 -25
- package/adapters/extension/pin-remote-flags.cjs +69 -30
- package/adapters/manifest.json +4 -4
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +69 -2
- package/adapters/shared/resolve-slot-ports-core.mjs +47 -19
- package/dist/adapters/extension/feature-flags.js +41 -0
- package/dist/adapters/extension/runtime-decision.js +2 -2
- package/dist/adapters/extension/runtime.js +4 -3
- package/dist/adapters/extension/verify.js +608 -0
- package/dist/adapters/mobile/feature-flags.js +127 -0
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters.js +6 -1
- package/dist/cli.js +1 -1
- package/dist/command-contract.js +6 -3
- package/dist/commands/doctor.js +9 -1
- package/dist/commands/fixtures.js +45 -3
- package/dist/commands/launch/extension.js +28 -6
- package/dist/commands/launch/index.js +128 -17
- package/dist/commands/launch/mobile.js +26 -2
- package/dist/commands/mobile-device-view.js +8 -1
- package/dist/commands/prepare.js +20 -3
- package/dist/commands/run-engine.js +10 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status-probe.js +14 -1
- package/dist/commands/status.js +13 -0
- package/dist/doctor.js +9 -1
- package/dist/feature-flags.js +193 -0
- package/dist/harness.js +3 -15
- package/dist/mm-harness-cli.js +15 -2
- package/library/actions/core/feature_flags/_unsupported.mjs +29 -0
- package/library/actions/core/feature_flags/clear.mjs +8 -0
- package/library/actions/core/feature_flags/read.mjs +8 -0
- package/library/actions/core/feature_flags/set.mjs +8 -0
- package/library/actions/extension/feature_flags/clear.mjs +48 -0
- package/library/actions/extension/feature_flags/read.mjs +27 -0
- package/library/actions/extension/feature_flags/set.mjs +36 -0
- package/library/actions/extension/feature_flags/state.mjs +129 -0
- package/library/actions/harness-exports.mjs +14 -0
- package/library/actions/mobile/feature_flags/clear.mjs +20 -0
- package/library/actions/mobile/feature_flags/controller.mjs +120 -0
- package/library/actions/mobile/feature_flags/read.mjs +20 -0
- package/library/actions/mobile/feature_flags/set.mjs +25 -0
- package/library/actions/mobile/wallet/reset.mjs +22 -1
- package/library/manifests/core.action-manifest.json +90 -0
- package/library/manifests/extension.action-manifest.json +70 -0
- package/library/manifests/mobile.action-manifest.json +90 -0
- package/package.json +1 -1
- package/adapters/extension/verify.sh +0 -572
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# verify.sh — extension slot verification (readiness/proof layer)
|
|
3
|
-
#
|
|
4
|
-
# Purpose:
|
|
5
|
-
# Recipe-tree verification for ONE extension instance: static dist checks,
|
|
6
|
-
# live readiness over CDP, and the smoke recipe run, aggregated into a
|
|
7
|
-
# verify summary.
|
|
8
|
-
#
|
|
9
|
-
# Inputs (flags / env):
|
|
10
|
-
# --target <metamask-extension> (default $PWD), --cdp-port <port>
|
|
11
|
-
# (numeric; empty = static-only), --out <recipes-dir> (validated under
|
|
12
|
-
# target), --static-only, --artifacts-dir <dir>
|
|
13
|
-
# env RECIPE_HARNESS_ROOT, RECIPE_RUNTIME_DIR
|
|
14
|
-
#
|
|
15
|
-
# Outputs:
|
|
16
|
-
# <artifacts>/summary.json + logs/ (readiness, recipe runs).
|
|
17
|
-
# Exit 0 — verify pass; 1 — checks failed / helpers missing; 2 — bad args
|
|
18
|
-
# or unsafe/missing --out.
|
|
19
|
-
#
|
|
20
|
-
# Never touches: product source files; the dist build (read-only);
|
|
21
|
-
# anything outside the harness dir and artifacts dir.
|
|
22
|
-
set -euo pipefail
|
|
23
|
-
|
|
24
|
-
TARGET="$PWD"
|
|
25
|
-
CDP_PORT=""
|
|
26
|
-
ARTIFACTS=""
|
|
27
|
-
STATIC_ONLY=false
|
|
28
|
-
OUT=""
|
|
29
|
-
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
|
|
30
|
-
while [ "$#" -gt 0 ]; do
|
|
31
|
-
case "$1" in
|
|
32
|
-
--target) require_value "$@"; TARGET="$2"; shift 2 ;;
|
|
33
|
-
--out) require_value "$@"; OUT="$2"; shift 2 ;;
|
|
34
|
-
--cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
|
|
35
|
-
--artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
|
|
36
|
-
--static-only) STATIC_ONLY=true; shift ;;
|
|
37
|
-
-h|--help) echo "Usage: verify.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--static-only]"; exit 0 ;;
|
|
38
|
-
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
39
|
-
esac
|
|
40
|
-
done
|
|
41
|
-
|
|
42
|
-
# Reject a non-numeric --cdp-port before it is interpolated into shell/HTTP/CDP
|
|
43
|
-
# strings. Empty is allowed (static-only verify needs no port).
|
|
44
|
-
if [ -n "$CDP_PORT" ]; then
|
|
45
|
-
case "$CDP_PORT" in
|
|
46
|
-
*[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
|
|
47
|
-
esac
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
51
|
-
# shellcheck disable=SC1091
|
|
52
|
-
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
|
|
53
|
-
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
54
|
-
done
|
|
55
|
-
unset _hp
|
|
56
|
-
if ! command -v harness_root >/dev/null 2>&1; then
|
|
57
|
-
echo "extension verify: shared lib adapters/shared/harness-path.sh not found; reinstall the harness." >&2
|
|
58
|
-
exit 1
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# Safe task-local --out resolution.
|
|
62
|
-
resolve_harness_out() {
|
|
63
|
-
local target="$1"
|
|
64
|
-
local out="$2"
|
|
65
|
-
node - "$target" "$out" <<'NODE'
|
|
66
|
-
const path = require('path');
|
|
67
|
-
|
|
68
|
-
const target = path.resolve(process.argv[2]);
|
|
69
|
-
const out = process.argv[3];
|
|
70
|
-
if (!out) process.exit(1);
|
|
71
|
-
if (out.split(/[\\/]+/).includes('..')) process.exit(1);
|
|
72
|
-
|
|
73
|
-
const resolved = path.resolve(target, out);
|
|
74
|
-
const relative = path.relative(target, resolved);
|
|
75
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
process.stdout.write(resolved);
|
|
80
|
-
NODE
|
|
81
|
-
}
|
|
82
|
-
TARGET="$(cd "$TARGET" && pwd)"
|
|
83
|
-
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
84
|
-
HARNESS_ROOT="$(harness_root)"
|
|
85
|
-
HARNESS_REL="$HARNESS_ROOT/extension"
|
|
86
|
-
HARNESS_DIR="$(harness_dir "$TARGET" extension)"
|
|
87
|
-
RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
|
|
88
|
-
# --out (optional): a task-local recipes dir. Resolve it safely within the target
|
|
89
|
-
# (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
|
|
90
|
-
# `live --out <dir>` does not silently fall back to the installed default.
|
|
91
|
-
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/extension/runner/smoke.recipe.json"
|
|
92
|
-
if [ -n "$OUT" ]; then
|
|
93
|
-
# Fail fast on a task-local --out that does not contain the requested recipe.
|
|
94
|
-
# Silently falling back to the installed default would validate a different
|
|
95
|
-
# (possibly stale) recipe than the caller explicitly asked for.
|
|
96
|
-
OUT_ABS="$(resolve_harness_out "$TARGET" "$OUT" 2>/dev/null || true)"
|
|
97
|
-
if [ -z "$OUT_ABS" ]; then
|
|
98
|
-
echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
|
|
99
|
-
exit 2
|
|
100
|
-
fi
|
|
101
|
-
CANONICAL_SMOKE_RECIPE="$OUT_ABS/extension/runner/smoke.recipe.json"
|
|
102
|
-
LEGACY_SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
|
|
103
|
-
if [ -f "$CANONICAL_SMOKE_RECIPE" ] && [ -f "$LEGACY_SMOKE_RECIPE" ]; then
|
|
104
|
-
echo "extension verify: --out '$OUT' declares runner.smoke for extension in both canonical and legacy paths." >&2
|
|
105
|
-
echo "Next: Remove $LEGACY_SMOKE_RECIPE and rerun verification." >&2
|
|
106
|
-
exit 2
|
|
107
|
-
fi
|
|
108
|
-
if [ -f "$CANONICAL_SMOKE_RECIPE" ]; then
|
|
109
|
-
SMOKE_RECIPE="$CANONICAL_SMOKE_RECIPE"
|
|
110
|
-
elif [ -f "$LEGACY_SMOKE_RECIPE" ]; then
|
|
111
|
-
SMOKE_RECIPE="$LEGACY_SMOKE_RECIPE"
|
|
112
|
-
else
|
|
113
|
-
echo "extension verify: --out '$OUT' has no extension runner.smoke recipe." >&2
|
|
114
|
-
echo "Next: Add extension/runner/smoke.recipe.json under $OUT_ABS or omit --out." >&2
|
|
115
|
-
exit 2
|
|
116
|
-
fi
|
|
117
|
-
unset CANONICAL_SMOKE_RECIPE LEGACY_SMOKE_RECIPE
|
|
118
|
-
fi
|
|
119
|
-
ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
|
|
120
|
-
mkdir -p "$ARTIFACTS/logs"
|
|
121
|
-
EXTENSION_ID_FILE="$TARGET/$RUNTIME_DIR/extension.id"
|
|
122
|
-
# Shared JSON reader: $SCRIPT_DIR/lib when running from the installed copy,
|
|
123
|
-
# else the runner canonical at adapters/shared.
|
|
124
|
-
# shellcheck disable=SC1091
|
|
125
|
-
for _lib in "$SCRIPT_DIR/lib/json-field.sh" "$SCRIPT_DIR/../../adapters/shared/json-field.sh"; do
|
|
126
|
-
[ -f "$_lib" ] && { . "$_lib"; break; }
|
|
127
|
-
done
|
|
128
|
-
unset _lib
|
|
129
|
-
# Fail fast with an actionable message if neither path loaded (e.g. an install that
|
|
130
|
-
# predates the lib co-location), instead of a cryptic later `set -e` abort.
|
|
131
|
-
if ! command -v read_runtime_context_field >/dev/null 2>&1; then
|
|
132
|
-
echo "extension verify: json-field helper missing (adapters/shared/json-field.sh). Run mm-harness extension prepare --target $TARGET" >&2
|
|
133
|
-
exit 1
|
|
134
|
-
fi
|
|
135
|
-
# readiness probe is app control: co-located in installed copies, under
|
|
136
|
-
# adapters/extension in a repo checkout.
|
|
137
|
-
READINESS_MJS=""
|
|
138
|
-
for _r in "$SCRIPT_DIR/readiness.mjs" "$SCRIPT_DIR/../../adapters/extension/readiness.mjs"; do
|
|
139
|
-
[ -f "$_r" ] && { READINESS_MJS="$_r"; break; }
|
|
140
|
-
done
|
|
141
|
-
unset _r
|
|
142
|
-
if [ -z "$READINESS_MJS" ]; then
|
|
143
|
-
echo "extension verify: readiness.mjs missing next to $SCRIPT_DIR; reinstall the runner." >&2
|
|
144
|
-
exit 1
|
|
145
|
-
fi
|
|
146
|
-
refresh_extension_id() {
|
|
147
|
-
if [ -f "$EXTENSION_ID_FILE" ]; then
|
|
148
|
-
RECIPE_HARNESS_EXTENSION_ID="$(tr -d '[:space:]' < "$EXTENSION_ID_FILE")"
|
|
149
|
-
export RECIPE_HARNESS_EXTENSION_ID
|
|
150
|
-
else
|
|
151
|
-
unset RECIPE_HARNESS_EXTENSION_ID || true
|
|
152
|
-
fi
|
|
153
|
-
}
|
|
154
|
-
CONTEXT_PATH="${RECIPE_RUNTIME_CONTEXT:-$TARGET/$RUNTIME_DIR/agentic-runtime.json}"
|
|
155
|
-
if [ -f "$CONTEXT_PATH" ]; then
|
|
156
|
-
CONTEXT_EXTENSION_ID="$(read_runtime_context_field "$CONTEXT_PATH" extensionId || true)"
|
|
157
|
-
if [[ "$CONTEXT_EXTENSION_ID" =~ ^[a-p]{32}$ ]]; then
|
|
158
|
-
mkdir -p "$(dirname "$EXTENSION_ID_FILE")"
|
|
159
|
-
printf '%s\n' "$CONTEXT_EXTENSION_ID" > "$EXTENSION_ID_FILE"
|
|
160
|
-
RECIPE_HARNESS_EXTENSION_ID="$CONTEXT_EXTENSION_ID"
|
|
161
|
-
export RECIPE_HARNESS_EXTENSION_ID
|
|
162
|
-
else
|
|
163
|
-
refresh_extension_id
|
|
164
|
-
fi
|
|
165
|
-
unset CONTEXT_EXTENSION_ID
|
|
166
|
-
else
|
|
167
|
-
refresh_extension_id
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
# Infer checkout-scoped CDP port when caller omitted --cdp-port.
|
|
171
|
-
if [ -z "$CDP_PORT" ]; then
|
|
172
|
-
for _rsp in "$SCRIPT_DIR/lib/resolve-slot-ports.sh" "$SCRIPT_DIR/../../adapters/shared/resolve-slot-ports.sh" "$SCRIPT_DIR/../shared/resolve-slot-ports.sh"; do
|
|
173
|
-
[ -f "$_rsp" ] && { # shellcheck disable=SC1090
|
|
174
|
-
. "$_rsp"; break; }
|
|
175
|
-
done
|
|
176
|
-
unset _rsp
|
|
177
|
-
if command -v apply_extension_slot_ports >/dev/null 2>&1; then
|
|
178
|
-
apply_extension_slot_ports "$TARGET"
|
|
179
|
-
fi
|
|
180
|
-
fi
|
|
181
|
-
if [ -n "$CDP_PORT" ]; then
|
|
182
|
-
case "$CDP_PORT" in
|
|
183
|
-
*[!0-9]*) echo "Invalid inferred --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
|
|
184
|
-
esac
|
|
185
|
-
fi
|
|
186
|
-
|
|
187
|
-
status="pass"
|
|
188
|
-
checks=()
|
|
189
|
-
|
|
190
|
-
fixture_status_json() {
|
|
191
|
-
TARGET_FOR_FIXTURE="$TARGET" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node <<'NODE'
|
|
192
|
-
const fs = require('fs');
|
|
193
|
-
const crypto = require('crypto');
|
|
194
|
-
const path = require('path');
|
|
195
|
-
const target = process.env.TARGET_FOR_FIXTURE;
|
|
196
|
-
const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
|
|
197
|
-
if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
|
|
198
|
-
const candidates = [
|
|
199
|
-
runtimeDir + '/wallet-fixture.json',
|
|
200
|
-
'test/e2e/seeder/withFixtures.js',
|
|
201
|
-
'test/e2e/fixtures',
|
|
202
|
-
'fixtures',
|
|
203
|
-
].map((rel) => path.join(target, rel));
|
|
204
|
-
const found = candidates.find((file) => fs.existsSync(file));
|
|
205
|
-
const extensionId = path.join(target, runtimeDir, 'extension.id');
|
|
206
|
-
const profileHints = [];
|
|
207
|
-
if (fs.existsSync(extensionId)) profileHints.push({ path: `${runtimeDir}/extension.id`, type: 'extension-id' });
|
|
208
|
-
if (!found) {
|
|
209
|
-
console.log(JSON.stringify({
|
|
210
|
-
status: 'MISSING_FIXTURES',
|
|
211
|
-
message: 'Fixture status: MISSING_FIXTURES. This run may depend on an inherited browser/profile state. Prefer a prepared debug profile or fixture seed before spending time repairing state manually.',
|
|
212
|
-
profileHints,
|
|
213
|
-
}));
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
|
-
const stat = fs.statSync(found);
|
|
217
|
-
const isFile = stat.isFile();
|
|
218
|
-
let sha256 = null;
|
|
219
|
-
let validJson = null;
|
|
220
|
-
let hasWalletPassword = false;
|
|
221
|
-
let mobileAccountShape = false;
|
|
222
|
-
const rel = path.relative(target, found);
|
|
223
|
-
const isWalletFixture = rel === runtimeDir + '/wallet-fixture.json';
|
|
224
|
-
if (isFile) {
|
|
225
|
-
const bytes = fs.readFileSync(found);
|
|
226
|
-
sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
227
|
-
if (found.endsWith('.json')) {
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(bytes.toString('utf8'));
|
|
230
|
-
validJson = true;
|
|
231
|
-
const accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
|
|
232
|
-
hasWalletPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
|
|
233
|
-
mobileAccountShape = accounts.some((account) => account?.type === 'mnemonic') &&
|
|
234
|
-
accounts.filter((account) => account?.type === 'privateKey').length >= 2;
|
|
235
|
-
} catch {
|
|
236
|
-
validJson = false;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const status = validJson === false
|
|
241
|
-
? 'STALE_OR_INVALID'
|
|
242
|
-
: isWalletFixture && hasWalletPassword
|
|
243
|
-
? 'READY'
|
|
244
|
-
: 'PROFILE_HINTS';
|
|
245
|
-
console.log(JSON.stringify({
|
|
246
|
-
status,
|
|
247
|
-
path: rel,
|
|
248
|
-
type: isFile ? 'file' : 'directory',
|
|
249
|
-
sha256,
|
|
250
|
-
modifiedAt: stat.mtime.toISOString(),
|
|
251
|
-
profileHints,
|
|
252
|
-
hasWalletPassword,
|
|
253
|
-
mobileAccountShape,
|
|
254
|
-
message: validJson === false
|
|
255
|
-
? `Fixture status: STALE_OR_INVALID (${rel}). Fix before relying on a clean sandbox.`
|
|
256
|
-
: status === 'READY'
|
|
257
|
-
? `Fixture status: READY (${rel}).`
|
|
258
|
-
: `Fixture status: PROFILE_HINTS (${rel}); no Mobile-shaped wallet fixture was found for automatic account parity validation.`,
|
|
259
|
-
}));
|
|
260
|
-
NODE
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
cdp_holder_json() {
|
|
264
|
-
local port="$1"
|
|
265
|
-
PORT_FOR_STATUS="$port" node <<'NODE'
|
|
266
|
-
const cp = require('child_process');
|
|
267
|
-
const http = require('http');
|
|
268
|
-
const port = process.env.PORT_FOR_STATUS;
|
|
269
|
-
function run(cmd) {
|
|
270
|
-
try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
|
|
271
|
-
catch { return ''; }
|
|
272
|
-
}
|
|
273
|
-
function getJson(path) {
|
|
274
|
-
return new Promise((resolve) => {
|
|
275
|
-
const req = http.get(`http://127.0.0.1:${port}${path}`, { timeout: 1000 }, (res) => {
|
|
276
|
-
let data = '';
|
|
277
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
278
|
-
res.on('end', () => {
|
|
279
|
-
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
283
|
-
req.on('error', () => resolve(null));
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
(async () => {
|
|
287
|
-
const pid = run(`lsof -iTCP:${port} -sTCP:LISTEN -t | head -1`);
|
|
288
|
-
// Validate pid is numeric before interpolating it into the `ps -p` shell string.
|
|
289
|
-
const safePid = /^[0-9]+$/.test(pid) ? pid : '';
|
|
290
|
-
const command = safePid ? run(`ps -p ${safePid} -o command=`) : '';
|
|
291
|
-
const version = await getJson('/json/version');
|
|
292
|
-
const targets = await getJson('/json/list');
|
|
293
|
-
const extensionTargets = Array.isArray(targets)
|
|
294
|
-
? targets.filter((target) => String(target.url || '').startsWith('chrome-extension://')).length
|
|
295
|
-
: 0;
|
|
296
|
-
console.log(JSON.stringify({
|
|
297
|
-
port,
|
|
298
|
-
listening: Boolean(pid),
|
|
299
|
-
pid: pid || null,
|
|
300
|
-
command: command || null,
|
|
301
|
-
cdpReachable: Boolean(version),
|
|
302
|
-
browser: version?.Browser || null,
|
|
303
|
-
targetCount: Array.isArray(targets) ? targets.length : null,
|
|
304
|
-
extensionTargets,
|
|
305
|
-
}));
|
|
306
|
-
})();
|
|
307
|
-
NODE
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
check_file() {
|
|
311
|
-
local rel="$1"
|
|
312
|
-
if [ -e "$TARGET/$rel" ]; then
|
|
313
|
-
checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
|
|
314
|
-
else
|
|
315
|
-
checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
|
|
316
|
-
status="fail"
|
|
317
|
-
fi
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
check_file "$HARNESS_REL/manifest.json"
|
|
321
|
-
check_file "$HARNESS_REL/action-manifest.json"
|
|
322
|
-
check_file "$HARNESS_REL/runner/bin/mm-harness"
|
|
323
|
-
check_file "$HARNESS_REL/runner/manifests/extension.action-manifest.json"
|
|
324
|
-
|
|
325
|
-
# dist-freshness + build-health now come from the runner's single source of
|
|
326
|
-
# truth (`runtime-decision`), which subsumes the probes this helper used to
|
|
327
|
-
# hand-roll — so the dist-id/source-dirty + webpack-log logic lives in ONE place
|
|
328
|
-
# (matching the runner readiness algorithm) and the three layers cannot disagree.
|
|
329
|
-
# git/fs only (no --cdp-port) so this stays harness-independent. The derived
|
|
330
|
-
# dist-freshness.json / build-health.json keep the same status vocab the case
|
|
331
|
-
# mappings + summary below already consume, so behavior is unchanged.
|
|
332
|
-
"$RUNNER_BIN" runtime-decision --adapter extension --target "$TARGET" --json \
|
|
333
|
-
> "$ARTIFACTS/logs/runtime-decision.json" 2>/dev/null \
|
|
334
|
-
|| printf '%s' '{}' > "$ARTIFACTS/logs/runtime-decision.json"
|
|
335
|
-
# Surface an empty/{} decision loudly: without it, dist-freshness/build-health
|
|
336
|
-
# silently degrade to WARN and the runtime-proof gate is effectively bypassed.
|
|
337
|
-
if [ ! -s "$ARTIFACTS/logs/runtime-decision.json" ] || [ "$(cat "$ARTIFACTS/logs/runtime-decision.json")" = "{}" ]; then
|
|
338
|
-
echo "recipe-harness verify: runtime-decision returned empty/{} — dist-freshness and build-health cannot be derived from the runner and will degrade to WARN (runtime-proof gate NOT fully validated). Check the runner/build state." >&2
|
|
339
|
-
fi
|
|
340
|
-
node -e '
|
|
341
|
-
const fs = require("fs");
|
|
342
|
-
const dir = process.argv[process.argv.length - 1];
|
|
343
|
-
let r = {};
|
|
344
|
-
try { r = JSON.parse(fs.readFileSync(dir + "/runtime-decision.json", "utf8")); } catch {}
|
|
345
|
-
const c = r.checks || {};
|
|
346
|
-
const dist = c.dist || { status: "unknown" };
|
|
347
|
-
const distMsg = dist.source === "release-artifact"
|
|
348
|
-
? ("release artifact " + (dist.manifestVersion || "?") + " sha256=" + (dist.artifactSha256 || "?") + " matches the loaded runtime snapshot.")
|
|
349
|
-
: dist.status === "fresh" ? "dist id matches HEAD; no uncommitted source."
|
|
350
|
-
: dist.status === "stale" ? (dist.reason === "uncommitted-source"
|
|
351
|
-
? ((dist.modified ? dist.modified.length : "some") + " uncommitted source file(s); rebuild or commit.")
|
|
352
|
-
: ("dist id " + (dist.distGitId || "?") + " != HEAD " + (dist.head || "?") + "; rebuild."))
|
|
353
|
-
: dist.status === "no-build" ? "no dist/chrome build."
|
|
354
|
-
: "no git id in dist or not a git checkout; cannot prove parity.";
|
|
355
|
-
fs.writeFileSync(dir + "/dist-freshness.json", JSON.stringify({ ...dist, message: distMsg }));
|
|
356
|
-
const bl = c.releaseArtifact?.status === "valid"
|
|
357
|
-
? { status: "no-watch", source: "release-artifact" }
|
|
358
|
-
: (c.buildLog || { status: "unknown" });
|
|
359
|
-
const blMsg = bl.status === "ok" ? "webpack compiled."
|
|
360
|
-
: bl.status === "no-watch" ? "no webpack watch log; build-health n/a (e.g. one-shot build)."
|
|
361
|
-
: bl.status === "building" ? "webpack has not reported a successful compile yet."
|
|
362
|
-
: bl.status === "errors" ? (bl.reason === "stale-cache"
|
|
363
|
-
? "webpack build failing on a stale cache (ENOENT on a deduped module). Run `mm-harness runtime-launch --adapter extension --target <repo> --cdp-port <port>` to auto-clear the cache and rebuild."
|
|
364
|
-
: "webpack build has errors; fix the source/build before validating.")
|
|
365
|
-
: "build-health unknown.";
|
|
366
|
-
fs.writeFileSync(dir + "/build-health.json", JSON.stringify({ status: bl.status, reason: bl.reason, excerpt: bl.excerpt, message: blMsg }));
|
|
367
|
-
' "$ARTIFACTS/logs" 2>/dev/null \
|
|
368
|
-
|| { printf '%s' '{"status":"unknown","message":"dist-freshness probe error"}' > "$ARTIFACTS/logs/dist-freshness.json"; printf '%s' '{"status":"unknown","message":"build-health probe error"}' > "$ARTIFACTS/logs/build-health.json"; }
|
|
369
|
-
df_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);const v=JSON.parse(fs.readFileSync(file,"utf8"));process.stdout.write(String(v[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/dist-freshness.json" "$1"; }
|
|
370
|
-
df_status="$(df_read status)"
|
|
371
|
-
echo "dist-freshness: ${df_status:-unknown} — $(df_read message)" >&2
|
|
372
|
-
case "$df_status" in
|
|
373
|
-
fresh) checks+=("{\"name\":\"dist-freshness\",\"status\":\"pass\"}") ;;
|
|
374
|
-
stale)
|
|
375
|
-
# A stale dist only fails a LIVE verify (runtime proof would use the wrong
|
|
376
|
-
# build). --static-only checks install/idempotency shape, not runtime, so
|
|
377
|
-
# there it is a warning — don't regress install-only verification.
|
|
378
|
-
if [ "$STATIC_ONLY" = false ]; then
|
|
379
|
-
checks+=("{\"name\":\"dist-freshness\",\"status\":\"fail\",\"detail\":\"see logs/dist-freshness.json\"}"); status="fail"
|
|
380
|
-
else
|
|
381
|
-
checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"stale (static-only); see logs/dist-freshness.json\"}")
|
|
382
|
-
fi
|
|
383
|
-
;;
|
|
384
|
-
*) checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"${df_status:-unknown}; see logs/dist-freshness.json\"}") ;;
|
|
385
|
-
esac
|
|
386
|
-
|
|
387
|
-
# build-health.json was produced by the runtime-decision derivation above (the
|
|
388
|
-
# runner reads the webpack watch log; same status vocab as before).
|
|
389
|
-
bh_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);process.stdout.write(String(JSON.parse(fs.readFileSync(file,"utf8"))[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/build-health.json" "$1"; }
|
|
390
|
-
bh_status="$(bh_read status)"
|
|
391
|
-
echo "build-health: ${bh_status:-unknown} — $(bh_read message)" >&2
|
|
392
|
-
case "$bh_status" in
|
|
393
|
-
ok|no-watch) checks+=("{\"name\":\"build-health\",\"status\":\"pass\"}") ;;
|
|
394
|
-
errors)
|
|
395
|
-
# A broken build only fails a LIVE verify; in --static-only (install-shape only)
|
|
396
|
-
# it is a loud warning so install/idempotency checks aren't regressed.
|
|
397
|
-
if [ "$STATIC_ONLY" = false ]; then
|
|
398
|
-
checks+=("{\"name\":\"build-health\",\"status\":\"fail\",\"detail\":\"see logs/build-health.json\"}"); status="fail"
|
|
399
|
-
else
|
|
400
|
-
checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"build errors (static-only); see logs/build-health.json\"}")
|
|
401
|
-
fi
|
|
402
|
-
;;
|
|
403
|
-
building) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"still compiling; see logs/build-health.json\"}") ;;
|
|
404
|
-
*) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"${bh_status:-unknown}; see logs/build-health.json\"}") ;;
|
|
405
|
-
esac
|
|
406
|
-
|
|
407
|
-
live_mode="static-only"
|
|
408
|
-
if [ "$STATIC_ONLY" = false ]; then
|
|
409
|
-
fixture_status_json > "$ARTIFACTS/logs/fixture-status.json"
|
|
410
|
-
node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.error(v.message || v.status);' "$ARTIFACTS/logs/fixture-status.json"
|
|
411
|
-
checks+=("{\"name\":\"fixture/profile status\",\"status\":\"$(node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.log(v.status === "READY" ? "pass" : "warn");' "$ARTIFACTS/logs/fixture-status.json")\"}")
|
|
412
|
-
|
|
413
|
-
if [ -z "$CDP_PORT" ]; then
|
|
414
|
-
echo "Live extension verify could not infer a checkout CDP port. Run from a prepared slot checkout or pass --cdp-port." > "$ARTIFACTS/logs/live-missing-cdp.log"
|
|
415
|
-
checks+=("{\"name\":\"live runtime CDP port\",\"status\":\"fail\",\"detail\":\"missing CDP port (checkout-scoped inference failed)\"}")
|
|
416
|
-
status="fail"
|
|
417
|
-
live_mode="missing-cdp"
|
|
418
|
-
else
|
|
419
|
-
live_mode="live"
|
|
420
|
-
cdp_holder_json "$CDP_PORT" > "$ARTIFACTS/logs/cdp-holder.json"
|
|
421
|
-
if "$RUNNER_BIN" ensure-ready --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-ensure-ready.json" 2>&1; then
|
|
422
|
-
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"pass\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
423
|
-
refresh_extension_id
|
|
424
|
-
else
|
|
425
|
-
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"fail\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
426
|
-
node - "$ARTIFACTS/logs/extension-ensure-ready.json" <<'NODE' >&2
|
|
427
|
-
const fs = require('fs');
|
|
428
|
-
const file = process.argv[2];
|
|
429
|
-
try {
|
|
430
|
-
const report = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
431
|
-
const code = report.health?.errorCode;
|
|
432
|
-
const findings = Array.isArray(report.health?.findings) ? report.health.findings : [];
|
|
433
|
-
if (code) {
|
|
434
|
-
const finding = findings[0] ?? 'Extension UI is not interactive.';
|
|
435
|
-
console.error(finding.startsWith(`${code}:`) ? finding : `${code}: ${finding}`);
|
|
436
|
-
}
|
|
437
|
-
else if (findings[0]) console.error(findings[0]);
|
|
438
|
-
if (report.health?.userAction) console.error(`Next: ${report.health.userAction}`);
|
|
439
|
-
} catch {
|
|
440
|
-
// The summary retains the raw helper output for diagnosis.
|
|
441
|
-
}
|
|
442
|
-
NODE
|
|
443
|
-
status="fail"
|
|
444
|
-
fi
|
|
445
|
-
|
|
446
|
-
if node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
|
|
447
|
-
checks+=("{\"name\":\"live extension readiness\",\"status\":\"pass\"}")
|
|
448
|
-
# extension-readiness.mjs may repair $RUNTIME_DIR/extension.id when the
|
|
449
|
-
# supplied Chrome profile loads a fresh extension ID. Reload it before
|
|
450
|
-
# running recipe smoke checks so the recipe bridge targets the live
|
|
451
|
-
# extension instead of a stale marker from a previous browser profile.
|
|
452
|
-
refresh_extension_id
|
|
453
|
-
else
|
|
454
|
-
checks+=("{\"name\":\"live extension readiness\",\"status\":\"fail\",\"detail\":\"see logs/extension-readiness.json\"}")
|
|
455
|
-
status="fail"
|
|
456
|
-
fi
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
cd "$TARGET"
|
|
460
|
-
"$RUNNER_BIN" actions --raw --adapter extension
|
|
461
|
-
) > "$ARTIFACTS/logs/runner-manifest.json" 2> "$ARTIFACTS/logs/runner-manifest.err"; then
|
|
462
|
-
checks+=("{\"name\":\"runner manifest\",\"status\":\"pass\"}")
|
|
463
|
-
else
|
|
464
|
-
checks+=("{\"name\":\"runner manifest\",\"status\":\"fail\",\"detail\":\"see logs/runner-manifest.err\"}")
|
|
465
|
-
status="fail"
|
|
466
|
-
fi
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
cd "$TARGET"
|
|
470
|
-
"$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
|
|
471
|
-
) > "$ARTIFACTS/logs/runner-smoke.log" 2>&1; then
|
|
472
|
-
checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"pass\"}")
|
|
473
|
-
else
|
|
474
|
-
checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-smoke.log\"}")
|
|
475
|
-
status="fail"
|
|
476
|
-
fi
|
|
477
|
-
fi
|
|
478
|
-
fi
|
|
479
|
-
|
|
480
|
-
if git -C "$TARGET" rev-parse --git-dir >/dev/null 2>&1; then
|
|
481
|
-
git -C "$TARGET" status --short -- . ":(exclude)$HARNESS_ROOT" > "$ARTIFACTS/logs/product-diff-excluding-harness.log" 2>&1 || true
|
|
482
|
-
fi
|
|
483
|
-
|
|
484
|
-
RECIPE_HARNESS_LIVE_MODE="$live_mode" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
|
|
485
|
-
const fs = require('fs');
|
|
486
|
-
const path = require('path');
|
|
487
|
-
const cp = require('child_process');
|
|
488
|
-
const [artifacts, target, status, ...checks] = process.argv.slice(2);
|
|
489
|
-
const parsedChecks = checks.map((entry) => JSON.parse(entry));
|
|
490
|
-
const liveMode = process.env.RECIPE_HARNESS_LIVE_MODE || 'unknown';
|
|
491
|
-
let fixtureStatus = null;
|
|
492
|
-
let cdpHolder = null;
|
|
493
|
-
let readinessReport = null;
|
|
494
|
-
let distFreshness = null;
|
|
495
|
-
try { distFreshness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/dist-freshness.json'), 'utf8')); } catch {}
|
|
496
|
-
try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
|
|
497
|
-
try { cdpHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/cdp-holder.json'), 'utf8')); } catch {}
|
|
498
|
-
try { readinessReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
|
|
499
|
-
let ensureReadyReport = null;
|
|
500
|
-
try { ensureReadyReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-ensure-ready.json'), 'utf8')); } catch {}
|
|
501
|
-
function runGit(args) {
|
|
502
|
-
try {
|
|
503
|
-
return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
504
|
-
} catch (error) {
|
|
505
|
-
// Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
|
|
510
|
-
if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
|
|
511
|
-
const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
|
|
512
|
-
const gitStatus = {
|
|
513
|
-
branch: runGit(['branch', '--show-current']),
|
|
514
|
-
head: runGit(['rev-parse', '--short', 'HEAD']),
|
|
515
|
-
dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
|
|
516
|
-
dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
|
|
517
|
-
};
|
|
518
|
-
const readiness = parsedChecks.find((check) => check.name === 'live extension readiness');
|
|
519
|
-
const extensionIdPath = path.join(target, process.env.RECIPE_RUNTIME_DIR, 'extension.id');
|
|
520
|
-
let markerExtensionId = null;
|
|
521
|
-
try {
|
|
522
|
-
const value = fs.readFileSync(extensionIdPath, 'utf8').trim();
|
|
523
|
-
if (/^[a-p]{32}$/.test(value)) markerExtensionId = value;
|
|
524
|
-
} catch {}
|
|
525
|
-
const cdpTarget = readinessReport?.cdp ? {
|
|
526
|
-
selectedExtensionId: readinessReport.cdp.selectedExtensionId || null,
|
|
527
|
-
markerExtensionId,
|
|
528
|
-
markerMatched: readinessReport.cdp.markerMatched ?? null,
|
|
529
|
-
markerRepaired: readinessReport.cdp.markerRepaired ?? null,
|
|
530
|
-
extensionIds: readinessReport.cdp.extensionIds || [],
|
|
531
|
-
targetCount: readinessReport.cdp.targetCount ?? null,
|
|
532
|
-
browser: readinessReport.cdp.browser || cdpHolder?.browser || null,
|
|
533
|
-
} : null;
|
|
534
|
-
const runtimeOwner = liveMode === 'static-only'
|
|
535
|
-
? 'static-only'
|
|
536
|
-
: liveMode === 'missing-cdp'
|
|
537
|
-
? 'none'
|
|
538
|
-
: cdpHolder?.cdpReachable
|
|
539
|
-
? (readiness?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
|
|
540
|
-
: 'none';
|
|
541
|
-
fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
542
|
-
adapter: 'extension',
|
|
543
|
-
status,
|
|
544
|
-
liveMode,
|
|
545
|
-
runtimeClassification: {
|
|
546
|
-
runtimeOwner,
|
|
547
|
-
recipeControllable: readiness?.status === 'pass',
|
|
548
|
-
startedByVerify: false,
|
|
549
|
-
},
|
|
550
|
-
cleanupOwnership: {
|
|
551
|
-
mayStop: false,
|
|
552
|
-
reason: 'extension verify inspects the supplied CDP runtime; wrapper/preflight ownership must be recorded by the caller before stopping processes',
|
|
553
|
-
},
|
|
554
|
-
gitStatus,
|
|
555
|
-
distFreshness,
|
|
556
|
-
runtimePolicy: {
|
|
557
|
-
runtimeReusePolicy: 'reuse a running harness-compatible CDP target when possible; wrapper auto-start must use a cached/watch-only prepare path unless the human explicitly permits a rebuild',
|
|
558
|
-
},
|
|
559
|
-
fixtureStatus,
|
|
560
|
-
cdpHolder,
|
|
561
|
-
ensureReady: ensureReadyReport,
|
|
562
|
-
cdpTarget,
|
|
563
|
-
checks: parsedChecks,
|
|
564
|
-
generatedAt: new Date().toISOString(),
|
|
565
|
-
}, null, 2)}\n`);
|
|
566
|
-
fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
|
|
567
|
-
artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
|
|
568
|
-
}, null, 2)}\n`);
|
|
569
|
-
NODE
|
|
570
|
-
|
|
571
|
-
echo "Extension harness verify $status: $ARTIFACTS/summary.json"
|
|
572
|
-
[ "$status" = "pass" ]
|