Merge pull request 'fix: kraiken-lib: fix broken tests + raise coverage to 95% (#286)' (#293) from fix/issue-286 into master

This commit is contained in:
johba 2026-02-26 02:32:27 +01:00
commit b161b4ecfb
19 changed files with 2476 additions and 6326 deletions

View file

@ -1 +1,2 @@
dist
coverage

View file

@ -1,13 +0,0 @@
module.exports = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
maxWorkers: 1,
extensionsToTreatAsEsm: ['.ts'],
resolver: './jest.resolver.cjs',
globals: {
'ts-jest': {
useESM: true,
tsconfig: './tsconfig.json'
}
}
};

View file

@ -1,16 +0,0 @@
module.exports = (request, options) => {
try {
return options.defaultResolver(request, options);
} catch (originalError) {
if (!request.endsWith('.js')) {
throw originalError;
}
const tsRequest = request.replace(/\.js$/, '.ts');
try {
return options.defaultResolver(tsRequest, options);
} catch (fallbackError) {
throw originalError;
}
}
};

File diff suppressed because it is too large Load diff

View file

@ -61,7 +61,8 @@
"/dist"
],
"scripts": {
"test": "jest",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"compile": "graphql-codegen",
"watch": "graphql-codegen -w",
"lint": "eslint 'src/**/*.ts'",
@ -88,16 +89,16 @@
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@types/jest": "^29.5.12",
"@types/node": "^24.6.0",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"@vitest/coverage-v8": "^3.0.0",
"eslint": "^9.36.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.2.3",
"picomatch": "^4.0.3",
"prettier": "^3.6.2",
"ts-jest": "^29.1.2",
"typescript": "^5.4.3"
"typescript": "^5.4.3",
"vitest": "^3.0.0"
}
}

View file

@ -22,6 +22,7 @@ export const STAKE_ABI = StakeForgeOutput.abi;
* LiquidityManager events-only ABI
* Tracks recenters, ETH reserve, and VWAP data
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const LiquidityManagerAbi = [
{"type":"event","name":"EthAbundance","inputs":[{"name":"currentTick","type":"int24","indexed":false},{"name":"ethBalance","type":"uint256","indexed":false},{"name":"outstandingSupply","type":"uint256","indexed":false},{"name":"vwap","type":"uint256","indexed":false},{"name":"vwapTick","type":"int24","indexed":false}],"anonymous":false},
{"type":"event","name":"EthScarcity","inputs":[{"name":"currentTick","type":"int24","indexed":false},{"name":"ethBalance","type":"uint256","indexed":false},{"name":"outstandingSupply","type":"uint256","indexed":false},{"name":"vwap","type":"uint256","indexed":false},{"name":"vwapTick","type":"int24","indexed":false}],"anonymous":false},

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test } from 'vitest';
import { weiToNumber, formatWei, compactNumber, commaNumber } from '../format.js';
describe('weiToNumber', () => {
@ -33,6 +33,11 @@ describe('weiToNumber', () => {
test('handles string with custom decimals', () => {
expect(weiToNumber('1000000', 6)).toBe(1);
});
test('handles null/undefined via nullish coalescing fallback', () => {
expect(weiToNumber(null as unknown as bigint)).toBe(0);
expect(weiToNumber(undefined as unknown as bigint)).toBe(0);
});
});
describe('formatWei', () => {

View file

@ -1,18 +1,10 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test } from 'vitest';
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from '../subgraph.js';
describe('BigInt Conversion Functions', () => {
test('converts uint256 to bytes and back (little endian)', async () => {
test('converts uint256 to bytes and back (little endian)', () => {
const mockId = uint256ToBytesLittleEndian(3n);
let hexString = '0x';
for (let i = 0; i < mockId.length; i++) {
// Convert each byte to a hexadecimal string and pad with zero if needed
hexString += mockId[i].toString(16).padStart(2, '0');
}
expect(hexString).toEqual('0x03000000');
// return hexString;
expect(mockId).toEqual(new Uint8Array([3, 0, 0, 0]));
expect(bytesToUint256LittleEndian(mockId)).toEqual(3n);
});
});

View file

@ -1,9 +1,9 @@
import { describe, expect, test } from '@jest/globals';
import { decodePositionId } from '../ids.js';
import { describe, expect, test } from 'vitest';
import { decodePositionId, toBigIntId } from '../ids.js';
import { uint256ToBytesLittleEndian } from '../subgraph.js';
describe('ids', () => {
test('decodePositionId works across representations', () => {
describe('decodePositionId', () => {
test('works across bigint, hex string, and Uint8Array representations', () => {
const id = 12345n;
const hex = `0x${id.toString(16)}`;
const bytes = uint256ToBytesLittleEndian(id);
@ -13,3 +13,40 @@ describe('ids', () => {
expect(decodePositionId(bytes)).toBe(id);
});
});
describe('toBigIntId', () => {
test('returns bigint unchanged', () => {
expect(toBigIntId(42n)).toBe(42n);
expect(toBigIntId(0n)).toBe(0n);
});
test('converts number to bigint', () => {
expect(toBigIntId(42)).toBe(42n);
expect(toBigIntId(0)).toBe(0n);
expect(toBigIntId(1000)).toBe(1000n);
});
test('converts hex string with 0x prefix to bigint', () => {
expect(toBigIntId('0x3039')).toBe(12345n);
expect(toBigIntId('0x0')).toBe(0n);
expect(toBigIntId('0x1')).toBe(1n);
});
test('converts string without 0x prefix (prepends 0x, treats as hex)', () => {
expect(toBigIntId('3039')).toBe(12345n);
expect(toBigIntId('0')).toBe(0n);
expect(toBigIntId('ff')).toBe(255n);
});
test('converts Uint8Array via little endian decoding', () => {
const bytes = uint256ToBytesLittleEndian(12345n);
expect(toBigIntId(bytes)).toBe(12345n);
const zeroBytes = uint256ToBytesLittleEndian(0n);
expect(toBigIntId(zeroBytes)).toBe(0n);
});
test('throws for unsupported type', () => {
expect(() => toBigIntId({} as unknown as bigint)).toThrow('Unsupported position id type');
});
});

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test } from 'vitest';
import { calculateActivePositionProfit, calculateClosedPositionProfit } from '../position.js';
describe('position profit calculations', () => {

View file

@ -1,16 +1,29 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test } from 'vitest';
import { getSnatchList, minimumTaxRate, selectSnatchPositions, type SnatchablePosition } from '../snatch.js';
import { uint256ToBytesLittleEndian } from '../subgraph.js';
import type { Positions as GraphPosition } from '../__generated__/graphql.js';
describe('snatch', () => {
test('minimumTaxRate finds the lowest tax', () => {
describe('minimumTaxRate', () => {
test('finds the lowest tax from a list', () => {
const rates = [{ taxRate: 0.12 }, { taxRate: 0.05 }, { taxRate: 0.08 }];
expect(minimumTaxRate(rates, 1)).toBeCloseTo(0.05);
});
test('returns fallback for empty array', () => {
expect(minimumTaxRate([], 0.42)).toBe(0.42);
});
test('selectSnatchPositions chooses cheapest positions first', () => {
test('returns fallback of 0 by default for empty array', () => {
expect(minimumTaxRate([])).toBe(0);
});
test('works with a single element', () => {
expect(minimumTaxRate([{ taxRate: 0.07 }])).toBeCloseTo(0.07);
});
});
describe('selectSnatchPositions', () => {
test('chooses cheapest positions first', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 30n, taxRate: 0.05, taxRateIndex: 5 },
{ id: 2n, stakeShares: 40n, taxRate: 0.03, taxRateIndex: 3 },
@ -28,7 +41,7 @@ describe('snatch', () => {
expect(result.maxSelectedTaxRateIndex).toBe(5);
});
test('selectSnatchPositions keeps track of remaining shortfall', () => {
test('keeps track of remaining shortfall when not enough candidates', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 10n, taxRate: 0.01 },
{ id: 2n, stakeShares: 10n, taxRate: 0.02 },
@ -42,18 +55,122 @@ describe('snatch', () => {
expect(result.remainingShortfall).toBe(30n);
});
test('getSnatchList converts subgraph positions', () => {
const stakeTotalSupply = 1_000_000n * 10n ** 18n;
test('returns empty result when shortfallShares is zero', () => {
const candidates: SnatchablePosition[] = [{ id: 1n, stakeShares: 100n, taxRate: 0.05 }];
const positions = [
{
__typename: 'positions',
id: uint256ToBytesLittleEndian(1n),
owner: '0xowner1',
share: 0.0001,
const result = selectSnatchPositions(candidates, {
shortfallShares: 0n,
maxTaxRate: 0.1,
});
expect(result.selected).toEqual([]);
expect(result.coveredShares).toBe(0n);
expect(result.remainingShortfall).toBe(0n);
});
test('filters out candidates at or above maxTaxRate', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 50n, taxRate: 0.1 }, // equal to maxTaxRate -> filtered
{ id: 2n, stakeShares: 50n, taxRate: 0.15 }, // above maxTaxRate -> filtered
{ id: 3n, stakeShares: 50n, taxRate: 0.05 }, // below maxTaxRate -> included
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 100n,
maxTaxRate: 0.1,
});
expect(result.selected.map(p => p.id)).toEqual([3n]);
expect(result.remainingShortfall).toBe(50n);
});
test('filters out positions owned by recipient when includeOwned is false', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 50n, taxRate: 0.05, owner: '0xABC' },
{ id: 2n, stakeShares: 50n, taxRate: 0.04, owner: '0xDEF' },
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 100n,
maxTaxRate: 0.1,
includeOwned: false,
recipientAddress: '0xabc', // same as owner of id=1 (case-insensitive)
});
// id=1 is owned by recipient -> filtered out
expect(result.selected.map(p => p.id)).toEqual([2n]);
});
test('includes owned positions when includeOwned is true', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 50n, taxRate: 0.05, owner: '0xABC' },
{ id: 2n, stakeShares: 50n, taxRate: 0.04, owner: '0xDEF' },
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 100n,
maxTaxRate: 0.1,
includeOwned: true,
recipientAddress: '0xabc',
});
expect(result.selected.map(p => p.id)).toEqual([2n, 1n]);
expect(result.coveredShares).toBe(100n);
});
test('skips candidates with zero stakeShares', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 0n, taxRate: 0.01 }, // zero shares -> skipped
{ id: 2n, stakeShares: 50n, taxRate: 0.02 },
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 30n,
maxTaxRate: 0.05,
});
expect(result.selected.map(p => p.id)).toEqual([2n]);
});
test('tracks maxSelectedTaxRateIndex for tie-breaking same tax rate', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 20n, taxRate: 0.05, taxRateIndex: 3 },
{ id: 2n, stakeShares: 20n, taxRate: 0.05, taxRateIndex: 7 }, // higher index, same rate
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 40n,
maxTaxRate: 0.1,
});
// Both are selected; the one with higher taxRateIndex should win
expect(result.maxSelectedTaxRateIndex).toBe(7);
});
test('handles tie-breaking when first candidate has no taxRateIndex (covers ?? -1 fallback)', () => {
const candidates: SnatchablePosition[] = [
{ id: 1n, stakeShares: 20n, taxRate: 0.05 }, // no taxRateIndex
{ id: 2n, stakeShares: 20n, taxRate: 0.05, taxRateIndex: 5 }, // same rate, has index
];
const result = selectSnatchPositions(candidates, {
shortfallShares: 40n,
maxTaxRate: 0.1,
});
expect(result.maxSelectedTaxRateIndex).toBe(5);
});
});
describe('getSnatchList', () => {
const makePosition = (id: bigint, share: number, taxRate: number, owner = '0xowner'): GraphPosition => ({
__typename: 'positions' as const,
id: uint256ToBytesLittleEndian(id) as unknown as string,
owner,
share,
creationTime: '0',
lastTaxTime: '0',
taxRate: 0.02,
taxRate,
status: 'Active',
createdAt: 0,
kraikenDeposit: '0',
@ -62,28 +179,27 @@ describe('snatch', () => {
stakeDeposit: '0',
taxPaid: '0',
totalSupplyInit: '0',
},
{
__typename: 'positions',
id: uint256ToBytesLittleEndian(2n),
owner: '0xowner2',
share: 0.0002,
creationTime: '0',
lastTaxTime: '0',
taxRate: 0.01,
status: 'Active',
createdAt: 0,
kraikenDeposit: '0',
payout: '0',
snatched: 0,
stakeDeposit: '0',
taxPaid: '0',
totalSupplyInit: '0',
},
] as unknown as GraphPosition[];
}) as unknown as GraphPosition;
test('converts subgraph positions and returns lowest-tax IDs', () => {
const stakeTotalSupply = 1_000_000n * 10n ** 18n;
const positions = [makePosition(1n, 0.0001, 0.02), makePosition(2n, 0.0002, 0.01)];
const result = getSnatchList(positions, 10n ** 18n, 0.05, stakeTotalSupply);
expect(result).toEqual([2n]);
});
test('throws when stakeTotalSupply is zero', () => {
const positions = [makePosition(1n, 0.5, 0.02)];
expect(() => getSnatchList(positions, 10n, 0.05, 0n)).toThrow('stakeTotalSupply must be greater than zero');
});
test('throws when there is not enough capacity to cover needed shares', () => {
// Only one small position, but need is larger than its shares
const stakeTotalSupply = 100n;
const positions = [makePosition(1n, 0.001, 0.01)]; // tiny share
expect(() => getSnatchList(positions, 1000n, 0.05, stakeTotalSupply)).toThrow('Not enough capacity');
});
});

View file

@ -1,8 +1,8 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test } from 'vitest';
import { calculateSnatchShortfall, isPositionDelinquent } from '../staking.js';
describe('staking', () => {
test('calculateSnatchShortfall returns zero when within cap', () => {
describe('calculateSnatchShortfall', () => {
test('returns zero when within cap', () => {
const outstanding = 100n;
const desired = 50n;
const total = 1000n;
@ -11,7 +11,7 @@ describe('staking', () => {
expect(result).toBe(0n);
});
test('calculateSnatchShortfall returns positive remainder when exceeding cap', () => {
test('returns positive remainder when exceeding cap', () => {
const outstanding = 200n;
const desired = 200n;
const total = 1000n;
@ -20,7 +20,29 @@ describe('staking', () => {
expect(result).toBe(200n);
});
test('isPositionDelinquent respects tax rate windows', () => {
test('returns exact overage when required equals cap + 1', () => {
// cap = (1000 * 2) / 10 = 200; required = 100 + 101 = 201; delta = 1
expect(calculateSnatchShortfall(100n, 101n, 1000n, 2n, 10n)).toBe(1n);
});
test('returns zero when required equals cap exactly', () => {
// cap = (1000 * 2) / 10 = 200; required = 100 + 100 = 200; delta = 0
expect(calculateSnatchShortfall(100n, 100n, 1000n, 2n, 10n)).toBe(0n);
});
test('uses default cap numerator/denominator when not provided', () => {
// defaults: capNumerator=2n, capDenominator=10n
// cap = (1000 * 2) / 10 = 200; outstanding=100, desired=50 -> required=150 <= 200 -> 0
expect(calculateSnatchShortfall(100n, 50n, 1000n)).toBe(0n);
});
test('throws when capDenominator is zero', () => {
expect(() => calculateSnatchShortfall(100n, 50n, 1000n, 2n, 0n)).toThrow('capDenominator must be greater than zero');
});
});
describe('isPositionDelinquent', () => {
test('respects tax rate windows', () => {
const now = 1_000_000;
const taxRate = 0.5; // 50%
const windowSeconds = (365 * 24 * 60 * 60) / taxRate;
@ -29,4 +51,26 @@ describe('staking', () => {
expect(isPositionDelinquent(now - windowSeconds - 10, taxRate, now)).toBe(true);
expect(isPositionDelinquent(now, 0, now)).toBe(false);
});
test('returns false for zero tax rate', () => {
expect(isPositionDelinquent(0, 0, 1_000_000)).toBe(false);
});
test('returns false for negative tax rate', () => {
expect(isPositionDelinquent(0, -0.1, 1_000_000)).toBe(false);
});
test('returns true when far past the allowance window', () => {
// taxRate=1.0 -> allowance = 31_536_000s (1 year); use now > 31_536_000
const now = 100_000_000;
const lastTax = 0;
const taxRate = 1.0; // 100% per year = 1 year window
expect(isPositionDelinquent(lastTax, taxRate, now)).toBe(true);
});
test('uses current time as default when referenceTimestamp not provided', () => {
// taxRate=1.0 -> allowance = 1 year; 2 years ago is definitely delinquent
const twoYearsAgoSeconds = Math.floor(Date.now() / 1000) - 2 * 365 * 24 * 60 * 60;
expect(isPositionDelinquent(twoYearsAgoSeconds, 1.0)).toBe(true);
});
});

View file

@ -0,0 +1,72 @@
import { describe, expect, test } from 'vitest';
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from '../subgraph.js';
describe('uint256ToBytesLittleEndian', () => {
test('converts zero to four zero bytes', () => {
expect(uint256ToBytesLittleEndian(0n)).toEqual(new Uint8Array([0, 0, 0, 0]));
});
test('converts 1 to [1, 0, 0, 0]', () => {
expect(uint256ToBytesLittleEndian(1n)).toEqual(new Uint8Array([1, 0, 0, 0]));
});
test('converts 3 to [3, 0, 0, 0]', () => {
const result = uint256ToBytesLittleEndian(3n);
let hexString = '0x';
for (let i = 0; i < result.length; i++) {
hexString += result[i].toString(16).padStart(2, '0');
}
expect(hexString).toEqual('0x03000000');
});
test('converts 256 to [0, 1, 0, 0] (second byte)', () => {
expect(uint256ToBytesLittleEndian(256n)).toEqual(new Uint8Array([0, 1, 0, 0]));
});
test('converts 65536 to [0, 0, 1, 0] (third byte)', () => {
expect(uint256ToBytesLittleEndian(65536n)).toEqual(new Uint8Array([0, 0, 1, 0]));
});
test('converts 16777216 (2^24) to [0, 0, 0, 1] (fourth byte)', () => {
expect(uint256ToBytesLittleEndian(16777216n)).toEqual(new Uint8Array([0, 0, 0, 1]));
});
test('returns a 4-byte Uint8Array', () => {
const result = uint256ToBytesLittleEndian(12345n);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(4);
});
});
describe('bytesToUint256LittleEndian', () => {
test('converts four zero bytes to zero', () => {
expect(bytesToUint256LittleEndian(new Uint8Array([0, 0, 0, 0]))).toBe(0n);
});
test('converts [1, 0, 0, 0] to 1', () => {
expect(bytesToUint256LittleEndian(new Uint8Array([1, 0, 0, 0]))).toBe(1n);
});
test('converts [0, 1, 0, 0] to 256', () => {
expect(bytesToUint256LittleEndian(new Uint8Array([0, 1, 0, 0]))).toBe(256n);
});
test('converts [255, 255, 255, 255] to 0xffffffff', () => {
expect(bytesToUint256LittleEndian(new Uint8Array([255, 255, 255, 255]))).toBe(0xffffffffn);
});
test('returns a bigint', () => {
const result = bytesToUint256LittleEndian(new Uint8Array([3, 0, 0, 0]));
expect(typeof result).toBe('bigint');
});
});
describe('roundtrip: uint256ToBytesLittleEndian -> bytesToUint256LittleEndian', () => {
test('roundtrip preserves value for common position IDs', () => {
const values = [0n, 1n, 2n, 3n, 100n, 255n, 256n, 12345n, 65535n, 16777215n];
for (const val of values) {
const bytes = uint256ToBytesLittleEndian(val);
expect(bytesToUint256LittleEndian(bytes)).toBe(val);
}
});
});

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from '@jest/globals';
import { TAX_RATE_OPTIONS } from '../taxRates.js';
import { describe, expect, test } from 'vitest';
import { TAX_RATE_OPTIONS, TAX_RATES_CHECKSUM, TAX_RATES_RAW } from '../taxRates.js';
describe('taxRates', () => {
test('tax rate options exported for consumers', () => {
@ -23,3 +23,41 @@ describe('taxRates', () => {
}
});
});
describe('TAX_RATES_RAW', () => {
test('is a non-empty array of positive numbers', () => {
expect(Array.isArray(TAX_RATES_RAW)).toBe(true);
expect(TAX_RATES_RAW.length).toBeGreaterThan(0);
TAX_RATES_RAW.forEach(rate => {
expect(typeof rate).toBe('number');
expect(rate).toBeGreaterThan(0);
});
});
test('matches TAX_RATE_OPTIONS year values', () => {
expect(TAX_RATES_RAW.length).toBe(TAX_RATE_OPTIONS.length);
TAX_RATES_RAW.forEach((raw, idx) => {
expect(raw).toBe(TAX_RATE_OPTIONS[idx].year);
});
});
test('first entry is 1 and last entry is 9700', () => {
expect(TAX_RATES_RAW[0]).toBe(1);
expect(TAX_RATES_RAW[TAX_RATES_RAW.length - 1]).toBe(9700);
});
});
describe('TAX_RATES_CHECKSUM', () => {
test('is a non-empty string', () => {
expect(typeof TAX_RATES_CHECKSUM).toBe('string');
expect(TAX_RATES_CHECKSUM.length).toBeGreaterThan(0);
});
test('has expected format (16 hex chars)', () => {
expect(TAX_RATES_CHECKSUM).toMatch(/^[0-9a-f]{16}$/);
});
test('has known value', () => {
expect(TAX_RATES_CHECKSUM).toBe('1e37f2312ef082e9');
});
});

View file

@ -0,0 +1,101 @@
import { describe, expect, test } from 'vitest';
import {
COMPATIBLE_CONTRACT_VERSIONS,
KRAIKEN_LIB_VERSION,
STACK_META_ID,
getVersionMismatchError,
isCompatibleVersion,
} from '../version.js';
describe('version constants', () => {
test('KRAIKEN_LIB_VERSION is a positive integer', () => {
expect(typeof KRAIKEN_LIB_VERSION).toBe('number');
expect(KRAIKEN_LIB_VERSION).toBeGreaterThan(0);
expect(Number.isInteger(KRAIKEN_LIB_VERSION)).toBe(true);
});
test('STACK_META_ID is the expected string', () => {
expect(STACK_META_ID).toBe('stack-meta');
});
test('COMPATIBLE_CONTRACT_VERSIONS is a non-empty array', () => {
expect(Array.isArray(COMPATIBLE_CONTRACT_VERSIONS)).toBe(true);
expect(COMPATIBLE_CONTRACT_VERSIONS.length).toBeGreaterThan(0);
});
test('COMPATIBLE_CONTRACT_VERSIONS includes KRAIKEN_LIB_VERSION', () => {
expect(COMPATIBLE_CONTRACT_VERSIONS).toContain(KRAIKEN_LIB_VERSION);
});
});
describe('isCompatibleVersion', () => {
test('returns true for all known compatible versions', () => {
for (const version of COMPATIBLE_CONTRACT_VERSIONS) {
expect(isCompatibleVersion(version)).toBe(true);
}
});
test('returns false for version 0', () => {
expect(isCompatibleVersion(0)).toBe(false);
});
test('returns false for a future unknown version', () => {
expect(isCompatibleVersion(9999)).toBe(false);
});
test('returns false for a negative version', () => {
expect(isCompatibleVersion(-1)).toBe(false);
});
});
describe('getVersionMismatchError', () => {
test('returns a multi-line string for ponder context', () => {
const error = getVersionMismatchError(99, 'ponder');
expect(typeof error).toBe('string');
expect(error.includes('\n')).toBe(true);
});
test('ponder context includes contract version number', () => {
const error = getVersionMismatchError(99, 'ponder');
expect(error).toContain('99');
});
test('ponder context includes VERSION MISMATCH header', () => {
const error = getVersionMismatchError(99, 'ponder');
expect(error).toContain('VERSION MISMATCH');
expect(error).toContain('ponder');
});
test('ponder context includes ponder-specific rebuild instructions', () => {
const error = getVersionMismatchError(99, 'ponder');
expect(error).toContain('build-kraiken-lib.sh');
});
test('frontend context includes VERSION MISMATCH header', () => {
const error = getVersionMismatchError(99, 'frontend');
expect(error).toContain('VERSION MISMATCH');
expect(error).toContain('frontend');
});
test('frontend context includes page refresh instruction', () => {
const error = getVersionMismatchError(99, 'frontend');
expect(error).toContain('refreshing');
});
test('error includes the library version', () => {
const error = getVersionMismatchError(99, 'ponder');
expect(error).toContain(String(KRAIKEN_LIB_VERSION));
});
test('error includes compatible versions list', () => {
const error = getVersionMismatchError(99, 'ponder');
const expectedVersions = COMPATIBLE_CONTRACT_VERSIONS.join(', ');
expect(error).toContain(expectedVersions);
});
test('frontend and ponder errors differ in instructions', () => {
const ponderError = getVersionMismatchError(99, 'ponder');
const frontendError = getVersionMismatchError(99, 'frontend');
expect(ponderError).not.toBe(frontendError);
});
});

View file

@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/tests/**', 'src/__generated__/**', 'src/abis.ts', 'src/index.ts'],
thresholds: {
lines: 100,
functions: 100,
branches: 100,
statements: 100,
},
},
},
});

File diff suppressed because it is too large Load diff

2832
package-lock.json generated

File diff suppressed because it is too large Load diff