mirror of
https://github.com/Heretek-AI/Token-Max.git
synced 2026-10-12 02:03:18 +00:00
test(pricing): add vitest suite for pricing math and stacking invariants
- covers agent-request cost formula, weighted/value scores, budget filters, plan-model matching, apples-to-apples rows, stack candidate selection, Dave stacks and Mix & Match budget/distinctness properties - run tests in the deploy workflow alongside lint and data validation
This commit is contained in:
@@ -36,6 +36,9 @@ jobs:
|
||||
- name: Validate data invariants
|
||||
run: npm run validate-data
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
|
||||
|
||||
Generated
+291
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "token-max-temp",
|
||||
"version": "0.0.0",
|
||||
"name": "token-max",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "token-max-temp",
|
||||
"version": "0.0.0",
|
||||
"name": "token-max",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.47.0",
|
||||
@@ -24,7 +24,8 @@
|
||||
"oxlint": "^1.81.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.3.0"
|
||||
"vite": "^8.3.0",
|
||||
"vitest": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
@@ -1309,6 +1310,17 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -1372,6 +1384,20 @@
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz",
|
||||
@@ -1438,6 +1464,74 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz",
|
||||
"integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.1",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.2.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz",
|
||||
"integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz",
|
||||
"integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1618,6 +1712,13 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz",
|
||||
@@ -1630,12 +1731,32 @@
|
||||
"tests/browser-compat"
|
||||
]
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -2016,6 +2137,20 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.83.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz",
|
||||
@@ -2300,6 +2435,13 @@
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2310,6 +2452,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||
@@ -2337,6 +2493,26 @@
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
|
||||
"integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
|
||||
"integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -2483,6 +2659,116 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
|
||||
"integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.1",
|
||||
"chai": "^6.2.2",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
"magic-string": "^1.2.3",
|
||||
"obug": "^2.1.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"std-env": "^4.2.0",
|
||||
"tinybench": "6.1.4",
|
||||
"tinyexec": "1.3.0",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.1",
|
||||
"@vitest/browser-preview": "5.0.1",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.1",
|
||||
"@vitest/coverage-v8": "5.0.1",
|
||||
"@vitest/ui": "5.0.1",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/magic-string": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz",
|
||||
"integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -12,7 +12,8 @@
|
||||
"update-data": "node scripts/fetch-models.mjs && node scripts/fetch-benchmarks.mjs && node scripts/build-data.mjs",
|
||||
"validate-data": "node scripts/validate-data.mjs",
|
||||
"lint": "oxlint",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
@@ -31,6 +32,7 @@
|
||||
"oxlint": "^1.81.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.3.0"
|
||||
"vite": "^8.3.0",
|
||||
"vitest": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
calculateAgentRequestCost,
|
||||
calculateBudgetResults,
|
||||
computeWeightedScore,
|
||||
computeValueScore,
|
||||
computeApplesToApples,
|
||||
buildStackCandidates,
|
||||
computeDaveStacks,
|
||||
computeMixAndMatch,
|
||||
matchesPlanModel,
|
||||
formatMillionTokens,
|
||||
} from './pricing';
|
||||
import type { CodingPlan, NormalizedModel, PlanTier, StackCandidate } from './types';
|
||||
|
||||
function model(overrides: Partial<NormalizedModel> = {}): NormalizedModel {
|
||||
return {
|
||||
id: 'anthropic/claude-sonnet-5',
|
||||
name: 'Claude Sonnet 5',
|
||||
provider: 'anthropic',
|
||||
modality: 'text->text',
|
||||
contextWindow: 200_000,
|
||||
maxOutput: 64_000,
|
||||
pricing: { input: 2, output: 10, cachedInput: null, cachedInputWrite: null, reasoning: null, webSearch: null },
|
||||
blendedCost: 4,
|
||||
costPer1kRequests: 14,
|
||||
benchmarks: { intelligenceIndex: 60, codingIndex: 70, agenticIndex: 50, valueScore: 175 },
|
||||
reasoning: null,
|
||||
isFree: false,
|
||||
isBatch: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function tier(overrides: Partial<PlanTier> = {}): PlanTier {
|
||||
return {
|
||||
name: 'Pro',
|
||||
monthlyPrice: 20,
|
||||
limits: { fastRequests: '100' },
|
||||
models: ['Claude Sonnet 5'],
|
||||
estimatedTokenBudget: {
|
||||
description: 'Test allowance',
|
||||
estimatedMillionTokens: 10,
|
||||
assumptions: 'test',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function plan(overrides: Partial<CodingPlan> = {}): CodingPlan {
|
||||
return {
|
||||
id: 'test-plan',
|
||||
name: 'Test Plan',
|
||||
category: 'coding-ide',
|
||||
url: 'https://example.com/pricing',
|
||||
lastVerified: '2026-09-18',
|
||||
tiers: [tier()],
|
||||
gotchas: ['test gotcha'],
|
||||
tosHighlights: [],
|
||||
dataTraining: 'No training',
|
||||
ipIndemnity: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function candidate(overrides: Partial<StackCandidate> = {}): StackCandidate {
|
||||
return {
|
||||
planId: 'a',
|
||||
planName: 'A',
|
||||
planCategory: 'coding-router',
|
||||
planUrl: 'https://example.com/a',
|
||||
tierName: 'Tier',
|
||||
modelName: 'Model',
|
||||
modelId: 'm',
|
||||
price: 10,
|
||||
tokens: 10,
|
||||
requests: 100,
|
||||
codingIndex: 60,
|
||||
lab: 'all',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('formatMillionTokens', () => {
|
||||
it('formats thousands, millions and billions', () => {
|
||||
expect(formatMillionTokens(0.5)).toBe('500K');
|
||||
expect(formatMillionTokens(5)).toBe('5.0M');
|
||||
expect(formatMillionTokens(1500)).toBe('1.5B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateAgentRequestCost', () => {
|
||||
it('prices 20k fresh input + 1k output at list rates with no cache', () => {
|
||||
const { costPerRequest, effectiveBlendedCost } = calculateAgentRequestCost(model(), 0);
|
||||
// 20k * $2/M + 1k * $10/M = 0.04 + 0.01 = 0.05
|
||||
expect(costPerRequest).toBeCloseTo(0.05, 10);
|
||||
expect(effectiveBlendedCost).toBeCloseTo((0.05 / 21000) * 1e6, 6);
|
||||
});
|
||||
|
||||
it('applies the documented provider cache discount when no cached price is stored', () => {
|
||||
const { costPerRequest } = calculateAgentRequestCost(model(), 0.75);
|
||||
// 5k fresh * $2/M + 15k cached * $0.2/M + 1k * $10/M = 0.01 + 0.003 + 0.01
|
||||
expect(costPerRequest).toBeCloseTo(0.023, 10);
|
||||
});
|
||||
|
||||
it('never returns a zero cost for a free request', () => {
|
||||
const free = model({ pricing: { input: 0, output: 0, cachedInput: null, cachedInputWrite: null, reasoning: null, webSearch: null } });
|
||||
const { costPerRequest } = calculateAgentRequestCost(free, 0);
|
||||
expect(costPerRequest).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeWeightedScore / computeValueScore', () => {
|
||||
it('weights coding 50%, agentic 30%, intelligence 20% when all present', () => {
|
||||
expect(computeWeightedScore(model())).toBeCloseTo(62, 6);
|
||||
});
|
||||
|
||||
it('computes value as quality per blended dollar', () => {
|
||||
expect(computeValueScore(model())).toBeCloseTo(1550, 6);
|
||||
});
|
||||
|
||||
it('returns 0 for a model with no benchmark data', () => {
|
||||
const bare = model({ benchmarks: { intelligenceIndex: null, codingIndex: null, agenticIndex: null, valueScore: null } });
|
||||
expect(computeWeightedScore(bare)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateBudgetResults', () => {
|
||||
const pool = [
|
||||
model({ id: 'ok', name: 'OK', blendedCost: 4 }),
|
||||
model({ id: 'weak', name: 'Weak', blendedCost: 1, benchmarks: { intelligenceIndex: 10, codingIndex: 30, agenticIndex: null, valueScore: null } }),
|
||||
model({ id: 'free', name: 'Free', blendedCost: 0, isFree: true }),
|
||||
model({ id: 'batch', name: 'Batch', blendedCost: 1, isBatch: true }),
|
||||
model({ id: 'zero', name: 'Zero', blendedCost: 0 }),
|
||||
];
|
||||
|
||||
it('excludes free, batch and zero-cost models', () => {
|
||||
const ids = calculateBudgetResults(pool, 20, 'max-tokens').map(r => r.modelId);
|
||||
expect(ids).toEqual(['weak', 'ok']);
|
||||
});
|
||||
|
||||
it('best-value only ranks models with coding index >= 40', () => {
|
||||
const ids = calculateBudgetResults(pool, 20, 'best-value').map(r => r.modelId);
|
||||
expect(ids).toEqual(['ok']);
|
||||
});
|
||||
|
||||
it('max-tokens ranks by raw volume', () => {
|
||||
const results = calculateBudgetResults(pool, 20, 'max-tokens');
|
||||
expect(results[0].modelId).toBe('weak');
|
||||
expect(results[0].millionTokens).toBeCloseTo(20, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPlanModel', () => {
|
||||
it('matches exact model ids and names', () => {
|
||||
expect(matchesPlanModel('Claude Sonnet 5', model())).toBe(true);
|
||||
expect(matchesPlanModel('claude-sonnet-5', model())).toBe(true);
|
||||
});
|
||||
|
||||
it('does not match unrelated models', () => {
|
||||
const gemini = model({ id: 'google/gemini-3.8-flash', name: 'Gemini 3.8 Flash' });
|
||||
expect(matchesPlanModel('Claude Opus 5', gemini)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeApplesToApples', () => {
|
||||
it('produces subscription rows at the plan price and API rows at agent-request rates', () => {
|
||||
const result = computeApplesToApples([model()], [plan()], 'all', 20, 0, 0.75);
|
||||
const sub = result.options.find(o => o.type === 'subscription');
|
||||
const api = result.options.find(o => o.type === 'api');
|
||||
expect(sub).toBeDefined();
|
||||
expect(sub!.monthlyCost).toBe(20);
|
||||
expect(sub!.monthlyTokens).toBeCloseTo(10, 6);
|
||||
expect(api).toBeDefined();
|
||||
expect(api!.monthlyTokens).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('excludes free, batch and excluded-legacy models from the API side', () => {
|
||||
const legacy = model({ id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo' });
|
||||
const free = model({ id: 'x/free', name: 'Free', isFree: true });
|
||||
const result = computeApplesToApples([model(), legacy, free], [], 'all', 20, 0, 0.75);
|
||||
const ids = result.options.map(o => o.id);
|
||||
expect(ids).toContain('anthropic/claude-sonnet-5');
|
||||
expect(ids).not.toContain('openai/gpt-3.5-turbo');
|
||||
expect(ids).not.toContain('x/free');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stack candidates', () => {
|
||||
it('includes paid tiers with a positive token budget', () => {
|
||||
const candidates = buildStackCandidates([model()], [plan()], 'all');
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(candidates[0].tokens).toBe(10);
|
||||
expect(candidates[0].requests).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('skips free tiers', () => {
|
||||
const freePlan = plan({ tiers: [tier({ monthlyPrice: 0 })] });
|
||||
expect(buildStackCandidates([model()], [freePlan], 'all')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeDaveStacks', () => {
|
||||
it('stacks whole copies that fit the budget', () => {
|
||||
const stacks = computeDaveStacks([candidate({ price: 20, tokens: 10 })], 50);
|
||||
expect(stacks).toHaveLength(1);
|
||||
expect(stacks[0].qty).toBe(2);
|
||||
expect(stacks[0].totalPrice).toBe(40);
|
||||
expect(stacks[0].totalTokens).toBe(20);
|
||||
});
|
||||
|
||||
it('needs at least two copies', () => {
|
||||
expect(computeDaveStacks([candidate({ price: 60, tokens: 10 })], 50)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeMixAndMatch', () => {
|
||||
it('only returns bundles within budget and with distinct plans', () => {
|
||||
const candidates = [
|
||||
candidate({ planId: 'a', price: 10, tokens: 10 }),
|
||||
candidate({ planId: 'b', price: 10, tokens: 8 }),
|
||||
candidate({ planId: 'c', price: 20, tokens: 15 }),
|
||||
candidate({ planId: 'd', price: 30, tokens: 40 }),
|
||||
];
|
||||
const bundles = computeMixAndMatch(candidates, 30, 3, 3);
|
||||
expect(bundles.length).toBeGreaterThan(0);
|
||||
for (const bundle of bundles) {
|
||||
expect(bundle.totalPrice).toBeLessThanOrEqual(30);
|
||||
expect(bundle.components.length).toBeGreaterThanOrEqual(2);
|
||||
expect(bundle.components.length).toBeLessThanOrEqual(3);
|
||||
expect(new Set(bundle.components.map(c => c.planId)).size).toBe(bundle.components.length);
|
||||
expect(bundle.totalTokens).toBe(bundle.components.reduce((sum, c) => sum + c.tokens, 0));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user