From 04c59b36de15a88a7c9126bd86a8e3b0f4a304bd Mon Sep 17 00:00:00 2001 From: Daniel Draper Date: Mon, 12 Jan 2026 12:00:53 +0100 Subject: [PATCH 01/14] edit test cases --- package.json | 1 + src/cli.ts | 13 ++++ src/tools/sync/push.ts | 4 +- src/tools/yamlMutations/edit.ts | 113 ++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/tools/yamlMutations/edit.ts 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..22213a4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -38,6 +38,7 @@ import { } from "./tools"; import { init, switchTestTarget } from "./tools/init"; import { update } from "./tools/update"; +import { edit } from "./tools/yamlMutations/edit"; import { version } from "./version"; export const BINARY_NAME = "octomind"; @@ -412,6 +413,18 @@ export const buildCmd = (): CompletableCommand => { ) .action(addTestTargetWrapper(pushTestTarget)); + // noinspection RequiredAttributes + createCommandWithCommonOptions(program, "edit-test-case") + .completer(testTargetIdCompleter) + .description("Edit yaml test case") + .helpGroup("test-cases") + .addOption(testTargetIdOption) + .requiredOption( + "-f, --file-path ", + "The path to the local yaml file you want to edit", + ) + .action(addTestTargetWrapper(edit)); + createCommandWithCommonOptions(program, "list-test-targets") .description("List all test targets") .helpGroup("test-targets") 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/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts new file mode 100644 index 0000000..b5f667a --- /dev/null +++ b/src/tools/yamlMutations/edit.ts @@ -0,0 +1,113 @@ +import fs from "fs"; +import fsPromises from "fs/promises"; +import path from "path"; + +import { Client } from "openapi-fetch"; +import { ErrorResponse } from "openapi-typescript-helpers"; +import yaml from "yaml"; + +import { paths } from "../../api"; +import { client, handleError } from "../client"; +import { checkForConsistency } from "../sync/consistency"; +import { draftPush } from "../sync/push"; +import { SyncTestCase } from "../sync/types"; +import { readTestCasesFromDir } from "../sync/yml"; + +type EditOptions = { + testTargetId: string; + filePath: string; +}; + +const findOctomindFolder = async ( + startPath: string, +): Promise => { + let currentDir = path.dirname(startPath); + + while (currentDir !== path.parse(currentDir).root) { + const octomindPath = path.join(currentDir, ".octomind"); + if ( + fs.existsSync(octomindPath) && + (await fsPromises.stat(octomindPath)).isDirectory() + ) { + return octomindPath; + } + currentDir = path.dirname(currentDir); + } + + const rootOctomind = path.join(currentDir, ".octomind"); + if ( + fs.existsSync(rootOctomind) && + (await fsPromises.stat(rootOctomind)).isDirectory() + ) { + return rootOctomind; + } + + return null; +}; + +const getRelevantTestCases = ( + testCasesById: Record, + startTestCase: SyncTestCase, +): SyncTestCase[] => { + let dependencyId = startTestCase.dependencyId; + const result: SyncTestCase[] = [startTestCase]; + + while (dependencyId) { + const currentTestCase = testCasesById[dependencyId]; + + if (!currentTestCase) { + throw new Error( + `Could not find dependency ${dependencyId} for ${startTestCase.id}`, + ); + } + + result.push(currentTestCase); + dependencyId = currentTestCase?.dependencyId; + } + + return result; +}; + +const loadTestCase = (path: string): SyncTestCase => { + try { + const content = fs.readFileSync(path, "utf8"); + return yaml.parse(content); + } catch (error) { + throw new Error(`Could not parse ${path}: ${error}`); + } +}; + +export const edit = async (options: EditOptions): Promise => { + console.log(options) + const resolvedPath = path.resolve(options.filePath); + + const testCaseToEdit = loadTestCase(resolvedPath); + + const octomindRoot = await findOctomindFolder(options.filePath); + + if (!octomindRoot) { + throw new Error( + "Could not find .octomind folder, make sure to pull before trying to edit", + ); + } + + const testCases = readTestCasesFromDir(octomindRoot); + const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); + const relevantTestCases = getRelevantTestCases(testCasesById, testCaseToEdit); + + console.log(relevantTestCases); + checkForConsistency(relevantTestCases); + + const response = await draftPush( + { + testCases: relevantTestCases, + }, + { + testTargetId: options.testTargetId, + client, + onError: handleError, + }, + ); + + console.log(response); +}; From 9f75a385c9137ffbe24158d2b7ec69e107f65301 Mon Sep 17 00:00:00 2001 From: Daniel Draper Date: Mon, 12 Jan 2026 13:54:49 +0100 Subject: [PATCH 02/14] remove logs --- src/tools/yamlMutations/edit.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts index b5f667a..ba9dce5 100644 --- a/src/tools/yamlMutations/edit.ts +++ b/src/tools/yamlMutations/edit.ts @@ -2,11 +2,8 @@ import fs from "fs"; import fsPromises from "fs/promises"; import path from "path"; -import { Client } from "openapi-fetch"; -import { ErrorResponse } from "openapi-typescript-helpers"; import yaml from "yaml"; -import { paths } from "../../api"; import { client, handleError } from "../client"; import { checkForConsistency } from "../sync/consistency"; import { draftPush } from "../sync/push"; @@ -68,17 +65,16 @@ const getRelevantTestCases = ( return result; }; -const loadTestCase = (path: string): SyncTestCase => { +const loadTestCase = (testCasePath: string): SyncTestCase => { try { - const content = fs.readFileSync(path, "utf8"); + const content = fs.readFileSync(testCasePath, "utf8"); return yaml.parse(content); } catch (error) { - throw new Error(`Could not parse ${path}: ${error}`); + throw new Error(`Could not parse ${testCasePath}: ${error}`); } }; export const edit = async (options: EditOptions): Promise => { - console.log(options) const resolvedPath = path.resolve(options.filePath); const testCaseToEdit = loadTestCase(resolvedPath); @@ -95,7 +91,6 @@ export const edit = async (options: EditOptions): Promise => { const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); const relevantTestCases = getRelevantTestCases(testCasesById, testCaseToEdit); - console.log(relevantTestCases); checkForConsistency(relevantTestCases); const response = await draftPush( From 9a96e3db81872d4f329c44549feb1c8a31c9d86d Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 14:04:00 +0100 Subject: [PATCH 03/14] folder name constant --- src/cli.ts | 7 ++++--- src/constants.ts | 1 + src/tools/yamlMutations/edit.ts | 10 ++++------ 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 src/constants.ts diff --git a/src/cli.ts b/src/cli.ts index 22213a4..800fa72 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -40,6 +40,7 @@ import { init, switchTestTarget } from "./tools/init"; import { update } from "./tools/update"; import { edit } from "./tools/yamlMutations/edit"; import { version } from "./version"; +import { OCTOMIND_FOLDER_NAME } from "./constants"; export const BINARY_NAME = "octomind"; @@ -197,7 +198,7 @@ export const buildCmd = (): CompletableCommand => { .option( "-s, --source ", "Source directory (defaults to current directory)", - "./.octomind", + OCTOMIND_FOLDER_NAME, ) .action(addTestTargetWrapper(executeLocalTestCases)); @@ -397,7 +398,7 @@ export const buildCmd = (): CompletableCommand => { .description("Pull test cases from the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) - .option("-d, --destination ", "Destination folder", "./.octomind") + .option("-d, --destination ", "Destination folder", OCTOMIND_FOLDER_NAME) .action(addTestTargetWrapper(pullTestTarget)); // noinspection RequiredAttributes @@ -409,7 +410,7 @@ export const buildCmd = (): CompletableCommand => { .option( "-s, --source ", "Source directory (defaults to current directory)", - ".octomind", + OCTOMIND_FOLDER_NAME, ) .action(addTestTargetWrapper(pushTestTarget)); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..0ec372d --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const OCTOMIND_FOLDER_NAME = ".octomind" diff --git a/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts index b5f667a..623180d 100644 --- a/src/tools/yamlMutations/edit.ts +++ b/src/tools/yamlMutations/edit.ts @@ -2,20 +2,19 @@ import fs from "fs"; import fsPromises from "fs/promises"; import path from "path"; -import { Client } from "openapi-fetch"; -import { ErrorResponse } from "openapi-typescript-helpers"; import yaml from "yaml"; -import { paths } from "../../api"; import { client, handleError } from "../client"; import { checkForConsistency } from "../sync/consistency"; import { draftPush } from "../sync/push"; import { SyncTestCase } from "../sync/types"; import { readTestCasesFromDir } from "../sync/yml"; +import { OCTOMIND_FOLDER_NAME } from "../../constants"; type EditOptions = { testTargetId: string; filePath: string; + sourceDir: string }; const findOctomindFolder = async ( @@ -24,7 +23,7 @@ const findOctomindFolder = async ( let currentDir = path.dirname(startPath); while (currentDir !== path.parse(currentDir).root) { - const octomindPath = path.join(currentDir, ".octomind"); + const octomindPath = path.join(currentDir, OCTOMIND_FOLDER_NAME); if ( fs.existsSync(octomindPath) && (await fsPromises.stat(octomindPath)).isDirectory() @@ -34,7 +33,7 @@ const findOctomindFolder = async ( currentDir = path.dirname(currentDir); } - const rootOctomind = path.join(currentDir, ".octomind"); + const rootOctomind = path.join(currentDir, OCTOMIND_FOLDER_NAME); if ( fs.existsSync(rootOctomind) && (await fsPromises.stat(rootOctomind)).isDirectory() @@ -78,7 +77,6 @@ const loadTestCase = (path: string): SyncTestCase => { }; export const edit = async (options: EditOptions): Promise => { - console.log(options) const resolvedPath = path.resolve(options.filePath); const testCaseToEdit = loadTestCase(resolvedPath); From 2114e3e99fedee6e66dd90f639c6aab1e1c74ca5 Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 15:28:20 +0100 Subject: [PATCH 04/14] make .octomind folder the default for push, pull, delete and edit. --- src/cli.ts | 12 -------- src/helpers.ts | 43 +++++++++++++++++++++++++++ src/tools/test-cases.ts | 52 +++++++++++++++++++++++---------- src/tools/test-targets.ts | 16 ++++++---- src/tools/yamlMutations/edit.ts | 43 +++++---------------------- 5 files changed, 98 insertions(+), 68 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 800fa72..ed6e970 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -40,7 +40,6 @@ import { init, switchTestTarget } from "./tools/init"; import { update } from "./tools/update"; import { edit } from "./tools/yamlMutations/edit"; import { version } from "./version"; -import { OCTOMIND_FOLDER_NAME } from "./constants"; export const BINARY_NAME = "octomind"; @@ -195,11 +194,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_FOLDER_NAME, - ) .action(addTestTargetWrapper(executeLocalTestCases)); createCommandWithCommonOptions(program, "test-report") @@ -398,7 +392,6 @@ export const buildCmd = (): CompletableCommand => { .description("Pull test cases from the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) - .option("-d, --destination ", "Destination folder", OCTOMIND_FOLDER_NAME) .action(addTestTargetWrapper(pullTestTarget)); // noinspection RequiredAttributes @@ -407,11 +400,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_FOLDER_NAME, - ) .action(addTestTargetWrapper(pushTestTarget)); // noinspection RequiredAttributes diff --git a/src/helpers.ts b/src/helpers.ts index 7980a26..1898578 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,6 +2,10 @@ import { stdin as input, stdout as output } from "node:process"; import { createInterface } from "node:readline"; import { loadConfig } from "./config"; +import path from "node:path"; +import fsPromises from "fs/promises"; +import { OCTOMIND_FOLDER_NAME } from "./constants"; + export function promptUser(question: string): Promise { const rl = createInterface({ input, output }); @@ -29,3 +33,42 @@ 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 = ({ filePath, octomindRoot }: { filePath: string, octomindRoot: string }): string | null => { + let resolvedPath: string + if (path.isAbsolute(filePath)) { + resolvedPath = filePath + } + resolvedPath = path.resolve(filePath, path.join(octomindRoot, filePath)); + + return resolvedPath.startsWith(octomindRoot) ? resolvedPath : null +} diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index 1949723..cb54a44 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -1,7 +1,11 @@ +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"; +import { unlink } from "fs/promises"; export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; @@ -13,26 +17,44 @@ 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); + ); - if (options.json) { - logJson(data); - return; + handleError(error); + if (options.json) { + logJson(data); + } + console.log("Test Case deleted successfully"); + } else { + const testCases = readTestCasesFromDir(octomindRoot); + const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); + const existingTestCase = testCasesById[options.testCaseId] + + if (existingTestCase) { + const existingTestCasePath = buildFilename( + existingTestCase, + octomindRoot, + ); + await unlink(path.join(octomindRoot, existingTestCasePath)) + console.log("Test Case deleted successfully"); + } + else { + console.log(`No test case with id ${options.testCaseId} found in folder ${octomindRoot}`) + } } - console.log("Test Case deleted successfully"); }; export const listTestCase = async ( diff --git a/src/tools/test-targets.ts b/src/tools/test-targets.ts index 112a2d2..0b2238f 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -4,6 +4,8 @@ import { getUrl } from "../url"; import { client, handleError, ListOptions, logJson } from "./client"; import { push } from "./sync/push"; import { writeYaml } from "./sync/yml"; +import { findOctomindFolder } from "../helpers"; +import { OCTOMIND_FOLDER_NAME } from "../constants"; export const getTestTargets = async () => { const { data, error } = await client.GET("/apiKey/v3/test-targets"); @@ -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,19 @@ 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/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts index 953dfde..114f09d 100644 --- a/src/tools/yamlMutations/edit.ts +++ b/src/tools/yamlMutations/edit.ts @@ -1,5 +1,4 @@ import fs from "fs"; -import fsPromises from "fs/promises"; import path from "path"; import yaml from "yaml"; @@ -9,40 +8,13 @@ import { checkForConsistency } from "../sync/consistency"; import { draftPush } from "../sync/push"; import { SyncTestCase } from "../sync/types"; import { readTestCasesFromDir } from "../sync/yml"; -import { OCTOMIND_FOLDER_NAME } from "../../constants"; +import { findOctomindFolder, getAbsoluteFilePathInOctomindRoot } from "../../helpers"; type EditOptions = { testTargetId: string; filePath: string; - sourceDir: string }; -const findOctomindFolder = async ( - startPath: string, -): Promise => { - let currentDir = path.dirname(startPath); - - while (currentDir !== path.parse(currentDir).root) { - const octomindPath = path.join(currentDir, OCTOMIND_FOLDER_NAME); - if ( - fs.existsSync(octomindPath) && - (await fsPromises.stat(octomindPath)).isDirectory() - ) { - return octomindPath; - } - currentDir = path.dirname(currentDir); - } - - const rootOctomind = path.join(currentDir, OCTOMIND_FOLDER_NAME); - if ( - fs.existsSync(rootOctomind) && - (await fsPromises.stat(rootOctomind)).isDirectory() - ) { - return rootOctomind; - } - - return null; -}; const getRelevantTestCases = ( testCasesById: Record, @@ -77,17 +49,18 @@ const loadTestCase = (testCasePath: string): SyncTestCase => { }; export const edit = async (options: EditOptions): Promise => { - const resolvedPath = path.resolve(options.filePath); - - const testCaseToEdit = loadTestCase(resolvedPath); - - const octomindRoot = await findOctomindFolder(options.filePath); - + const octomindRoot = await findOctomindFolder(); if (!octomindRoot) { throw new Error( "Could not find .octomind folder, make sure to pull before trying to edit", ); } + const testCaseFilePath = getAbsoluteFilePathInOctomindRoot({ octomindRoot, filePath: options.filePath }) + if (!testCaseFilePath) { + throw new Error(`Could not find ${options.filePath} in folder ${octomindRoot}`) + } + + const testCaseToEdit = loadTestCase(testCaseFilePath); const testCases = readTestCasesFromDir(octomindRoot); const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); From d33c197207e4b34da7a02ef85016ae7c72c6212a Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 15:43:06 +0100 Subject: [PATCH 05/14] use octomind folder in local execution --- src/debugtopus/index.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/debugtopus/index.ts b/src/debugtopus/index.ts index c8de521..4631c64 100644 --- a/src/debugtopus/index.ts +++ b/src/debugtopus/index.ts @@ -19,6 +19,7 @@ import { import { client, handleError } from "../tools/client"; import { readTestCasesFromDir } from "../tools/sync/yml"; import { ensureChromiumIsInstalled } from "./installation"; +import { findOctomindFolder } from "../helpers"; export type DebugtopusOptions = { testCaseId?: string; @@ -207,8 +208,8 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { .filter((testCase) => options.grep ? testCase.description - ?.toLowerCase() - .includes(options.grep.toLowerCase()) + ?.toLowerCase() + .includes(options.grep.toLowerCase()) : true, ) .map(async (testCase) => ({ @@ -262,9 +263,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, make sure to pull before trying to edit", + ); + } + + const testCases = readTestCasesFromDir(octomindRoot); const body = { testCases, testTargetId: options.testTargetId, From cd8046765e814c583511e8b9b6b7e66d07394fa6 Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 15:59:51 +0100 Subject: [PATCH 06/14] fix .octomind folder name in logs --- src/debugtopus/index.ts | 3 ++- src/tools/yamlMutations/edit.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/debugtopus/index.ts b/src/debugtopus/index.ts index 4631c64..23bae21 100644 --- a/src/debugtopus/index.ts +++ b/src/debugtopus/index.ts @@ -20,6 +20,7 @@ import { client, handleError } from "../tools/client"; import { readTestCasesFromDir } from "../tools/sync/yml"; import { ensureChromiumIsInstalled } from "./installation"; import { findOctomindFolder } from "../helpers"; +import { OCTOMIND_FOLDER_NAME } from "../constants"; export type DebugtopusOptions = { testCaseId?: string; @@ -268,7 +269,7 @@ export const executeLocalTestCases = async ( const octomindRoot = await findOctomindFolder() if (!octomindRoot) { throw new Error( - "Could not find .octomind folder, make sure to pull before trying to edit", + `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to execute locally`, ); } diff --git a/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts index 114f09d..c612827 100644 --- a/src/tools/yamlMutations/edit.ts +++ b/src/tools/yamlMutations/edit.ts @@ -9,6 +9,7 @@ import { draftPush } from "../sync/push"; import { SyncTestCase } from "../sync/types"; import { readTestCasesFromDir } from "../sync/yml"; import { findOctomindFolder, getAbsoluteFilePathInOctomindRoot } from "../../helpers"; +import { OCTOMIND_FOLDER_NAME } from "../../constants"; type EditOptions = { testTargetId: string; @@ -52,7 +53,7 @@ export const edit = async (options: EditOptions): Promise => { const octomindRoot = await findOctomindFolder(); if (!octomindRoot) { throw new Error( - "Could not find .octomind folder, make sure to pull before trying to edit", + `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to edit`, ); } const testCaseFilePath = getAbsoluteFilePathInOctomindRoot({ octomindRoot, filePath: options.filePath }) From 8f6ada4d0f3d2fe4f180426d93a0ad7a54a374f9 Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 16:39:18 +0100 Subject: [PATCH 07/14] fix octomind folder helpers and add tests. --- src/helpers.ts | 15 +-- src/tools/yamlMutations/edit.ts | 2 +- tests/helpers.spec.ts | 186 ++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 tests/helpers.spec.ts diff --git a/src/helpers.ts b/src/helpers.ts index 1898578..59fd43f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -63,12 +63,13 @@ export const findOctomindFolder = async (): Promise => { return null }; -export const getAbsoluteFilePathInOctomindRoot = ({ filePath, octomindRoot }: { filePath: string, octomindRoot: string }): string | null => { - let resolvedPath: string - if (path.isAbsolute(filePath)) { - resolvedPath = filePath +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; } - resolvedPath = path.resolve(filePath, path.join(octomindRoot, filePath)); - - return resolvedPath.startsWith(octomindRoot) ? resolvedPath : null } diff --git a/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts index c612827..09ee3e5 100644 --- a/src/tools/yamlMutations/edit.ts +++ b/src/tools/yamlMutations/edit.ts @@ -56,7 +56,7 @@ export const edit = async (options: EditOptions): Promise => { `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to edit`, ); } - const testCaseFilePath = getAbsoluteFilePathInOctomindRoot({ octomindRoot, filePath: options.filePath }) + const testCaseFilePath = await getAbsoluteFilePathInOctomindRoot({ octomindRoot, filePath: options.filePath }) if (!testCaseFilePath) { throw new Error(`Could not find ${options.filePath} in folder ${octomindRoot}`) } diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts new file mode 100644 index 0000000..2b28b27 --- /dev/null +++ b/tests/helpers.spec.ts @@ -0,0 +1,186 @@ +import fsPromises from "fs/promises"; +import os from "os"; +import path from "path"; + +import { + findOctomindFolder, + getAbsoluteFilePathInOctomindRoot, +} from "../src/helpers"; +import { OCTOMIND_FOLDER_NAME } from "../src/constants"; + +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(); + }); + }); +}); From afb21b5801abf2048021e70f7aa096189114ed8f Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:04:56 +0100 Subject: [PATCH 08/14] use early exits in deleteTestCase and add tests for local deletion. --- src/tools/test-cases.ts | 34 +++++------ tests/tools/test-cases.spec.ts | 103 +++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 34 deletions(-) diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index cb54a44..ab41e77 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -5,7 +5,7 @@ import { getUrl } from "../url"; import { client, handleError, ListOptions, logJson } from "./client"; import { getEnvironments } from "./environments"; import { buildFilename, readTestCasesFromDir } from "./sync/yml"; -import { unlink } from "fs/promises"; +import fsPromises from "fs/promises"; export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; @@ -37,24 +37,24 @@ export const deleteTestCase = async ( logJson(data); } console.log("Test Case deleted successfully"); - } else { - const testCases = readTestCasesFromDir(octomindRoot); - const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); - const existingTestCase = testCasesById[options.testCaseId] - - if (existingTestCase) { - const existingTestCasePath = buildFilename( - existingTestCase, - octomindRoot, - ); - await unlink(path.join(octomindRoot, existingTestCasePath)) - console.log("Test Case deleted successfully"); - } - else { - console.log(`No test case with id ${options.testCaseId} found in folder ${octomindRoot}`) - } + return + } + + 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"); }; export const listTestCase = async ( diff --git a/tests/tools/test-cases.spec.ts b/tests/tools/test-cases.spec.ts index 3f1d147..5ccac83 100644 --- a/tests/tools/test-cases.spec.ts +++ b/tests/tools/test-cases.spec.ts @@ -1,38 +1,107 @@ +import fsPromises from "fs/promises"; +import os from "os"; +import path from "path"; + import { client, handleError } from "../../src/tools/client"; +import { findOctomindFolder } from "../../src/helpers"; +import { readTestCasesFromDir, buildFilename } 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" }); }); }); From d316985fcf1b7321bb84cb1d519b20eb44fbe36b Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:30:42 +0100 Subject: [PATCH 09/14] remove edit feature. --- src/cli.ts | 13 ---- src/tools/yamlMutations/edit.ts | 84 -------------------- tests/tools/yamlMutation/edit.spec.ts | 106 ++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 97 deletions(-) delete mode 100644 src/tools/yamlMutations/edit.ts create mode 100644 tests/tools/yamlMutation/edit.spec.ts diff --git a/src/cli.ts b/src/cli.ts index ed6e970..3ffc061 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -38,7 +38,6 @@ import { } from "./tools"; import { init, switchTestTarget } from "./tools/init"; import { update } from "./tools/update"; -import { edit } from "./tools/yamlMutations/edit"; import { version } from "./version"; export const BINARY_NAME = "octomind"; @@ -402,18 +401,6 @@ export const buildCmd = (): CompletableCommand => { .addOption(testTargetIdOption) .action(addTestTargetWrapper(pushTestTarget)); - // noinspection RequiredAttributes - createCommandWithCommonOptions(program, "edit-test-case") - .completer(testTargetIdCompleter) - .description("Edit yaml test case") - .helpGroup("test-cases") - .addOption(testTargetIdOption) - .requiredOption( - "-f, --file-path ", - "The path to the local yaml file you want to edit", - ) - .action(addTestTargetWrapper(edit)); - createCommandWithCommonOptions(program, "list-test-targets") .description("List all test targets") .helpGroup("test-targets") diff --git a/src/tools/yamlMutations/edit.ts b/src/tools/yamlMutations/edit.ts deleted file mode 100644 index 09ee3e5..0000000 --- a/src/tools/yamlMutations/edit.ts +++ /dev/null @@ -1,84 +0,0 @@ -import fs from "fs"; -import path from "path"; - -import yaml from "yaml"; - -import { client, handleError } from "../client"; -import { checkForConsistency } from "../sync/consistency"; -import { draftPush } from "../sync/push"; -import { SyncTestCase } from "../sync/types"; -import { readTestCasesFromDir } from "../sync/yml"; -import { findOctomindFolder, getAbsoluteFilePathInOctomindRoot } from "../../helpers"; -import { OCTOMIND_FOLDER_NAME } from "../../constants"; - -type EditOptions = { - testTargetId: string; - filePath: string; -}; - - -const getRelevantTestCases = ( - testCasesById: Record, - startTestCase: SyncTestCase, -): SyncTestCase[] => { - let dependencyId = startTestCase.dependencyId; - const result: SyncTestCase[] = [startTestCase]; - - while (dependencyId) { - const currentTestCase = testCasesById[dependencyId]; - - if (!currentTestCase) { - throw new Error( - `Could not find dependency ${dependencyId} for ${startTestCase.id}`, - ); - } - - result.push(currentTestCase); - dependencyId = currentTestCase?.dependencyId; - } - - return result; -}; - -const loadTestCase = (testCasePath: string): SyncTestCase => { - try { - const content = fs.readFileSync(testCasePath, "utf8"); - return yaml.parse(content); - } catch (error) { - throw new Error(`Could not parse ${testCasePath}: ${error}`); - } -}; - -export const edit = async (options: EditOptions): Promise => { - const octomindRoot = await findOctomindFolder(); - if (!octomindRoot) { - throw new Error( - `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to edit`, - ); - } - const testCaseFilePath = await getAbsoluteFilePathInOctomindRoot({ octomindRoot, filePath: options.filePath }) - if (!testCaseFilePath) { - throw new Error(`Could not find ${options.filePath} in folder ${octomindRoot}`) - } - - const testCaseToEdit = loadTestCase(testCaseFilePath); - - const testCases = readTestCasesFromDir(octomindRoot); - const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); - const relevantTestCases = getRelevantTestCases(testCasesById, testCaseToEdit); - - checkForConsistency(relevantTestCases); - - const response = await draftPush( - { - testCases: relevantTestCases, - }, - { - testTargetId: options.testTargetId, - client, - onError: handleError, - }, - ); - - console.log(response); -}; diff --git a/tests/tools/yamlMutation/edit.spec.ts b/tests/tools/yamlMutation/edit.spec.ts new file mode 100644 index 0000000..f8b7703 --- /dev/null +++ b/tests/tools/yamlMutation/edit.spec.ts @@ -0,0 +1,106 @@ +import fs from "fs"; + +import { findOctomindFolder, getAbsoluteFilePathInOctomindRoot } from "../../../src/helpers"; +import { readTestCasesFromDir } from "../../../src/tools/sync/yml"; +import { checkForConsistency } from "../../../src/tools/sync/consistency"; +import { draftPush } from "../../../src/tools/sync/push"; +import { edit } from "../../../src/tools/yamlMutations/edit"; +import { OCTOMIND_FOLDER_NAME } from "../../../src/constants"; + +jest.mock("../../../src/helpers"); +jest.mock("../../../src/tools/sync/yml"); +jest.mock("../../../src/tools/sync/consistency"); +jest.mock("../../../src/tools/sync/push"); +jest.mock("fs"); + +const OCTOMIND_ROOT = "/project/.octomind"; +const TEST_CASE_PATH = `${OCTOMIND_ROOT}/myTestCase.yaml`; + +describe("edit", () => { + const originalConsoleLog = console.log; + + beforeEach(() => { + jest.clearAllMocks(); + console.log = jest.fn(); + + // Default happy path mocks + jest.mocked(findOctomindFolder).mockResolvedValue(OCTOMIND_ROOT); + jest.mocked(getAbsoluteFilePathInOctomindRoot).mockResolvedValue(TEST_CASE_PATH); + jest.mocked(fs.readFileSync).mockReturnValue(""); + jest.mocked(readTestCasesFromDir).mockReturnValue([]); + jest.mocked(draftPush).mockResolvedValue({ success: true, versionIds: [] }); + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it("should throw error if octomind folder not found", async () => { + jest.mocked(findOctomindFolder).mockResolvedValue(null); + + await expect( + edit({ testTargetId: "test-target-id", filePath: "test.yaml" }), + ).rejects.toThrow( + `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to edit`, + ); + }); + + it("should throw error if file path not found in octomind root", async () => { + jest.mocked(getAbsoluteFilePathInOctomindRoot).mockResolvedValue(null); + + await expect( + edit({ testTargetId: "test-target-id", filePath: "nonexistent.yaml" }), + ).rejects.toThrow( + `Could not find nonexistent.yaml in folder ${OCTOMIND_ROOT}`, + ); + }); + + it("should load test case and call draftPush with relevant test cases", async () => { + const testCase = { id: "test-case-1", description: "My Test Case" }; + jest.mocked(fs.readFileSync).mockReturnValue("id: test-case-1\ndescription: My Test Case"); + jest.mocked(readTestCasesFromDir).mockReturnValue([testCase] as ReturnType); + + await edit({ testTargetId: "test-target-id", filePath: "myTestCase.yaml" }); + + expect(checkForConsistency).toHaveBeenCalledWith([testCase]); + expect(draftPush).toHaveBeenCalledWith( + { testCases: [testCase] }, + expect.objectContaining({ testTargetId: "test-target-id" }), + ); + }); + + it("should include dependency chain in relevant test cases", async () => { + const parentTestCase = { id: "parent-id", description: "Parent" }; + const childTestCase = { id: "child-id", description: "Child", dependencyId: "parent-id" }; + jest.mocked(fs.readFileSync).mockReturnValue("id: child-id\ndescription: Child\ndependencyId: parent-id"); + jest.mocked(readTestCasesFromDir).mockReturnValue([parentTestCase, childTestCase] as ReturnType); + + await edit({ testTargetId: "test-target-id", filePath: "childTest.yaml" }); + + expect(checkForConsistency).toHaveBeenCalledWith([childTestCase, parentTestCase]); + expect(draftPush).toHaveBeenCalledWith( + { testCases: [childTestCase, parentTestCase] }, + expect.objectContaining({ testTargetId: "test-target-id" }), + ); + }); + + it("should throw error if dependency not found", async () => { + const orphanTestCase = { id: "orphan-id", description: "Orphan", dependencyId: "missing-id" }; + jest.mocked(fs.readFileSync).mockReturnValue("id: orphan-id\ndescription: Orphan\ndependencyId: missing-id"); + jest.mocked(readTestCasesFromDir).mockReturnValue([orphanTestCase] as ReturnType); + + await expect( + edit({ testTargetId: "test-target-id", filePath: "orphan.yaml" }), + ).rejects.toThrow("Could not find dependency missing-id for orphan-id"); + }); + + it("should throw error if test case file cannot be parsed", async () => { + jest.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File not found"); + }); + + await expect( + edit({ testTargetId: "test-target-id", filePath: "invalid.yaml" }), + ).rejects.toThrow(`Could not parse ${TEST_CASE_PATH}`); + }); +}); From 2c0507e4e04faa82a25c1930e11d3831322031ff Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:33:46 +0100 Subject: [PATCH 10/14] remove edit tests --- tests/tools/yamlMutation/edit.spec.ts | 106 -------------------------- 1 file changed, 106 deletions(-) delete mode 100644 tests/tools/yamlMutation/edit.spec.ts diff --git a/tests/tools/yamlMutation/edit.spec.ts b/tests/tools/yamlMutation/edit.spec.ts deleted file mode 100644 index f8b7703..0000000 --- a/tests/tools/yamlMutation/edit.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from "fs"; - -import { findOctomindFolder, getAbsoluteFilePathInOctomindRoot } from "../../../src/helpers"; -import { readTestCasesFromDir } from "../../../src/tools/sync/yml"; -import { checkForConsistency } from "../../../src/tools/sync/consistency"; -import { draftPush } from "../../../src/tools/sync/push"; -import { edit } from "../../../src/tools/yamlMutations/edit"; -import { OCTOMIND_FOLDER_NAME } from "../../../src/constants"; - -jest.mock("../../../src/helpers"); -jest.mock("../../../src/tools/sync/yml"); -jest.mock("../../../src/tools/sync/consistency"); -jest.mock("../../../src/tools/sync/push"); -jest.mock("fs"); - -const OCTOMIND_ROOT = "/project/.octomind"; -const TEST_CASE_PATH = `${OCTOMIND_ROOT}/myTestCase.yaml`; - -describe("edit", () => { - const originalConsoleLog = console.log; - - beforeEach(() => { - jest.clearAllMocks(); - console.log = jest.fn(); - - // Default happy path mocks - jest.mocked(findOctomindFolder).mockResolvedValue(OCTOMIND_ROOT); - jest.mocked(getAbsoluteFilePathInOctomindRoot).mockResolvedValue(TEST_CASE_PATH); - jest.mocked(fs.readFileSync).mockReturnValue(""); - jest.mocked(readTestCasesFromDir).mockReturnValue([]); - jest.mocked(draftPush).mockResolvedValue({ success: true, versionIds: [] }); - }); - - afterEach(() => { - console.log = originalConsoleLog; - }); - - it("should throw error if octomind folder not found", async () => { - jest.mocked(findOctomindFolder).mockResolvedValue(null); - - await expect( - edit({ testTargetId: "test-target-id", filePath: "test.yaml" }), - ).rejects.toThrow( - `Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to edit`, - ); - }); - - it("should throw error if file path not found in octomind root", async () => { - jest.mocked(getAbsoluteFilePathInOctomindRoot).mockResolvedValue(null); - - await expect( - edit({ testTargetId: "test-target-id", filePath: "nonexistent.yaml" }), - ).rejects.toThrow( - `Could not find nonexistent.yaml in folder ${OCTOMIND_ROOT}`, - ); - }); - - it("should load test case and call draftPush with relevant test cases", async () => { - const testCase = { id: "test-case-1", description: "My Test Case" }; - jest.mocked(fs.readFileSync).mockReturnValue("id: test-case-1\ndescription: My Test Case"); - jest.mocked(readTestCasesFromDir).mockReturnValue([testCase] as ReturnType); - - await edit({ testTargetId: "test-target-id", filePath: "myTestCase.yaml" }); - - expect(checkForConsistency).toHaveBeenCalledWith([testCase]); - expect(draftPush).toHaveBeenCalledWith( - { testCases: [testCase] }, - expect.objectContaining({ testTargetId: "test-target-id" }), - ); - }); - - it("should include dependency chain in relevant test cases", async () => { - const parentTestCase = { id: "parent-id", description: "Parent" }; - const childTestCase = { id: "child-id", description: "Child", dependencyId: "parent-id" }; - jest.mocked(fs.readFileSync).mockReturnValue("id: child-id\ndescription: Child\ndependencyId: parent-id"); - jest.mocked(readTestCasesFromDir).mockReturnValue([parentTestCase, childTestCase] as ReturnType); - - await edit({ testTargetId: "test-target-id", filePath: "childTest.yaml" }); - - expect(checkForConsistency).toHaveBeenCalledWith([childTestCase, parentTestCase]); - expect(draftPush).toHaveBeenCalledWith( - { testCases: [childTestCase, parentTestCase] }, - expect.objectContaining({ testTargetId: "test-target-id" }), - ); - }); - - it("should throw error if dependency not found", async () => { - const orphanTestCase = { id: "orphan-id", description: "Orphan", dependencyId: "missing-id" }; - jest.mocked(fs.readFileSync).mockReturnValue("id: orphan-id\ndescription: Orphan\ndependencyId: missing-id"); - jest.mocked(readTestCasesFromDir).mockReturnValue([orphanTestCase] as ReturnType); - - await expect( - edit({ testTargetId: "test-target-id", filePath: "orphan.yaml" }), - ).rejects.toThrow("Could not find dependency missing-id for orphan-id"); - }); - - it("should throw error if test case file cannot be parsed", async () => { - jest.mocked(fs.readFileSync).mockImplementation(() => { - throw new Error("File not found"); - }); - - await expect( - edit({ testTargetId: "test-target-id", filePath: "invalid.yaml" }), - ).rejects.toThrow(`Could not parse ${TEST_CASE_PATH}`); - }); -}); From 3d74178f1b56df8027b2c786e84bb41d9da2b24d Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:48:21 +0100 Subject: [PATCH 11/14] fix linting --- biome.json | 2 +- src/constants.ts | 2 +- src/debugtopus/index.ts | 10 +++++----- src/helpers.ts | 20 ++++++++++++-------- src/tools/test-cases.ts | 22 +++++++++++----------- src/tools/test-targets.ts | 14 +++++++++----- tests/helpers.spec.ts | 17 +++++++++++++---- tests/tools/test-cases.spec.ts | 20 +++++++++++++------- 8 files changed, 65 insertions(+), 42 deletions(-) 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/src/constants.ts b/src/constants.ts index 0ec372d..5701db5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1 @@ -export const OCTOMIND_FOLDER_NAME = ".octomind" +export const OCTOMIND_FOLDER_NAME = ".octomind"; diff --git a/src/debugtopus/index.ts b/src/debugtopus/index.ts index 23bae21..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, @@ -19,8 +21,6 @@ import { import { client, handleError } from "../tools/client"; import { readTestCasesFromDir } from "../tools/sync/yml"; import { ensureChromiumIsInstalled } from "./installation"; -import { findOctomindFolder } from "../helpers"; -import { OCTOMIND_FOLDER_NAME } from "../constants"; export type DebugtopusOptions = { testCaseId?: string; @@ -209,8 +209,8 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { .filter((testCase) => options.grep ? testCase.description - ?.toLowerCase() - .includes(options.grep.toLowerCase()) + ?.toLowerCase() + .includes(options.grep.toLowerCase()) : true, ) .map(async (testCase) => ({ @@ -266,7 +266,7 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { export const executeLocalTestCases = async ( options: DebugtopusOptions, ): Promise => { - const octomindRoot = await findOctomindFolder() + 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`, diff --git a/src/helpers.ts b/src/helpers.ts index 59fd43f..06fd58e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,12 +1,11 @@ +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 path from "node:path"; -import fsPromises from "fs/promises"; import { OCTOMIND_FOLDER_NAME } from "./constants"; - export function promptUser(question: string): Promise { const rl = createInterface({ input, output }); @@ -34,7 +33,6 @@ export const resolveTestTargetId = async ( return config.testTargetId; }; - const isDirectory = async (dirPath: string): Promise => { try { const stat = await fsPromises.stat(dirPath); @@ -60,16 +58,22 @@ export const findOctomindFolder = async (): Promise => { return rootOctomind; } - return null + return null; }; -export const getAbsoluteFilePathInOctomindRoot = async ({ filePath, octomindRoot }: { filePath: string, octomindRoot: string }): Promise => { +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) + path.isAbsolute(filePath) ? filePath : path.join(octomindRoot, filePath), ); return resolvedPath.startsWith(octomindRoot) ? resolvedPath : null; } catch { return null; } -} +}; diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index ab41e77..c831fe0 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -1,11 +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"; -import fsPromises from "fs/promises"; export type TestCaseResponse = components["schemas"]["TestCaseResponse"]; export type TestCasesResponse = components["schemas"]["TestCasesResponse"]; @@ -17,7 +18,7 @@ export type DeleteTestCaseParams = export const deleteTestCase = async ( options: DeleteTestCaseParams & ListOptions, ): Promise => { - const octomindRoot = await findOctomindFolder() + const octomindRoot = await findOctomindFolder(); if (!octomindRoot) { const { data, error } = await client.DELETE( @@ -37,23 +38,22 @@ export const deleteTestCase = async ( logJson(data); } console.log("Test Case deleted successfully"); - return + return; } const testCases = readTestCasesFromDir(octomindRoot); const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc])); - const existingTestCase = testCasesById[options.testCaseId] + const existingTestCase = testCasesById[options.testCaseId]; if (!existingTestCase) { - console.log(`No test case with id ${options.testCaseId} found in folder ${octomindRoot}`) - return + 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)) + 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 0b2238f..77b185f 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -1,11 +1,11 @@ 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"; import { writeYaml } from "./sync/yml"; -import { findOctomindFolder } from "../helpers"; -import { OCTOMIND_FOLDER_NAME } from "../constants"; export const getTestTargets = async () => { const { data, error } = await client.GET("/apiKey/v3/test-targets"); @@ -70,7 +70,9 @@ export const pullTestTarget = async ( return; } - const destination = await findOctomindFolder() ?? path.join(process.cwd(), OCTOMIND_FOLDER_NAME) + const destination = + (await findOctomindFolder()) ?? + path.join(process.cwd(), OCTOMIND_FOLDER_NAME); writeYaml(data, destination); console.log("Test Target pulled successfully"); @@ -79,9 +81,11 @@ export const pullTestTarget = async ( export const pushTestTarget = async ( options: { testTargetId: string } & ListOptions, ): Promise => { - const sourceDir = await findOctomindFolder() + const sourceDir = await findOctomindFolder(); if (!sourceDir) { - throw new Error(`No ${OCTOMIND_FOLDER_NAME} folder found, please pull first.`) + throw new Error( + `No ${OCTOMIND_FOLDER_NAME} folder found, please pull first.`, + ); } const data = await push({ diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts index 2b28b27..a4f65b6 100644 --- a/tests/helpers.spec.ts +++ b/tests/helpers.spec.ts @@ -2,11 +2,11 @@ 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"; -import { OCTOMIND_FOLDER_NAME } from "../src/constants"; describe("helpers", () => { describe("findOctomindFolder", () => { @@ -14,7 +14,9 @@ describe("helpers", () => { const originalCwd = process.cwd; beforeEach(async () => { - tmpDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "octomind-test-")); + tmpDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), "octomind-test-"), + ); }); afterEach(async () => { @@ -91,7 +93,9 @@ describe("helpers", () => { let octomindRoot: string; beforeEach(async () => { - tmpDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "octomind-test-")); + tmpDir = await fsPromises.mkdtemp( + path.join(os.tmpdir(), "octomind-test-"), + ); octomindRoot = path.join(tmpDir, OCTOMIND_FOLDER_NAME); await fsPromises.mkdir(octomindRoot); }); @@ -173,7 +177,12 @@ describe("helpers", () => { 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 malformedPath = path.join( + octomindRoot, + tmpDir, + OCTOMIND_FOLDER_NAME, + "a.yaml", + ); const result = await getAbsoluteFilePathInOctomindRoot({ filePath: malformedPath, diff --git a/tests/tools/test-cases.spec.ts b/tests/tools/test-cases.spec.ts index 5ccac83..49e1d9b 100644 --- a/tests/tools/test-cases.spec.ts +++ b/tests/tools/test-cases.spec.ts @@ -2,9 +2,9 @@ import fsPromises from "fs/promises"; import os from "os"; import path from "path"; -import { client, handleError } from "../../src/tools/client"; import { findOctomindFolder } from "../../src/helpers"; -import { readTestCasesFromDir, buildFilename } from "../../src/tools/sync/yml"; +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"); @@ -40,7 +40,9 @@ describe("test-cases", () => { testCaseId: "test-case-id", }); expect(handleError).toHaveBeenCalledWith(undefined); - expect(console.log).toHaveBeenCalledWith("Test Case deleted successfully"); + expect(console.log).toHaveBeenCalledWith( + "Test Case deleted successfully", + ); }); it("should handle error", async () => { @@ -76,9 +78,11 @@ describe("test-cases", () => { const filePath = path.join(tmpDir, fileName); await fsPromises.writeFile(filePath, ""); - jest.mocked(readTestCasesFromDir).mockReturnValue([ - { id: testCaseId, description: "My Test Case" }, - ] as ReturnType); + jest + .mocked(readTestCasesFromDir) + .mockReturnValue([ + { id: testCaseId, description: "My Test Case" }, + ] as ReturnType); jest.mocked(buildFilename).mockReturnValue(fileName); await deleteTestCase({ @@ -86,7 +90,9 @@ describe("test-cases", () => { testCaseId, }); - expect(console.log).toHaveBeenCalledWith("Test Case deleted successfully"); + expect(console.log).toHaveBeenCalledWith( + "Test Case deleted successfully", + ); await expect(fsPromises.access(filePath)).rejects.toThrow(); }); From e3a0b8149930b12232246b453aa45dc85b7a900f Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:54:48 +0100 Subject: [PATCH 12/14] fix typing --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 From 9872e33d492c3cd75b0686cb5e82d895e5af3bd4 Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 17:59:07 +0100 Subject: [PATCH 13/14] fix tests --- tests/debugtopous/index.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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({ From 969000e54e974cadbdad6ab14940758231b7d945 Mon Sep 17 00:00:00 2001 From: Fabian Both Date: Mon, 12 Jan 2026 18:02:28 +0100 Subject: [PATCH 14/14] fix tests --- tests/tools/test-targets.spec.ts | 3 +++ 1 file changed, 3 insertions(+) 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",