Skip to content

Commit aba61aa

Browse files
satoshai-devclaude
andcommitted
test: improve coverage and fix typecheck for test files
- add tsconfig.check.json so tsc --noEmit typechecks test files - rewrite cli.test.ts to properly test fetchCommand (stdout TS/JSON, invalid format) - add fetcher tests: devnet URL, custom URL, network errors, malformed JSON - tests go from 26 to 31 Closes #5, closes #7, closes #15 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 68ff3a8 commit aba61aa

File tree

5 files changed

+113
-26
lines changed

5 files changed

+113
-26
lines changed

.changeset/fix-test-coverage.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@satoshai/abi-cli": patch
3+
---
4+
5+
Improve test coverage and fix typecheck to include test files

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"dev": "tsup --watch",
3232
"test": "vitest run",
3333
"test:watch": "vitest",
34-
"typecheck": "tsc --noEmit",
34+
"typecheck": "tsc --noEmit -p tsconfig.check.json",
3535
"lint": "eslint src",
3636
"clean": "rm -rf dist",
3737
"changeset": "changeset",

tests/unit/cli.test.ts

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2-
import { parseContractId } from '../../src/fetcher.js';
3-
import { generateTypescript, generateJson } from '../../src/codegen.js';
2+
import { fetchCommand } from '../../src/commands/fetch.js';
43
import { sampleAbi } from './fixtures.js';
54

6-
describe('CLI integration', () => {
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
const runFetch = (args: Record<string, any>) =>
7+
fetchCommand.run!({ args, rawArgs: [], cmd: fetchCommand } as never);
8+
9+
describe('fetchCommand', () => {
710
const originalFetch = globalThis.fetch;
811

912
beforeEach(() => {
@@ -14,46 +17,72 @@ describe('CLI integration', () => {
1417
globalThis.fetch = originalFetch;
1518
});
1619

17-
it('end-to-end: parse contract, fetch ABI, generate TypeScript', async () => {
20+
it('writes TypeScript to stdout', async () => {
1821
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
1922
ok: true,
2023
json: async () => sampleAbi,
2124
} as Response);
2225

23-
const contractId = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait';
24-
const { address, name } = parseContractId(contractId);
26+
const chunks: string[] = [];
27+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
28+
chunks.push(String(chunk));
29+
return true;
30+
});
31+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
2532

26-
expect(address).toBe('SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9');
27-
expect(name).toBe('nft-trait');
33+
await runFetch({
34+
contract: 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait',
35+
network: 'mainnet',
36+
format: 'ts',
37+
stdout: true,
38+
});
2839

29-
const response = await globalThis.fetch(
30-
`https://api.hiro.so/v2/contracts/interface/${address}/${name}`,
31-
);
32-
const abi = await (response as Response).json();
40+
const output = chunks.join('');
41+
expect(output).toContain('export const abi =');
42+
expect(output).toContain('as const;');
43+
expect(output).toContain('"transfer"');
3344

34-
const tsOutput = generateTypescript(contractId, abi);
35-
expect(tsOutput).toContain('export const abi =');
36-
expect(tsOutput).toContain('as const;');
37-
expect(tsOutput).toContain('"transfer"');
45+
writeSpy.mockRestore();
46+
errorSpy.mockRestore();
3847
});
3948

40-
it('end-to-end: parse contract, fetch ABI, generate JSON', async () => {
49+
it('writes JSON to stdout', async () => {
4150
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
4251
ok: true,
4352
json: async () => sampleAbi,
4453
} as Response);
4554

46-
const contractId = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait';
47-
const { address, name } = parseContractId(contractId);
55+
const chunks: string[] = [];
56+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
57+
chunks.push(String(chunk));
58+
return true;
59+
});
60+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
4861

49-
const response = await globalThis.fetch(
50-
`https://api.hiro.so/v2/contracts/interface/${address}/${name}`,
51-
);
52-
const abi = await (response as Response).json();
62+
await runFetch({
63+
contract: 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait',
64+
network: 'mainnet',
65+
format: 'json',
66+
stdout: true,
67+
});
5368

54-
const jsonOutput = generateJson(abi);
55-
const parsed = JSON.parse(jsonOutput);
69+
const output = chunks.join('');
70+
const parsed = JSON.parse(output);
5671
expect(parsed.functions).toBeDefined();
5772
expect(parsed.functions.length).toBe(4);
73+
74+
writeSpy.mockRestore();
75+
errorSpy.mockRestore();
76+
});
77+
78+
it('throws on invalid format', async () => {
79+
await expect(
80+
runFetch({
81+
contract: 'SP2P.nft-trait',
82+
network: 'mainnet',
83+
format: 'yaml',
84+
stdout: true,
85+
}),
86+
).rejects.toThrow('Invalid format "yaml"');
5887
});
5988
});

tests/unit/fetcher.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,49 @@ describe('fetchContractAbi', () => {
9999
'https://api.testnet.hiro.so/v2/contracts/interface/ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM/my-contract',
100100
);
101101
});
102+
103+
it('uses the correct URL for devnet', async () => {
104+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
105+
ok: true,
106+
json: async () => sampleAbi,
107+
} as Response);
108+
109+
await fetchContractAbi('devnet', 'ST123', 'my-contract');
110+
111+
expect(globalThis.fetch).toHaveBeenCalledWith(
112+
'http://localhost:3999/v2/contracts/interface/ST123/my-contract',
113+
);
114+
});
115+
116+
it('uses the correct URL for custom network', async () => {
117+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
118+
ok: true,
119+
json: async () => sampleAbi,
120+
} as Response);
121+
122+
await fetchContractAbi('https://custom.example.com', 'SP123', 'my-contract');
123+
124+
expect(globalThis.fetch).toHaveBeenCalledWith(
125+
'https://custom.example.com/v2/contracts/interface/SP123/my-contract',
126+
);
127+
});
128+
129+
it('throws on network error', async () => {
130+
vi.mocked(globalThis.fetch).mockRejectedValueOnce(new TypeError('fetch failed'));
131+
132+
await expect(
133+
fetchContractAbi('mainnet', 'SP123', 'broken'),
134+
).rejects.toThrow('Network error fetching ABI for SP123.broken on mainnet');
135+
});
136+
137+
it('throws on invalid JSON response', async () => {
138+
vi.mocked(globalThis.fetch).mockResolvedValueOnce({
139+
ok: true,
140+
json: async () => { throw new SyntaxError('Unexpected token'); },
141+
} as unknown as Response);
142+
143+
await expect(
144+
fetchContractAbi('mainnet', 'SP123', 'broken'),
145+
).rejects.toThrow('Invalid JSON response for SP123.broken on mainnet');
146+
});
102147
});

tsconfig.check.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "."
5+
},
6+
"include": ["src/**/*", "tests/**/*"],
7+
"exclude": ["node_modules", "dist"]
8+
}

0 commit comments

Comments
 (0)