Merge pull request 'fix: lint: Ban waitForTimeout, setTimeout-as-delay, and fixed sleep patterns (#442)' (#443) from fix/issue-442 into master

This commit is contained in:
johba 2026-03-03 23:37:46 +01:00
commit b2594a28b3
25 changed files with 140 additions and 3 deletions

4
.lintstagedrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"tests/**/*.ts": ["eslint"],
"scripts/**/*.ts": ["eslint"]
}

58
eslint.config.js Normal file
View file

@ -0,0 +1,58 @@
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
export default [
{
name: 'tests/files-to-lint',
files: ['tests/**/*.ts', 'scripts/harb-evaluator/**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
process: 'readonly',
console: 'readonly',
fetch: 'readonly',
setTimeout: 'readonly',
Date: 'readonly',
Promise: 'readonly',
},
},
plugins: {
'@typescript-eslint': tseslint,
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
{
name: 'arch/no-fixed-delays',
files: ['tests/**/*.ts', 'scripts/harb-evaluator/**/*.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='waitForTimeout']",
message:
'[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.',
},
{
selector:
"NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']",
message:
'[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.',
},
],
},
},
];

View file

@ -60,6 +60,17 @@ export default [
message: message:
'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.', 'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.',
}, },
{
selector: "CallExpression[callee.property.name='waitForTimeout']",
message:
'[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.',
},
{
selector:
"NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']",
message:
'[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.',
},
], ],
}, },
}, },

View file

@ -2,13 +2,18 @@
"name": "harb", "name": "harb",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.55.1", "@playwright/test": "^1.55.1",
"playwright-mcp": "^0.0.12", "@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"ethers": "^6.11.1", "ethers": "^6.11.1",
"husky": "^9.0.11" "husky": "^9.0.11",
"playwright-mcp": "^0.0.12"
}, },
"scripts": { "scripts": {
"prepare": "husky", "prepare": "husky",
"test:e2e": "playwright test" "test:e2e": "playwright test",
"lint:tests": "eslint tests/ scripts/harb-evaluator/"
}, },
"type": "module", "type": "module",
"workspaces": [ "workspaces": [

View file

@ -53,6 +53,7 @@ async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20):
} }
return; // status === '0x1' — success return; // status === '0x1' — success
} }
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no event source for transaction receipt over HTTP RPC (eth_subscribe not available). See AGENTS.md #Engineering Principles.
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));
} }
throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`); throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`);

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for Vue component animation settling and wagmi wallet connector state transitions. See AGENTS.md #Engineering Principles. */
/** /**
* Shared wallet helpers for holdout scenarios. * Shared wallet helpers for holdout scenarios.
* *

View file

@ -82,6 +82,17 @@ export default [
message: message:
"Ponder findMany() called without a limit parameter. Unbounded queries will grow without limit as the chain is indexed — never use findMany() without a limit. Always specify `limit` in the options object. Use the ring buffer pattern from docs/ARCHITECTURE.md.", "Ponder findMany() called without a limit parameter. Unbounded queries will grow without limit as the chain is indexed — never use findMany() without a limit. Always specify `limit` in the options object. Use the ring buffer pattern from docs/ARCHITECTURE.md.",
}, },
{
selector: "CallExpression[callee.property.name='waitForTimeout']",
message:
'[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.',
},
{
selector:
"NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']",
message:
'[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.',
},
], ],
}, },
}, },

View file

@ -62,5 +62,24 @@ export default [
'max-statements': 'off', 'max-statements': 'off',
}, },
}, },
{
name: 'arch/no-fixed-delays',
rules: {
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='waitForTimeout']",
message:
'[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.',
},
{
selector:
"NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']",
message:
'[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.',
},
],
},
},
eslintConfigPrettier, eslintConfigPrettier,
]; ];

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles. */
import { expect, test, type APIRequestContext } from '@playwright/test'; import { expect, test, type APIRequestContext } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../setup/wallet-provider'; import { createWalletContext } from '../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles. */
import { expect, test, type APIRequestContext } from '@playwright/test'; import { expect, test, type APIRequestContext } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../setup/wallet-provider'; import { createWalletContext } from '../setup/wallet-provider';

View file

@ -62,6 +62,7 @@ test.describe('GraphQL URL Verification', () => {
// Give more time for Vue components to mount and composables to initialize // Give more time for Vue components to mount and composables to initialize
console.log('[TEST] Waiting for composables to initialize...'); console.log('[TEST] Waiting for composables to initialize...');
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source for Ponder/chain indexing delay in CI cold-start. See AGENTS.md #Engineering Principles.
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
// Log findings // Log findings

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout/Promise+setTimeout: no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout is used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles. */
import { test, expect, type APIRequestContext } from '@playwright/test'; import { test, expect, type APIRequestContext } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../setup/wallet-provider'; import { createWalletContext } from '../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { test } from '@playwright/test'; import { test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -246,6 +246,7 @@ async function waitForReceipt(rpcUrl: string, txHash: string, timeoutMs = 15000)
}); });
const data = await resp.json(); const data = await resp.json();
if (data.result) return data.result; if (data.result) return data.result;
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no event source for transaction receipt over HTTP RPC (eth_subscribe not available). See AGENTS.md #Engineering Principles.
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));
} }
throw new Error(`Transaction ${txHash} not mined within ${timeoutMs}ms`); throw new Error(`Transaction ${txHash} not mined within ${timeoutMs}ms`);

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
/** /**
* Test B: Comprehensive Staker Journey (v2) * Test B: Comprehensive Staker Journey (v2)
* *

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -277,6 +277,7 @@ for (const persona of personas) {
// Navigate to variant // Navigate to variant
await page.goto(variant.url); await page.goto(variant.url);
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('domcontentloaded');
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source for CSS animation completion. See AGENTS.md #Engineering Principles.
await page.waitForTimeout(1000); // Let animations settle await page.waitForTimeout(1000); // Let animations settle
// Take screenshot // Take screenshot

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers'; import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider'; import { createWalletContext } from '../../setup/wallet-provider';

View file

@ -92,6 +92,17 @@ export default [
message: message:
'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.', 'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.',
}, },
{
selector: "CallExpression[callee.property.name='waitForTimeout']",
message:
'[BANNED] waitForTimeout is a fixed delay. → Subscribe to events instead (eth_newFilter for on-chain, waitForSelector/waitForURL for DOM). → Polling with timeout is acceptable only if no event source exists. → See AGENTS.md #Engineering Principles.',
},
{
selector:
"NewExpression[callee.name='Promise'] > ArrowFunctionExpression CallExpression[callee.name='setTimeout']",
message:
'[BANNED] Promise+setTimeout sleep pattern. → Use event subscription or polling with timeout instead. → See AGENTS.md #Engineering Principles.',
},
], ],
}, },
}, },

View file

@ -213,6 +213,7 @@ async function changeTax(id: bigint, nextTaxRateIndex: number | null) {
async function unstakePosition() { async function unstakePosition() {
await unstake.exitPosition(props.id); await unstake.exitPosition(props.id);
loading.value = true; loading.value = true;
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no push event exists for Ponder indexing completion; Ponder GraphQL has no subscription endpoint. See AGENTS.md #Engineering Principles.
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
await loadPositions(currentChainId.value); await loadPositions(currentChainId.value);
loading.value = false; loading.value = false;