Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
116 changes: 116 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mock.calls[0];
const body = JSON.parse(call[1].body as string) as Record<string, unknown>;
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<typeof vi.fn>).mock.calls[0];
const body = JSON.parse(call[1].body as string) as Record<string, unknown>;
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<typeof vi.fn>).mock.calls[0];
const url = call[0] as string;
expect(url).toBe(`https://api.agentscore.sh/v1/reputation/${WALLET}?chain=ethereum`);
});
});
26 changes: 25 additions & 1 deletion tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
}
});
});
10 changes: 10 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
});
Loading