diff --git a/README.md b/README.md index e813153..90441bb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ This way even entityIds like environmentIds or testCaseIds will be autocompleted # octomind -Octomind cli tool. Version: 3.8.0. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 3.8.1. Additional documentation see https://octomind.dev/docs/api-reference/ **Usage:** `octomind [options] [command]` @@ -243,7 +243,6 @@ Execute local YAML test cases | `--bypass-proxy` | bypass proxy when accessing the test target | No | | | `--browser [CHROMIUM, FIREFOX, SAFARI]` | Browser type | No | CHROMIUM | | `--breakpoint [DESKTOP, MOBILE, TABLET]` | Breakpoint | No | DESKTOP | -| `-s, --source ` | Source directory (defaults to current directory) | Yes | ./.octomind | ## create-discovery @@ -432,7 +431,6 @@ Pull test cases from the test target |:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `-d, --destination ` | Destination folder | Yes | ./.octomind | ## push @@ -446,7 +444,6 @@ Push local YAML test cases to the test target |:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | -| `-s, --source ` | Source directory (defaults to current directory) | Yes | .octomind | ## Test Reports diff --git a/biome.json b/biome.json index 5ba9215..e30cda2 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.9/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/package.json b/package.json index f9521d6..0c31ce4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "gendoc": "tsx scripts/generate-docs.ts > README.md", "lint": "pnpm apigen && npx genversion -des src/version.ts && biome check", "tsc": "tsc --project tsconfig.build.json", + "dev:local": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.local.json OCTOMIND_API_URL=http://localhost:3000/api tsx src/index.ts", "dev:staging": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.staging.json OCTOMIND_API_URL=https://preview.octomind.dev/api tsx src/index.ts", "dev:prod": "pnpm apigen && OCTOMIND_CONFIG_FILE=octomind.dev.prod.json OCTOMIND_API_URL=https://app.octomind.dev/api tsx src/index.ts", "apigen": "openapi-typescript https://preview.octomind.dev/openapi.yaml --output src/api.ts && orval", diff --git a/src/cli.ts b/src/cli.ts index 59ddf3e..3ffc061 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -193,11 +193,6 @@ export const buildCmd = (): CompletableCommand => { .option("--bypass-proxy", "bypass proxy when accessing the test target") .option("--browser [CHROMIUM, FIREFOX, SAFARI]", "Browser type", "CHROMIUM") .option("--breakpoint [DESKTOP, MOBILE, TABLET]", "Breakpoint", "DESKTOP") - .option( - "-s, --source ", - "Source directory (defaults to current directory)", - "./.octomind", - ) .action(addTestTargetWrapper(executeLocalTestCases)); createCommandWithCommonOptions(program, "test-report") @@ -396,7 +391,6 @@ export const buildCmd = (): CompletableCommand => { .description("Pull test cases from the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) - .option("-d, --destination ", "Destination folder", "./.octomind") .action(addTestTargetWrapper(pullTestTarget)); // noinspection RequiredAttributes @@ -405,11 +399,6 @@ export const buildCmd = (): CompletableCommand => { .description("Push local YAML test cases to the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) - .option( - "-s, --source ", - "Source directory (defaults to current directory)", - ".octomind", - ) .action(addTestTargetWrapper(pushTestTarget)); createCommandWithCommonOptions(program, "list-test-targets") diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..5701db5 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const OCTOMIND_FOLDER_NAME = ".octomind"; diff --git a/src/debugtopus/index.ts b/src/debugtopus/index.ts index c8de521..8450165 100644 --- a/src/debugtopus/index.ts +++ b/src/debugtopus/index.ts @@ -10,6 +10,8 @@ import { promisify } from "util"; import { Open } from "unzipper"; +import { OCTOMIND_FOLDER_NAME } from "../constants"; +import { findOctomindFolder } from "../helpers"; import { getEnvironments, getPlaywrightCode, @@ -262,9 +264,16 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { }; export const executeLocalTestCases = async ( - options: DebugtopusOptions & { source: string }, + options: DebugtopusOptions, ): Promise => { - const testCases = readTestCasesFromDir(options.source); + const octomindRoot = await findOctomindFolder(); + if (!octomindRoot) { + throw new Error( + `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to execute locally`, + ); + } + + const testCases = readTestCasesFromDir(octomindRoot); const body = { testCases, testTargetId: options.testTargetId, diff --git a/src/helpers.ts b/src/helpers.ts index 7980a26..06fd58e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,7 +1,10 @@ +import path from "node:path"; import { stdin as input, stdout as output } from "node:process"; import { createInterface } from "node:readline"; +import fsPromises from "fs/promises"; import { loadConfig } from "./config"; +import { OCTOMIND_FOLDER_NAME } from "./constants"; export function promptUser(question: string): Promise { const rl = createInterface({ input, output }); @@ -29,3 +32,48 @@ export const resolveTestTargetId = async ( } return config.testTargetId; }; + +const isDirectory = async (dirPath: string): Promise => { + try { + const stat = await fsPromises.stat(dirPath); + return stat.isDirectory(); + } catch { + return false; + } +}; + +export const findOctomindFolder = async (): Promise => { + let currentDir = process.cwd(); + + while (currentDir !== path.parse(currentDir).root) { + const octomindPath = path.join(currentDir, OCTOMIND_FOLDER_NAME); + if (await isDirectory(octomindPath)) { + return octomindPath; + } + currentDir = path.dirname(currentDir); + } + + const rootOctomind = path.join(currentDir, OCTOMIND_FOLDER_NAME); + if (await isDirectory(rootOctomind)) { + return rootOctomind; + } + + return null; +}; + +export const getAbsoluteFilePathInOctomindRoot = async ({ + filePath, + octomindRoot, +}: { + filePath: string; + octomindRoot: string; +}): Promise => { + try { + const resolvedPath = await fsPromises.realpath( + path.isAbsolute(filePath) ? filePath : path.join(octomindRoot, filePath), + ); + return resolvedPath.startsWith(octomindRoot) ? resolvedPath : null; + } catch { + return null; + } +}; diff --git a/src/tools/sync/push.ts b/src/tools/sync/push.ts index 647e01e..173f548 100644 --- a/src/tools/sync/push.ts +++ b/src/tools/sync/push.ts @@ -65,9 +65,9 @@ const defaultPush = async ( return data; }; -const draftPush = async ( +export const draftPush = async ( body: TestTargetSyncData, - options: PushOptions & ListOptions, + options: Omit & ListOptions, ): Promise<{ success: boolean; versionIds: string[] } | undefined> => { const { data, error } = await options.client.POST( "/apiKey/beta/test-targets/{testTargetId}/draft/push", diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index 1949723..c831fe0 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -1,7 +1,12 @@ +import fsPromises from "fs/promises"; +import path from "path"; + import type { components, paths } from "../api"; // generated by openapi-typescript +import { findOctomindFolder } from "../helpers"; import { getUrl } from "../url"; import { client, handleError, ListOptions, logJson } from "./client"; import { getEnvironments } from "./environments"; +import { buildFilename, readTestCasesFromDir } from "./sync/yml"; export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; @@ -13,25 +18,42 @@ export type DeleteTestCaseParams = export const deleteTestCase = async ( options: DeleteTestCaseParams & ListOptions, ): Promise => { - const { data, error } = await client.DELETE( - "/apiKey/v3/test-targets/{testTargetId}/test-cases/{testCaseId}", - { - params: { - path: { - testTargetId: options.testTargetId, - testCaseId: options.testCaseId, + const octomindRoot = await findOctomindFolder(); + + if (!octomindRoot) { + const { data, error } = await client.DELETE( + "/apiKey/v3/test-targets/{testTargetId}/test-cases/{testCaseId}", + { + params: { + path: { + testTargetId: options.testTargetId, + testCaseId: options.testCaseId, + }, }, }, - }, - ); + ); - handleError(error); + handleError(error); + if (options.json) { + logJson(data); + } + console.log("Test Case deleted successfully"); + return; + } - if (options.json) { - logJson(data); + const testCases = readTestCasesFromDir(octomindRoot); + const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); + const existingTestCase = testCasesById[options.testCaseId]; + + if (!existingTestCase) { + console.log( + `No test case with id ${options.testCaseId} found in folder ${octomindRoot}`, + ); return; } + const existingTestCasePath = buildFilename(existingTestCase, octomindRoot); + await fsPromises.unlink(path.join(octomindRoot, existingTestCasePath)); console.log("Test Case deleted successfully"); }; diff --git a/src/tools/test-targets.ts b/src/tools/test-targets.ts index 112a2d2..77b185f 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -1,5 +1,7 @@ import path from "path"; +import { OCTOMIND_FOLDER_NAME } from "../constants"; +import { findOctomindFolder } from "../helpers"; import { getUrl } from "../url"; import { client, handleError, ListOptions, logJson } from "./client"; import { push } from "./sync/push"; @@ -44,7 +46,7 @@ export const listTestTargets = async (options: ListOptions): Promise => { }; export const pullTestTarget = async ( - options: { testTargetId: string; destination?: string } & ListOptions, + options: { testTargetId: string } & ListOptions, ): Promise => { const { data, error } = await client.GET( "/apiKey/beta/test-targets/{testTargetId}/pull", @@ -68,17 +70,23 @@ export const pullTestTarget = async ( return; } - writeYaml(data, options.destination); + const destination = + (await findOctomindFolder()) ?? + path.join(process.cwd(), OCTOMIND_FOLDER_NAME); + writeYaml(data, destination); console.log("Test Target pulled successfully"); }; export const pushTestTarget = async ( - options: { testTargetId: string; source?: string } & ListOptions, + options: { testTargetId: string } & ListOptions, ): Promise => { - const sourceDir = options.source - ? path.resolve(options.source) - : process.cwd(); + const sourceDir = await findOctomindFolder(); + if (!sourceDir) { + throw new Error( + `No ${OCTOMIND_FOLDER_NAME} folder found, please pull first.`, + ); + } const data = await push({ ...options, diff --git a/tests/debugtopous/index.spec.ts b/tests/debugtopous/index.spec.ts index e01b2ce..2c8d7be 100644 --- a/tests/debugtopous/index.spec.ts +++ b/tests/debugtopous/index.spec.ts @@ -11,6 +11,7 @@ import { readZipFromResponseBody, } from "../../src/debugtopus/index"; import { ensureChromiumIsInstalled } from "../../src/debugtopus/installation"; +import { findOctomindFolder } from "../../src/helpers"; import { client, handleError } from "../../src/tools/client"; import { getPlaywrightConfig } from "../../src/tools/playwright"; import { readTestCasesFromDir } from "../../src/tools/sync/yml"; @@ -23,6 +24,7 @@ jest.mock("../../src/tools/client"); jest.mock("../../src/tools/sync/yml"); jest.mock("../../src/tools/playwright"); jest.mock("../../src/debugtopus/installation"); +jest.mock("../../src/helpers"); jest.mock("child_process"); jest.mock("node:stream/promises"); @@ -198,6 +200,8 @@ describe("debugtopus", () => { }); describe("executeLocalTestCases", () => { + const OCTOMIND_ROOT = "/project/.octomind"; + it("should execute local test cases from zip response body", async () => { const mockTestCases = [createMockSyncTestCase()]; const mockZipBuffer = Buffer.from([0x50, 0x4b, 0x03, 0x04]); @@ -216,6 +220,7 @@ describe("debugtopus", () => { }); const mockDirectory = { extract: jest.fn().mockResolvedValue(undefined) }; + jest.mocked(findOctomindFolder).mockResolvedValue(OCTOMIND_ROOT); mockedReadTestCasesFromDir.mockReturnValue(mockTestCases); mockedClient.POST.mockResolvedValue({ error: undefined, @@ -257,11 +262,10 @@ describe("debugtopus", () => { await executeLocalTestCases({ testTargetId: "test-target-id", url: "https://example.com", - source: "/test/source", headless: true, }); - expect(mockedReadTestCasesFromDir).toHaveBeenCalledWith("/test/source"); + expect(mockedReadTestCasesFromDir).toHaveBeenCalledWith(OCTOMIND_ROOT); expect(mockedClient.POST).toHaveBeenCalledWith( "/apiKey/beta/test-targets/{testTargetId}/code", expect.objectContaining({ diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts new file mode 100644 index 0000000..a4f65b6 --- /dev/null +++ b/tests/helpers.spec.ts @@ -0,0 +1,195 @@ +import fsPromises from "fs/promises"; +import os from "os"; +import path from "path"; + +import { OCTOMIND_FOLDER_NAME } from "../src/constants"; +import { + findOctomindFolder, + getAbsoluteFilePathInOctomindRoot, +} from "../src/helpers"; + +describe("helpers", () => { + describe("findOctomindFolder", () => { + let tmpDir: string; + const originalCwd = process.cwd; + + beforeEach(async () => { + tmpDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), "octomind-test-"), + ); + }); + + afterEach(async () => { + process.cwd = originalCwd; + await fsPromises.rm(tmpDir, { recursive: true }); + }); + + it("should find .octomind folder in current directory", async () => { + const octomindPath = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + await fsPromises.mkdir(octomindPath); + process.cwd = () => tmpDir; + + const result = await findOctomindFolder(); + + expect(result).toBe(octomindPath); + }); + + it("should find .octomind folder in parent directory", async () => { + const octomindPath = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + const subdir = path.join(tmpDir, "subdir"); + await fsPromises.mkdir(octomindPath); + await fsPromises.mkdir(subdir); + process.cwd = () => subdir; + + const result = await findOctomindFolder(); + + expect(result).toBe(octomindPath); + }); + + it("should traverse multiple levels to find .octomind folder", async () => { + const octomindPath = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + const deepDir = path.join(tmpDir, "a", "b", "c"); + await fsPromises.mkdir(octomindPath); + await fsPromises.mkdir(deepDir, { recursive: true }); + process.cwd = () => deepDir; + + const result = await findOctomindFolder(); + + expect(result).toBe(octomindPath); + }); + + it("should return null when .octomind folder does not exist", async () => { + process.cwd = () => tmpDir; + + const result = await findOctomindFolder(); + + expect(result).toBeNull(); + }); + + it("should return null when .octomind is a file instead of directory", async () => { + const octomindPath = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + await fsPromises.writeFile(octomindPath, ""); + process.cwd = () => tmpDir; + + const result = await findOctomindFolder(); + + expect(result).toBeNull(); + }); + + it("should find .octomind folder when inside a subfolder of it", async () => { + const octomindPath = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + const subfolderInOctomind = path.join(octomindPath, "subfolder"); + await fsPromises.mkdir(subfolderInOctomind, { recursive: true }); + process.cwd = () => subfolderInOctomind; + + const result = await findOctomindFolder(); + + expect(result).toBe(octomindPath); + }); + }); + + describe("getAbsoluteFilePathInOctomindRoot", () => { + let tmpDir: string; + let octomindRoot: string; + + beforeEach(async () => { + tmpDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), "octomind-test-"), + ); + octomindRoot = path.join(tmpDir, OCTOMIND_FOLDER_NAME); + await fsPromises.mkdir(octomindRoot); + }); + + afterEach(async () => { + await fsPromises.rm(tmpDir, { recursive: true }); + }); + + it("should resolve relative path within octomind root", async () => { + const filePath = path.join(octomindRoot, "test-case.yaml"); + await fsPromises.writeFile(filePath, ""); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: "test-case.yaml", + octomindRoot, + }); + + expect(result).toBe(filePath); + }); + + it("should resolve nested relative path within octomind root", async () => { + const subdir = path.join(octomindRoot, "subdir"); + await fsPromises.mkdir(subdir); + const filePath = path.join(subdir, "test-case.yaml"); + await fsPromises.writeFile(filePath, ""); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: "subdir/test-case.yaml", + octomindRoot, + }); + + expect(result).toBe(filePath); + }); + + it("should accept absolute path within octomind root", async () => { + const filePath = path.join(octomindRoot, "test-case.yaml"); + await fsPromises.writeFile(filePath, ""); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath, + octomindRoot, + }); + + expect(result).toBe(filePath); + }); + + it("should return null for absolute path outside octomind root", async () => { + const outsideFile = path.join(tmpDir, "outside.yaml"); + await fsPromises.writeFile(outsideFile, ""); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: outsideFile, + octomindRoot, + }); + + expect(result).toBeNull(); + }); + + it("should return null for path traversal attempts", async () => { + const outsideFile = path.join(tmpDir, "outside.yaml"); + await fsPromises.writeFile(outsideFile, ""); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: "../outside.yaml", + octomindRoot, + }); + + expect(result).toBeNull(); + }); + + it("should return null for non-existent file", async () => { + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: "does-not-exist.yaml", + octomindRoot, + }); + + expect(result).toBeNull(); + }); + + it("should return null for malformed path with nested octomind structure", async () => { + // e.g. /some/folder/.octomind/some/folder/.octomind/a.yaml + const malformedPath = path.join( + octomindRoot, + tmpDir, + OCTOMIND_FOLDER_NAME, + "a.yaml", + ); + + const result = await getAbsoluteFilePathInOctomindRoot({ + filePath: malformedPath, + octomindRoot, + }); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/tests/tools/test-cases.spec.ts b/tests/tools/test-cases.spec.ts index 3f1d147..49e1d9b 100644 --- a/tests/tools/test-cases.spec.ts +++ b/tests/tools/test-cases.spec.ts @@ -1,38 +1,113 @@ +import fsPromises from "fs/promises"; +import os from "os"; +import path from "path"; + +import { findOctomindFolder } from "../../src/helpers"; import { client, handleError } from "../../src/tools/client"; +import { buildFilename, readTestCasesFromDir } from "../../src/tools/sync/yml"; import { deleteTestCase } from "../../src/tools/test-cases"; jest.mock("../../src/tools/client"); +jest.mock("../../src/helpers"); +jest.mock("../../src/tools/sync/yml"); describe("test-cases", () => { + const originalConsoleLog = console.log; let clientDELETE: jest.Mock; + beforeEach(() => { jest.clearAllMocks(); clientDELETE = client.DELETE as jest.Mock; console.log = jest.fn(); }); - it("should delete a test case", async () => { - clientDELETE.mockResolvedValue({ - data: { success: true }, - error: undefined, + afterEach(() => { + console.log = originalConsoleLog; + }); + + describe("deleteTestCase via API", () => { + beforeEach(() => { + jest.mocked(findOctomindFolder).mockResolvedValue(null); }); - await deleteTestCase({ - testTargetId: "test-target-id", - testCaseId: "test-case-id", + + it("should delete a test case", async () => { + clientDELETE.mockResolvedValue({ + data: { success: true }, + error: undefined, + }); + await deleteTestCase({ + testTargetId: "test-target-id", + testCaseId: "test-case-id", + }); + expect(handleError).toHaveBeenCalledWith(undefined); + expect(console.log).toHaveBeenCalledWith( + "Test Case deleted successfully", + ); + }); + + it("should handle error", async () => { + clientDELETE.mockResolvedValue({ + data: undefined, + error: { message: "error" }, + }); + await deleteTestCase({ + testTargetId: "test-target-id", + testCaseId: "test-case-id", + }); + expect(handleError).toHaveBeenCalledWith({ message: "error" }); }); - expect(handleError).toHaveBeenCalledWith(undefined); - expect(console.log).toHaveBeenCalledWith("Test Case deleted successfully"); }); - it("should handle error", async () => { - clientDELETE.mockResolvedValue({ - data: undefined, - error: { message: "error" }, + describe("deleteTestCase via local filesystem", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), "octomind-test-"), + ); + jest.mocked(findOctomindFolder).mockResolvedValue(tmpDir); + }); + + afterEach(async () => { + await fsPromises.rm(tmpDir, { recursive: true }); }); - await deleteTestCase({ - testTargetId: "test-target-id", - testCaseId: "test-case-id", + + it("should delete a test case file when it exists locally", async () => { + const testCaseId = "test-case-id"; + const fileName = "myTestCase.yaml"; + const filePath = path.join(tmpDir, fileName); + await fsPromises.writeFile(filePath, ""); + + jest + .mocked(readTestCasesFromDir) + .mockReturnValue([ + { id: testCaseId, description: "My Test Case" }, + ] as ReturnType); + jest.mocked(buildFilename).mockReturnValue(fileName); + + await deleteTestCase({ + testTargetId: "test-target-id", + testCaseId, + }); + + expect(console.log).toHaveBeenCalledWith( + "Test Case deleted successfully", + ); + await expect(fsPromises.access(filePath)).rejects.toThrow(); + }); + + it("should log message when test case ID is not found locally", async () => { + jest.mocked(readTestCasesFromDir).mockReturnValue([]); + + await deleteTestCase({ + testTargetId: "test-target-id", + testCaseId: "non-existent-id", + }); + + expect(console.log).toHaveBeenCalledWith( + `No test case with id non-existent-id found in folder ${tmpDir}`, + ); + expect(clientDELETE).not.toHaveBeenCalled(); }); - expect(handleError).toHaveBeenCalledWith({ message: "error" }); }); }); diff --git a/tests/tools/test-targets.spec.ts b/tests/tools/test-targets.spec.ts index f3d8ade..94e892a 100644 --- a/tests/tools/test-targets.spec.ts +++ b/tests/tools/test-targets.spec.ts @@ -1,16 +1,19 @@ import { mock } from "jest-mock-extended"; +import { findOctomindFolder } from "../../src/helpers"; import { pushTestTarget } from "../../src/tools"; import { client } from "../../src/tools/client"; import { getGitContext } from "../../src/tools/sync/git"; import { readTestCasesFromDir } from "../../src/tools/sync/yml"; +jest.mock("../../src/helpers"); jest.mock("../../src/tools/sync/git"); jest.mock("../../src/tools/sync/yml"); jest.mock("../../src/tools/client"); describe("push", () => { beforeEach(() => { + jest.mocked(findOctomindFolder).mockResolvedValue("/project/.octomind"); jest.mocked(getGitContext).mockResolvedValue({ defaultBranch: "refs/heads/main", ref: "refs/heads/main",