From 066f1171222994c24a7352c44dec5ab7b173f87e Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Tue, 31 Mar 2026 18:15:04 -0700 Subject: [PATCH] test: add coverage reporting, edge case tests, and thresholds Add @vitest/coverage-v8 with 95% statement threshold. Add 8 edge case tests (URL encoding, malformed errors, concurrent requests, option handling) and 2 E2E integration tests. CI now reports coverage. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 2 +- bun.lock | 31 ++++++++++ package.json | 1 + tests/index.test.ts | 116 ++++++++++++++++++++++++++++++++++++++ tests/integration.test.ts | 26 ++++++++- vitest.config.ts | 10 ++++ 6 files changed, 184 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 677d88c..2afe0f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,5 +18,5 @@ jobs: - run: bun install --frozen-lockfile - run: bun run lint - run: bunx tsc --noEmit - - run: bun run test + - run: bun run test -- --coverage - run: bun run build diff --git a/bun.lock b/bun.lock index a387b2f..0efa447 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "@agentscore/sdk", "devDependencies": { "@eslint/js": "^9.39.4", + "@vitest/coverage-v8": "^4.1.2", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", @@ -17,6 +18,16 @@ }, }, "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], @@ -231,6 +242,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.2", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.2", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.2", "vitest": "4.1.2" }, "optionalPeers": ["@vitest/browser"] }, "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg=="], + "@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="], "@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="], @@ -271,6 +284,8 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -439,6 +454,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -499,8 +516,16 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -551,6 +576,10 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -789,6 +818,8 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="], "vitest/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], diff --git a/package.json b/package.json index b727c9a..78e15a1 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@vitest/coverage-v8": "^4.1.2", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", diff --git a/tests/index.test.ts b/tests/index.test.ts index 166a61d..4da8fdd 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -403,3 +403,119 @@ describe('Timeout and network errors', () => { } }); }); + +// --------------------------------------------------------------------------- +// Edge Cases +// --------------------------------------------------------------------------- + +describe('Edge cases', () => { + afterEach(() => vi.restoreAllMocks()); + + it('getReputation encodes special characters in address', async () => { + mockFetchOk(REPUTATION_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + const weirdAddress = '0xabc/def?foo=bar&baz=qux#hash'; + await client.getReputation(weirdAddress); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toContain(encodeURIComponent(weirdAddress)); + expect(url).not.toContain('0xabc/def'); + }); + + it('falls back to unknown_error when response.json() throws', async () => { + expect.assertions(3); + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + status: 502, + json: vi.fn().mockRejectedValueOnce(new SyntaxError('Unexpected token')), + } as unknown as Response); + + const client = new AgentScore({ apiKey: API_KEY }); + try { + await client.getReputation(WALLET); + } catch (e) { + expect(e).toBeInstanceOf(AgentScoreError); + const err = e as AgentScoreError; + expect(err.code).toBe('unknown_error'); + expect(err.status).toBe(502); + } + }); + + it('getAgents omits undefined option values from query params', async () => { + mockFetchOk(AGENTS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.getAgents({ chain: 'base', limit: undefined }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toContain('chain=base'); + expect(url).not.toContain('limit'); + }); + + it('assess sends chain, refresh, and policy all at once', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(WALLET, { + chain: 'base', + refresh: true, + policy: { min_score: 50, require_verified_payment_activity: true }, + }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.address).toBe(WALLET); + expect(body.chain).toBe('base'); + expect(body.refresh).toBe(true); + expect(body.policy).toEqual({ min_score: 50, require_verified_payment_activity: true }); + }); + + it('two concurrent getReputation calls both resolve correctly', async () => { + const response2 = { ...REPUTATION_RESPONSE, subject: { chains: ['ethereum'], address: '0xdef456' } }; + global.fetch = vi.fn() + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValueOnce(REPUTATION_RESPONSE), + } as unknown as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValueOnce(response2), + } as unknown as Response); + + const client = new AgentScore({ apiKey: API_KEY }); + const [r1, r2] = await Promise.all([ + client.getReputation(WALLET), + client.getReputation('0xdef456'), + ]); + expect(r1).toMatchObject(REPUTATION_RESPONSE); + expect(r2).toMatchObject(response2); + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + + it('getAgents passes through empty items array', async () => { + const emptyResponse = { items: [], next_cursor: null, count: 0, version: '1' }; + mockFetchOk(emptyResponse); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.getAgents({ chain: 'base' }); + expect(result.items).toEqual([]); + expect(result.count).toBe(0); + }); + + it('assess includes refresh: false in request body', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(WALLET, { refresh: false }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body).toHaveProperty('refresh'); + expect(body.refresh).toBe(false); + }); + + it('getReputation appends chain to query string', async () => { + mockFetchOk(REPUTATION_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.getReputation(WALLET, { chain: 'ethereum' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toBe(`https://api.agentscore.sh/v1/reputation/${WALLET}?chain=ethereum`); + }); +}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 9718791..0149450 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -7,7 +7,7 @@ const TEST_ADDRESS = '0x339559a2d1cd15059365fc7bd36b3047bba480e0'; const describeIf = API_KEY ? describe : describe.skip; -describeIf('integration: real API', () => { +describeIf('integration: real API', { timeout: 15_000 }, () => { let client: AgentScore; beforeAll(() => { @@ -117,4 +117,28 @@ describeIf('integration: real API', () => { expect(typeof rep.reputation.client_count).toBe('number'); } }); + + it('assess then check reputation for same address', async () => { + const assessed = await client.assess(TEST_ADDRESS); + expect(assessed.score.value).toBeDefined(); + + const rep = await client.getReputation(TEST_ADDRESS); + expect(rep.score.value).toBeDefined(); + expect(typeof rep.score.value).toBe('number'); + expect(rep.subject.address.toLowerCase()).toBe(TEST_ADDRESS.toLowerCase()); + }); + + it('getAgents returns items matching browse results', async () => { + const result = await client.getAgents(); + + expect(result.items).toBeInstanceOf(Array); + expect(result.items.length).toBeGreaterThan(0); + + for (const item of result.items) { + expect(item.owner_address).toBeDefined(); + expect(item.chain).toBeDefined(); + expect(typeof item.token_id).toBe('number'); + expect('name' in item).toBe(true); + } + }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 83d684f..88e2719 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,5 +7,15 @@ export default defineConfig({ define: { __VERSION__: JSON.stringify(version) }, test: { environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'json-summary'], + thresholds: { + statements: 95, + branches: 90, + functions: 95, + lines: 95, + }, + }, }, });