Add automated tests for skill version detection
Task #13: lock in the fingerprint/relation logic behind SkillGuard's identical/modified/new version detection with automated tests. What was added - Set up Vitest in artifacts/api-server (dev dep + `test` script + vitest.config.ts using the "workspace" resolve condition so @workspace/* resolve to source). - Unit tests (no DB): - src/lib/skillFingerprint.test.ts — hashText/hashBytes stability & agreement, computeFingerprint stable + order-independent + sensitive to content/path/add/remove, jaccard overlap/symmetry/empty handling. - src/lib/lineDiff.test.ts — lineSimilarity ratios (identical, single-edit, disjoint, symmetric, CRLF), lineDiff context/add/remove with line numbers and the 2000-line cap. - DB-backed tests (use the existing DATABASE_URL): - src/routes/relation.test.ts — computeRelation: identical content under a different name -> "identical" + check-counter (countFingerprint) increments; one-line edit to a single-file skill -> "modified" with sensible similarity; unrelated skill -> "new". Also direct computeContentSimilarity cases. Fixtures use randomized content to avoid collisions with shared dev data and are cleaned up afterEach. - src/routes/compare.test.ts — e2e GET /api/scans/:id/compare/:otherId via a live server: asserts unchanged/modified/added/removed statuses, sorted file order, the line diff for the modified file, null diffs elsewhere, and 404 for missing scans. Production code change - Exported computeRelation, computeContentSimilarity, countFingerprint from src/routes/scans.ts so the relation logic can be unit-tested. No behavior change. Verification - `pnpm --filter @workspace/api-server run test` -> 34 tests, 4 files, all pass. - `pnpm --filter @workspace/api-server run typecheck` passes (rebuilt stale lib/db declarations via `pnpm run typecheck:libs`). - Production build unaffected: esbuild only bundles from src/index.ts, so *.test.ts files are not included. Replit-Task-Id: e9ae5e24-1480-4a09-8436-1718c535573a
This commit is contained in:
parent
54323706b5
commit
532f42117f
8 changed files with 803 additions and 5 deletions
|
|
@ -7,7 +7,8 @@
|
||||||
"dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
|
"dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
|
||||||
"build": "node ./build.mjs",
|
"build": "node ./build.mjs",
|
||||||
"start": "node --enable-source-maps ./dist/index.mjs",
|
"start": "node --enable-source-maps ./dist/index.mjs",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workspace/api-zod": "workspace:*",
|
"@workspace/api-zod": "workspace:*",
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
"esbuild": "0.27.3",
|
"esbuild": "0.27.3",
|
||||||
"esbuild-plugin-pino": "^2.3.3",
|
"esbuild-plugin-pino": "^2.3.3",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"thread-stream": "3.1.0"
|
"thread-stream": "3.1.0",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
artifacts/api-server/src/lib/lineDiff.test.ts
Normal file
95
artifacts/api-server/src/lib/lineDiff.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { lineSimilarity, lineDiff } from "./lineDiff";
|
||||||
|
|
||||||
|
describe("lineSimilarity", () => {
|
||||||
|
it("is 1 for identical text", () => {
|
||||||
|
const text = "line one\nline two\nline three";
|
||||||
|
expect(lineSimilarity(text, text)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is 1 for two empty strings", () => {
|
||||||
|
expect(lineSimilarity("", "")).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is high for a single changed line", () => {
|
||||||
|
const prev = "a\nb\nc\nd";
|
||||||
|
const curr = "a\nB\nc\nd"; // one of four lines changed
|
||||||
|
const sim = lineSimilarity(prev, curr);
|
||||||
|
// LCS = {a,c,d} = 3, ratio = 2*3 / (4+4) = 0.75
|
||||||
|
expect(sim).toBeCloseTo(0.75, 10);
|
||||||
|
expect(sim).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is 0 when nothing is in common", () => {
|
||||||
|
expect(lineSimilarity("a\nb\nc", "x\ny\nz")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is symmetric", () => {
|
||||||
|
const a = "one\ntwo\nthree\nfour";
|
||||||
|
const b = "one\ntwo\nTHREE\nfour\nfive";
|
||||||
|
expect(lineSimilarity(a, b)).toBeCloseTo(lineSimilarity(b, a), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles \\r\\n and \\n line endings equivalently", () => {
|
||||||
|
expect(lineSimilarity("a\r\nb", "a\nb")).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("lineDiff", () => {
|
||||||
|
it("marks every line as context for identical input", () => {
|
||||||
|
const text = "a\nb\nc";
|
||||||
|
const diff = lineDiff(text, text);
|
||||||
|
expect(diff).not.toBeNull();
|
||||||
|
expect(diff!.every((d) => d.type === "context")).toBe(true);
|
||||||
|
expect(diff!.map((d) => d.text)).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records a removed and an added line for a single-line edit", () => {
|
||||||
|
const diff = lineDiff("a\nb\nc", "a\nB\nc");
|
||||||
|
expect(diff).not.toBeNull();
|
||||||
|
const removed = diff!.filter((d) => d.type === "remove");
|
||||||
|
const added = diff!.filter((d) => d.type === "add");
|
||||||
|
expect(removed.map((d) => d.text)).toEqual(["b"]);
|
||||||
|
expect(added.map((d) => d.text)).toEqual(["B"]);
|
||||||
|
// unchanged lines remain context
|
||||||
|
expect(diff!.filter((d) => d.type === "context").map((d) => d.text)).toEqual(
|
||||||
|
["a", "c"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets correct line numbers for context, add and remove", () => {
|
||||||
|
const diff = lineDiff("a\nb", "a\nb\nc")!;
|
||||||
|
const added = diff.find((d) => d.type === "add")!;
|
||||||
|
expect(added.text).toBe("c");
|
||||||
|
expect(added.previousLine).toBeNull();
|
||||||
|
expect(added.currentLine).toBe(3);
|
||||||
|
|
||||||
|
const firstContext = diff.find((d) => d.type === "context")!;
|
||||||
|
expect(firstContext.previousLine).toBe(1);
|
||||||
|
expect(firstContext.currentLine).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a pure addition as only added lines", () => {
|
||||||
|
const diff = lineDiff("a", "a\nb\nc")!;
|
||||||
|
expect(diff.filter((d) => d.type === "remove")).toHaveLength(0);
|
||||||
|
expect(diff.filter((d) => d.type === "add").map((d) => d.text)).toEqual([
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a pure removal as only removed lines", () => {
|
||||||
|
const diff = lineDiff("a\nb\nc", "a")!;
|
||||||
|
expect(diff.filter((d) => d.type === "add")).toHaveLength(0);
|
||||||
|
expect(diff.filter((d) => d.type === "remove").map((d) => d.text)).toEqual([
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when either side exceeds the diff line cap", () => {
|
||||||
|
const big = Array.from({ length: 2001 }, (_, i) => `line ${i}`).join("\n");
|
||||||
|
expect(lineDiff(big, "a")).toBeNull();
|
||||||
|
expect(lineDiff("a", big)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
artifacts/api-server/src/lib/skillFingerprint.test.ts
Normal file
98
artifacts/api-server/src/lib/skillFingerprint.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
151
artifacts/api-server/src/routes/compare.test.ts
Normal file
151
artifacts/api-server/src/routes/compare.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
import type { Server } from "node:http";
|
||||||
|
import app from "../app";
|
||||||
|
import { db, pool, scansTable, scanFilesTable } from "@workspace/db";
|
||||||
|
import { inArray } from "drizzle-orm";
|
||||||
|
import { hashText, computeFingerprint } from "../lib/skillFingerprint";
|
||||||
|
|
||||||
|
const emptyCounts = {
|
||||||
|
critical: 0,
|
||||||
|
high: 0,
|
||||||
|
medium: 0,
|
||||||
|
low: 0,
|
||||||
|
info: 0,
|
||||||
|
security: 0,
|
||||||
|
privacy: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
type FixtureFile = { path: string; content: string };
|
||||||
|
|
||||||
|
const createdScanIds: number[] = [];
|
||||||
|
let server: Server;
|
||||||
|
let baseUrl: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server = app.listen(0, () => resolve());
|
||||||
|
});
|
||||||
|
const { port } = server.address() as AddressInfo;
|
||||||
|
baseUrl = `http://127.0.0.1:${port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (createdScanIds.length > 0) {
|
||||||
|
await db
|
||||||
|
.delete(scansTable)
|
||||||
|
.where(inArray(scansTable.id, createdScanIds.splice(0)));
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((err) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
await pool.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function insertScan(
|
||||||
|
name: string,
|
||||||
|
files: FixtureFile[],
|
||||||
|
): Promise<number> {
|
||||||
|
const withHash = files.map((f) => ({ ...f, hash: hashText(f.content) }));
|
||||||
|
const fingerprint = computeFingerprint(
|
||||||
|
withHash.map((f) => ({ path: f.path, hash: f.hash })),
|
||||||
|
);
|
||||||
|
const [scan] = await db
|
||||||
|
.insert(scansTable)
|
||||||
|
.values({
|
||||||
|
name,
|
||||||
|
source: "zip",
|
||||||
|
status: "completed",
|
||||||
|
verdict: "pass",
|
||||||
|
riskScore: 0,
|
||||||
|
fileCount: files.length,
|
||||||
|
aiUsed: false,
|
||||||
|
findingCounts: emptyCounts,
|
||||||
|
fingerprint,
|
||||||
|
relation: "new",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
createdScanIds.push(scan.id);
|
||||||
|
await db.insert(scanFilesTable).values(
|
||||||
|
withHash.map((f) => ({
|
||||||
|
scanId: scan.id,
|
||||||
|
path: f.path,
|
||||||
|
kind: f.path.toLowerCase().endsWith(".md") ? "instruction" : "resource",
|
||||||
|
language: null,
|
||||||
|
size: Buffer.byteLength(f.content, "utf-8"),
|
||||||
|
hash: f.hash,
|
||||||
|
content: f.content,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return scan.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GET /api/scans/:id/compare/:otherId", () => {
|
||||||
|
it("returns correct per-file statuses and a line diff for the modified file", async () => {
|
||||||
|
const previousId = await insertScan("v1", [
|
||||||
|
{ path: "SKILL.md", content: "# Skill\nstep one\nstep two\nstep three" },
|
||||||
|
{ path: "removed.md", content: "this file goes away" },
|
||||||
|
{ path: "stable.md", content: "never changes" },
|
||||||
|
]);
|
||||||
|
const currentId = await insertScan("v2", [
|
||||||
|
{
|
||||||
|
path: "SKILL.md",
|
||||||
|
content: "# Skill\nstep one\nstep TWO\nstep three",
|
||||||
|
},
|
||||||
|
{ path: "added.md", content: "brand new file" },
|
||||||
|
{ path: "stable.md", content: "never changes" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${baseUrl}/api/scans/${currentId}/compare/${previousId}`,
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = (await res.json()) as {
|
||||||
|
current: { id: number };
|
||||||
|
previous: { id: number };
|
||||||
|
files: {
|
||||||
|
path: string;
|
||||||
|
status: string;
|
||||||
|
lineDiff: { type: string; text: string }[] | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(body.current.id).toBe(currentId);
|
||||||
|
expect(body.previous.id).toBe(previousId);
|
||||||
|
|
||||||
|
const byPath: Record<string, (typeof body.files)[number]> =
|
||||||
|
Object.fromEntries(body.files.map((f) => [f.path, f]));
|
||||||
|
|
||||||
|
expect(byPath["SKILL.md"].status).toBe("modified");
|
||||||
|
expect(byPath["stable.md"].status).toBe("unchanged");
|
||||||
|
expect(byPath["added.md"].status).toBe("added");
|
||||||
|
expect(byPath["removed.md"].status).toBe("removed");
|
||||||
|
|
||||||
|
// Files are returned sorted by path.
|
||||||
|
expect(body.files.map((f: { path: string }) => f.path)).toEqual(
|
||||||
|
["SKILL.md", "added.md", "removed.md", "stable.md"],
|
||||||
|
);
|
||||||
|
|
||||||
|
// The modified text file carries a line diff with the changed line.
|
||||||
|
const diff = byPath["SKILL.md"].lineDiff as
|
||||||
|
| { type: string; text: string }[]
|
||||||
|
| null;
|
||||||
|
expect(diff).not.toBeNull();
|
||||||
|
expect(diff!.find((d) => d.type === "remove")?.text).toBe("step two");
|
||||||
|
expect(diff!.find((d) => d.type === "add")?.text).toBe("step TWO");
|
||||||
|
expect(diff!.filter((d) => d.type === "context").map((d) => d.text)).toEqual(
|
||||||
|
["# Skill", "step one", "step three"],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unchanged / added / removed files do not get a line diff.
|
||||||
|
expect(byPath["stable.md"].lineDiff).toBeNull();
|
||||||
|
expect(byPath["added.md"].lineDiff).toBeNull();
|
||||||
|
expect(byPath["removed.md"].lineDiff).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 when a scan does not exist", async () => {
|
||||||
|
const res = await fetch(`${baseUrl}/api/scans/999999999/compare/999999998`);
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
195
artifacts/api-server/src/routes/relation.test.ts
Normal file
195
artifacts/api-server/src/routes/relation.test.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { describe, it, expect, afterEach, afterAll } from "vitest";
|
||||||
|
import { db, pool, scansTable, scanFilesTable } from "@workspace/db";
|
||||||
|
import { eq, inArray } from "drizzle-orm";
|
||||||
|
import { hashText, computeFingerprint } from "../lib/skillFingerprint";
|
||||||
|
import type { ParsedFile } from "../lib/ruleCatalog";
|
||||||
|
import {
|
||||||
|
computeRelation,
|
||||||
|
computeContentSimilarity,
|
||||||
|
countFingerprint,
|
||||||
|
} from "./scans";
|
||||||
|
|
||||||
|
const emptyCounts = {
|
||||||
|
critical: 0,
|
||||||
|
high: 0,
|
||||||
|
medium: 0,
|
||||||
|
low: 0,
|
||||||
|
info: 0,
|
||||||
|
security: 0,
|
||||||
|
privacy: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdScanIds: number[] = [];
|
||||||
|
|
||||||
|
function file(path: string, content: string): ParsedFile {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
kind: path.toLowerCase().endsWith(".md") ? "instruction" : "resource",
|
||||||
|
language: null,
|
||||||
|
content,
|
||||||
|
size: Buffer.byteLength(content, "utf-8"),
|
||||||
|
hash: hashText(content),
|
||||||
|
isBinary: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertScan(name: string, files: ParsedFile[]): Promise<number> {
|
||||||
|
const fingerprint = computeFingerprint(
|
||||||
|
files.map((f) => ({ path: f.path, hash: f.hash })),
|
||||||
|
);
|
||||||
|
const [scan] = await db
|
||||||
|
.insert(scansTable)
|
||||||
|
.values({
|
||||||
|
name,
|
||||||
|
source: "text",
|
||||||
|
status: "completed",
|
||||||
|
verdict: "pass",
|
||||||
|
riskScore: 0,
|
||||||
|
fileCount: files.length,
|
||||||
|
aiUsed: false,
|
||||||
|
findingCounts: emptyCounts,
|
||||||
|
fingerprint,
|
||||||
|
relation: "new",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
createdScanIds.push(scan.id);
|
||||||
|
await db.insert(scanFilesTable).values(
|
||||||
|
files.map((f) => ({
|
||||||
|
scanId: scan.id,
|
||||||
|
path: f.path,
|
||||||
|
kind: f.kind,
|
||||||
|
language: f.language,
|
||||||
|
size: f.size,
|
||||||
|
hash: f.hash,
|
||||||
|
content: f.isBinary ? null : f.content,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return scan.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (createdScanIds.length > 0) {
|
||||||
|
await db
|
||||||
|
.delete(scansTable)
|
||||||
|
.where(inArray(scansTable.id, createdScanIds.splice(0)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await pool.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeRelation", () => {
|
||||||
|
it("flags byte-identical content (with a different name) as identical and bumps the check counter", async () => {
|
||||||
|
// Use highly unique content so the fingerprint cannot collide with any
|
||||||
|
// pre-existing scans in the shared dev database.
|
||||||
|
const body =
|
||||||
|
"# Unique relation-test skill " +
|
||||||
|
Math.random().toString(36).slice(2) +
|
||||||
|
"\nDo a very specific thing.\n";
|
||||||
|
const original = [file("SKILL.md", body)];
|
||||||
|
const baselineId = await insertScan("Original Name", original);
|
||||||
|
|
||||||
|
const fingerprint = computeFingerprint(
|
||||||
|
original.map((f) => ({ path: f.path, hash: f.hash })),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A re-upload of the exact same bytes under a different display name.
|
||||||
|
const relation = await computeRelation(fingerprint, original);
|
||||||
|
expect(relation.relation).toBe("identical");
|
||||||
|
expect(relation.similarity).toBe(100);
|
||||||
|
expect(relation.comparedScanId).toBe(baselineId);
|
||||||
|
|
||||||
|
// Check counter: only the baseline exists so far -> 1.
|
||||||
|
expect(await countFingerprint(fingerprint)).toBe(1);
|
||||||
|
// Storing the identical re-upload increments the counter to 2.
|
||||||
|
await insertScan("Different Name", original);
|
||||||
|
expect(await countFingerprint(fingerprint)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags a one-line edit to a single-file skill as modified with a sensible similarity", async () => {
|
||||||
|
const tag = Math.random().toString(36).slice(2);
|
||||||
|
const baseLines = [
|
||||||
|
`# Modified-test skill ${tag}`,
|
||||||
|
"Step one: gather inputs.",
|
||||||
|
"Step two: process them.",
|
||||||
|
"Step three: report results.",
|
||||||
|
"Step four: clean up.",
|
||||||
|
];
|
||||||
|
const original = [file("SKILL.md", baseLines.join("\n"))];
|
||||||
|
const baselineId = await insertScan("Edited Skill", original);
|
||||||
|
|
||||||
|
// Change exactly one line.
|
||||||
|
const editedLines = [...baseLines];
|
||||||
|
editedLines[2] = "Step two: process them carefully.";
|
||||||
|
const edited = [file("SKILL.md", editedLines.join("\n"))];
|
||||||
|
const fingerprint = computeFingerprint(
|
||||||
|
edited.map((f) => ({ path: f.path, hash: f.hash })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const relation = await computeRelation(fingerprint, edited);
|
||||||
|
expect(relation.relation).toBe("modified");
|
||||||
|
expect(relation.comparedScanId).toBe(baselineId);
|
||||||
|
expect(relation.similarity).not.toBeNull();
|
||||||
|
// One of five lines changed -> high but not perfect similarity.
|
||||||
|
expect(relation.similarity!).toBeGreaterThanOrEqual(60);
|
||||||
|
expect(relation.similarity!).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flags an unrelated single-file skill as new", async () => {
|
||||||
|
const tag = Math.random().toString(36).slice(2);
|
||||||
|
// Establish a baseline skill.
|
||||||
|
await insertScan("Baseline Skill", [
|
||||||
|
file(
|
||||||
|
"SKILL.md",
|
||||||
|
`# Baseline ${tag}\nalpha bravo charlie\ndelta echo foxtrot\ngolf hotel india`,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// A completely different single-file skill. It shares the common path
|
||||||
|
// "SKILL.md" but has no content overlap, so it must be "new".
|
||||||
|
const unrelated = [
|
||||||
|
file(
|
||||||
|
"SKILL.md",
|
||||||
|
`# Totally different ${tag}\nzulu yankee xray\nwhiskey victor uniform\ntango sierra romeo`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const fingerprint = computeFingerprint(
|
||||||
|
unrelated.map((f) => ({ path: f.path, hash: f.hash })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const relation = await computeRelation(fingerprint, unrelated);
|
||||||
|
expect(relation.relation).toBe("new");
|
||||||
|
expect(relation.similarity).toBeNull();
|
||||||
|
expect(relation.comparedScanId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeContentSimilarity", () => {
|
||||||
|
it("is 100 when every file is byte-identical", () => {
|
||||||
|
const files = [file("SKILL.md", "same content\nline two")];
|
||||||
|
const prior = new Map(
|
||||||
|
files.map((f) => [f.path, { hash: f.hash, content: f.content }]),
|
||||||
|
);
|
||||||
|
expect(computeContentSimilarity(files, prior)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses line-level similarity for a changed text file", () => {
|
||||||
|
const current = [file("SKILL.md", "a\nB\nc\nd")];
|
||||||
|
const prior = new Map([
|
||||||
|
["SKILL.md", { hash: hashText("a\nb\nc\nd"), content: "a\nb\nc\nd" }],
|
||||||
|
]);
|
||||||
|
// 3 of 4 lines shared -> 0.75 -> 75 (single shared path).
|
||||||
|
expect(computeContentSimilarity(current, prior)).toBe(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("counts added or removed files as zero contribution", () => {
|
||||||
|
const current = [file("SKILL.md", "shared"), file("extra.md", "new file")];
|
||||||
|
const prior = new Map([
|
||||||
|
["SKILL.md", { hash: hashText("shared"), content: "shared" }],
|
||||||
|
]);
|
||||||
|
// SKILL.md identical (1.0), extra.md added (0). Union of paths = 2 -> 50.
|
||||||
|
expect(computeContentSimilarity(current, prior)).toBe(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -90,7 +90,7 @@ async function resolveComparedScan(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function countFingerprint(fingerprint: string): Promise<number> {
|
export async function countFingerprint(fingerprint: string): Promise<number> {
|
||||||
if (!fingerprint) return 1;
|
if (!fingerprint) return 1;
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({ c: count() })
|
.select({ c: count() })
|
||||||
|
|
@ -156,7 +156,7 @@ type RelationInfo = {
|
||||||
* skill (when it overlaps enough or shares a byte-identical file) -> modified;
|
* skill (when it overlaps enough or shares a byte-identical file) -> modified;
|
||||||
* nothing meaningfully in common -> new.
|
* nothing meaningfully in common -> new.
|
||||||
*/
|
*/
|
||||||
async function computeRelation(
|
export async function computeRelation(
|
||||||
fingerprint: string,
|
fingerprint: string,
|
||||||
files: ParsedFile[],
|
files: ParsedFile[],
|
||||||
): Promise<RelationInfo> {
|
): Promise<RelationInfo> {
|
||||||
|
|
@ -261,7 +261,7 @@ const MODIFIED_SIMILARITY_THRESHOLD = 40;
|
||||||
* scan. Identical files (same hash) count fully; changed text files use the
|
* scan. Identical files (same hash) count fully; changed text files use the
|
||||||
* line-level similarity; added/removed or changed binary files count as 0.
|
* line-level similarity; added/removed or changed binary files count as 0.
|
||||||
*/
|
*/
|
||||||
function computeContentSimilarity(
|
export function computeContentSimilarity(
|
||||||
newFiles: ParsedFile[],
|
newFiles: ParsedFile[],
|
||||||
prior: Map<string, { hash: string; content: string | null }>,
|
prior: Map<string, { hash: string; content: string | null }>,
|
||||||
): number {
|
): number {
|
||||||
|
|
|
||||||
11
artifacts/api-server/vitest.config.ts
Normal file
11
artifacts/api-server/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
conditions: ["workspace"],
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
environment: "node",
|
||||||
|
},
|
||||||
|
});
|
||||||
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
|
|
@ -218,6 +218,9 @@ importers:
|
||||||
thread-stream:
|
thread-stream:
|
||||||
specifier: 3.1.0
|
specifier: 3.1.0
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.8
|
||||||
|
version: 4.1.8(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)
|
||||||
|
|
||||||
artifacts/mockup-sandbox:
|
artifacts/mockup-sandbox:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|
@ -1568,6 +1571,9 @@ packages:
|
||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@tabby_ai/hijri-converter@1.0.5':
|
'@tabby_ai/hijri-converter@1.0.5':
|
||||||
resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
|
resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
@ -1631,6 +1637,9 @@ packages:
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
|
|
@ -1669,6 +1678,9 @@ packages:
|
||||||
'@types/d3-timer@3.0.2':
|
'@types/d3-timer@3.0.2':
|
||||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2':
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
|
@ -1719,6 +1731,35 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.8':
|
||||||
|
resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==}
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.8':
|
||||||
|
resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.8':
|
||||||
|
resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.8':
|
||||||
|
resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.8':
|
||||||
|
resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.8':
|
||||||
|
resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.8':
|
||||||
|
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -1757,6 +1798,10 @@ packages:
|
||||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
@ -1802,6 +1847,10 @@ packages:
|
||||||
caniuse-lite@1.0.30001792:
|
caniuse-lite@1.0.30001792:
|
||||||
resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
|
resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
|
||||||
|
|
||||||
|
chai@6.2.2:
|
||||||
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
@ -2109,6 +2158,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0:
|
||||||
|
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
es-object-atoms@1.1.1:
|
||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2138,6 +2190,9 @@ packages:
|
||||||
escape-html@1.0.3:
|
escape-html@1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -2153,6 +2208,10 @@ packages:
|
||||||
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
||||||
engines: {node: ^18.19.0 || >=20.5.0}
|
engines: {node: ^18.19.0 || >=20.5.0}
|
||||||
|
|
||||||
|
expect-type@1.3.0:
|
||||||
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
@ -2527,6 +2586,10 @@ packages:
|
||||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
obug@2.1.2:
|
||||||
|
resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2:
|
on-exit-leak-free@2.1.2:
|
||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
@ -2900,6 +2963,9 @@ packages:
|
||||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
signal-exit@4.1.0:
|
signal-exit@4.1.0:
|
||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -2925,10 +2991,16 @@ packages:
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
statuses@2.0.2:
|
statuses@2.0.2:
|
||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
std-env@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||||
|
|
||||||
string-argv@0.3.2:
|
string-argv@0.3.2:
|
||||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||||
engines: {node: '>=0.6.19'}
|
engines: {node: '>=0.6.19'}
|
||||||
|
|
@ -2966,10 +3038,21 @@ packages:
|
||||||
tiny-invariant@1.3.3:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@1.2.4:
|
||||||
|
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
tinyglobby@0.2.16:
|
||||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
@ -3136,11 +3219,56 @@ packages:
|
||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.8:
|
||||||
|
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.8
|
||||||
|
'@vitest/browser-preview': 4.1.8
|
||||||
|
'@vitest/browser-webdriverio': 4.1.8
|
||||||
|
'@vitest/coverage-istanbul': 4.1.8
|
||||||
|
'@vitest/coverage-v8': 4.1.8
|
||||||
|
'@vitest/ui': 4.1.8
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
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
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
wouter@3.10.0:
|
wouter@3.10.0:
|
||||||
resolution: {integrity: sha512-zTfddD80zc2/J5l8JKcdvzOK6AwP0kpyHEI3DxRN2bn8U1oJPnrSVm8v+X3WwDamvLAOxTO7ZvkxkpRWlyeJ1Q==}
|
resolution: {integrity: sha512-zTfddD80zc2/J5l8JKcdvzOK6AwP0kpyHEI3DxRN2bn8U1oJPnrSVm8v+X3WwDamvLAOxTO7ZvkxkpRWlyeJ1Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -4245,6 +4373,8 @@ snapshots:
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tabby_ai/hijri-converter@1.0.5': {}
|
'@tabby_ai/hijri-converter@1.0.5': {}
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.0':
|
'@tailwindcss/node@4.3.0':
|
||||||
|
|
@ -4313,6 +4443,11 @@ snapshots:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 25.6.2
|
'@types/node': 25.6.2
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.6.2
|
'@types/node': 25.6.2
|
||||||
|
|
@ -4349,6 +4484,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/d3-timer@3.0.2': {}
|
'@types/d3-timer@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.1.1':
|
'@types/express-serve-static-core@5.1.1':
|
||||||
|
|
@ -4415,6 +4552,47 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.8(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.8': {}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.8':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|
@ -4447,6 +4625,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
@ -4497,6 +4677,8 @@ snapshots:
|
||||||
|
|
||||||
caniuse-lite@1.0.30001792: {}
|
caniuse-lite@1.0.30001792: {}
|
||||||
|
|
||||||
|
chai@6.2.2: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
@ -4685,6 +4867,8 @@ snapshots:
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0: {}
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
es-object-atoms@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
@ -4705,6 +4889,10 @@ snapshots:
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
@ -4726,6 +4914,8 @@ snapshots:
|
||||||
strip-final-newline: 4.0.0
|
strip-final-newline: 4.0.0
|
||||||
yoctocolors: 2.1.2
|
yoctocolors: 2.1.2
|
||||||
|
|
||||||
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 2.0.0
|
accepts: 2.0.0
|
||||||
|
|
@ -5071,6 +5261,8 @@ snapshots:
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
|
obug@2.1.2: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1:
|
||||||
|
|
@ -5494,6 +5686,8 @@ snapshots:
|
||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
slash@5.1.0: {}
|
slash@5.1.0: {}
|
||||||
|
|
@ -5511,8 +5705,12 @@ snapshots:
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
std-env@4.1.0: {}
|
||||||
|
|
||||||
string-argv@0.3.2: {}
|
string-argv@0.3.2: {}
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
|
|
@ -5539,11 +5737,17 @@ snapshots:
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@1.2.4: {}
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
tinyglobby@0.2.16:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
@ -5673,10 +5877,52 @@ snapshots:
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.4
|
yaml: 2.8.4
|
||||||
|
|
||||||
|
vitest@4.1.8(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.8
|
||||||
|
'@vitest/mocker': 4.1.8(vite@7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4))
|
||||||
|
'@vitest/pretty-format': 4.1.8
|
||||||
|
'@vitest/runner': 4.1.8
|
||||||
|
'@vitest/snapshot': 4.1.8
|
||||||
|
'@vitest/spy': 4.1.8
|
||||||
|
'@vitest/utils': 4.1.8
|
||||||
|
es-module-lexer: 2.1.0
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.2
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
std-env: 4.1.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.2.4
|
||||||
|
tinyglobby: 0.2.16
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
vite: 7.3.3(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 25.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- jiti
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- msw
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
wouter@3.10.0(react@19.1.0):
|
wouter@3.10.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
mitt: 3.0.1
|
mitt: 3.0.1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue