Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .act-replies-43.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
PRRT_kwDOTyoI9s6YxeZ4 Fixed: resolved steps now set runtime.workingDir to ctx.root so checks execute in the project directory.
PRRT_kwDOTyoI9s6Yxe3t Fixed: synthesizeCheckSteps now returns ResolvedCheck[] and preserves resolver outputs for findings extraction.
PRRT_kwDOTyoI9s6Yxgj9 Fixed: shell command arguments are individually quoted before joining, preventing unsafe space-join behavior.
PRRT_kwDOTyoI9s6YxgkA Fixed: synthesis now preserves ResolvedCheck.outputs instead of discarding them.
PRRT_kwDOTyoI9s6YxgkD Fixed: deduplication and the generated step ID both use resolved.checkId consistently (checks/${resolved.checkId}).
PRRT_kwDOTyoI9s6YxkDh Fixed: shell command args are safely quoted and runtime.workingDir is set to ctx.root.
PRRT_kwDOTyoI9s6YxkDs Fixed: synthesizeCheckSteps preserves outputs and uses the same resolved.checkId for dedup and step id.
6 changes: 5 additions & 1 deletion packages/checks/src/__tests__/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ describe("public API", () => {
expect(typeof api.createBuiltinResolver).toBe("function");
});

it("exports synthesizeCheckSteps function", () => {
expect(typeof api.synthesizeCheckSteps).toBe("function");
});

it("exports extractFindings function", () => {
expect(typeof api.extractFindings).toBe("function");
});
Expand All @@ -23,6 +27,6 @@ describe("public API", () => {

it("does not export unexpected runtime values", () => {
const runtimeKeys = Object.keys(api).sort();
expect(runtimeKeys).toEqual(["CheckError", "createBuiltinResolver", "extractFindings"]);
expect(runtimeKeys).toEqual(["CheckError", "createBuiltinResolver", "extractFindings", "synthesizeCheckSteps"]);
});
});
69 changes: 28 additions & 41 deletions packages/checks/src/__tests__/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,90 +11,80 @@
it("resolves typecheck to bun run typecheck", () => {
const r = resolver.resolve(makeCheck("typecheck"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("bun");
expect(r!.operation.args).toEqual(["run", "typecheck"]);
expect(r!.operation.kind).toBe("check");
expect(r!.operation.id).toBe("prop-typecheck");
expect(r!.operation.name).toBe("typecheck");
expect(r!.operation.description).toBe("test");
expect(r!.step.id).toBe("checks/typecheck");
expect(r!.step.operations).toHaveLength(1);
expect(r!.step.operations[0]!.kind).toBe("shell");
expect((r!.step.operations[0] as { command: string }).command).toBe("bun run typecheck");
expect(r!.step.runtime.mode).toBe("host");
expect(r!.step.runtime.workingDir).toBe(ctx.root);
});

it("resolves lint to bun run lint", () => {
const r = resolver.resolve(makeCheck("lint"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("bun");
expect(r!.operation.args).toEqual(["run", "lint"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("bun run lint");
});

it("resolves test to bun run test", () => {
const r = resolver.resolve(makeCheck("test"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("bun");
expect(r!.operation.args).toEqual(["run", "test"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("bun run test");
});
});

describe("createBuiltinResolver — Node (npm/yarn/pnpm)", () => {
it("resolves typecheck with npm", () => {
const r = resolver.resolve(makeCheck("typecheck"), makeContext(["npm"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("npm");
expect(r!.operation.args).toEqual(["run", "typecheck"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("npm run typecheck");
});

it("resolves lint with yarn", () => {
const r = resolver.resolve(makeCheck("lint"), makeContext(["yarn"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("yarn");
expect(r!.operation.args).toEqual(["run", "lint"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("yarn run lint");
});

it("resolves test with pnpm", () => {
const r = resolver.resolve(makeCheck("test"), makeContext(["pnpm"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("pnpm");
expect(r!.operation.args).toEqual(["run", "test"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("pnpm run test");
});
});

describe("createBuiltinResolver — Python", () => {
it("resolves lint to ruff check", () => {
const r = resolver.resolve(makeCheck("lint"), makeContext(["poetry"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("ruff");
expect(r!.operation.args).toEqual(["check"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("ruff check");
});

it("resolves test to pytest", () => {
const r = resolver.resolve(makeCheck("test"), makeContext(["pip"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("pytest");
expect(r!.operation.args).toEqual([]);
expect((r!.step.operations[0] as { command: string }).command).toBe("pytest");
});
});

describe("createBuiltinResolver — Rust", () => {
const ctx = makeContext(["cargo"]);

it("resolves clippy to cargo clippy", () => {

Check warning on line 72 in packages/checks/src/__tests__/resolver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 3 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_9DS1TIeMYNSpD2iUe&open=AZ_9DS1TIeMYNSpD2iUe&pullRequest=43
const r = resolver.resolve(makeCheck("clippy"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("cargo");
expect(r!.operation.args).toEqual(["clippy"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("cargo clippy");
});

it("resolves fmt-check to cargo fmt --check", () => {
const r = resolver.resolve(makeCheck("fmt-check"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("cargo");
expect(r!.operation.args).toEqual(["fmt", "--check"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("cargo fmt --check");
});

it("resolves test to cargo test", () => {
const r = resolver.resolve(makeCheck("test"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("cargo");
expect(r!.operation.args).toEqual(["test"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("cargo test");
});
});

Expand All @@ -104,15 +94,13 @@
it("resolves vet to go vet ./...", () => {
const r = resolver.resolve(makeCheck("vet"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("go");
expect(r!.operation.args).toEqual(["vet", "./..."]);
expect((r!.step.operations[0] as { command: string }).command).toBe("go vet ./...");
});

it("resolves test to go test ./...", () => {
const r = resolver.resolve(makeCheck("test"), ctx);
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("go");
expect(r!.operation.args).toEqual(["test", "./..."]);
expect((r!.step.operations[0] as { command: string }).command).toBe("go test ./...");
});
});

Expand All @@ -133,16 +121,14 @@
const r = resolver.resolve(makeCheck("test"), makeContext(["cargo", "bun"]));
expect(r).not.toBeNull();
// Node entries come before cargo in table order, so bun wins.
expect(r!.operation.command).toBe("bun");
expect(r!.operation.args).toEqual(["run", "test"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("bun run test");
});

it("honours proposal reason over table order in polyglot projects", () => {
const rustCheck = makeCheck("test", "Rust project defaults");
const r = resolver.resolve(rustCheck, makeContext(["cargo", "bun"]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("cargo");
expect(r!.operation.args).toEqual(["test"]);
expect((r!.step.operations[0] as { command: string }).command).toBe("cargo test");
});
});

Expand Down Expand Up @@ -173,20 +159,21 @@
resolve() {
return {
checkId: "custom",
operation: {
id: "op-1",
kind: "check",
name: "custom",
command: "my-tool",
args: ["--sarif", "out.sarif"],
step: {
id: "checks/custom",
runtime: { mode: "host" },
operations: [{ kind: "shell", command: "my-tool --sarif out.sarif" }],
inputs: [],
outputs: [],
dependencies: [],
},
outputs: [{ path: "out.sarif", format: "sarif" }],
};
},
};
const r = custom.resolve(makeCheck("custom"), makeContext([]));
expect(r).not.toBeNull();
expect(r!.operation.command).toBe("my-tool");
expect((r!.step.operations[0] as { command: string }).command).toBe("my-tool --sarif out.sarif");
expect(r!.outputs).toHaveLength(1);
expect(r!.outputs[0]!.format).toBe("sarif");
});
Expand Down
81 changes: 81 additions & 0 deletions packages/checks/src/__tests__/synthesize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { synthesizeCheckSteps } from "../synthesize.js";
import { createBuiltinResolver } from "../resolver.js";
import type { CheckResolver } from "../resolver.js";
import { makeCheck, makeContext } from "./helpers/fixtures.js";

describe("synthesizeCheckSteps", () => {
it("converts proposed checks to StepDefinitions", () => {
const ctx = makeContext(["bun"]);
const checks = [makeCheck("typecheck"), makeCheck("lint"), makeCheck("test")];
const resolved = synthesizeCheckSteps(checks, ctx, createBuiltinResolver());
expect(resolved).toHaveLength(3);
expect(resolved[0]!.step.id).toBe("checks/typecheck");
expect(resolved[1]!.step.id).toBe("checks/lint");
expect(resolved[2]!.step.id).toBe("checks/test");
});

it("skips checks that fail resolution", () => {
const ctx = makeContext(["bun"]);
const checks = [makeCheck("typecheck"), makeCheck("clippy"), makeCheck("lint")];
// clippy requires cargo, not bun — should be skipped.
const resolved = synthesizeCheckSteps(checks, ctx, createBuiltinResolver());
expect(resolved).toHaveLength(2);
expect(resolved.map((r) => r.step.id)).toEqual(["checks/typecheck", "checks/lint"]);
});

it("deduplicates by checkId", () => {
const ctx = makeContext(["bun"]);
const checks = [makeCheck("typecheck"), makeCheck("typecheck"), makeCheck("lint")];
const resolved = synthesizeCheckSteps(checks, ctx, createBuiltinResolver());
expect(resolved).toHaveLength(2);
expect(resolved.map((r) => r.step.id)).toEqual(["checks/typecheck", "checks/lint"]);
});

it("step IDs follow checks/<checkId> pattern", () => {
const ctx = makeContext(["bun"]);
const checks = [makeCheck("typecheck")];
const resolved = synthesizeCheckSteps(checks, ctx, createBuiltinResolver());
expect(resolved[0]!.step.id).toMatch(/^checks\//);
});

it("steps have runtime.mode === host", () => {
const ctx = makeContext(["bun"]);
const checks = [makeCheck("typecheck"), makeCheck("lint"), makeCheck("test")];
const resolved = synthesizeCheckSteps(checks, ctx, createBuiltinResolver());
for (const r of resolved) {
expect(r.step.runtime.mode).toBe("host");
}
});

it("returns empty array for empty input", () => {
const ctx = makeContext(["bun"]);
const resolved = synthesizeCheckSteps([], ctx, createBuiltinResolver());
expect(resolved).toHaveLength(0);
});

it("preserves outputs from custom resolvers", () => {
const custom: CheckResolver = {
resolve(check) {
return {
checkId: check.checkId,
step: {
id: `checks/${check.checkId}`,
runtime: { mode: "host" },
operations: [{ kind: "shell", command: "echo hello" }],
inputs: [],
outputs: [],
dependencies: [],
},
outputs: [{ path: "out.sarif", format: "sarif" }],
};
},
};
const ctx = makeContext([]);
const checks = [makeCheck("custom1"), makeCheck("custom2")];
const resolved = synthesizeCheckSteps(checks, ctx, custom);
expect(resolved).toHaveLength(2);
expect(resolved[0]!.step.id).toBe("checks/custom1");
expect(resolved[0]!.outputs).toEqual([{ path: "out.sarif", format: "sarif" }]);
});
});
3 changes: 2 additions & 1 deletion packages/checks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @sverka/checks — public API
// @sverka/checks — public API. Spec 14.

export type { CheckResolver, ResolvedCheck, CheckOutput } from "./resolver.js";
export { createBuiltinResolver } from "./resolver.js";
export { synthesizeCheckSteps } from "./synthesize.js";
export { extractFindings } from "./extract.js";
export { CheckError, type CheckErrorCode } from "./errors.js";
Loading