@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 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
- const deadline = Date.now() + 10_000;
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
- const found = await page.evaluate(`(() => Boolean(document.querySelector(${JSON.stringify(optionSelector)})))()`);
35
- if (!found) {
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(page, timeoutMs = 200) {
15
- await page.evaluate(`new Promise((resolve) => setTimeout(resolve, ${JSON.stringify(timeoutMs)}))`);
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(page);
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(page);
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(page, 500);
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(page);
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(page, 2000);
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(page);
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(page);
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(page);
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(page, 500);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -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
- if (!fs.existsSync(requiredPath)) {
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
- const requiredStat = fs.lstatSync(requiredPath);
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
- let document;
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] = document;
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 stat = fs.lstatSync(artifactPath);
263
- if (stat.isSymbolicLink() || !stat.isFile()) continue;
264
- try {
265
- recipes[digest] = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
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) {