@deeeed/metamask-harness 0.38.0 → 0.38.1
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 +6 -0
- package/library/actions/extension/perps/open_balance_action.mjs +1 -6
- package/library/actions/extension/perps/select_market_filter.mjs +3 -2
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +11 -11
- package/package.json +1 -1
- package/scripts/validate-release-report.mjs +28 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.38.1 - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Read release evidence through verified regular-file descriptors, escape generated Markdown safely, and keep UI waits out of dynamically constructed page expressions.
|
|
10
|
+
|
|
5
11
|
## 0.38.0 - 2026-08-14
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -13,12 +13,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
13
13
|
}
|
|
14
14
|
await page.click(dataTestId('perps-balance-dropdown-balance'));
|
|
15
15
|
const actionSelector = dataTestId(actionTestId);
|
|
16
|
-
|
|
17
|
-
while (Date.now() <= deadline) {
|
|
18
|
-
const present = await page.evaluate(`(() => Boolean(document.querySelector(${JSON.stringify(actionSelector)})))()`);
|
|
19
|
-
if (present) break;
|
|
20
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
21
|
-
}
|
|
16
|
+
await page.waitForSelector(actionSelector, { timeoutMs: 10_000 });
|
|
22
17
|
await page.click(actionSelector);
|
|
23
18
|
const expected = balanceAction === 'withdraw'
|
|
24
19
|
? ['perps-withdraw-page', 'perps-withdraw-container', 'perps-withdraw-view']
|
|
@@ -31,8 +31,9 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
31
31
|
});
|
|
32
32
|
await page.click(dataTestId('filter-select-button'));
|
|
33
33
|
const optionSelector = dataTestId(`filter-select-option-${option}`);
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
try {
|
|
35
|
+
await page.waitForSelector(optionSelector, { timeoutMs: 2_000 });
|
|
36
|
+
} catch {
|
|
36
37
|
throw new Error(`Perps market filter ${JSON.stringify(filter)} is not available in this release.`);
|
|
37
38
|
}
|
|
38
39
|
await page.click(optionSelector);
|
|
@@ -11,8 +11,8 @@ const selectors = {
|
|
|
11
11
|
swap: dataTestId('bottom-nav-swaps'),
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
async function pause(
|
|
15
|
-
await
|
|
14
|
+
async function pause(timeoutMs = 200) {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async function activatePoint(page, point) {
|
|
@@ -66,14 +66,14 @@ async function openActivity(page, timeoutMs) {
|
|
|
66
66
|
if (isActivityRoute(state.route) && await visible(page, selectors.swap) && Date.now() - lastResetAt >= 1000) {
|
|
67
67
|
await activateSelector(page, selectors.swap);
|
|
68
68
|
lastResetAt = Date.now();
|
|
69
|
-
await pause(
|
|
69
|
+
await pause();
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
72
|
if (await visible(page, selectors.activity) && Date.now() - lastActivationAt >= 1000) {
|
|
73
73
|
await activateSelector(page, selectors.activity);
|
|
74
74
|
lastActivationAt = Date.now();
|
|
75
75
|
}
|
|
76
|
-
await pause(
|
|
76
|
+
await pause();
|
|
77
77
|
}
|
|
78
78
|
throw new Error('Activity did not open after swap.');
|
|
79
79
|
}
|
|
@@ -107,11 +107,11 @@ async function submittableQuote(page, timeoutMs) {
|
|
|
107
107
|
const quote = await quoteState(page);
|
|
108
108
|
if (/^(?:Get new quote|Refresh quote)$/iu.test(String(quote.cta)) && quote.ctaEnabled) {
|
|
109
109
|
await page.click(selectors.cta);
|
|
110
|
-
await pause(
|
|
110
|
+
await pause(500);
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
if (/^(?:Swap|Bridge)$/iu.test(String(quote.cta)) && quote.ctaEnabled && !quote.loading) return quote;
|
|
114
|
-
await pause(
|
|
114
|
+
await pause();
|
|
115
115
|
}
|
|
116
116
|
throw new Error('Swap did not expose a fresh submittable quote.');
|
|
117
117
|
}
|
|
@@ -186,7 +186,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
186
186
|
|| quote.warning) {
|
|
187
187
|
throw new Error(`Swap quote is not safe to submit: ${JSON.stringify(quote)}.`);
|
|
188
188
|
}
|
|
189
|
-
await pause(
|
|
189
|
+
await pause(2000);
|
|
190
190
|
quote = await submittableQuote(page, Math.min(timeoutMs, 60_000));
|
|
191
191
|
if (!quote.sourceSymbol.toUpperCase().includes(sourceSymbol)
|
|
192
192
|
|| !quote.destinationSymbol.toUpperCase().includes(destinationSymbol)
|
|
@@ -210,14 +210,14 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
210
210
|
while (Date.now() < submissionDeadline) {
|
|
211
211
|
if (await visible(page, selectors.warningProceed)) {
|
|
212
212
|
await page.click(selectors.warningProceed);
|
|
213
|
-
await pause(
|
|
213
|
+
await pause();
|
|
214
214
|
continue;
|
|
215
215
|
}
|
|
216
216
|
if (await visible(page, selectors.confirm)) {
|
|
217
217
|
await page.click(selectors.confirm);
|
|
218
218
|
confirmed = true;
|
|
219
219
|
submitted = true;
|
|
220
|
-
await pause(
|
|
220
|
+
await pause();
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
const state = await bodyState(page);
|
|
@@ -228,7 +228,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
228
228
|
if (submitted && (state.route === '' || state.route === '#/' || isActivityRoute(state.route) || /Processing|Transaction complete|submitted/iu.test(state.text))) {
|
|
229
229
|
break;
|
|
230
230
|
}
|
|
231
|
-
await pause(
|
|
231
|
+
await pause();
|
|
232
232
|
}
|
|
233
233
|
if (!submitted) {
|
|
234
234
|
throw new Error('Swap submission did not reach a signed or submitted state.');
|
|
@@ -259,7 +259,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
259
259
|
if (row?.status === 'confirmed') return row;
|
|
260
260
|
if (Date.now() - lastActivityRefresh >= 5000 && await visible(page, selectors.home)) {
|
|
261
261
|
await page.click(selectors.home);
|
|
262
|
-
await pause(
|
|
262
|
+
await pause(500);
|
|
263
263
|
await openActivity(page, Math.min(10_000, Math.max(1, timeoutMs)));
|
|
264
264
|
lastActivityRefresh = Date.now();
|
|
265
265
|
}
|
package/package.json
CHANGED
|
@@ -106,23 +106,20 @@ for (const entry of verdictCases) {
|
|
|
106
106
|
];
|
|
107
107
|
for (const required of requiredDocuments) {
|
|
108
108
|
const requiredPath = path.join(resolved, required);
|
|
109
|
-
|
|
109
|
+
const requiredDocument = readRegularJsonFile(requiredPath);
|
|
110
|
+
if (requiredDocument.status === 'missing') {
|
|
110
111
|
errors.push(`${entry.id ?? '<missing-id>'} evidence directory is missing ${required}: ${resolved}`);
|
|
111
112
|
continue;
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
-
if (requiredStat.isSymbolicLink() || !requiredStat.isFile()) {
|
|
114
|
+
if (requiredDocument.status === 'unsafe') {
|
|
115
115
|
errors.push(`${entry.id ?? '<missing-id>'} evidence directory ${required} must be a regular file: ${resolved}`);
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
document = JSON.parse(fs.readFileSync(requiredPath, 'utf8'));
|
|
121
|
-
} catch {
|
|
118
|
+
if (requiredDocument.status === 'invalid') {
|
|
122
119
|
errors.push(`${entry.id ?? '<missing-id>'} evidence directory ${required} must contain valid JSON: ${resolved}`);
|
|
123
120
|
continue;
|
|
124
121
|
}
|
|
125
|
-
documents[required] =
|
|
122
|
+
documents[required] = requiredDocument.value;
|
|
126
123
|
}
|
|
127
124
|
if (documents['summary.json'] && entry.status === 'PASS' && documents['summary.json'].status !== 'pass') {
|
|
128
125
|
errors.push(`${entry.id ?? '<missing-id>'} PASS evidence summary.json must have status pass: ${resolved}`);
|
|
@@ -216,6 +213,24 @@ function readJson(file, label) {
|
|
|
216
213
|
}
|
|
217
214
|
}
|
|
218
215
|
|
|
216
|
+
function readRegularJsonFile(file) {
|
|
217
|
+
let descriptor;
|
|
218
|
+
try {
|
|
219
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
220
|
+
if (!fs.fstatSync(descriptor).isFile()) return { status: 'unsafe' };
|
|
221
|
+
const contents = fs.readFileSync(descriptor, 'utf8');
|
|
222
|
+
try {
|
|
223
|
+
return { status: 'ok', value: JSON.parse(contents) };
|
|
224
|
+
} catch {
|
|
225
|
+
return { status: 'invalid' };
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { status: error?.code === 'ENOENT' ? 'missing' : 'unsafe' };
|
|
229
|
+
} finally {
|
|
230
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
function duplicates(values) {
|
|
220
235
|
const seen = new Set();
|
|
221
236
|
const repeated = new Set();
|
|
@@ -259,11 +274,10 @@ function readResolvedRecipes(root, resolution, caseId, validationErrors) {
|
|
|
259
274
|
if (!fs.existsSync(artifactPath)) continue;
|
|
260
275
|
const canonicalArtifact = fs.realpathSync(artifactPath);
|
|
261
276
|
if (!canonicalArtifact.startsWith(`${canonicalRoot}${path.sep}`)) continue;
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch {
|
|
277
|
+
const recipe = readRegularJsonFile(artifactPath);
|
|
278
|
+
if (recipe.status === 'ok') {
|
|
279
|
+
recipes[digest] = recipe.value;
|
|
280
|
+
} else if (recipe.status === 'invalid') {
|
|
267
281
|
validationErrors.push(`${caseId} evidence directory resolved recipe must contain valid JSON: ${artifactPath}`);
|
|
268
282
|
}
|
|
269
283
|
}
|
|
@@ -294,7 +308,7 @@ function renderMarkdown(verdicts, inventoryCases, counts, releaseStatus) {
|
|
|
294
308
|
}
|
|
295
309
|
|
|
296
310
|
function escapeTable(value) {
|
|
297
|
-
return String(value).replace(/\|/gu, '\\|').replace(/\s+/gu, ' ').trim();
|
|
311
|
+
return String(value).replace(/\\/gu, '\\\\').replace(/\|/gu, '\\|').replace(/\s+/gu, ' ').trim();
|
|
298
312
|
}
|
|
299
313
|
|
|
300
314
|
function parseArgs(argv) {
|