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
Show all changes
24 commits
Select commit Hold shift + click to select a range
04c59b3
edit test cases
Germandrummer92 Jan 12, 2026
9f75a38
remove logs
Germandrummer92 Jan 12, 2026
9a96e3d
folder name constant
Jan 12, 2026
bc3ef29
Merge branch 'edit-test-case' of github.com:OctoMind-dev/cli into edi…
Jan 12, 2026
2114e3e
make .octomind folder the default for push, pull, delete and edit.
Jan 12, 2026
d33c197
use octomind folder in local execution
Jan 12, 2026
cd80467
fix .octomind folder name in logs
Jan 12, 2026
838990e
create test case via cli
Jan 12, 2026
8f6ada4
fix octomind folder helpers and add tests.
Jan 12, 2026
afb21b5
use early exits in deleteTestCase and add tests for local deletion.
Jan 12, 2026
56e8d1e
Merge branch 'require-octomind-folder' of github.com:OctoMind-dev/cli…
Jan 12, 2026
de28bf2
fix linting
Jan 12, 2026
924c327
Merge branch 'main' of github.com:OctoMind-dev/cli into create-test-c…
Jan 12, 2026
4dad428
Merge branch 'main' of github.com:OctoMind-dev/cli into create-test-c…
Jan 15, 2026
5e8f77d
add dependency option
Jan 15, 2026
1cac41d
optionally push dependencies in create command
Jan 15, 2026
f8f7f89
create logic to open and poll new test case
Jan 15, 2026
2bde7ee
correctly build output path
Jan 15, 2026
ea150ec
add tests for create command
Jan 15, 2026
53246b3
improve mocking for create and edit
Jan 15, 2026
0fedddd
fix expect mocking, using called with instead.
Jan 15, 2026
bb1d6ed
mini naming fixes in test case
Jan 15, 2026
dce59f3
move load test case method
Jan 15, 2026
5edcbd8
split openAndPoll method
Jan 15, 2026
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
19 changes: 19 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from "./tools";
import { init, switchTestTarget } from "./tools/init";
import { update } from "./tools/update";
import { create } from "./tools/yamlMutations/create";
import { edit } from "./tools/yamlMutations/edit";
import { version } from "./version";

Expand Down Expand Up @@ -402,6 +403,24 @@ export const buildCmd = (): CompletableCommand => {
.addOption(testTargetIdOption)
.action(addTestTargetWrapper(pushTestTarget));

// noinspection RequiredAttributes
createCommandWithCommonOptions(program, "create-test-case")
.completer(testTargetIdCompleter)
.description("Create a new test case")
.helpGroup("test-cases")
.addOption(testTargetIdOption)
.requiredOption(
"-n, --name <string>",
"The name of the test case you want to create",
)
.addOption(
new Option(
"-d, --dependency-path <path>",
"The path of to test case you want to use as dependency",
),
)
.action(addTestTargetWrapper(create));

// noinspection RequiredAttributes
createCommandWithCommonOptions(program, "edit-test-case")
.completer(testTargetIdCompleter)
Expand Down
9 changes: 9 additions & 0 deletions src/tools/sync/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ export const readTestCasesFromDir = (startDir: string): SyncTestCase[] => {
return testCases;
};

export 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 cleanupFilesystem = ({
newTestCases,
destination,
Expand Down
180 changes: 180 additions & 0 deletions src/tools/yamlMutations/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import path from "node:path";

import { createTwoFilesPatch } from "diff";
import open from "open";
import yaml from "yaml";

import { OCTOMIND_FOLDER_NAME } from "../../constants";
import {
findOctomindFolder,
getAbsoluteFilePathInOctomindRoot,
} from "../../helpers";
import { BASE_URL, client, handleError } from "../client";
import { checkForConsistency } from "../sync/consistency";
import { draftPush } from "../sync/push";
import { SyncDataByStableId, SyncTestCase } from "../sync/types";
import {
buildFilename,
buildFolderName,
loadTestCase,
readTestCasesFromDir,
writeSingleTestCaseYaml,
} from "../sync/yaml";
import { getRelevantTestCases } from "./getRelevantTestCases";
import { waitForLocalChangesToBeFinished } from "./waitForLocalChanges";

type CreateOptions = {
testTargetId: string;
name: string;
dependencyPath?: string;
};

const getDependencyTestCases = async ({
octomindRoot,
dependencyPath,
}: {
octomindRoot: string;
dependencyPath: string;
}): Promise<{
dependencyTestCase: SyncTestCase;
relevantTestCases: SyncTestCase[];
}> => {
const dependencyFilePath = await getAbsoluteFilePathInOctomindRoot({
octomindRoot,
filePath: dependencyPath,
});

if (!dependencyFilePath) {
throw new Error(
`Could not find dependency test case ${dependencyPath} in folder ${octomindRoot}`,
);
}
const dependencyTestCase = loadTestCase(dependencyFilePath);

const testCases = readTestCasesFromDir(octomindRoot);
const testCasesById = Object.fromEntries(testCases.map((tc) => [tc.id, tc]));

const relevantTestCases = getRelevantTestCases(
testCasesById,
dependencyTestCase,
);
return { dependencyTestCase, relevantTestCases };
};

const openBrowser = async ({
versionId,
testTargetId,
}: {
versionId: string;
testTargetId: string;
}): Promise<void> => {
const parsedBaseUrl = URL.parse(BASE_URL);
const localEditingUrl = new URL(
`${parsedBaseUrl?.protocol}//${parsedBaseUrl?.host}/testtargets/${testTargetId}/testcases/${versionId}/localEdit`,
);

await open(localEditingUrl.href);

console.log(
`Navigating to local url, open it manually if a browser didn't open already: ${localEditingUrl}`,
);
};

const writeOutput = async ({
testCaseFilePath,
createResult,
}: {
testCaseFilePath: string;
createResult: SyncTestCase | "cancelled";
}): Promise<void> => {
if (createResult === "cancelled") {
console.log("Cancelled editing test case, exiting");
return;
}

await writeSingleTestCaseYaml(testCaseFilePath, createResult);

const diff = createTwoFilesPatch(
"old.yaml",
"new.yaml",
"",
yaml.stringify(createResult),
);

console.log(`Created test case successfully`);
console.log(diff);
};

export const create = async (options: CreateOptions): Promise<void> => {
const octomindRoot = await findOctomindFolder();
if (!octomindRoot) {
throw new Error(
`Could not find ${OCTOMIND_FOLDER_NAME} folder, make sure to pull before trying to create a test case`,
);
}

let dependencyId: string | undefined = undefined;
const testCasesToPush: SyncTestCase[] = [];
if (options.dependencyPath) {
const { dependencyTestCase, relevantTestCases } =
await getDependencyTestCases({
octomindRoot,
dependencyPath: options.dependencyPath,
});
dependencyId = dependencyTestCase.id;
testCasesToPush.push(...relevantTestCases);
}

const newTestCase: SyncTestCase = {
version: "1",
id: crypto.randomUUID(),
dependencyId,
elements: [],
runStatus: "OFF",
description: options.name,
prompt: "",
localEditingStatus: "IN_PROGRESS" as const,
};
testCasesToPush.push(newTestCase);
checkForConsistency(testCasesToPush);

const response = await draftPush(
{
testCases: testCasesToPush,
},
{
testTargetId: options.testTargetId,
client,
onError: handleError,
},
);

if (!response) {
throw new Error(
`Could not create new test case with id '${newTestCase.id}'`,
);
}
const syncData = response.syncDataByStableId[newTestCase.id];
if (!syncData) {
throw new Error(`Could not create test case with id '${newTestCase.id}'`);
}

await openBrowser({
versionId: syncData.versionId,
testTargetId: options.testTargetId,
});
const createResult = await waitForLocalChangesToBeFinished(
syncData.versionId,
newTestCase,
{ testTargetId: options.testTargetId },
);

const testCaseFolder = buildFolderName(
newTestCase,
testCasesToPush,
octomindRoot,
);
const newTestCaseName = buildFilename(newTestCase, octomindRoot);
const newTestCasePath = path.join(testCaseFolder, newTestCaseName);
await writeOutput({ createResult, testCaseFilePath: newTestCasePath });
};
20 changes: 5 additions & 15 deletions src/tools/yamlMutations/edit.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import fs from "fs";

import { createTwoFilesPatch } from "diff";
import open from "open";
import ora from "ora";
import yaml from "yaml";

import { OCTOMIND_FOLDER_NAME } from "../../constants";
import {
findOctomindFolder,
getAbsoluteFilePathInOctomindRoot,
sleep,
} from "../../helpers";
import { BASE_URL, client, handleError } from "../client";
import { checkForConsistency } from "../sync/consistency";
import { draftPush } from "../sync/push";
import { SyncTestCase } from "../sync/types";
import { readTestCasesFromDir, writeSingleTestCaseYaml } from "../sync/yaml";
import {
loadTestCase,
readTestCasesFromDir,
writeSingleTestCaseYaml,
} from "../sync/yaml";
import { getRelevantTestCases } from "./getRelevantTestCases";
import { waitForLocalChangesToBeFinished } from "./waitForLocalChanges";

Expand All @@ -24,15 +23,6 @@ type EditOptions = {
filePath: string;
};

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<void> => {
const octomindRoot = await findOctomindFolder();
if (!octomindRoot) {
Expand Down
17 changes: 16 additions & 1 deletion tests/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
TestCaseResponse,
TestReport,
} from "../src/tools";
import { SyncTestCase } from "../src/tools/sync/types";
import { SyncDataByStableId, SyncTestCase } from "../src/tools/sync/types";

export const createMockSyncTestCase = (
overrides?: Partial<SyncTestCase>,
Expand Down Expand Up @@ -76,3 +76,18 @@ export const createMockTestReport = (
updatedAt: new Date().toISOString(),
...overrides,
});

export const createMockDraftPushResponse = (
testCaseId: string,
overrides?: Partial<SyncDataByStableId[string]>,
): {
success: boolean;
versionIds: string[];
syncDataByStableId: SyncDataByStableId;
} => ({
success: true,
versionIds: [],
syncDataByStableId: {
[testCaseId]: { versionId: "version-123", ...overrides },
},
});
Loading