import { describe, it, expect } from "vitest"; import { hashText, hashBytes, computeFingerprint, jaccard, } from "./skillFingerprint"; describe("hashText / hashBytes", () => { it("produces a stable sha256 hex digest", () => { const h = hashText("hello world"); expect(h).toBe( "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", ); expect(h).toMatch(/^[0-9a-f]{64}$/); }); it("agrees between hashText and hashBytes for the same content", () => { const text = "some skill content\nwith two lines"; expect(hashText(text)).toBe(hashBytes(Buffer.from(text, "utf-8"))); }); it("changes when the content changes", () => { expect(hashText("a")).not.toBe(hashText("b")); }); }); describe("computeFingerprint", () => { const files = [ { path: "SKILL.md", hash: "aaa" }, { path: "scripts/run.sh", hash: "bbb" }, { path: "data.json", hash: "ccc" }, ]; it("is stable across calls with the same input", () => { expect(computeFingerprint(files)).toBe(computeFingerprint(files)); }); it("is independent of the order files are supplied in", () => { const reordered = [files[2], files[0], files[1]]; expect(computeFingerprint(reordered)).toBe(computeFingerprint(files)); }); it("changes when a file's content hash changes", () => { const changed = [ files[0], { path: "scripts/run.sh", hash: "DIFFERENT" }, files[2], ]; expect(computeFingerprint(changed)).not.toBe(computeFingerprint(files)); }); it("changes when a file's path changes", () => { const renamed = [ { path: "SKILL.md", hash: "aaa" }, { path: "scripts/start.sh", hash: "bbb" }, files[2], ]; expect(computeFingerprint(renamed)).not.toBe(computeFingerprint(files)); }); it("changes when a file is added or removed", () => { const fewer = [files[0], files[1]]; expect(computeFingerprint(fewer)).not.toBe(computeFingerprint(files)); }); it("returns a 64-char hex digest", () => { expect(computeFingerprint(files)).toMatch(/^[0-9a-f]{64}$/); }); }); describe("jaccard", () => { it("is 1 for identical non-empty sets", () => { expect(jaccard(new Set(["a", "b"]), new Set(["a", "b"]))).toBe(1); }); it("is 0 for disjoint sets", () => { expect(jaccard(new Set(["a"]), new Set(["b"]))).toBe(0); }); it("computes intersection over union for partial overlap", () => { // intersection {a} = 1, union {a,b,c} = 3 expect(jaccard(new Set(["a", "b"]), new Set(["a", "c"]))).toBeCloseTo( 1 / 3, 10, ); }); it("is symmetric", () => { const a = new Set(["a", "b", "c"]); const b = new Set(["b", "c", "d"]); expect(jaccard(a, b)).toBe(jaccard(b, a)); }); it("treats two empty sets as 0 (no overlap to report)", () => { expect(jaccard(new Set(), new Set())).toBe(0); }); });