// File: composite.test.ts // Path: packages/scoring/src/composite.test.ts // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Snapshot tests against the published methodology worked example. import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { HIGH_EXPOSURE_THRESHOLD, scoreOccupation } from "./index"; // Published worked example — METHODOLOGY.md §5-6. Changing it requires an INDEX_VERSION bump. const example = JSON.parse( readFileSync(new URL("../../../docs/methodology/examples/example-analyst.json", import.meta.url), "utf8"), ); const BAND_KEYS = ["substitution", "exposure", "augmentation"] as const; describe("published methodology example (example-analyst.json)", () => { const result = scoreOccupation(example.tasks); it("matches the expected task scores", () => { expect(result.tasks).toHaveLength(example.expected.taskScores.length); for (const [index, expected] of example.expected.taskScores.entries()) { const actual = result.tasks[index]; expect(actual.taskId).toBe(expected.taskId); for (const key of BAND_KEYS) { expect(actual[key].low).toBeCloseTo(expected[key].low, 3); expect(actual[key].score).toBeCloseTo(expected[key].score, 3); expect(actual[key].high).toBeCloseTo(expected[key].high, 3); } } }); it("matches the expected occupation scores", () => { for (const key of BAND_KEYS) { expect(result[key].low).toBeCloseTo(example.expected.occupation[key].low, 3); expect(result[key].score).toBeCloseTo(example.expected.occupation[key].score, 3); expect(result[key].high).toBeCloseTo(example.expected.occupation[key].high, 3); } }); it("reports the highly exposed task share", () => { expect(HIGH_EXPOSURE_THRESHOLD).toBe(70); expect(result.highlyExposedTaskShare).toBeCloseTo( example.expected.occupation.highlyExposedTaskShare, 3, ); }); }); describe("scoreOccupation input handling", () => { it("rejects an empty task list", () => { expect(() => scoreOccupation([])).toThrow(RangeError); }); it("fills missing importance with the occupation mean", () => { const [t1, t2, t3] = example.tasks; const withMissing = [t1, { ...t2, importance: undefined }, t3]; const result = scoreOccupation(withMissing); // t2 gets mean(4, 1) = 2.5 → weights 4/7.5, 2.5/7.5, 1/7.5 const expected = (4 / 7.5) * 71.25 + (2.5 / 7.5) * 27.5 + (1 / 7.5) * 86.25; expect(result.substitution.score).toBeCloseTo(expected, 3); }); it("rejects out-of-range ratings", () => { const bad = JSON.parse(JSON.stringify(example.tasks[0])); bad.ratings.automatability.mid = 6; expect(() => scoreOccupation([bad])).toThrow(RangeError); }); it("rejects inverted bands (low > high)", () => { const bad = JSON.parse(JSON.stringify(example.tasks[0])); bad.ratings.feasibility = { low: 4, mid: 3, high: 2 }; expect(() => scoreOccupation([bad])).toThrow(RangeError); }); });