diff --git a/.gitignore b/.gitignore index 6d80c54..d8211cd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ octomind-cli-debug/ vhs/ .octomind src/schemas +**/.DS_Store diff --git a/src/cli.ts b/src/cli.ts index eb22c4c..c451950 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -405,6 +405,7 @@ export const buildCmd = (): CompletableCommand => { .description("Push local YAML test cases to the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) + .option("-y, --yes", "Skip confirmation prompt") .action(addTestTargetWrapper(pushTestTarget)); // noinspection RequiredAttributes diff --git a/src/helpers.ts b/src/helpers.ts index 11b7b80..c8bb65d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -17,6 +17,11 @@ export function promptUser(question: string): Promise { }); } +export const confirmAction = async (message: string): Promise => { + const answer = await promptUser(`${message} (y/N): `); + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"; +}; + export const resolveTestTargetId = async ( providedTestTargetId?: string, ): Promise => { diff --git a/src/tools/test-targets.ts b/src/tools/test-targets.ts index 12fe8a1..614a807 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -3,11 +3,11 @@ import path from "path"; import ora from "ora"; import { OCTOMIND_FOLDER_NAME } from "../constants"; -import { findOctomindFolder } from "../helpers"; +import { confirmAction, findOctomindFolder } from "../helpers"; import { getUrl } from "../url"; import { client, handleError, ListOptions, logJson } from "./client"; import { push } from "./sync/push"; -import { readTestCasesFromDir, writeYaml } from "./sync/yaml"; +import { writeYaml } from "./sync/yaml"; export const getTestTargets = async () => { const { data, error } = await client.GET("/apiKey/v3/test-targets"); @@ -21,6 +21,27 @@ export const getTestTargets = async () => { return data; }; +export const getTestTarget = async (id: string) => { + const { data, error } = await client.GET( + "/apiKey/v3/test-targets/{testTargetId}", + { + params: { + path: { + testTargetId: id, + }, + }, + }, + ); + + handleError(error); + + if (!data) { + throw Error(`No test target with id ${id} found`); + } + + return data; +}; + export const listTestTargets = async (options: ListOptions): Promise => { const testTargets = await getTestTargets(); if (options.json) { @@ -81,16 +102,31 @@ export const pullTestTarget = async ( }; export const pushTestTarget = async ( - options: { testTargetId: string } & ListOptions, + options: { testTargetId: string; yes?: boolean } & ListOptions, ): Promise => { + const localThrobber = ora("Reading local test cases").start(); const sourceDir = await findOctomindFolder(); if (!sourceDir) { throw new Error( `No ${OCTOMIND_FOLDER_NAME} folder found, please pull first.`, ); } - const throbber = ora("Pushing test cases").start(); + const testTarget = await getTestTarget(options.testTargetId); + + localThrobber.succeed("Local test cases read successfully"); + + if (!options.yes) { + const confirmed = await confirmAction( + `Push local changes to test target "${testTarget.app}" with id "${testTarget.id}"?`, + ); + if (!confirmed) { + console.log("Push cancelled."); + return; + } + } + + const pushThrobber = ora("Pushing test cases").start(); const data = await push({ ...options, sourceDir, @@ -103,5 +139,5 @@ export const pushTestTarget = async ( logJson(data); } - throbber.succeed("Test cases pushed successfully"); + pushThrobber.succeed("Test cases pushed successfully"); }; diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts index cd1c92a..65c8077 100644 --- a/tests/helpers.spec.ts +++ b/tests/helpers.spec.ts @@ -2,7 +2,7 @@ import fsPromises from "fs/promises"; import os from "os"; import path from "path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { OCTOMIND_FOLDER_NAME } from "../src/constants"; import { diff --git a/tests/tools/test-targets.spec.ts b/tests/tools/test-targets.spec.ts index c44b312..92e0bcb 100644 --- a/tests/tools/test-targets.spec.ts +++ b/tests/tools/test-targets.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; -import { findOctomindFolder } from "../../src/helpers"; +import { confirmAction, findOctomindFolder } from "../../src/helpers"; import { pushTestTarget } from "../../src/tools"; import { client } from "../../src/tools/client"; import { getGitContext } from "../../src/tools/sync/git"; @@ -15,6 +15,7 @@ vi.mock("../../src/tools/client"); describe("push", () => { beforeEach(() => { vi.mocked(findOctomindFolder).mockResolvedValue("/project/.octomind"); + vi.mocked(confirmAction).mockResolvedValue(true); vi.mocked(getGitContext).mockResolvedValue({ defaultBranch: "refs/heads/main", ref: "refs/heads/main", @@ -24,6 +25,11 @@ describe("push", () => { }); vi.mocked(readTestCasesFromDir).mockReturnValue([]); + vi.mocked(client).GET.mockResolvedValue({ + data: { id: "someId", app: "My Test App" }, + error: undefined, + response: mock(), + }); vi.mocked(client).POST.mockResolvedValue({ data: undefined, error: undefined, @@ -43,6 +49,7 @@ describe("push", () => { await pushTestTarget({ testTargetId: "someId", + yes: true, }); expect(client.POST).toHaveBeenCalledWith( @@ -62,6 +69,7 @@ describe("push", () => { await pushTestTarget({ testTargetId: "someId", + yes: true, }); expect(client.POST).toHaveBeenCalledWith( @@ -69,4 +77,44 @@ describe("push", () => { expect.anything(), ); }); + + describe("confirmation", () => { + it("prompts for confirmation with test target name", async () => { + const id = "someId"; + const name = "My Test App"; + vi.mocked(client).GET.mockResolvedValue({ + data: { id, app: name }, + error: undefined, + response: mock(), + }); + await pushTestTarget({ + testTargetId: "someId", + }); + + expect(confirmAction).toHaveBeenCalledWith( + `Push local changes to test target "${name}" with id "${id}"?`, + ); + }); + + it("skips confirmation when --yes flag is provided", async () => { + await pushTestTarget({ + testTargetId: "someId", + yes: true, + }); + + expect(confirmAction).not.toHaveBeenCalled(); + expect(client.POST).toHaveBeenCalled(); + }); + + it("does not push when user declines confirmation", async () => { + vi.mocked(confirmAction).mockResolvedValue(false); + + await pushTestTarget({ + testTargetId: "someId", + }); + + expect(client.POST).not.toHaveBeenCalled(); + expect(console.log).toHaveBeenCalledWith("Push cancelled."); + }); + }); });