diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..0f569d8 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "tests/**/*.ts": ["eslint"], + "scripts/**/*.ts": ["eslint"] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..f925329 --- /dev/null +++ b/eslint.config.js @@ -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.', + }, + ], + }, + }, +]; diff --git a/landing/eslint.config.js b/landing/eslint.config.js index 83f4fc0..b8c0602 100644 --- a/landing/eslint.config.js +++ b/landing/eslint.config.js @@ -60,6 +60,17 @@ export default [ 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.', }, + { + 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.', + }, ], }, }, diff --git a/package.json b/package.json index 34e605e..d60f11e 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,18 @@ "name": "harb", "devDependencies": { "@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", - "husky": "^9.0.11" + "husky": "^9.0.11", + "playwright-mcp": "^0.0.12" }, "scripts": { "prepare": "husky", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "lint:tests": "eslint tests/ scripts/harb-evaluator/" }, "type": "module", "workspaces": [ diff --git a/scripts/harb-evaluator/helpers/swap.ts b/scripts/harb-evaluator/helpers/swap.ts index 86e27a8..1a0e27b 100644 --- a/scripts/harb-evaluator/helpers/swap.ts +++ b/scripts/harb-evaluator/helpers/swap.ts @@ -50,6 +50,7 @@ async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): } 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)); } throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`); diff --git a/scripts/harb-evaluator/helpers/wallet.ts b/scripts/harb-evaluator/helpers/wallet.ts index ddd1e9d..7b24730 100644 --- a/scripts/harb-evaluator/helpers/wallet.ts +++ b/scripts/harb-evaluator/helpers/wallet.ts @@ -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. * diff --git a/services/ponder/eslint.config.js b/services/ponder/eslint.config.js index 8b6deed..77fb894 100644 --- a/services/ponder/eslint.config.js +++ b/services/ponder/eslint.config.js @@ -82,6 +82,17 @@ export default [ 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.", }, + { + 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.', + }, ], }, }, diff --git a/services/txnBot/eslint.config.js b/services/txnBot/eslint.config.js index 455f8f4..76f6112 100644 --- a/services/txnBot/eslint.config.js +++ b/services/txnBot/eslint.config.js @@ -62,5 +62,24 @@ export default [ '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, ]; diff --git a/tests/e2e/01-acquire-and-stake.spec.ts b/tests/e2e/01-acquire-and-stake.spec.ts index 9ed1643..d074131 100644 --- a/tests/e2e/01-acquire-and-stake.spec.ts +++ b/tests/e2e/01-acquire-and-stake.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; diff --git a/tests/e2e/02-max-stake-all-tax-rates.spec.ts b/tests/e2e/02-max-stake-all-tax-rates.spec.ts index 9848b31..b142ead 100644 --- a/tests/e2e/02-max-stake-all-tax-rates.spec.ts +++ b/tests/e2e/02-max-stake-all-tax-rates.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; diff --git a/tests/e2e/03-verify-graphql-url.spec.ts b/tests/e2e/03-verify-graphql-url.spec.ts index 50b9a4e..90ab18a 100644 --- a/tests/e2e/03-verify-graphql-url.spec.ts +++ b/tests/e2e/03-verify-graphql-url.spec.ts @@ -62,6 +62,7 @@ test.describe('GraphQL URL Verification', () => { // Give more time for Vue components to mount and 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); // Log findings diff --git a/tests/e2e/06-dashboard-pages.spec.ts b/tests/e2e/06-dashboard-pages.spec.ts index ab47ba6..e4491f1 100644 --- a/tests/e2e/06-dashboard-pages.spec.ts +++ b/tests/e2e/06-dashboard-pages.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; diff --git a/tests/e2e/usertest/alex-newcomer.spec.ts b/tests/e2e/usertest/alex-newcomer.spec.ts index c2b71d6..d0f3333 100644 --- a/tests/e2e/usertest/alex-newcomer.spec.ts +++ b/tests/e2e/usertest/alex-newcomer.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/all-personas.spec.ts b/tests/e2e/usertest/all-personas.spec.ts index d3e8771..3e1bdfb 100644 --- a/tests/e2e/usertest/all-personas.spec.ts +++ b/tests/e2e/usertest/all-personas.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/helpers.ts b/tests/e2e/usertest/helpers.ts index ef4e4f2..4d152b1 100644 --- a/tests/e2e/usertest/helpers.ts +++ b/tests/e2e/usertest/helpers.ts @@ -246,6 +246,7 @@ async function waitForReceipt(rpcUrl: string, txHash: string, timeoutMs = 15000) }); const data = await resp.json(); 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)); } throw new Error(`Transaction ${txHash} not mined within ${timeoutMs}ms`); diff --git a/tests/e2e/usertest/marcus-degen.spec.ts b/tests/e2e/usertest/marcus-degen.spec.ts index 5aab791..f7e8c69 100644 --- a/tests/e2e/usertest/marcus-degen.spec.ts +++ b/tests/e2e/usertest/marcus-degen.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/priya-institutional.spec.ts b/tests/e2e/usertest/priya-institutional.spec.ts index 4564b5d..0d540e1 100644 --- a/tests/e2e/usertest/priya-institutional.spec.ts +++ b/tests/e2e/usertest/priya-institutional.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/sarah-yield-farmer.spec.ts b/tests/e2e/usertest/sarah-yield-farmer.spec.ts index 60354f3..166f513 100644 --- a/tests/e2e/usertest/sarah-yield-farmer.spec.ts +++ b/tests/e2e/usertest/sarah-yield-farmer.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/test-a-passive-holder.spec.ts b/tests/e2e/usertest/test-a-passive-holder.spec.ts index c9efb08..48341bc 100644 --- a/tests/e2e/usertest/test-a-passive-holder.spec.ts +++ b/tests/e2e/usertest/test-a-passive-holder.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/test-b-staker-v2.spec.ts b/tests/e2e/usertest/test-b-staker-v2.spec.ts index e3223a9..9422c25 100644 --- a/tests/e2e/usertest/test-b-staker-v2.spec.ts +++ b/tests/e2e/usertest/test-b-staker-v2.spec.ts @@ -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) * diff --git a/tests/e2e/usertest/test-b-staker.spec.ts b/tests/e2e/usertest/test-b-staker.spec.ts index 23984c4..d2200de 100644 --- a/tests/e2e/usertest/test-b-staker.spec.ts +++ b/tests/e2e/usertest/test-b-staker.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/tests/e2e/usertest/test-landing-variants.spec.ts b/tests/e2e/usertest/test-landing-variants.spec.ts index 4cd8812..05ecbcc 100644 --- a/tests/e2e/usertest/test-landing-variants.spec.ts +++ b/tests/e2e/usertest/test-landing-variants.spec.ts @@ -277,6 +277,7 @@ for (const persona of personas) { // Navigate to variant await page.goto(variant.url); 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 // Take screenshot diff --git a/tests/e2e/usertest/tyler-retail-degen.spec.ts b/tests/e2e/usertest/tyler-retail-degen.spec.ts index e961c02..50ccb30 100644 --- a/tests/e2e/usertest/tyler-retail-degen.spec.ts +++ b/tests/e2e/usertest/tyler-retail-degen.spec.ts @@ -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 { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; diff --git a/web-app/eslint.config.js b/web-app/eslint.config.js index 9cf877b..2993c17 100644 --- a/web-app/eslint.config.js +++ b/web-app/eslint.config.js @@ -92,6 +92,17 @@ export default [ 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.', }, + { + 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.', + }, ], }, },