Skip to content
This repository was archived by the owner on Jul 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`

Expand Down Expand Up @@ -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 <path>` | Source directory (defaults to current directory) | Yes | ./.octomind |

## create-discovery

Expand Down Expand Up @@ -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 <path>` | Destination folder | Yes | ./.octomind |

## push

Expand All @@ -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 <path>` | Source directory (defaults to current directory) | Yes | .octomind |

## Test Reports

Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 0 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>",
"Source directory (defaults to current directory)",
"./.octomind",
)
.action(addTestTargetWrapper(executeLocalTestCases));

createCommandWithCommonOptions(program, "test-report")
Expand Down Expand Up @@ -396,7 +391,6 @@ export const buildCmd = (): CompletableCommand => {
.description("Pull test cases from the test target")
.helpGroup("test-cases")
.addOption(testTargetIdOption)
.option("-d, --destination <path>", "Destination folder", "./.octomind")
.action(addTestTargetWrapper(pullTestTarget));

// noinspection RequiredAttributes
Expand All @@ -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 <path>",
"Source directory (defaults to current directory)",
".octomind",
)
.action(addTestTargetWrapper(pushTestTarget));

createCommandWithCommonOptions(program, "list-test-targets")
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const OCTOMIND_FOLDER_NAME = ".octomind";
13 changes: 11 additions & 2 deletions src/debugtopus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -262,9 +264,16 @@ export const runDebugtopus = async (options: DebugtopusOptions) => {
};

export const executeLocalTestCases = async (
options: DebugtopusOptions & { source: string },
options: DebugtopusOptions,
): Promise<void> => {
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,
Expand Down
48 changes: 48 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
const rl = createInterface({ input, output });
Expand Down Expand Up @@ -29,3 +32,48 @@ export const resolveTestTargetId = async (
}
return config.testTargetId;
};

const isDirectory = async (dirPath: string): Promise<boolean> => {
try {
const stat = await fsPromises.stat(dirPath);
return stat.isDirectory();
} catch {
return false;
}
};

export const findOctomindFolder = async (): Promise<string | null> => {
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<string | null> => {
try {
const resolvedPath = await fsPromises.realpath(
path.isAbsolute(filePath) ? filePath : path.join(octomindRoot, filePath),
);
return resolvedPath.startsWith(octomindRoot) ? resolvedPath : null;
} catch {
return null;
}
};
4 changes: 2 additions & 2 deletions src/tools/sync/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ const defaultPush = async (
return data;
};

const draftPush = async (
export const draftPush = async (
body: TestTargetSyncData,
options: PushOptions & ListOptions,
options: Omit<PushOptions, "sourceDir"> & ListOptions,
): Promise<{ success: boolean; versionIds: string[] } | undefined> => {
const { data, error } = await options.client.POST(
"/apiKey/beta/test-targets/{testTargetId}/draft/push",
Expand Down
46 changes: 34 additions & 12 deletions src/tools/test-cases.ts
Original file line number Diff line number Diff line change
@@ -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"];
Expand All @@ -13,25 +18,42 @@ export type DeleteTestCaseParams =
export const deleteTestCase = async (
options: DeleteTestCaseParams & ListOptions,
): Promise<void> => {
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");
};

Expand Down
20 changes: 14 additions & 6 deletions src/tools/test-targets.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -44,7 +46,7 @@ export const listTestTargets = async (options: ListOptions): Promise<void> => {
};

export const pullTestTarget = async (
options: { testTargetId: string; destination?: string } & ListOptions,
options: { testTargetId: string } & ListOptions,
): Promise<void> => {
const { data, error } = await client.GET(
"/apiKey/beta/test-targets/{testTargetId}/pull",
Expand All @@ -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<void> => {
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,
Expand Down
8 changes: 6 additions & 2 deletions tests/debugtopous/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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");

Expand Down Expand Up @@ -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]);
Expand All @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
Loading