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
3 changes: 3 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
- name: Install
run: npm ci

- name: Type-check
run: npm run build:check

- name: Test
run: npm test

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Unreleased

- Preserve nested IDs during merge/grouping and renew derived report IDs; retain existing name-based historical test matching.
- Update CTRF to 0.5.0 and refresh AI, JUnit, Teams and Handlebars integrations, dependencies and tooling.
- Bundle all CTRF schemas and retain compatibility with legacy report inputs.
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@ this project as easy and transparent as possible, whether it's:
- Proposing new features

Thank you for your interest in contributing


## TypeScript toolchain

The `tsc` command uses native TypeScript 7.0.2 through the exact
`@typescript/native` npm alias. The `typescript` dependency aliases
`@typescript/typescript6@6.0.2` to retain the compiler API needed by build and
documentation tools; `tsc6` is available for compatibility checks. Keep these
aliases separate when updating dependencies. Both are development dependencies
and do not change the package’s production Node requirement.

See [Microsoft’s side-by-side setup](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0).
232 changes: 232 additions & 0 deletions __tests__/ctrf/spec-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { enrichReportWithInsights } from "../../src/ctrf/core/src/methods/run-insights.js";
import { describe, expect, it } from "vitest";
import { validateStrict } from "../../node_modules/ctrf/dist/index.js";
import type { CTRFReport } from "ctrf";
import { mergeReports } from "../../src/ctrf/core/src/methods/merge-reports.js";
import {
groupTestsByFile,
groupTestsBySuiteOrFilePath,
} from "../../src/ctrf/group-test-results.js";

const report = (name: string, specVersion = "0.2.0"): CTRFReport => ({
reportFormat: "CTRF",
specVersion,
results: {
tool: { name: "vitest" },
summary: {
tests: 1,
passed: 1,
failed: 0,
skipped: 0,
pending: 0,
other: 0,
start: 1,
stop: 2,
},
tests: [
{
name,
status: "passed",
duration: 1,
suite: ["suite"],
filePath: "test.ts",
},
],
},
});

describe("generated report specification versions", () => {
it("emits strict CTRF 0.2.0 when merging conforming reports", () => {
const merged = mergeReports([report("first"), report("second")]);
expect(merged.specVersion).toBe("0.2.0");
expect(merged.results.summary.tests).toBe(2);
expect(() =>
validateStrict(merged, { specVersion: "0.2.0" }),
).not.toThrow();
});
it.each(["suites", "files"])(
"preserves the current version in grouped %s",
(field) => {
const grouped =
field === "files"
? groupTestsByFile(report("first"))
: groupTestsBySuiteOrFilePath(report("first"), true);
for (const child of grouped.results.extra?.[field] ?? []) {
expect(child.specVersion).toBe("0.2.0");
expect(() =>
validateStrict(child, { specVersion: "0.2.0" }),
).not.toThrow();
}
expect(grouped.results.extra?.[field]).toHaveLength(1);
},
);
it("preserves legacy versions when grouping existing reports", () => {
const grouped = groupTestsByFile(report("first", "0.0.3"));
expect(grouped.specVersion).toBe("0.0.3");
expect(grouped.results.extra?.files[0].specVersion).toBe("0.0.3");
});
it("does not claim 0.2.0 for merged legacy retries missing their history", () => {
const legacy = report("first", "0.0.3");
legacy.results.tests[0].retries = 2;
const merged = mergeReports([legacy, report("second", "0.0.3")]);
expect(merged.specVersion).toBe("0.0.3");
expect(merged.results.tests[0].retries).toBe(2);
});
it("does not certify invalid input already labeled 0.2.0", () => {
const invalid = report("first", "0.2.0");
invalid.results.tests[0].retries = 2;
expect(mergeReports([invalid, report("second")]).specVersion).toBe("0.0.0");
});
});

const identityReport = () => ({
reportFormat: "CTRF",
specVersion: "0.2.0",
reportId: "11111111-1111-4111-8111-111111111111",
runId: "run",
results: {
tool: { name: "fixture" },
environment: { shardId: "one" },
summary: {
tests: 1,
passed: 1,
failed: 0,
skipped: 0,
pending: 0,
other: 0,
start: 1,
stop: 2,
},
tests: [
{
name: "same",
testId: "logical",
executionId: "execution",
attemptId: "final-attempt",
status: "passed",
duration: 1,
retries: 1,
flaky: true,
retryAttempts: [
{
attempt: 1,
attemptId: "attempt",
status: "failed",
attachments: [
{
name: "log",
path: "prior.txt",
contentType: "text/plain",
attachmentId: "prior-ref",
},
],
},
],
attachments: [
{
name: "log",
path: "final.txt",
contentType: "text/plain",
attachmentId: "final-ref",
},
],
},
],
},
});

describe("identity-aware consumers", () => {
it("creates a new aggregate artifact and preserves a shared run without one source shard label", () => {
const one = identityReport() as CTRFReport;
const two = identityReport() as CTRFReport;
two.results.environment!.shardId = "two";
two.results.tests[0].executionId = "execution-two";
const original = JSON.stringify(one);
const merged = mergeReports([one, two]);
expect(merged.reportId).not.toBe(one.reportId);
expect(merged.runId).toBe("run");
expect(merged.results.tests[0].attemptId).toBe("final-attempt");
expect(merged.results.environment?.shardId).toBeUndefined();
expect(merged.results.tests[0].retryAttempts?.[0].attemptId).toBe(
"attempt",
);
expect(JSON.stringify(one)).toBe(original);
expect(merged.results.tests[0].executionId).toBe("execution");
expect(merged.results.tests[1].executionId).toBe("execution-two");
expect(merged.results.tests[0].attachments).toEqual(
one.results.tests[0].attachments,
);
expect(merged.results.tests[0].retryAttempts).toEqual(
one.results.tests[0].retryAttempts,
);
for (const group of [
groupTestsByFile(merged).results.extra!.files[0],
groupTestsBySuiteOrFilePath(merged, true).results.extra!.suites[0],
]) {
expect(group.reportId).not.toBe(merged.reportId);
expect(group.runId).toBe("run");
expect(group.results.tests).toEqual(merged.results.tests);
}
two.runId = "different-run";
expect(mergeReports([one, two]).runId).toBeUndefined();
delete one.runId;
delete two.runId;
expect(mergeReports([one, two]).runId).toBeUndefined();
});
});

describe("historical name matching", () => {
it.each([
{
scenario: "name-only reports",
currentIdentity: {},
previousIdentity: {},
},
{
scenario: "a new test ID with name-only history",
currentIdentity: { testId: "logical" },
previousIdentity: {},
},
{
scenario: "legacy ID to test ID migration",
currentIdentity: { testId: "logical" },
previousIdentity: { id: "logical" },
},
{
scenario: "legacy IDs changing between runs",
currentIdentity: { id: "current" },
previousIdentity: { id: "previous" },
},
{
scenario: "different logical IDs with the same name",
currentIdentity: { testId: "current" },
previousIdentity: { testId: "previous" },
},
])(
"preserves existing comparisons for $scenario",
({ currentIdentity, previousIdentity }) => {
const current = report("same");
Object.assign(current.results.tests[0], currentIdentity);
current.results.summary.start = 200;
current.results.summary.stop = 201;
const previous = report("same", "0.0.3");
Object.assign(previous.results.tests[0], previousIdentity, {
status: "failed",
});
previous.results.summary.passed = 0;
previous.results.summary.failed = 1;
previous.results.summary.start = 100;
previous.results.summary.stop = 101;

const output = enrichReportWithInsights(current, [previous], previous);
expect(output.results.tests[0].insights?.failRate).toEqual({
current: 0.5,
baseline: 1,
change: -0.5,
});
expect(output.results.tests[0].insights?.executedInRuns).toBe(2);
expect(output.insights?.extra?.testsAdded).toEqual([]);
expect(output.insights?.extra?.testsRemoved).toEqual([]);
},
);
});
7 changes: 7 additions & 0 deletions __tests__/github/artifacts.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { requestId } from "../../src/client/github/ids.js";
import { vi, describe, it, expect, beforeEach } from "vitest";
import AdmZip from "adm-zip";
import type { components } from "@octokit/openapi-types";
Expand Down Expand Up @@ -350,3 +351,9 @@ describe("processArtifactsFromRun", () => {
expect(api.downloaded).toEqual(["/artifacts/201/zip"]);
});
});

it("adapts numeric and bigint response IDs without truncating large values", () => {
expect(requestId(123)).toBe(123);
expect(requestId(123n)).toBe(123);
expect(() => requestId(9007199254740993n)).toThrow(RangeError);
});
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand All @@ -24,7 +24,7 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"preset": "recommended"
}
},
"javascript": {
Expand Down
Loading
Loading