Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 233 additions & 2 deletions frontend/src/lib/__tests__/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { equalWeights, parseManifest } from "../manifest";
import {
equalWeights,
parseCargoLock,
parseManifest,
parsePackageLock,
} from "../manifest";

describe("parseCargoToml", () => {
it("reads all common dependency forms", () => {
Expand Down Expand Up @@ -37,16 +42,242 @@ libc = "0.2" # comment
describe("parsePackageJson", () => {
it("reads deps, peer and dev deps without duplicates", () => {
const m = parseManifest(
JSON.stringify({ name: "x", dependencies: { a: "^1" }, peerDependencies: { b: "2" }, devDependencies: { a: "1", c: "3" } }),
JSON.stringify({
name: "x",
dependencies: { a: "^1" },
peerDependencies: { b: "2" },
devDependencies: { a: "1", c: "3" },
}),
);
expect(m.ecosystem).toBe("npm");
expect(m.deps.map((d) => `${d.name}:${d.kind}`)).toEqual(["a:normal", "b:peer", "c:dev"]);
});

it("errors on invalid json", () => {
expect(() => parseManifest("{ nope", "package.json")).toThrow(/valid JSON/);
});
});

describe("parseCargoLock", () => {
const cargoLockFixture = `
# This file is automatically @generated by Cargo.
version = 3

[[package]]
name = "flowpulse-contracts"
version = "0.1.0"
dependencies = [
"soroban-sdk",
"tokio 1.35.0",
]

[[package]]
name = "soroban-sdk"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"soroban-env-host",
"soroban-sdk-macros",
]

[[package]]
name = "soroban-env-host"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "soroban-sdk-macros"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "tokio"
version = "1.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"pin-project-lite",
]

[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
`;

it("extracts direct dependencies only by default", () => {
const m = parseCargoLock(cargoLockFixture);
expect(m.ecosystem).toBe("cargo");
expect(m.name).toBe("flowpulse-contracts");
expect(m.deps.map((d) => d.name)).toEqual(["soroban-sdk", "tokio"]);
expect(m.deps.find((d) => d.name === "tokio")?.version).toBe("1.35.0");
});

it("includes transitive dependencies when includeTransitive is true", () => {
const m = parseCargoLock(cargoLockFixture, { includeTransitive: true });
expect(m.name).toBe("flowpulse-contracts");
// Direct deps first: soroban-sdk, tokio. Then transitive: soroban-env-host, soroban-sdk-macros, pin-project-lite
const names = m.deps.map((d) => d.name);
expect(names).toContain("soroban-sdk");
expect(names).toContain("tokio");
expect(names).toContain("soroban-env-host");
expect(names).toContain("soroban-sdk-macros");
expect(names).toContain("pin-project-lite");
// Root package itself must not be included
expect(names).not.toContain("flowpulse-contracts");
expect(names.length).toBe(5);
});

it("caps total dependencies at 32 rows (or custom maxDeps)", () => {
// Generate a Cargo.lock with 40 packages
let bigLock = `[[package]]\nname = "root"\nversion = "0.1.0"\ndependencies = [\n`;
for (let i = 0; i < 40; i++) {
bigLock += ` "pkg-${i}",\n`;
}
bigLock += `]\n\n`;
for (let i = 0; i < 40; i++) {
bigLock += `[[package]]\nname = "pkg-${i}"\nversion = "1.0.0"\nsource = "registry"\n\n`;
}

const defaultCapped = parseCargoLock(bigLock);
expect(defaultCapped.deps.length).toBe(32);

const customCapped = parseCargoLock(bigLock, { maxDeps: 10 });
expect(customCapped.deps.length).toBe(10);
});
});

describe("parsePackageLock", () => {
const packageLockV3Fixture = JSON.stringify({
name: "flowpulse-frontend",
version: "0.1.0",
lockfileVersion: 3,
packages: {
"": {
name: "flowpulse-frontend",
version: "0.1.0",
dependencies: {
"@stellar/stellar-sdk": "^13.0.0",
react: "^19.0.0",
},
devDependencies: {
vite: "^6.0.0",
},
},
"node_modules/@stellar/stellar-sdk": {
version: "13.1.2",
dependencies: {
"@stellar/stellar-base": "13.1.2",
axios: "^1.7.0",
},
},
"node_modules/@stellar/stellar-base": {
version: "13.1.2",
},
"node_modules/axios": {
version: "1.7.4",
dependencies: {
"follow-redirects": "^1.15.6",
},
},
"node_modules/follow-redirects": {
version: "1.15.6",
},
"node_modules/react": {
version: "19.0.0",
},
"node_modules/vite": {
version: "6.0.7",
},
},
});

it("parses v3 direct dependencies only by default", () => {
const m = parsePackageLock(packageLockV3Fixture);
expect(m.ecosystem).toBe("npm");
expect(m.name).toBe("flowpulse-frontend");
const names = m.deps.map((d) => d.name);
expect(names).toEqual(["@stellar/stellar-sdk", "react", "vite"]);
expect(m.deps.find((d) => d.name === "@stellar/stellar-sdk")?.version).toBe("13.1.2");
});

it("parses v3 transitive dependencies when includeTransitive is true", () => {
const m = parsePackageLock(packageLockV3Fixture, { includeTransitive: true });
const names = m.deps.map((d) => d.name);
// Direct: @stellar/stellar-sdk, react, vite
// Transitive: @stellar/stellar-base, axios, follow-redirects
expect(names).toContain("@stellar/stellar-sdk");
expect(names).toContain("react");
expect(names).toContain("vite");
expect(names).toContain("@stellar/stellar-base");
expect(names).toContain("axios");
expect(names).toContain("follow-redirects");
expect(names).not.toContain("flowpulse-frontend");
expect(names.length).toBe(6);
});

const packageLockV1Fixture = JSON.stringify({
name: "legacy-app",
version: "1.0.0",
lockfileVersion: 1,
dependencies: {
lodash: {
version: "4.17.21",
},
request: {
version: "2.88.2",
dependencies: {
qs: {
version: "6.5.3",
},
},
},
},
});

it("parses v1 lockfiles with direct and transitive dependencies", () => {
const directOnly = parsePackageLock(packageLockV1Fixture, { includeTransitive: false });
expect(directOnly.deps.map((d) => d.name)).toEqual(["lodash", "request"]);

const withTransitive = parsePackageLock(packageLockV1Fixture, { includeTransitive: true });
expect(withTransitive.deps.map((d) => d.name)).toEqual(["lodash", "request", "qs"]);
});

it("errors on invalid json package-lock", () => {
expect(() => parsePackageLock("{ not json")).toThrow(/valid JSON/);
});
});

describe("parseManifest routing & integration", () => {
it("detects and parses Cargo.lock by filename", () => {
const lock = `[[package]]\nname = "root"\nversion = "1.0"\ndependencies = ["depA"]\n\n[[package]]\nname = "depA"\nversion = "0.2"`;
const m = parseManifest(lock, "Cargo.lock");
expect(m.ecosystem).toBe("cargo");
expect(m.deps.map((d) => d.name)).toEqual(["depA"]);
});

it("detects and parses Cargo.lock by [[package]] content without filename", () => {
const lock = `[[package]]\nname = "root"\nversion = "1.0"\ndependencies = ["depA"]\n\n[[package]]\nname = "depA"\nversion = "0.2"`;
const m = parseManifest(lock);
expect(m.ecosystem).toBe("cargo");
expect(m.deps.map((d) => d.name)).toEqual(["depA"]);
});

it("detects and parses package-lock.json by filename and content", () => {
const lock = JSON.stringify({
name: "app",
lockfileVersion: 3,
packages: {
"": { dependencies: { chalk: "^5" } },
"node_modules/chalk": { version: "5.3.0" },
},
});
const m = parseManifest(lock, "package-lock.json");
expect(m.ecosystem).toBe("npm");
expect(m.deps[0].name).toBe("chalk");
expect(m.deps[0].version).toBe("5.3.0");
});
});

describe("equalWeights", () => {
it("sums exactly to the target", () => {
const w = equalWeights(7, 9000);
Expand Down
Loading