diff --git a/.github/shared/src/exec.js b/.github/shared/src/exec.js index e542ec8aa7a9..c98712e30057 100644 --- a/.github/shared/src/exec.js +++ b/.github/shared/src/exec.js @@ -36,7 +36,7 @@ export function isExecError(error) { * Wraps `child_process.execFile()`, adding logging and a larger default maxBuffer. * * @param {string} file - * @param {string[]} args + * @param {string[]} [args] * @param {ExecOptions} [options] * @returns {Promise} * @throws {ExecError} diff --git a/.github/tsconfig.json b/.github/tsconfig.json index fd8d4c43ea86..49aa7c7cbf78 100644 --- a/.github/tsconfig.json +++ b/.github/tsconfig.json @@ -8,8 +8,4 @@ "incremental": false, "noEmit": true, }, - "include": [ - // Only check runtime sources. Tests currently have too many errors. - "**/src/**/*.js", - ], } diff --git a/.github/vitest.config.js b/.github/vitest.config.js index ae56b27cb71c..a357e6a05544 100644 --- a/.github/vitest.config.js +++ b/.github/vitest.config.js @@ -4,13 +4,14 @@ export default defineConfig({ esbuild: { // Ignore tsconfig.json, since it's only used for type checking, and causes // a warning if vitest tries to load it + // @ts-expect-error: tsConfig' does not exist in type 'ESBuildOptions' tsConfig: false, }, test: { coverage: { exclude: [ - ...configDefaults.coverage.exclude, + ...(configDefaults.coverage.exclude ?? []), // Not worth testing CLI code "**/cmd/**", diff --git a/.github/workflows/test/arm-auto-signoff.test.js b/.github/workflows/test/arm-auto-signoff.test.js index d354f08b54bf..f95c98e1dcee 100644 --- a/.github/workflows/test/arm-auto-signoff.test.js +++ b/.github/workflows/test/arm-auto-signoff.test.js @@ -43,7 +43,9 @@ function createMockGithub({ incrementalTypeSpec }) { describe("getLabelActionImpl", () => { it("throws if inputs null", async () => { - await expect(getLabelActionImpl({})).rejects.toThrow(); + await expect( + getLabelActionImpl(/** @type {Parameters[0]} */ ({})), + ).rejects.toThrow(); }); it("throws if no artifact from incremental typespec", async () => { diff --git a/.github/workflows/test/arm-incremental-typespec.test.js b/.github/workflows/test/arm-incremental-typespec.test.js index 668613e1acd1..65979bd07e21 100644 --- a/.github/workflows/test/arm-incremental-typespec.test.js +++ b/.github/workflows/test/arm-incremental-typespec.test.js @@ -16,11 +16,20 @@ import { swaggerHandWritten, swaggerTypeSpecGenerated, } from "../../shared/test/examples.js"; -import incrementalTypeSpec from "../src/arm-incremental-typespec.js"; +import incrementalTypeSpecImpl from "../src/arm-incremental-typespec.js"; import { createMockCore } from "./mocks.js"; const core = createMockCore(); +/** + * @param {unknown} asyncFunctionArgs + */ +function incrementalTypeSpec(asyncFunctionArgs) { + return incrementalTypeSpecImpl( + /** @type {import("@actions/github-script").AsyncFunctionArguments} */ (asyncFunctionArgs), + ); +} + describe("incrementalTypeSpec", () => { afterEach(() => { vi.clearAllMocks(); @@ -131,8 +140,27 @@ describe("incrementalTypeSpec", () => { const showSpy = vi .mocked(simpleGit.simpleGit().show) - .mockImplementation(async ([treePath]) => - treePath.split(":")[0] == "HEAD" ? swaggerTypeSpecGenerated : swaggerHandWritten, + .mockImplementation( + ( + /** @type {import("simple-git").SimpleGitTaskCallback|string|string[]|undefined} */ optionsOrCallback, + ) => { + /** @type {string} */ + let option; + + if (Array.isArray(optionsOrCallback)) { + option = optionsOrCallback[0]; + } else if (typeof optionsOrCallback === "string") { + option = optionsOrCallback; + } else { + throw new Error(`Unexpected argument: ${optionsOrCallback}`); + } + + return /** @type {import("simple-git").Response} */ ( + Promise.resolve( + option?.split(":")[0] == "HEAD" ? swaggerTypeSpecGenerated : swaggerHandWritten, + ) + ); + }, ); const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); @@ -199,16 +227,37 @@ describe("incrementalTypeSpec", () => { vi.spyOn(changedFiles, "getChangedFiles").mockResolvedValue([readmePath]); - const showSpy = vi.mocked(simpleGit.simpleGit().show).mockImplementation(async ([treePath]) => { - const path = treePath.split(":")[1]; - if (path === swaggerPath) { - return swaggerTypeSpecGenerated; - } else if (path === readmePath) { - return contosoReadme; - } else { - throw new Error("does not exist"); - } - }); + const showSpy = vi + .mocked(simpleGit.simpleGit().show) + .mockImplementation( + ( + /** @type {import("simple-git").SimpleGitTaskCallback|string|string[]|undefined} */ optionsOrCallback, + ) => { + /** @type {string} */ + let option; + + if (Array.isArray(optionsOrCallback)) { + option = optionsOrCallback[0]; + } else if (typeof optionsOrCallback === "string") { + option = optionsOrCallback; + } else { + throw new Error(`Unexpected argument: ${optionsOrCallback}`); + } + + const path = option.split(":")[1]; + if (path === swaggerPath) { + return /** @type {import("simple-git").Response} */ ( + Promise.resolve(swaggerTypeSpecGenerated) + ); + } else if (path === readmePath) { + return /** @type {import("simple-git").Response} */ ( + Promise.resolve(contosoReadme) + ); + } else { + throw new Error("does not exist"); + } + }, + ); const lsTreeSpy = vi.mocked(simpleGit.simpleGit().raw).mockResolvedValue(swaggerPath); diff --git a/.github/workflows/test/artifacts.test.js b/.github/workflows/test/artifacts.test.js index ac5810967cf1..6296b77ccf5d 100644 --- a/.github/workflows/test/artifacts.test.js +++ b/.github/workflows/test/artifacts.test.js @@ -12,7 +12,8 @@ vi.mock("../src/context.js", () => ({ })); // Mock global fetch -global.fetch = vi.fn(); +const mockFetch = vi.fn(); +global.fetch = mockFetch; const mockCore = createMockCore(); describe("getAzurePipelineArtifact function", () => { @@ -78,7 +79,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to capture headers and return appropriate responses - global.fetch.mockImplementation((url, options) => { + mockFetch.mockImplementation((url, options) => { // For all calls, verify the headers include the authorization token if (options && options.headers) { expect(options.headers).toEqual(expectedHeaders); @@ -152,7 +153,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch with a spy to capture the headers - global.fetch.mockImplementation((url, options) => { + mockFetch.mockImplementation((url, options) => { if (url.includes("artifacts?artifactName=")) { // Verify headers contain Authorization expect(options.headers).toHaveProperty("Authorization", `Bearer ${testToken}`); @@ -189,7 +190,7 @@ describe("getAzurePipelineArtifact function", () => { it("should handle API failure", async () => { // Mock fetch failure - global.fetch.mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: "Server Error", @@ -216,7 +217,7 @@ describe("getAzurePipelineArtifact function", () => { it("should complete without op when artifact does not exist", async () => { // Mock fetch failure - global.fetch.mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 404, statusText: "Not Found", @@ -289,7 +290,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to return different responses based on the URL - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { // First attempted artifact request with 404 if (url.includes(`artifacts?artifactName=${inputs.artifactName}&api-version=7.0`)) { return mockInitialResponse; @@ -340,7 +341,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } @@ -368,7 +369,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } @@ -408,7 +409,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -430,7 +431,7 @@ describe("getAzurePipelineArtifact function", () => { it("should handle exception during processing", async () => { // Mock fetch to throw an error - global.fetch.mockImplementation(() => { + mockFetch.mockImplementation(() => { throw new Error("Network error"); }); @@ -473,7 +474,7 @@ describe("getAzurePipelineArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -587,7 +588,7 @@ describe("fetchFailedArtifact function", () => { }; // Setup fetch with a spy to capture the headers - global.fetch.mockImplementation((url, options) => { + mockFetch.mockImplementation((url, options) => { // Verify that the custom headers are included in all requests expect(options.headers).toEqual(customHeaders); @@ -654,7 +655,7 @@ describe("fetchFailedArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?api-version")) { return mockListResponse; } else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt2")) { @@ -689,7 +690,7 @@ describe("fetchFailedArtifact function", () => { statusText: "OK", }; - global.fetch.mockResolvedValue(mockListResponse); + mockFetch.mockResolvedValue(mockListResponse); // Call the function and expect it to return a 404 response const response = await fetchFailedArtifact(defaultParams); @@ -708,7 +709,7 @@ describe("fetchFailedArtifact function", () => { statusText: "Internal Server Error", }; - global.fetch.mockResolvedValue(mockErrorResponse); + mockFetch.mockResolvedValue(mockErrorResponse); // Call the function and expect it to throw await expect(fetchFailedArtifact(defaultParams)).rejects.toThrow( @@ -751,7 +752,7 @@ describe("fetchFailedArtifact function", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?api-version")) { return mockListResponse; } else if (url.includes("artifactName=spec-gen-sdk-artifact-FailedAttempt3")) { diff --git a/.github/workflows/test/avocado-code.test.js b/.github/workflows/test/avocado-code.test.js index 909c782b21b0..4f8fe8589b79 100644 --- a/.github/workflows/test/avocado-code.test.js +++ b/.github/workflows/test/avocado-code.test.js @@ -5,10 +5,9 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn() })); import * as fs from "fs/promises"; -/** @type {import("vitest").Mock} */ -const readFileMock = fs.readFile; +const readFileMock = /** @type {import("vitest").Mock} */ (fs.readFile); -import generateJobSummary from "../src/avocado-code.js"; +import generateJobSummaryImpl from "../src/avocado-code.js"; import { MessageLevel, MessageType } from "../src/message.js"; import { stringify } from "../src/ndjson.js"; import { createMockCore } from "./mocks.js"; @@ -17,6 +16,15 @@ const core = createMockCore(); const outputFile = "avocado.ndjson"; describe("generateJobSummary", () => { + /** + * @param {unknown} asyncFunctionArgs + */ + function generateJobSummary(asyncFunctionArgs) { + return generateJobSummaryImpl( + /** @type {import("@actions/github-script").AsyncFunctionArguments} */ (asyncFunctionArgs), + ); + } + beforeEach(() => { vi.stubEnv("AVOCADO_OUTPUT_FILE", outputFile); readFileMock.mockReset(); @@ -31,7 +39,9 @@ describe("generateJobSummary", () => { `[Error: Env var AVOCADO_OUTPUT_FILE must be set]`, ); - expect(core.info.mock.calls.at(-1)[0]).toMatchInlineSnapshot(`"avocadoOutputFile: undefined"`); + expect(core.info.mock.calls.at(-1)?.[0]).toMatchInlineSnapshot( + `"avocadoOutputFile: undefined"`, + ); }); it("no-ops if file cannot be read", async () => { @@ -41,7 +51,7 @@ describe("generateJobSummary", () => { await expect(generateJobSummary({ core })).resolves.toBeUndefined(); - expect(core.info.mock.calls.at(-1)[0]).toMatchInlineSnapshot( + expect(core.info.mock.calls.at(-1)?.[0]).toMatchInlineSnapshot( `"Error reading 'avocado.ndjson': Error: ENOENT: no such file or directory, open 'avocado.ndjson'"`, ); }); @@ -51,7 +61,7 @@ describe("generateJobSummary", () => { await expect(generateJobSummary({ core })).resolves.toBeUndefined(); - expect(core.notice.mock.calls.at(-1)[0]).toMatchInlineSnapshot( + expect(core.notice.mock.calls.at(-1)?.[0]).toMatchInlineSnapshot( `"No messages in 'avocado.ndjson'"`, ); }); diff --git a/.github/workflows/test/breaking-change-add-label-artifacts.test.js b/.github/workflows/test/breaking-change-add-label-artifacts.test.js index a36b9fc4b56c..14aec9b63ecb 100644 --- a/.github/workflows/test/breaking-change-add-label-artifacts.test.js +++ b/.github/workflows/test/breaking-change-add-label-artifacts.test.js @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { REVIEW_REQUIRED_LABELS } from "../../shared/src/breaking-change.js"; import { PER_PAGE_MAX } from "../../shared/src/github.js"; -import getLabelActions, { +import getLabelActionsImpl, { CROSS_VERSION_BREAKING_CHANGE_WORKFLOW_NAME, SWAGGER_BREAKING_CHANGE_WORKFLOW_NAME, } from "../src/breaking-change-add-label-artifacts.js"; @@ -12,10 +12,19 @@ vi.mock("../src/context.js", () => ({ extractInputs: vi.fn(), })); +/** + * @param {Partial} asyncFunctionArgs + */ +function getLabelActions(asyncFunctionArgs) { + return getLabelActionsImpl( + /** @type {import("@actions/github-script").AsyncFunctionArguments} */ (asyncFunctionArgs), + ); +} + describe("breaking-change-add-label-artifacts", () => { - let mockGithub; - let mockContext; - let mockCore; + /** @type {ReturnType} */ let mockGithub; + /** @type {ReturnType} */ let mockContext; + /** @type {ReturnType} */ let mockCore; beforeEach(() => { // Reset all mocks before each test @@ -35,9 +44,9 @@ describe("breaking-change-add-label-artifacts", () => { }; const createMockWorkflowRun = ( - name, + /** @type {string} */ name, status = "completed", - conclusion = "success", + /** @type {string|null} */ conclusion = "success", id = 1, updated_at = "2024-01-01T12:00:00Z", ) => ({ @@ -48,21 +57,26 @@ describe("breaking-change-add-label-artifacts", () => { updated_at, }); - const createMockArtifact = (name) => ({ name }); + const createMockArtifact = (/** @type {string} */ name) => ({ name }); // Shared setup helpers const setupMockInputs = async () => { const { extractInputs } = await import("../src/context.js"); - extractInputs.mockResolvedValue(mockInputs); + /** @type {import("vitest").Mock} */ (extractInputs).mockResolvedValue(mockInputs); }; - const setupWorkflowRunsMock = (workflowRuns) => { + const setupWorkflowRunsMock = ( + /** @type {{ id: number; name: string; status: string; conclusion: string | null; updated_at: string; }[]} */ workflowRuns, + ) => { mockGithub.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ data: { workflow_runs: workflowRuns }, }); }; - const setupArtifactsMock = (breakingChangeArtifacts, crossVersionArtifacts) => { + const setupArtifactsMock = ( + /** @type {{ name: string; }[]} */ breakingChangeArtifacts, + /** @type {{ name: string; }[]} */ crossVersionArtifacts, + ) => { mockGithub.rest.actions.listWorkflowRunArtifacts .mockResolvedValueOnce({ data: { artifacts: breakingChangeArtifacts } }) .mockResolvedValueOnce({ data: { artifacts: crossVersionArtifacts } }); @@ -78,7 +92,10 @@ describe("breaking-change-add-label-artifacts", () => { expect(mockCore.setOutput).toHaveBeenCalledWith("issue_number", mockInputs.issue_number); }; - const expectStandardOutputs = (breakingChangeValue, versioningValue) => { + const expectStandardOutputs = ( + /** @type {boolean} */ breakingChangeValue, + /** @type {boolean} */ versioningValue, + ) => { expectRequiredOutputs(); expect(mockCore.setOutput).toHaveBeenCalledWith( @@ -96,7 +113,7 @@ describe("breaking-change-add-label-artifacts", () => { expect(mockCore.setOutput).toHaveBeenCalledWith("versioningReviewLabelValue", versioningValue); }; - const expectEarlyReturn = (infoMessage) => { + const expectEarlyReturn = (/** @type {string} */ infoMessage) => { expectRequiredOutputs(); // Ensure setOutput was *only* called with the two required outputs diff --git a/.github/workflows/test/context.test.js b/.github/workflows/test/context.test.js index ffc7ca11ba84..d38299355b85 100644 --- a/.github/workflows/test/context.test.js +++ b/.github/workflows/test/context.test.js @@ -1,9 +1,19 @@ import { describe, expect, it } from "vitest"; import { PER_PAGE_MAX } from "../../shared/src/github.js"; import { fullGitSha } from "../../shared/test/examples.js"; -import { extractInputs } from "../src/context.js"; +import { extractInputs as extractInputsImpl } from "../src/context.js"; import { createMockCore, createMockGithub } from "./mocks.js"; +/** + * + * @param {import("./mocks.js").GitHub} github + * @param {unknown} context + * @param {import("./mocks.js").Core} core + */ +function extractInputs(github, context, core) { + return extractInputsImpl(github, /** @type {import("./mocks.js").Context} */ (context), core); +} + describe("extractInputs", () => { it("unsupported_event", async () => { const context = { diff --git a/.github/workflows/test/github.test.js b/.github/workflows/test/github.test.js index 825c8691d696..c4b6d936339c 100644 --- a/.github/workflows/test/github.test.js +++ b/.github/workflows/test/github.test.js @@ -15,7 +15,7 @@ const mockCore = createMockCore(); describe("getCheckRuns", () => { it("returns matching check_run", async () => { const githubMock = createMockGithub(); - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + githubMock.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [ { @@ -27,13 +27,7 @@ describe("getCheckRuns", () => { }, }); - const actual = await getCheckRuns( - githubMock, - createMockContext(), - createMockCore(), - "checkRunName", - "head_sha", - ); + const actual = await getCheckRuns(githubMock, createMockContext(), "checkRunName", "head_sha"); expect(actual).toEqual([ expect.objectContaining({ @@ -46,7 +40,7 @@ describe("getCheckRuns", () => { it("returns null when no check matches", async () => { const githubMock = createMockGithub(); - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + githubMock.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [], }, @@ -61,7 +55,7 @@ describe("getCheckRuns", () => { const githubMock = createMockGithub(); const earlierDate = "2025-04-01T00:00:00Z"; const laterDate = "2025-04-02T00:00:00Z"; - githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + githubMock.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [ { @@ -102,7 +96,7 @@ describe("getCheckRuns", () => { describe("getWorkflowRuns", () => { it("returns matching workflow_run", async () => { const githubMock = createMockGithub(); - githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + githubMock.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ data: { workflow_runs: [ { @@ -132,7 +126,7 @@ describe("getWorkflowRuns", () => { it("returns null when no workflow matches", async () => { const githubMock = createMockGithub(); - githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + githubMock.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ data: { workflow_runs: [ { @@ -156,7 +150,7 @@ describe("getWorkflowRuns", () => { const githubMock = createMockGithub(); const earlyDate = "2025-04-01T00:00:00Z"; const laterDate = "2025-04-02T00:00:00Z"; - githubMock.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + githubMock.rest.actions.listWorkflowRunsForRepo.mockResolvedValue({ data: { workflow_runs: [ { @@ -221,7 +215,10 @@ describe("writeToActionsSummary function", () => { describe("createLogHook", () => { it("logs request info with body", () => { const mockLogger = createMockLogger(); - const logHook = createLogHook((r) => r, mockLogger); + const logHook = createLogHook( + /** @type {import("@octokit/types").EndpointInterface} */ ((/** @type {any} */ r) => r), + mockLogger, + ); expect( logHook({ @@ -241,7 +238,10 @@ describe("createLogHook", () => { it("logs request info without body", () => { const mockLogger = createMockLogger(); - const logHook = createLogHook((r) => r, mockLogger); + const logHook = createLogHook( + /** @type {import("@octokit/types").EndpointInterface} */ ((/** @type {any} */ r) => r), + mockLogger, + ); expect( logHook({ @@ -273,7 +273,11 @@ describe("createRateLimitHook", () => { const mockLogger = createMockLogger(); const ratelimitHook = createRateLimitHook(mockLogger); - expect(ratelimitHook({ headers: {} })).toBeUndefined(); + expect( + ratelimitHook( + /** @type {import("@octokit/types").OctokitResponse} */ ({ headers: {} }), + ), + ).toBeUndefined(); expect(mockLogger.info).toBeCalledTimes(0); expect(mockLogger.debug).toBeCalledTimes(1); @@ -291,13 +295,15 @@ describe("createRateLimitHook", () => { const reset = (add(new Date(), 30 * Duration.Minute).getTime() / Duration.Second).toFixed(0); expect( - ratelimitHook({ - headers: { - "x-ratelimit-limit": "100", - "x-ratelimit-remaining": "50", - "x-ratelimit-reset": reset, - }, - }), + ratelimitHook( + /** @type {import("@octokit/types").OctokitResponse} */ ({ + headers: { + "x-ratelimit-limit": "100", + "x-ratelimit-remaining": "50", + "x-ratelimit-reset": reset, + }, + }), + ), ).toBeUndefined(); expect(mockLogger.info).toBeCalledTimes(1); expect(mockLogger.info.mock.calls[0]).toMatchInlineSnapshot(` @@ -307,13 +313,15 @@ describe("createRateLimitHook", () => { `); expect( - ratelimitHook({ - headers: { - "x-ratelimit-limit": "100", - "x-ratelimit-remaining": "75", - "x-ratelimit-reset": reset, - }, - }), + ratelimitHook( + /** @type {import("@octokit/types").OctokitResponse} */ ({ + headers: { + "x-ratelimit-limit": "100", + "x-ratelimit-remaining": "75", + "x-ratelimit-reset": reset, + }, + }), + ), ).toBeUndefined(); expect(mockLogger.info).toBeCalledTimes(2); expect(mockLogger.info.mock.calls[1]).toMatchInlineSnapshot(` @@ -323,13 +331,15 @@ describe("createRateLimitHook", () => { `); expect( - ratelimitHook({ - headers: { - "x-ratelimit-limit": "100", - "x-ratelimit-remaining": "25", - "x-ratelimit-reset": reset, - }, - }), + ratelimitHook( + /** @type {import("@octokit/types").OctokitResponse} */ ({ + headers: { + "x-ratelimit-limit": "100", + "x-ratelimit-remaining": "25", + "x-ratelimit-reset": reset, + }, + }), + ), ).toBeUndefined(); expect(mockLogger.info).toBeCalledTimes(3); expect(mockLogger.info.mock.calls[2]).toMatchInlineSnapshot(` @@ -339,13 +349,15 @@ describe("createRateLimitHook", () => { `); expect( - ratelimitHook({ - headers: { - "x-ratelimit-limit": "100", - "x-ratelimit-remaining": "0", - "x-ratelimit-reset": reset, - }, - }), + ratelimitHook( + /** @type {import("@octokit/types").OctokitResponse} */ ({ + headers: { + "x-ratelimit-limit": "100", + "x-ratelimit-remaining": "0", + "x-ratelimit-reset": reset, + }, + }), + ), ).toBeUndefined(); expect(mockLogger.info).toBeCalledTimes(4); expect(mockLogger.info.mock.calls[3]).toMatchInlineSnapshot(` diff --git a/.github/workflows/test/issues.test.js b/.github/workflows/test/issues.test.js index e9eb083c1bcc..75779c391d69 100644 --- a/.github/workflows/test/issues.test.js +++ b/.github/workflows/test/issues.test.js @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { defaultLogger } from "../../shared/src/logger.js"; import { getIssueNumber } from "../src/issues.js"; -import { asGitHub, createMockGithub, createMockLogger } from "./mocks.js"; +import { createMockGithub, createMockLogger } from "./mocks.js"; /** @typedef {import('@actions/github-script').AsyncFunctionArguments["github"]} GitHub */ @@ -14,9 +14,7 @@ describe("getIssueNumber", () => { }); it("should return NaN when head_sha is missing", async () => { // Call function and expect it to throw - await expect( - getIssueNumber(asGitHub(mockGithub), ""), - ).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(getIssueNumber(mockGithub, "")).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: head_sha is required when trying to search a PR.]`, ); }); @@ -36,7 +34,7 @@ describe("getIssueNumber", () => { }); // Call function - const result = await getIssueNumber(asGitHub(mockGithub), "abc123", mockLogger); + const result = await getIssueNumber(mockGithub, "abc123", mockLogger); // Verify result uses first PR expect(result.issueNumber).toBe(123); @@ -57,7 +55,7 @@ describe("getIssueNumber", () => { }); // Call function - const result = await getIssueNumber(asGitHub(mockGithub), "abc123", defaultLogger); + const result = await getIssueNumber(mockGithub, "abc123", defaultLogger); // Verify result expect(result.issueNumber).toBeNaN(); @@ -69,6 +67,6 @@ describe("getIssueNumber", () => { mockGithub.rest.search.issuesAndPullRequests.mockRejectedValue(searchError); // Call function and expect it to throw - await expect(getIssueNumber(asGitHub(mockGithub), "abc123", defaultLogger)).rejects.toThrow(); + await expect(getIssueNumber(mockGithub, "abc123", defaultLogger)).rejects.toThrow(); }); }); diff --git a/.github/workflows/test/message.test.js b/.github/workflows/test/message.test.js index 78209e033420..24bf69a8805d 100644 --- a/.github/workflows/test/message.test.js +++ b/.github/workflows/test/message.test.js @@ -9,6 +9,11 @@ import { generateMarkdownTable, } from "../src/message.js"; +/** + * @param {import("zod").ZodType} schema + * @param {unknown} input + * @param {string | RegExp | import("@vitest/utils").Constructable | Error | undefined} expectedError + */ function testSchemaParse(schema, input, expectedError) { if (expectedError) { expect(() => schema.parse(input)).toThrowError(expectedError); @@ -18,19 +23,24 @@ function testSchemaParse(schema, input, expectedError) { } describe("MessageLevelSchema", () => { - it.each([["foo", ZodError], [MessageLevel.Error], [MessageLevel.Info], [MessageLevel.Warning]])( - "parse(%o)", - (input, expectedError) => { - testSchemaParse(MessageLevelSchema, input, expectedError); - }, - ); + it.each([ + ["foo", ZodError], + [MessageLevel.Error, undefined], + [MessageLevel.Info, undefined], + [MessageLevel.Warning, undefined], + ])("parse(%o)", (input, expectedError) => { + testSchemaParse(MessageLevelSchema, input, expectedError); + }); }); describe("BaseMessageRecordSchema", () => { it.each([ [{}, ZodError], [{ level: "foo", message: 1, time: "bar" }, ZodError], - [{ level: MessageLevel.Error, message: "test-message", time: new Date().toISOString() }], + [ + { level: MessageLevel.Error, message: "test-message", time: new Date().toISOString() }, + undefined, + ], [ { level: MessageLevel.Warning, @@ -41,6 +51,7 @@ describe("BaseMessageRecordSchema", () => { extra: { extraKey: "extraVal" }, groupName: "test-group-name", }, + undefined, ], ])("parse(%o)", (input, expectedError) => { testSchemaParse(BaseMessageRecordSchema, input, expectedError); @@ -67,6 +78,7 @@ describe("MessageRecordSchema", () => { message: "test-message", time: new Date().toISOString(), }, + undefined, ], [ { @@ -79,6 +91,7 @@ describe("MessageRecordSchema", () => { extra: { extraKey: "extraVal" }, groupName: "test-group-name", }, + undefined, ], [ { @@ -98,6 +111,7 @@ describe("MessageRecordSchema", () => { { tag: "tag2", path: "path2" }, ], }, + undefined, ], ])("parse(%o)", (input, expectedError) => { testSchemaParse(MessageRecordSchema, input, expectedError); @@ -106,6 +120,9 @@ describe("MessageRecordSchema", () => { describe("generateMarkdownTable", () => { it("no messages", () => { + /** + * @type {import("../src/message.js").MessageRecord[]} + */ const messages = []; expect(generateMarkdownTable(messages)).toMatchInlineSnapshot(` diff --git a/.github/workflows/test/mocks.js b/.github/workflows/test/mocks.js index 920eb1e752d5..bf444f415c27 100644 --- a/.github/workflows/test/mocks.js +++ b/.github/workflows/test/mocks.js @@ -3,16 +3,25 @@ import { vi } from "vitest"; /** * @typedef {import('@actions/github-script').AsyncFunctionArguments["github"]} GitHub + * @typedef {import('@actions/github-script').AsyncFunctionArguments["context"]} Context + * @typedef {import('@actions/github-script').AsyncFunctionArguments["core"]} Core */ -// Partial mock of `github` parameter passed into github-script actions +/** + * @returns {GitHub & ReturnType} + */ export function createMockGithub() { + return /** @type {GitHub & ReturnType} */ (createMockGithubImpl()); +} + +// Partial mock of `github` parameter passed into github-script actions +function createMockGithubImpl() { return { hook: { after: vi.fn(), before: vi.fn(), }, - paginate: async (func, params) => { + paginate: async (/** @type {(arg0: any) => any} */ func, /** @type {any} */ params) => { // Assume all test data fits in single page const data = (await func(params)).data; @@ -55,15 +64,18 @@ export function createMockGithub() { } /** - * @returns {GitHub} - * @param {any} mockGithub + * @returns {Core & ReturnType} */ -export function asGitHub(mockGithub) { - return /** @type {GitHub} */ mockGithub; +export function createMockCore() { + return /** @type {Core & ReturnType} */ (createMockCoreImpl()); } // Partial mock of `core` parameter passed into to github-script actions -export function createMockCore() { +function createMockCoreImpl() { + const summary = {}; + summary.addRaw = vi.fn().mockReturnValue(summary); + summary.write = vi.fn().mockResolvedValue(undefined); + return { debug: vi.fn(console.debug), info: vi.fn(console.log), @@ -73,13 +85,7 @@ export function createMockCore() { isDebug: vi.fn().mockReturnValue(true), setOutput: vi.fn((name, value) => console.log(`setOutput('${name}', '${value}')`)), setFailed: vi.fn((msg) => console.log(`setFailed('${msg}')`)), - summary: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - addRaw: vi.fn(function (content) { - return this; // Return 'this' for method chaining - }), - write: vi.fn().mockResolvedValue(undefined), - }, + summary, }; } @@ -94,8 +100,15 @@ export function createMockRequestError(status) { }); } -// Partial mock of `context` parameter passed into github-script actions +/** + * @returns {Context & ReturnType} + */ export function createMockContext() { + return /** @type {Context & ReturnType} */ (createMockContextImpl()); +} + +// Partial mock of `context` parameter passed into github-script actions +function createMockContextImpl() { return { payload: {}, repo: { diff --git a/.github/workflows/test/ndjson.test.js b/.github/workflows/test/ndjson.test.js index 94da6cc20d92..f5416fe76383 100644 --- a/.github/workflows/test/ndjson.test.js +++ b/.github/workflows/test/ndjson.test.js @@ -21,6 +21,7 @@ describe("ndjson", () => { }); it("empty array", () => { + /** @type {string[]} */ let values = []; const stringifiedValues = stringify(values); diff --git a/.github/workflows/test/retries.test.js b/.github/workflows/test/retries.test.js index 49895789f26b..a1c4c68cb38d 100644 --- a/.github/workflows/test/retries.test.js +++ b/.github/workflows/test/retries.test.js @@ -55,9 +55,13 @@ describe("retry function", () => { }); describe("fetchWithRetry function", () => { + /** @type {import("vitest").Mock} */ + let mockFetch; + beforeEach(() => { + mockFetch = vi.fn(); // Mock global fetch - global.fetch = vi.fn(); + global.fetch = mockFetch; }); it("should call fetch with provided url and options", async () => { @@ -65,7 +69,7 @@ describe("fetchWithRetry function", () => { ok: true, json: () => Promise.resolve({ data: "test" }), }; - global.fetch.mockResolvedValue(mockResponse); + mockFetch.mockResolvedValue(mockResponse); const url = "https://example.com/api"; const options = { method: "POST", body: JSON.stringify({ key: "value" }) }; diff --git a/.github/workflows/test/sdk-breaking-change-labels.test.js b/.github/workflows/test/sdk-breaking-change-labels.test.js index a2dca7492c61..9b2e58cc9d49 100644 --- a/.github/workflows/test/sdk-breaking-change-labels.test.js +++ b/.github/workflows/test/sdk-breaking-change-labels.test.js @@ -1,7 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { sdkLabels } from "../../shared/src/sdk-types.js"; import { LabelAction } from "../src/label.js"; -import { getLabelAndAction, getLabelAndActionImpl } from "../src/sdk-breaking-change-labels.js"; +import { + getLabelAndActionImpl, + getLabelAndAction as getLabelAndActionSrc, +} from "../src/sdk-breaking-change-labels.js"; import { createMockContext, createMockCore, createMockGithub } from "./mocks.js"; // Mock dependencies @@ -9,13 +12,23 @@ vi.mock("../src/context.js", () => ({ extractInputs: vi.fn(), })); +const mockFetch = vi.fn(); // Mock global fetch -global.fetch = vi.fn(); +global.fetch = mockFetch; const mockGithub = createMockGithub(); const mockContext = createMockContext(); const mockCore = createMockCore(); +/** + * @param {Partial} asyncFunctionArgs + */ +function getLabelAndAction(asyncFunctionArgs) { + return getLabelAndActionSrc( + /** @type {import("@actions/github-script").AsyncFunctionArguments} */ (asyncFunctionArgs), + ); +} + describe("sdk-breaking-change-labels", () => { beforeEach(() => { // Reset mocks @@ -32,7 +45,7 @@ describe("sdk-breaking-change-labels", () => { // Setup mock implementation for extractInputs const { extractInputs } = await import("../src/context.js"); - extractInputs.mockResolvedValue(mockInputs); + /** @type {import("vitest").Mock} */ (extractInputs).mockResolvedValue(mockInputs); // Mock fetch responses // First fetch - artifact metadata @@ -60,7 +73,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -91,7 +104,7 @@ describe("sdk-breaking-change-labels", () => { // Setup mock for extractInputs const { extractInputs } = await import("../src/context.js"); - extractInputs.mockResolvedValue(inputs); + /** @type {import("vitest").Mock} */ (extractInputs)(inputs); // Mock artifact responses with 'remove' action const mockArtifactResponse = { @@ -116,7 +129,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -147,7 +160,7 @@ describe("sdk-breaking-change-labels", () => { // Setup mock for extractInputs const { extractInputs } = await import("../src/context.js"); - extractInputs.mockResolvedValue(inputs); + /** @type {import("vitest").Mock} */ (extractInputs)(inputs); // Mock artifact responses with 'remove' action const mockArtifactResponse = { @@ -172,7 +185,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -203,7 +216,7 @@ describe("sdk-breaking-change-labels", () => { // Setup mock for extractInputs const { extractInputs } = await import("../src/context.js"); - extractInputs.mockResolvedValue(inputs); + /** @type {import("vitest").Mock} */ (extractInputs).mockResolvedValue(inputs); // Call function and expect it to throw await expect( @@ -225,7 +238,7 @@ describe("sdk-breaking-change-labels", () => { }; // Mock fetch failure - global.fetch.mockResolvedValue({ + mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: "Server Error", @@ -235,8 +248,6 @@ describe("sdk-breaking-change-labels", () => { // Call function const result = await getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, }); @@ -262,7 +273,7 @@ describe("sdk-breaking-change-labels", () => { }; // Mock fetch to handle the artifact URL with 404 error and fallback behavior - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=spec-gen-sdk-artifact")) { // Initial fetch for the specific artifact returns 404 return Promise.resolve({ @@ -306,8 +317,6 @@ describe("sdk-breaking-change-labels", () => { // Call function const result = await getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, }); @@ -334,7 +343,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } @@ -344,8 +353,6 @@ describe("sdk-breaking-change-labels", () => { await expect( getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, }), ).rejects.toThrow(); @@ -367,7 +374,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } @@ -377,8 +384,6 @@ describe("sdk-breaking-change-labels", () => { await expect( getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, }), ).rejects.toThrow(); @@ -412,7 +417,7 @@ describe("sdk-breaking-change-labels", () => { }; // Setup fetch to return different responses for each call - global.fetch.mockImplementation((url) => { + mockFetch.mockImplementation((url) => { if (url.includes("artifacts?artifactName=")) { return mockArtifactResponse; } else { @@ -424,8 +429,6 @@ describe("sdk-breaking-change-labels", () => { await expect( getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, }), ).rejects.toThrow(); @@ -439,15 +442,13 @@ describe("sdk-breaking-change-labels", () => { }; // Mock fetch to throw an error - global.fetch.mockImplementation(() => { + mockFetch.mockImplementation(() => { throw new Error("Network error"); }); // Start the async operation that will retry const promise = getLabelAndActionImpl({ details_url: inputs.details_url, - head_sha: inputs.head_sha, - github: mockGithub, core: mockCore, // Change default retry delay from 1000ms to 1ms to reduce test time retryOptions: { initialDelayMs: 1 }, diff --git a/.github/workflows/test/set-status.test.js b/.github/workflows/test/set-status.test.js index 48c02adbd6eb..39728bc7de6a 100644 --- a/.github/workflows/test/set-status.test.js +++ b/.github/workflows/test/set-status.test.js @@ -5,7 +5,10 @@ import { setStatusImpl } from "../src/set-status.js"; import { createMockCore, createMockGithub } from "./mocks.js"; describe("setStatusImpl", () => { + /** @type {ReturnType} */ let core; + + /** @type {ReturnType} */ let github; beforeEach(() => { @@ -14,10 +17,9 @@ describe("setStatusImpl", () => { }); it("throws if inputs null", async () => { - // @ts-expect-error Testing invalid input type - await expect(setStatusImpl({})).rejects.toMatchInlineSnapshot( - `[Error: head_sha is not a valid full git SHA: 'undefined']`, - ); + await expect( + setStatusImpl(/** @type {Parameters[0]} */ ({})), + ).rejects.toMatchInlineSnapshot(`[Error: head_sha is not a valid full git SHA: 'undefined']`); }); it.each([null, undefined, "", "abc123"])("throws when head_sha is %o", async (head_sha) => { @@ -25,8 +27,7 @@ describe("setStatusImpl", () => { setStatusImpl({ owner: "test-owner", repo: "test-repo", - // @ts-expect-error - Testing invalid input - head_sha, + head_sha: /** @type {string} */ (head_sha), issue_number: 123, target_url: "https://test.com/set_status_url", github, @@ -44,8 +45,7 @@ describe("setStatusImpl", () => { owner: "test-owner", repo: "test-repo", head_sha: fullGitSha, - // @ts-expect-error - Testing invalid input - issue_number, + issue_number: /** @type {Number} */ (issue_number), target_url: "https://test.com/set_status_url", github, core, diff --git a/.github/workflows/test/spec-gen-sdk-status.test.js b/.github/workflows/test/spec-gen-sdk-status.test.js index a1aef18a6eb7..e9ae2eb107d0 100644 --- a/.github/workflows/test/spec-gen-sdk-status.test.js +++ b/.github/workflows/test/spec-gen-sdk-status.test.js @@ -6,10 +6,19 @@ import { setSpecGenSdkStatusImpl } from "../src/spec-gen-sdk-status.js"; import { createMockCore, createMockGithub } from "./mocks.js"; describe("spec-gen-sdk-status", () => { + /** @type {ReturnType} */ let mockGithub; + + /** @type {ReturnType} */ let mockCore; + + /** @type {import("vitest").MockInstance} */ let getAzurePipelineArtifactMock; + + /** @type {import("vitest").MockInstance} */ let writeToActionsSummaryMock; + + /** @type {import("vitest").MockInstance} */ let appendFileSyncMock; beforeEach(() => { @@ -128,6 +137,7 @@ describe("spec-gen-sdk-status", () => { target_url: "https://example.com", github: mockGithub, core: mockCore, + issue_number: 123, }); // Verify the right status was set @@ -194,6 +204,7 @@ describe("spec-gen-sdk-status", () => { target_url: "https://example.com", github: mockGithub, core: mockCore, + issue_number: 123, }); // Verify the right status was set @@ -232,6 +243,7 @@ describe("spec-gen-sdk-status", () => { target_url: "https://example.com", github: mockGithub, core: mockCore, + issue_number: 123, }); // Verify summary was written @@ -267,6 +279,7 @@ describe("spec-gen-sdk-status", () => { target_url: "https://example.com", github: mockGithub, core: mockCore, + issue_number: 123, }), ).rejects.toThrow("Artifact 'spec-gen-sdk-artifact' not found"); }); @@ -323,6 +336,7 @@ describe("spec-gen-sdk-status", () => { target_url: "https://example.com", github: mockGithub, core: mockCore, + issue_number: 123, }); // Verify the right status was set (success since only non-required failed) diff --git a/.github/workflows/test/summarize-checks/labelling.test.js b/.github/workflows/test/summarize-checks/labelling.test.js index 21b2dc9e8ba0..30214802d743 100644 --- a/.github/workflows/test/summarize-checks/labelling.test.js +++ b/.github/workflows/test/summarize-checks/labelling.test.js @@ -303,7 +303,7 @@ describe("ARM review process labelling", () => { it.each(testCases)( "$description", async ({ existingLabels, expectedLabelsToAdd, expectedLabelsToRemove }) => { - /** @type {import("./labelling.js").LabelContext} */ + /** @type {import("../../src/summarize-checks/labelling.js").LabelContext} */ const labelContext = { present: new Set(), toAdd: new Set(), diff --git a/.github/workflows/test/summarize-checks/summarize-checks.test.js b/.github/workflows/test/summarize-checks/summarize-checks.test.js index 77a902e65c1d..07208a8ff7b5 100644 --- a/.github/workflows/test/summarize-checks/summarize-checks.test.js +++ b/.github/workflows/test/summarize-checks/summarize-checks.test.js @@ -12,12 +12,20 @@ import { } from "../../src/summarize-checks/summarize-checks.js"; import { createMockCore, createMockGithub } from "../mocks.js"; +/** + * @typedef {import("../../src/summarize-checks/summarize-checks.js").CheckRunData} CheckRunData + */ + const mockCore = createMockCore(); const WORKFLOW_URL = "http://github.com/a/fake/workflowrun/url"; /** * Find and extract the "Next Steps to Merge" existing comment on a PR. * Used in the integration test. + * @param {import("../mocks.js").GitHub} github + * @param {string} owner + * @param {string} repo + * @param {number} prNumber */ async function getNextStepsComment(github, owner, repo, prNumber) { try { @@ -29,12 +37,12 @@ async function getNextStepsComment(github, owner, repo, prNumber) { // Find comment containing "Next Steps to Merge" const nextStepsComment = comments.find((comment) => - comment.body.includes("

Next Steps to Merge

"), + comment.body?.includes("

Next Steps to Merge

"), ); return nextStepsComment ? nextStepsComment.body : null; } catch (error) { - console.error(`Error getting comments for PR #${prNumber}:`, error.message); + console.error(`Error getting comments for PR #${prNumber}:`, error); return null; } } @@ -57,9 +65,13 @@ describe("Summarize Checks Integration Tests", () => { "Monitor", ]; - const github = new Octokit({ - auth: process.env.GITHUB_TOKEN, - }); + const github = /** @type {import("../mocks.js").GitHub} */ ( + /** @type {unknown} */ ( + new Octokit({ + auth: process.env.GITHUB_TOKEN, + }) + ) + ); const { data: pr } = await github.rest.pulls.get({ owner: owner, @@ -126,8 +138,11 @@ describe("Summarize Checks Unit Tests", () => { it("should generate success summary for no matched check suites (completed impactAssessment)", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; + /** @type {CheckRunData[]} */ const requiredCheckRuns = []; const expectedOutput = [ '

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.', @@ -155,7 +170,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate success summary for all completed check suites", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedOutput = [ '

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.', @@ -264,6 +281,7 @@ describe("Summarize Checks Unit Tests", () => { it("should generate success summary with completed required checks but in-progress FYI", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; const expectedOutput = [ '

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.', @@ -336,6 +354,7 @@ describe("Summarize Checks Unit Tests", () => { it("should generate success summary with 0 required checks but in-progress FYI", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; const expectedOutput = [ '

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.', @@ -346,6 +365,7 @@ describe("Summarize Checks Unit Tests", () => { }, ]; + /** @type {CheckRunData[]} */ const requiredCheckRuns = []; const fyiCheckRuns = [ @@ -392,6 +412,7 @@ describe("Summarize Checks Unit Tests", () => { it("should generate success with warning for error FYI", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; const expectedOutput = [ `

Next Steps to Merge

Important checks have failed. As of today they are not blocking this PR, but in near future they may.
Addressing the following failures is highly recommended:
  • ⚠️ The check named license/cla has failed. Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the aka.ms/ci-fix guide
If you still want to proceed merging this PR without addressing the above failures, refer to step 4 in the PR workflow diagram.

Comment generated by summarize-checks workflow run.`, @@ -437,8 +458,9 @@ describe("Summarize Checks Unit Tests", () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; const labelNames = ["CI-NewRPNamespaceWithoutRPaaS", "RPaaSException"]; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; - const expectedCommentBody = `

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.`; + const expectedCommentBody = `

Next Steps to Merge

✅ All automated merging requirements have been met! To get your PR merged, see aka.ms/azsdk/specreview/merge.

Comment generated by summarize-checks workflow run.`; const expectedCheckOutput = { name: "Automated merging requirements met", result: "SUCCESS", @@ -462,6 +484,7 @@ describe("Summarize Checks Unit Tests", () => { requiredCheckRuns, fyiCheckRuns, true, // assessmentCompleted + "target_url", ); expect(commentBody).toEqual(expectedCommentBody); @@ -471,7 +494,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate pending summary when checks are partially in progress", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedOutput = [ `

Next Steps to Merge

⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛

Comment generated by summarize-checks workflow run.`, @@ -520,7 +545,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate pending summary when checks are in progress", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedOutput = [ `

Next Steps to Merge

⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛

Comment generated by summarize-checks workflow run.`, @@ -569,7 +596,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate pending summary when impact assessment is not completed", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedOutput = [ `

Next Steps to Merge

⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛

Comment generated by summarize-checks workflow run.`, @@ -678,7 +707,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate pending summary when checks are in progress", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedOutput = [ `

Next Steps to Merge

⌛ Please wait. Next steps to merge this PR are being evaluated by automation. ⌛

Comment generated by summarize-checks workflow run.`, @@ -814,6 +845,7 @@ describe("Summarize Checks Unit Tests", () => { "TypeSpec", "VersioningReviewRequired", ]; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedComment = `

Next Steps to Merge

Next steps that must be taken to merge this PR:
  • ❌ ` + @@ -944,6 +976,7 @@ describe("Summarize Checks Unit Tests", () => { "TypeSpec", "VersioningReviewRequired", ]; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedComment = "

    Next Steps to Merge

    Next steps that must be taken to merge this PR:
      " + @@ -1067,6 +1100,7 @@ describe("Summarize Checks Unit Tests", () => { it("should generate error summary with a failed required check, and failed FYI checks", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; const expectedCheckOutput = { name: "Automated merging requirements met", @@ -1112,6 +1146,7 @@ describe("Summarize Checks Unit Tests", () => { it("should generate error summary with a failed required check, and in-progress FYI checks", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; const expectedCheckOutput = { name: "Automated merging requirements met", @@ -1146,6 +1181,7 @@ describe("Summarize Checks Unit Tests", () => { requiredCheckRuns, fyiCheckRuns, true, // assessmentCompleted + "target_url", ); expect(automatedCheckOutput).toEqual(expectedCheckOutput); @@ -1155,8 +1191,9 @@ describe("Summarize Checks Unit Tests", () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; const labelNames = ["CI-NewRPNamespaceWithoutRPaaS"]; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; - const expectedCommentBody = `

      Next Steps to Merge

      Next steps that must be taken to merge this PR:
      • ❌ This PR has CI-NewRPNamespaceWithoutRPaaS label. This means it is introducing a new RP (Resource Provider) namespace that has not been onboarded with RPaaS. Merging this PR to the main branch is blocked as RPaaS is required for new RPs.
        To resolve, use RPaaS to onboard the new RP. To apply for exception, see aka.ms/RPaaSException.


      Comment generated by summarize-checks workflow run.`; + const expectedCommentBody = `

      Next Steps to Merge

      Next steps that must be taken to merge this PR:
      • ❌ This PR has CI-NewRPNamespaceWithoutRPaaS label. This means it is introducing a new RP (Resource Provider) namespace that has not been onboarded with RPaaS. Merging this PR to the main branch is blocked as RPaaS is required for new RPs.
        To resolve, use RPaaS to onboard the new RP. To apply for exception, see aka.ms/RPaaSException.


      Comment generated by summarize-checks workflow run.`; const expectedCheckOutput = { name: "Automated merging requirements met", result: "FAILURE", @@ -1181,6 +1218,7 @@ describe("Summarize Checks Unit Tests", () => { requiredCheckRuns, fyiCheckRuns, true, // assessmentCompleted + "target_url", ); expect(commentBody).toEqual(expectedCommentBody); @@ -1190,7 +1228,9 @@ describe("Summarize Checks Unit Tests", () => { it("should generate error summary when checks are in error state", async () => { const repo = "azure-rest-api-specs"; const targetBranch = "main"; + /** @type {string[]} */ const labelNames = []; + /** @type {CheckRunData[]} */ const fyiCheckRuns = []; const expectedCheckOutput = { name: "Automated merging requirements met", @@ -1216,6 +1256,7 @@ describe("Summarize Checks Unit Tests", () => { requiredCheckRuns, fyiCheckRuns, true, // assessmentCompleted + "target_url", ); expect(automatedCheckOutput).toEqual(expectedCheckOutput); diff --git a/.github/workflows/test/update-labels.test.js b/.github/workflows/test/update-labels.test.js index e9cfdd97c869..0c4d60fae0c6 100644 --- a/.github/workflows/test/update-labels.test.js +++ b/.github/workflows/test/update-labels.test.js @@ -1,9 +1,18 @@ import { describe, expect, it } from "vitest"; import { PER_PAGE_MAX } from "../../shared/src/github.js"; import { fullGitSha } from "../../shared/test/examples.js"; -import updateLabels, { updateLabelsImpl } from "../src/update-labels.js"; +import updateLabelsSrc, { updateLabelsImpl } from "../src/update-labels.js"; import { createMockCore, createMockGithub, createMockRequestError } from "./mocks.js"; +/** + * @param {unknown} asyncFunctionArgs + */ +function updateLabels(asyncFunctionArgs) { + return updateLabelsSrc( + /** @type {import("@actions/github-script").AsyncFunctionArguments} */ (asyncFunctionArgs), + ); +} + describe("updateLabels", () => { it("loads inputs from context", async () => { const core = createMockCore(); @@ -63,6 +72,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: 123, run_id: NaN, github: github, @@ -90,6 +100,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: NaN, run_id: 456, github: github, @@ -108,6 +119,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: NaN, run_id: 456, github: github, @@ -139,6 +151,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: 123, run_id: 456, github: github, @@ -189,6 +202,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: 123, run_id: 456, github: github, @@ -226,6 +240,7 @@ describe("updateLabelsImpl", () => { updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: 123, run_id: 456, github: github, @@ -257,6 +272,7 @@ describe("updateLabelsImpl", () => { const updateLabelsImplPromise = updateLabelsImpl({ owner: "owner", repo: "repo", + head_sha: fullGitSha, issue_number: 123, run_id: 456, github: github, diff --git a/.github/workflows/test/verify-run-status.test.js b/.github/workflows/test/verify-run-status.test.js index 7a6280e9d26a..31e9100f627d 100644 --- a/.github/workflows/test/verify-run-status.test.js +++ b/.github/workflows/test/verify-run-status.test.js @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { verifyRunStatusImpl } from "../src/verify-run-status.js"; +import { verifyRunStatusImpl as verifyRunStatusImplSrc } from "../src/verify-run-status.js"; import { createMockCore, createMockGithub } from "./mocks.js"; vi.mock("../src/context.js", () => { @@ -18,6 +18,35 @@ vi.mock("../src/github.js", () => { }; }); +/** + * @param {Object} params + * @param {import('@actions/github-script').AsyncFunctionArguments["github"]} params.github + * @param {Partial} params.context + * @param {import('@actions/github-script').AsyncFunctionArguments["core"]} params.core + * @param {string} params.checkRunName + * @param {string} [params.commitStatusName] + * @param {string} [params.workflowName] + */ +export async function verifyRunStatusImpl({ + github, + context, + core, + checkRunName, + commitStatusName, + workflowName, +}) { + return verifyRunStatusImplSrc({ + github, + context: /** @type {import('@actions/github-script').AsyncFunctionArguments["context"]} */ ( + context + ), + core, + checkRunName, + commitStatusName, + workflowName, + }); +} + describe("verifyRunStatusImpl", () => { // Reset mock call history before each test beforeEach(() => { @@ -27,7 +56,7 @@ describe("verifyRunStatusImpl", () => { it("verifies status when check_run event fires", async () => { const github = createMockGithub(); const { getWorkflowRuns } = await import("../src/github.js"); - getWorkflowRuns.mockResolvedValue([ + /** @type {import("vitest").Mock} */ (getWorkflowRuns).mockResolvedValue([ { name: "workflowName", status: "completed", @@ -64,7 +93,7 @@ describe("verifyRunStatusImpl", () => { it("verifies status when workflow_run event fires", async () => { const github = createMockGithub(); - github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + github.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [ { @@ -103,7 +132,7 @@ describe("verifyRunStatusImpl", () => { it("returns early during workflow_run event when no matching check_run is found", async () => { const github = createMockGithub(); - github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + github.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [], }, @@ -134,7 +163,7 @@ describe("verifyRunStatusImpl", () => { it("returns early during check_run event when no matching workflow_run is found", async () => { const { getWorkflowRuns } = await import("../src/github.js"); - getWorkflowRuns.mockResolvedValue([]); + /** @type {import("vitest").Mock} */ (getWorkflowRuns).mockResolvedValue([]); const context = { eventName: "check_run", @@ -185,7 +214,7 @@ describe("verifyRunStatusImpl", () => { it("throws if check_run conclusion does not match workflow_run conclusion", async () => { const { getWorkflowRuns } = await import("../src/github.js"); - getWorkflowRuns.mockResolvedValue([ + /** @type {import("vitest").Mock} */ (getWorkflowRuns).mockResolvedValue([ { name: "workflowName", status: "completed", @@ -217,7 +246,7 @@ describe("verifyRunStatusImpl", () => { it("throws when in check_suite event but no check_run with name is found", async () => { const github = createMockGithub(); - github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + github.rest.checks.listForRef.mockResolvedValue({ data: { check_runs: [], }, @@ -248,14 +277,14 @@ describe("verifyRunStatusImpl", () => { it("fetches commit status from API when not status event", async () => { const { getCheckRuns, getCommitStatuses } = await import("../src/github.js"); - getCheckRuns.mockResolvedValue([ + /** @type {import("vitest").Mock}*/ (getCheckRuns).mockResolvedValue([ { name: "checkRunName", conclusion: "success", html_url: "https://example.com/check", }, ]); - getCommitStatuses.mockResolvedValue([ + /** @type {import("vitest").Mock}*/ (getCommitStatuses).mockResolvedValue([ { context: "commitStatusName", state: "success", @@ -292,14 +321,16 @@ describe("verifyRunStatusImpl", () => { it("handles API error when fetching commit status", async () => { const { getCheckRuns, getCommitStatuses } = await import("../src/github.js"); - getCheckRuns.mockResolvedValue([ + /** @type {import("vitest").Mock}*/ (getCheckRuns).mockResolvedValue([ { name: "checkRunName", conclusion: "success", html_url: "https://example.com/check", }, ]); - getCommitStatuses.mockRejectedValue(new Error("API Error")); + /** @type {import("vitest").Mock}*/ (getCommitStatuses).mockRejectedValue( + new Error("API Error"), + ); const context = { eventName: "workflow_run", @@ -327,7 +358,7 @@ describe("verifyRunStatusImpl", () => { it("verifies neutral check run matches success workflow run", async () => { const { getCheckRuns } = await import("../src/github.js"); - getCheckRuns.mockResolvedValue([ + /** @type {import("vitest").Mock}*/ (getCheckRuns).mockResolvedValue([ { name: "checkRunName", conclusion: "neutral",