- Add vitest ^2 + @vitest/coverage-v8 ^2 as devDependencies - Add `test` and `test:coverage` scripts to package.json - Create vitest.config.ts with resolve.alias to mock ponder virtual modules (ponder:schema, ponder:registry) and point kraiken-lib/version to source - Add coverage/ to .gitignore - Add tests/**/* and vitest.config.ts to tsconfig.json include - Create tests/__mocks__/ponder-schema.ts and ponder-registry.ts stubs - Create tests/stats.test.ts — 48 tests covering ring buffer logic, segment updates, hourly advancement, projections, ETH reserve snapshots, all exported async helpers with mock Ponder contexts - Create tests/version.test.ts — 14 tests covering isCompatibleVersion, getVersionMismatchError, and validateContractVersion (compatible / mismatch / error paths, existing-meta upsert path) - Create tests/abi.test.ts — 6 tests covering validateAbi and validateContractAbi Tests placed at tests/ (not src/tests/) so Ponder's Vite build does not attempt to execute test files as event handlers on startup. Result: 68 tests pass, 100% line/statement/function coverage on all helpers (stats.ts, version.ts, abi.ts, logger.ts) — exceeds 95% target. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import {
|
|
isCompatibleVersion,
|
|
getVersionMismatchError,
|
|
KRAIKEN_LIB_VERSION,
|
|
COMPATIBLE_CONTRACT_VERSIONS,
|
|
} from 'kraiken-lib/version';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// isCompatibleVersion (from kraiken-lib/version, aliased to source)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('isCompatibleVersion', () => {
|
|
it('returns true for each version in COMPATIBLE_CONTRACT_VERSIONS', () => {
|
|
for (const v of COMPATIBLE_CONTRACT_VERSIONS) {
|
|
expect(isCompatibleVersion(v)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('returns false for version 0', () => {
|
|
expect(isCompatibleVersion(0)).toBe(false);
|
|
});
|
|
|
|
it('returns false for a future version not yet listed', () => {
|
|
expect(isCompatibleVersion(9999)).toBe(false);
|
|
});
|
|
|
|
it('returns false for negative version', () => {
|
|
expect(isCompatibleVersion(-1)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getVersionMismatchError (from kraiken-lib/version)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('getVersionMismatchError', () => {
|
|
it('includes the bad contract version in the output (ponder context)', () => {
|
|
const msg = getVersionMismatchError(99, 'ponder');
|
|
expect(msg).toContain('99');
|
|
});
|
|
|
|
it('includes the library version in the output', () => {
|
|
const msg = getVersionMismatchError(99, 'ponder');
|
|
expect(msg).toContain(String(KRAIKEN_LIB_VERSION));
|
|
});
|
|
|
|
it('includes ponder-specific remediation steps', () => {
|
|
const msg = getVersionMismatchError(99, 'ponder');
|
|
expect(msg).toContain('COMPATIBLE_CONTRACT_VERSIONS');
|
|
});
|
|
|
|
it('includes frontend-specific remediation steps', () => {
|
|
const msg = getVersionMismatchError(99, 'frontend');
|
|
expect(msg).toContain('administrator');
|
|
});
|
|
|
|
it('formats compatible versions list', () => {
|
|
const msg = getVersionMismatchError(99, 'ponder');
|
|
for (const v of COMPATIBLE_CONTRACT_VERSIONS) {
|
|
expect(msg).toContain(String(v));
|
|
}
|
|
});
|
|
|
|
it('returns a multi-line string (box drawing)', () => {
|
|
const msg = getVersionMismatchError(99, 'ponder');
|
|
expect(msg.split('\n').length).toBeGreaterThan(3);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// validateContractVersion (helpers/version.ts) — mock the Ponder context
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('validateContractVersion', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('logs success and upserts metadata on compatible version', async () => {
|
|
const { validateContractVersion } = await import('../src/helpers/version.js');
|
|
|
|
const setFn = vi.fn().mockResolvedValue(undefined);
|
|
const ctx = {
|
|
client: {
|
|
readContract: vi.fn().mockResolvedValue(KRAIKEN_LIB_VERSION),
|
|
},
|
|
contracts: {
|
|
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
|
},
|
|
db: {
|
|
find: vi.fn().mockResolvedValue(null),
|
|
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
|
update: vi.fn().mockReturnValue({ set: setFn }),
|
|
},
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
|
|
|
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
expect(ctx.logger.info).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls process.exit on incompatible contract version', async () => {
|
|
const { validateContractVersion } = await import('../src/helpers/version.js');
|
|
|
|
const ctx = {
|
|
client: {
|
|
readContract: vi.fn().mockResolvedValue(9999n),
|
|
},
|
|
contracts: {
|
|
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
|
},
|
|
db: {
|
|
find: vi.fn().mockResolvedValue(null),
|
|
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
|
update: vi.fn().mockReturnValue({ set: vi.fn().mockResolvedValue(undefined) }),
|
|
},
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
|
|
|
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it('calls process.exit when readContract throws', async () => {
|
|
const { validateContractVersion } = await import('../src/helpers/version.js');
|
|
|
|
const ctx = {
|
|
client: {
|
|
readContract: vi.fn().mockRejectedValue(new Error('network error')),
|
|
},
|
|
contracts: {
|
|
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
|
},
|
|
db: {
|
|
find: vi.fn().mockResolvedValue(null),
|
|
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
|
update: vi.fn().mockReturnValue({ set: vi.fn().mockResolvedValue(undefined) }),
|
|
},
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
|
|
|
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it('updates existing metadata when a row already exists', async () => {
|
|
const { validateContractVersion } = await import('../src/helpers/version.js');
|
|
|
|
const setFn = vi.fn().mockResolvedValue(undefined);
|
|
const existingMeta = { id: 'stack-meta', contractVersion: 1 };
|
|
const ctx = {
|
|
client: {
|
|
readContract: vi.fn().mockResolvedValue(KRAIKEN_LIB_VERSION),
|
|
},
|
|
contracts: {
|
|
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
|
},
|
|
db: {
|
|
find: vi.fn().mockResolvedValue(existingMeta),
|
|
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
|
update: vi.fn().mockReturnValue({ set: setFn }),
|
|
},
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
|
|
|
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
|
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
expect(ctx.db.update).toHaveBeenCalled();
|
|
expect(ctx.db.insert).not.toHaveBeenCalled();
|
|
});
|
|
});
|