From 956878ff3f51b57d17031ac25c7896bc8f082b0f Mon Sep 17 00:00:00 2001 From: kastale Date: Fri, 27 Feb 2026 16:17:26 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20unify=20skill=20baselin?= =?UTF-8?q?e=20for=20openclaw=20coverage=20docs=20and=20codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/coverage-badge.yml | 34 +++ .github/workflows/publish.yml | 4 +- .github/workflows/test.yml | 7 +- README.md | 17 ++ README.zh-CN.md | 17 ++ SKILL.md | 32 ++ bin/generate-openclaw.ts | 5 + openclaw.json | 427 +++++++++++++++++++++++---- package.json | 4 +- scripts/coverage-gate.ts | 109 +++++++ tests/unit/core-api-coverage.test.ts | 239 +++++++++++++++ 11 files changed, 826 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/coverage-badge.yml create mode 100644 SKILL.md create mode 100644 scripts/coverage-gate.ts create mode 100644 tests/unit/core-api-coverage.test.ts diff --git a/.github/workflows/coverage-badge.yml b/.github/workflows/coverage-badge.yml new file mode 100644 index 0000000..d19cca7 --- /dev/null +++ b/.github/workflows/coverage-badge.yml @@ -0,0 +1,34 @@ +name: Coverage Badge + +on: + push: + branches: + - main + - master + - 'codex/**' + workflow_dispatch: + +permissions: + contents: write + +jobs: + coverage-badge: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - run: bun install --frozen-lockfile + - run: bun run test:coverage:ci + - run: bun run coverage:badge + - run: touch coverage/.nojekyll + + - name: Deploy coverage badge to gh-pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./coverage + publish_branch: gh-pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a30a8d..b4112fe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,8 +26,8 @@ jobs: - name: Type check run: bunx tsc --noEmit - - name: Run tests - run: bun run test + - name: Coverage gate + run: bun run test:coverage:ci - name: Verify generated openclaw config run: bun run build:openclaw:check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3f7f73..4546741 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,11 +29,8 @@ jobs: - name: Type check run: bunx tsc --noEmit - - name: Run tests - run: bun run test - - - name: Unit coverage - run: bun run test:unit:coverage + - name: Coverage gate + run: bun run test:coverage:ci - name: Verify generated openclaw config run: bun run build:openclaw:check diff --git a/README.md b/README.md index 26cb899..19ccdf5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # AelfScan Skill Kit +[English](./README.md) | [中文](./README.zh-CN.md) + +[![Unit Tests](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml/badge.svg)](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml) +[![Coverage](https://img.shields.io/endpoint?url=https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)](https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json) + AelfScan explorer skill toolkit for AI agents, with **SDK + MCP + CLI + OpenClaw** interfaces. ## Features @@ -51,6 +56,13 @@ cp .env.example .env bun run mcp ``` +### OpenClaw + +```bash +bun run build:openclaw +bun run build:openclaw:check +``` + ### Run CLI ```bash @@ -111,3 +123,8 @@ RUN_LIVE_TESTS=1 bun run test:e2e ## License MIT + +## Security + +- Keep API tokens and private keys in env/config only. +- Never leak secret values in tool outputs. diff --git a/README.zh-CN.md b/README.zh-CN.md index 62d1c07..2ce4090 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,10 @@ # AelfScan Skill Kit +[中文](./README.zh-CN.md) | [English](./README.md) + +[![Unit Tests](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml/badge.svg)](https://github.com/AelfScanProject/aelfscan-skill/actions/workflows/test.yml) +[![Coverage](https://img.shields.io/endpoint?url=https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json)](https://AelfScanProject.github.io/aelfscan-skill/coverage-badge.json) + 面向 AI Agent 的 AelfScan 浏览器能力工具包,提供 **SDK + MCP + CLI + OpenClaw** 四种使用方式。 ## 功能覆盖 @@ -51,6 +56,13 @@ cp .env.example .env bun run mcp ``` +### OpenClaw + +```bash +bun run build:openclaw +bun run build:openclaw:check +``` + ### CLI 示例 ```bash @@ -111,3 +123,8 @@ RUN_LIVE_TESTS=1 bun run test:e2e ## License MIT + +## 安全 + +- API Token 和私钥仅通过环境变量或配置注入。 +- 工具输出中禁止泄露敏感字段。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..d1cd6c1 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,32 @@ +--- +name: "aelfscan-skill" +description: "AelfScan explorer data retrieval and analytics skill for agents." +--- + +# AelfScan Skill + +## When to use +- Use this skill when you need AelfScan explorer search and analytics data retrieval tasks. + +## Capabilities +- Domain coverage: search, blockchain, address, token, NFT, statistics +- Single tool descriptor source for SDK/CLI/MCP/OpenClaw +- MCP output governance controls and standardized trace-aware errors +- Supports SDK, CLI, MCP, and OpenClaw integration from one codebase. + +## Safe usage rules +- Never print private keys, mnemonics, or tokens in channel outputs. +- For write operations, require explicit user confirmation and validate parameters before sending transactions. +- Prefer `simulate` or read-only queries first when available. + +## Command recipes +- Start MCP server: `bun run mcp` +- Run CLI entry: `bun run cli` +- Generate OpenClaw config: `bun run build:openclaw` +- Verify OpenClaw config: `bun run build:openclaw:check` +- Run CI coverage gate: `bun run test:coverage:ci` + +## Limits / Non-goals +- This skill focuses on domain operations and adapters; it is not a full wallet custody system. +- Do not hardcode environment secrets in source code or docs. +- Avoid bypassing validation for external service calls. diff --git a/bin/generate-openclaw.ts b/bin/generate-openclaw.ts index f274527..0df2995 100755 --- a/bin/generate-openclaw.ts +++ b/bin/generate-openclaw.ts @@ -15,6 +15,11 @@ const openclaw = { command: 'bun', args: ['run', 'aelfscan_skill.ts', descriptor.domain, descriptor.action], cwd: '.', + inputSchema: { + type: 'object', + properties: {}, + additionalProperties: true, + }, })), }; diff --git a/openclaw.json b/openclaw.json index d4d62d8..4099215 100644 --- a/openclaw.json +++ b/openclaw.json @@ -12,7 +12,12 @@ "search", "filters" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_search", @@ -24,7 +29,12 @@ "search", "query" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_blocks", @@ -36,7 +46,12 @@ "blockchain", "blocks" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_blocks_latest", @@ -48,7 +63,12 @@ "blockchain", "blocks-latest" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_block_detail", @@ -60,7 +80,12 @@ "blockchain", "block-detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_transactions", @@ -72,7 +97,12 @@ "blockchain", "transactions" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_transactions_latest", @@ -84,7 +114,12 @@ "blockchain", "transactions-latest" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_transaction_detail", @@ -96,7 +131,12 @@ "blockchain", "transaction-detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_blockchain_overview", @@ -108,7 +148,12 @@ "blockchain", "overview" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_transaction_data_chart", @@ -120,7 +165,12 @@ "blockchain", "transaction-data-chart" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_address_dictionary", @@ -132,7 +182,12 @@ "blockchain", "address-dictionary" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_log_events", @@ -144,7 +199,12 @@ "blockchain", "log-events" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_accounts", @@ -156,7 +216,12 @@ "address", "accounts" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_contracts", @@ -168,7 +233,12 @@ "address", "contracts" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_address_detail", @@ -180,7 +250,12 @@ "address", "detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_address_tokens", @@ -192,7 +267,12 @@ "address", "tokens" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_address_nft_assets", @@ -204,7 +284,12 @@ "address", "nft-assets" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_address_transfers", @@ -216,7 +301,12 @@ "address", "transfers" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_contract_history", @@ -228,7 +318,12 @@ "address", "contract-history" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_contract_events", @@ -240,7 +335,12 @@ "address", "contract-events" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_contract_source", @@ -252,7 +352,12 @@ "address", "contract-source" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_tokens", @@ -264,7 +369,12 @@ "token", "list" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_token_detail", @@ -276,7 +386,12 @@ "token", "detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_token_transfers", @@ -288,7 +403,12 @@ "token", "transfers" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_token_holders", @@ -300,7 +420,12 @@ "token", "holders" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_collections", @@ -312,7 +437,12 @@ "nft", "collections" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_collection_detail", @@ -324,7 +454,12 @@ "nft", "collection-detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_transfers", @@ -336,7 +471,12 @@ "nft", "transfers" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_holders", @@ -348,7 +488,12 @@ "nft", "holders" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_inventory", @@ -360,7 +505,12 @@ "nft", "inventory" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_item_detail", @@ -372,7 +522,12 @@ "nft", "item-detail" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_item_holders", @@ -384,7 +539,12 @@ "nft", "item-holders" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_nft_item_activity", @@ -396,7 +556,12 @@ "nft", "item-activity" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics", @@ -408,7 +573,12 @@ "statistics", "metric" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_transactions", @@ -420,7 +590,12 @@ "statistics", "daily-transactions" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_unique_addresses", @@ -432,7 +607,12 @@ "statistics", "unique-addresses" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_active_addresses", @@ -444,7 +624,12 @@ "statistics", "daily-active-addresses" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_monthly_active_addresses", @@ -456,7 +641,12 @@ "statistics", "monthly-active-addresses" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_block_produce_rate", @@ -468,7 +658,12 @@ "statistics", "block-produce-rate" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_avg_block_duration", @@ -480,7 +675,12 @@ "statistics", "avg-block-duration" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_cycle_count", @@ -492,7 +692,12 @@ "statistics", "cycle-count" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_node_block_produce", @@ -504,7 +709,12 @@ "statistics", "node-block-produce" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_avg_transaction_fee", @@ -516,7 +726,12 @@ "statistics", "daily-avg-transaction-fee" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_tx_fee", @@ -528,7 +743,12 @@ "statistics", "daily-tx-fee" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_total_burnt", @@ -540,7 +760,12 @@ "statistics", "daily-total-burnt" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_elf_price", @@ -552,7 +777,12 @@ "statistics", "daily-elf-price" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_deploy_contract", @@ -564,7 +794,12 @@ "statistics", "daily-deploy-contract" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_block_reward", @@ -576,7 +811,12 @@ "statistics", "daily-block-reward" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_avg_block_size", @@ -588,7 +828,12 @@ "statistics", "daily-avg-block-size" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_top_contract_call", @@ -600,7 +845,12 @@ "statistics", "top-contract-call" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_contract_call", @@ -612,7 +862,12 @@ "statistics", "daily-contract-call" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_supply_growth", @@ -624,7 +879,12 @@ "statistics", "daily-supply-growth" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_market_cap", @@ -636,7 +896,12 @@ "statistics", "daily-market-cap" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_staked", @@ -648,7 +913,12 @@ "statistics", "daily-staked" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_holder", @@ -660,7 +930,12 @@ "statistics", "daily-holder" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_tvl", @@ -672,7 +947,12 @@ "statistics", "daily-tvl" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_node_current_produce_info", @@ -684,7 +964,12 @@ "statistics", "node-current-produce-info" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_elf_supply", @@ -696,7 +981,12 @@ "statistics", "elf-supply" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_transaction_info", @@ -708,7 +998,12 @@ "statistics", "daily-transaction-info" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_daily_activity_address", @@ -720,7 +1015,12 @@ "statistics", "daily-activity-address" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } }, { "name": "aelfscan_statistics_currency_price", @@ -732,7 +1032,12 @@ "statistics", "currency-price" ], - "cwd": "." + "cwd": ".", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": true + } } ] } diff --git a/package.json b/package.json index 947118a..86e122f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "test:unit:coverage": "bun run test:unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage", "coverage:badge": "bun run bin/generate-coverage-badge.ts", "test:integration": "bun test tests/integration/", - "test:e2e": "bun test tests/e2e/" + "test:e2e": "bun test tests/e2e/", + "coverage:gate": "bun run scripts/coverage-gate.ts", + "test:coverage:ci": "COVERAGE_MIN_LINES=85 COVERAGE_MIN_FUNCS=80 bun run test:unit:coverage && bun run coverage:gate" }, "keywords": [ "aelfscan", diff --git a/scripts/coverage-gate.ts b/scripts/coverage-gate.ts new file mode 100644 index 0000000..47f0853 --- /dev/null +++ b/scripts/coverage-gate.ts @@ -0,0 +1,109 @@ +#!/usr/bin/env bun +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +type SectionTotals = { + linesFound: number; + linesHit: number; + funcsFound: number; + funcsHit: number; +}; + +function isSrcFile(sfPath: string): boolean { + const normalized = sfPath.replace(/\\/g, '/'); + return normalized.startsWith('src/') || normalized.includes('/src/'); +} + +function parseLcov(lcovText: string): SectionTotals { + const totals: SectionTotals = { + linesFound: 0, + linesHit: 0, + funcsFound: 0, + funcsHit: 0, + }; + + let currentFile = ''; + + for (const rawLine of lcovText.split('\n')) { + const line = rawLine.trim(); + if (!line) continue; + + if (line.startsWith('SF:')) { + currentFile = line.slice(3); + continue; + } + + if (!currentFile || !isSrcFile(currentFile)) { + continue; + } + + if (line.startsWith('LF:')) { + totals.linesFound += Number(line.slice(3)) || 0; + continue; + } + + if (line.startsWith('LH:')) { + totals.linesHit += Number(line.slice(3)) || 0; + continue; + } + + if (line.startsWith('FNF:')) { + totals.funcsFound += Number(line.slice(4)) || 0; + continue; + } + + if (line.startsWith('FNH:')) { + totals.funcsHit += Number(line.slice(4)) || 0; + continue; + } + } + + return totals; +} + +function percent(hit: number, found: number): number { + if (found <= 0) return 0; + return (hit / found) * 100; +} + +function main() { + const minLines = Number(process.env.COVERAGE_MIN_LINES || '85'); + const minFuncs = Number(process.env.COVERAGE_MIN_FUNCS || '80'); + const lcovFile = process.env.COVERAGE_LCOV_FILE || 'coverage/lcov.info'; + const lcovPath = resolve(process.cwd(), lcovFile); + + if (!existsSync(lcovPath)) { + console.error(`[coverage-gate] lcov file not found: ${lcovPath}`); + process.exit(1); + } + + const lcov = readFileSync(lcovPath, 'utf8'); + const totals = parseLcov(lcov); + + if (totals.linesFound === 0 || totals.funcsFound === 0) { + console.error('[coverage-gate] no src/** lines/functions coverage data found'); + process.exit(1); + } + + const linePct = percent(totals.linesHit, totals.linesFound); + const funcPct = percent(totals.funcsHit, totals.funcsFound); + + const failures: string[] = []; + if (linePct < minLines) { + failures.push(`lines ${linePct.toFixed(2)}% < ${minLines}%`); + } + if (funcPct < minFuncs) { + failures.push(`funcs ${funcPct.toFixed(2)}% < ${minFuncs}%`); + } + + if (failures.length) { + console.error(`[coverage-gate] failed: ${failures.join(', ')}`); + process.exit(1); + } + + console.log( + `[coverage-gate] passed: lines=${linePct.toFixed(2)}% funcs=${funcPct.toFixed(2)}% (threshold lines>=${minLines} funcs>=${minFuncs})`, + ); +} + +main(); diff --git a/tests/unit/core-api-coverage.test.ts b/tests/unit/core-api-coverage.test.ts new file mode 100644 index 0000000..f68c30d --- /dev/null +++ b/tests/unit/core-api-coverage.test.ts @@ -0,0 +1,239 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { + getAddressDictionary, + getBlockDetail, + getBlockchainOverview, + getBlocks, + getLatestBlocks, + getLatestTransactions, + getLogEvents, + getTransactionDataChart, + getTransactionDetail, + getTransactions, +} from '../../src/core/blockchain.js'; +import { + getAccounts, + getAddressDetail, + getAddressNftAssets, + getAddressTokens, + getAddressTransfers, + getContractEvents, + getContractHistory, + getContracts, + getContractSource, +} from '../../src/core/address.js'; +import { getSearchFilters, search } from '../../src/core/search.js'; +import { getTokenDetail, getTokenHolders, getTokens, getTokenTransfers } from '../../src/core/token.js'; +import { + getNftCollectionDetail, + getNftCollections, + getNftHolders, + getNftInventory, + getNftItemActivity, + getNftItemDetail, + getNftItemHolders, + getNftTransfers, +} from '../../src/core/nft.js'; +import { + STATISTICS_METRICS, + getCurrencyPrice, + getDailyActivityAddress, + getDailyTransactionInfo, + getDailyTransactions, + getStatisticsByMetric, + getUniqueAddresses, +} from '../../src/core/statistics.js'; + +const originalFetch = globalThis.fetch; +const seenRequests: Array<{ url: string; method: string; body?: string | null }> = []; + +function okData(url: string) { + return { + code: '20000', + message: '', + data: { + items: [], + totalCount: 0, + marker: url, + result: 'ok', + }, + }; +} + +function installFetchMock() { + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const method = init?.method || 'GET'; + const body = typeof init?.body === 'string' ? init.body : null; + seenRequests.push({ url, method, body }); + + return new Response(JSON.stringify(okData(url)), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }) as unknown as typeof fetch; +} + +afterEach(() => { + seenRequests.length = 0; + globalThis.fetch = originalFetch; +}); + +describe('aelfscan core api coverage', () => { + test('search + blockchain happy path', async () => { + installFetchMock(); + + const filters = await getSearchFilters({ chainId: 'AELF' }); + const searchResult = await search({ chainId: 'AELF', keyword: 'ELF', filterType: 0, searchType: 0 }); + const blocks = await getBlocks({ chainId: 'AELF', skipCount: 0, maxResultCount: 2 }); + const latestBlocks = await getLatestBlocks({ chainId: 'AELF', maxResultCount: 3 }); + const blockDetail = await getBlockDetail({ chainId: 'AELF', blockHeight: 123 }); + const txs = await getTransactions({ chainId: 'AELF', skipCount: 0, maxResultCount: 2, address: 'ELF_addr' }); + const latestTxs = await getLatestTransactions({ chainId: 'AELF', maxResultCount: 2 }); + const txDetail = await getTransactionDetail({ chainId: 'AELF', transactionId: '0xabc' }); + const overview = await getBlockchainOverview({ chainId: 'AELF', blockTime: '1D' as any }); + const chart = await getTransactionDataChart({ chainId: 'AELF' }); + const dict = await getAddressDictionary({ + chainId: 'AELF', + name: 'test-dict', + addresses: ['ELF_123_AELF', 'ELF_456_AELF'], + }); + const events = await getLogEvents({ + chainId: 'AELF', + contractAddress: '2mABCDEF1234567890', + skipCount: 0, + maxResultCount: 1, + }); + + for (const result of [ + filters, + searchResult, + blocks, + latestBlocks, + blockDetail, + txs, + latestTxs, + txDetail, + overview, + chart, + dict, + events, + ]) { + expect(result.success).toBe(true); + } + + expect(seenRequests.length).toBeGreaterThanOrEqual(12); + expect(seenRequests.some((entry) => entry.url.includes('/api/app/blockchain/blocks'))).toBe(true); + expect(seenRequests.some((entry) => entry.url.includes('/api/app/blockchain/search'))).toBe(true); + expect(seenRequests.some((entry) => entry.method === 'POST')).toBe(true); + }); + + test('address + token + nft happy path', async () => { + installFetchMock(); + + const accounts = await getAccounts({ chainId: 'AELF', skipCount: 0, maxResultCount: 2 }); + const contracts = await getContracts({ chainId: 'AELF', skipCount: 0, maxResultCount: 2 }); + const detail = await getAddressDetail({ chainId: 'AELF', address: 'ELF_abc_AELF' }); + const tokens = await getAddressTokens({ chainId: 'AELF', address: 'ELF_abc_AELF', skipCount: 0, maxResultCount: 2 }); + const nftAssets = await getAddressNftAssets({ chainId: 'AELF', address: 'ELF_abc_AELF', skipCount: 0, maxResultCount: 2 }); + const transfers = await getAddressTransfers({ chainId: 'AELF', address: 'ELF_abc_AELF', skipCount: 0, maxResultCount: 2 }); + const history = await getContractHistory({ chainId: 'AELF', address: 'ELF_abc_AELF' }); + const contractEvents = await getContractEvents({ chainId: 'AELF', contractAddress: 'ELF_contract_AELF', skipCount: 0, maxResultCount: 2 }); + const source = await getContractSource({ chainId: 'AELF', address: 'ELF_contract_AELF' }); + + const tokenList = await getTokens({ chainId: 'AELF', skipCount: 0, maxResultCount: 2, symbols: ['ELF'] }); + const tokenDetail = await getTokenDetail({ chainId: 'AELF', symbol: 'ELF' }); + const tokenTransfers = await getTokenTransfers({ chainId: 'AELF', symbol: 'ELF', skipCount: 0, maxResultCount: 2 }); + const holders = await getTokenHolders({ chainId: 'AELF', symbol: 'ELF', skipCount: 0, maxResultCount: 2 }); + + const collections = await getNftCollections({ chainId: 'AELF', skipCount: 0, maxResultCount: 2 }); + const collectionDetail = await getNftCollectionDetail({ chainId: 'AELF', collectionSymbol: 'COOL' }); + const nftTransfers = await getNftTransfers({ chainId: 'AELF', collectionSymbol: 'COOL', skipCount: 0, maxResultCount: 2 }); + const nftHolders = await getNftHolders({ chainId: 'AELF', collectionSymbol: 'COOL', skipCount: 0, maxResultCount: 2 }); + const inventory = await getNftInventory({ chainId: 'AELF', collectionSymbol: 'COOL', skipCount: 0, maxResultCount: 2 }); + const itemDetail = await getNftItemDetail({ chainId: 'AELF', symbol: 'COOL-1' }); + const itemHolders = await getNftItemHolders({ chainId: 'AELF', symbol: 'COOL-1', skipCount: 0, maxResultCount: 2 }); + const itemActivity = await getNftItemActivity({ chainId: 'AELF', symbol: 'COOL-1', skipCount: 0, maxResultCount: 2 }); + + for (const result of [ + accounts, + contracts, + detail, + tokens, + nftAssets, + transfers, + history, + contractEvents, + source, + tokenList, + tokenDetail, + tokenTransfers, + holders, + collections, + collectionDetail, + nftTransfers, + nftHolders, + inventory, + itemDetail, + itemHolders, + itemActivity, + ]) { + expect(result.success).toBe(true); + } + + expect(seenRequests.some((entry) => entry.url.includes('/api/app/address/detail'))).toBe(true); + expect(seenRequests.some((entry) => entry.url.includes('/api/app/token/detail'))).toBe(true); + expect(seenRequests.some((entry) => entry.url.includes('/api/app/token/nft/item-activity'))).toBe(true); + }); + + test('statistics happy path + metric router', async () => { + installFetchMock(); + + const daily = await getDailyTransactions({ chainId: 'AELF' }); + const unique = await getUniqueAddresses({ chainId: 'AELF' }); + const txInfo = await getDailyTransactionInfo({ chainId: 'AELF', startDate: '2026-02-01', endDate: '2026-02-27' }); + const active = await getDailyActivityAddress({ chainId: 'AELF', startDate: '2026-02-01', endDate: '2026-02-27' }); + const currency = await getCurrencyPrice({ chainId: 'AELF' }); + + expect(daily.success).toBe(true); + expect(unique.success).toBe(true); + expect(txInfo.success).toBe(true); + expect(active.success).toBe(true); + expect(currency.success).toBe(true); + + for (const metric of STATISTICS_METRICS) { + const payload = + metric === 'dailyTransactionInfo' || metric === 'dailyActivityAddress' + ? { metric, chainId: 'AELF', startDate: '2026-02-01', endDate: '2026-02-27' } + : { metric, chainId: 'AELF' }; + const result = await getStatisticsByMetric(payload as any); + expect(result.success).toBe(true); + } + + expect(seenRequests.some((entry) => entry.url.includes('/api/app/statistics/dailyTransactions'))).toBe(true); + expect(seenRequests.some((entry) => entry.url.includes('/api/app/statistics/currencyPrice'))).toBe(true); + }); + + test('required input guards return failure envelope', async () => { + installFetchMock(); + + const missingKeyword = await search({ chainId: 'AELF', keyword: '' as any }); + const missingBlockHeight = await getBlockDetail({ chainId: 'AELF' } as any); + const missingAddress = await getAddressDetail({ chainId: 'AELF', address: '' } as any); + const missingSymbol = await getTokenDetail({ chainId: 'AELF', symbol: '' } as any); + const missingCollection = await getNftCollectionDetail({ chainId: 'AELF', collectionSymbol: '' } as any); + const missingDateRange = await getDailyTransactionInfo({ chainId: 'AELF', startDate: '' as any, endDate: '2026-02-27' }); + + for (const result of [ + missingKeyword, + missingBlockHeight, + missingAddress, + missingSymbol, + missingCollection, + missingDateRange, + ]) { + expect(result.success).toBe(false); + expect(result.error?.code).toBe('INVALID_INPUT'); + } + }); +}); From d1b10e5f5e4426913ce6d609fcb8867a6bff0d38 Mon Sep 17 00:00:00 2001 From: kastale Date: Fri, 27 Feb 2026 16:48:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20remove=20unsupported?= =?UTF-8?q?=20generatedAt=20from=20coverage=20badge=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/generate-coverage-badge.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/generate-coverage-badge.ts b/bin/generate-coverage-badge.ts index 52ec84c..d76846e 100755 --- a/bin/generate-coverage-badge.ts +++ b/bin/generate-coverage-badge.ts @@ -46,7 +46,6 @@ const badge = { label: 'coverage', message: `${rounded}%`, color, - generatedAt: new Date().toISOString(), }; fs.mkdirSync(path.dirname(outputPath), { recursive: true });