From 664f0e2f18b3529315970220a4440f5d7cb1528a Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Wed, 21 Jan 2026 16:31:45 +0100 Subject: [PATCH 1/4] filtered pull and test plan --- src/cli.ts | 4 +++ src/tools/index.ts | 1 + src/tools/test-plans.ts | 26 ++++++++++++++ src/tools/test-targets.ts | 7 +++- tests/tools/test-plans.spec.ts | 65 ++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/tools/test-plans.ts create mode 100644 tests/tools/test-plans.spec.ts diff --git a/src/cli.ts b/src/cli.ts index 1e8b77d..c4a1f6d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -397,6 +397,10 @@ export const buildCmd = (): CompletableCommand => { .description("Pull test cases from the test target") .helpGroup("test-cases") .addOption(testTargetIdOption) + .option( + "-p, --test-plan-id [id]", + "Optional test plan ID to filter test cases", + ) .action(addTestTargetWrapper(pullTestTarget)); // noinspection RequiredAttributes diff --git a/src/tools/index.ts b/src/tools/index.ts index e6da8ad..b8d3045 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -4,5 +4,6 @@ export * from "./notifications"; export * from "./playwright"; export * from "./private-locations"; export * from "./test-cases"; +export * from "./test-plans"; export * from "./test-reports"; export * from "./test-targets"; diff --git a/src/tools/test-plans.ts b/src/tools/test-plans.ts new file mode 100644 index 0000000..24f2689 --- /dev/null +++ b/src/tools/test-plans.ts @@ -0,0 +1,26 @@ +import type { components, paths } from "../api"; +import { client, handleError } from "./client"; + +export type TestPlanResponse = components["schemas"]["ExternalTestPlanSchema"]; +export type GetTestPlanParams = + paths["/apiKey/beta/test-plans/{id}"]["get"]["parameters"]["path"]; + +export const getTestPlan = async ( + options: GetTestPlanParams, +): Promise => { + const { data, error } = await client.GET("/apiKey/beta/test-plans/{id}", { + params: { + path: { + id: options.id, + }, + }, + }); + + handleError(error); + + if (!data) { + throw new Error(`No test plan with id ${options.id} found`); + } + + return data; +}; diff --git a/src/tools/test-targets.ts b/src/tools/test-targets.ts index cf39a33..08a82b5 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -69,7 +69,7 @@ export const listTestTargets = async (options: ListOptions): Promise => { }; export const pullTestTarget = async ( - options: { testTargetId: string } & ListOptions, + options: { testTargetId: string; testPlanId?: string } & ListOptions, ): Promise => { const { data, error } = await client.GET( "/apiKey/beta/test-targets/{testTargetId}/pull", @@ -78,6 +78,11 @@ export const pullTestTarget = async ( path: { testTargetId: options.testTargetId, }, + query: options.testPlanId + ? { + testPlanId: options.testPlanId, + } + : undefined, }, }, ); diff --git a/tests/tools/test-plans.spec.ts b/tests/tools/test-plans.spec.ts new file mode 100644 index 0000000..bea8aa5 --- /dev/null +++ b/tests/tools/test-plans.spec.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { mock } from "vitest-mock-extended"; + +import { client, handleError } from "../../src/tools/client"; +import { getTestPlan } from "../../src/tools/test-plans"; + +vi.mock("../../src/tools/client"); + +describe("test-plans", () => { + beforeEach(() => { + console.log = vi.fn(); + }); + + describe("getTestPlan", () => { + it("should retrieve a test plan by id", async () => { + const testPlanId = "test-plan-123"; + const mockTestPlan = { + id: testPlanId, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + status: "DONE", + testTargetId: "test-target-123", + environmentId: "env-123", + baseUrl: "https://example.com", + prompt: "Test plan prompt", + traceUrl: "https://trace.example.com", + videoUrl: null, + context: {}, + }; + + vi.mocked(client.GET).mockResolvedValue({ + data: mockTestPlan, + error: undefined, + response: mock(), + }); + + const result = await getTestPlan({ id: testPlanId }); + + expect(client.GET).toHaveBeenCalledWith("/apiKey/beta/test-plans/{id}", { + params: { + path: { + id: testPlanId, + }, + }, + }); + expect(handleError).toHaveBeenCalledWith(undefined); + expect(result).toEqual(mockTestPlan); + }); + + it("should handle error when test plan is not found", async () => { + const testPlanId = "non-existent-id"; + + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: { message: "Not found" }, + response: mock(), + }); + + await expect(getTestPlan({ id: testPlanId })).rejects.toThrow( + `No test plan with id ${testPlanId} found`, + ); + expect(handleError).toHaveBeenCalledWith({ message: "Not found" }); + }); + }); +}); From 9d0ab94734c062fc1dc38d9d5e10eedbba877b81 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 08:56:20 +0100 Subject: [PATCH 2/4] adjust route --- README.md | 3 ++- package.json | 2 +- src/tools/test-plans.ts | 16 ++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7882e66..4e528f2 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: 4.3.3. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 4.4.0. Additional documentation see https://octomind.dev/docs/api-reference/ **Usage:** `octomind [options] [command]` @@ -432,6 +432,7 @@ 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 | | +| `-p, --test-plan-id [id]` | Optional test plan ID to filter test cases | No | | ## push diff --git a/package.json b/package.json index b3909ef..f645a66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "4.3.3", + "version": "4.4.0", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316", diff --git a/src/tools/test-plans.ts b/src/tools/test-plans.ts index 24f2689..4a98f62 100644 --- a/src/tools/test-plans.ts +++ b/src/tools/test-plans.ts @@ -3,18 +3,22 @@ import { client, handleError } from "./client"; export type TestPlanResponse = components["schemas"]["ExternalTestPlanSchema"]; export type GetTestPlanParams = - paths["/apiKey/beta/test-plans/{id}"]["get"]["parameters"]["path"]; + paths["/apiKey/beta/test-targets/{testTargetId}/test-plans/{id}"]["get"]["parameters"]["path"]; export const getTestPlan = async ( options: GetTestPlanParams, ): Promise => { - const { data, error } = await client.GET("/apiKey/beta/test-plans/{id}", { - params: { - path: { - id: options.id, + const { data, error } = await client.GET( + "/apiKey/beta/test-targets/{testTargetId}/test-plans/{id}", + { + params: { + path: { + id: options.id, + testTargetId: options.testTargetId, + }, }, }, - }); + ); handleError(error); From b6eddabee68de43b6427865cbb154b79b725aa76 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 09:02:31 +0100 Subject: [PATCH 3/4] pr comments adapt removing when partial pull --- src/tools/sync/yaml.ts | 23 ++++++++++++++------ src/tools/test-targets.ts | 2 +- tests/tools/sync/yaml.spec.ts | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/tools/sync/yaml.ts b/src/tools/sync/yaml.ts index 4114c35..32fa285 100644 --- a/src/tools/sync/yaml.ts +++ b/src/tools/sync/yaml.ts @@ -57,10 +57,12 @@ export const writeSingleTestCaseYaml = async ( export const writeYaml = async ( data: TestTargetSyncData, destination?: string, + partialSync = false, ): Promise => { cleanupFilesystem({ remoteTestCases: data.testCases, destination, + partialSync, }); for (const testCase of data.testCases) { @@ -231,9 +233,11 @@ export const removeEmptyDirectoriesRecursively = ( export const cleanupFilesystem = ({ remoteTestCases, destination, + partialSync, }: { remoteTestCases: SyncTestCase[]; destination: string | undefined; + partialSync: boolean; }) => { const rootFolderPath = destination ?? process.cwd(); @@ -270,13 +274,18 @@ export const cleanupFilesystem = ({ } } } - for (const localTestCase of localTestCases) { - // If the local test case is not in the remote test cases, remove it - if (!remoteTestCasesById.has(localTestCase.id) && localTestCase.filePath) { - fs.rmSync(localTestCase.filePath, { force: true }); - - const dirPath = path.dirname(localTestCase.filePath); - removeEmptyDirectoriesRecursively(dirPath, rootFolderPath); + if (!partialSync) { + for (const localTestCase of localTestCases) { + // If the local test case is not in the remote test cases, remove it + if ( + !remoteTestCasesById.has(localTestCase.id) && + localTestCase.filePath + ) { + fs.rmSync(localTestCase.filePath, { force: true }); + + const dirPath = path.dirname(localTestCase.filePath); + removeEmptyDirectoriesRecursively(dirPath, rootFolderPath); + } } } }; diff --git a/src/tools/test-targets.ts b/src/tools/test-targets.ts index b307621..dc6e8f1 100644 --- a/src/tools/test-targets.ts +++ b/src/tools/test-targets.ts @@ -102,7 +102,7 @@ export const pullTestTarget = async ( const destination = (await findOctomindFolder()) ?? path.join(process.cwd(), OCTOMIND_FOLDER_NAME); - await writeYaml(data, destination); + await writeYaml(data, destination, !!options.testPlanId); logger.info("Test Target pulled successfully"); }; diff --git a/tests/tools/sync/yaml.spec.ts b/tests/tools/sync/yaml.spec.ts index c4d3cff..a200340 100644 --- a/tests/tools/sync/yaml.spec.ts +++ b/tests/tools/sync/yaml.spec.ts @@ -230,6 +230,7 @@ describe("yaml", () => { cleanupFilesystem({ remoteTestCases: [updatedTestCase], destination: tmpDir, + partialSync: false, }); expect(fs.existsSync(oldFilePath)).toBe(false); @@ -267,6 +268,7 @@ describe("yaml", () => { cleanupFilesystem({ remoteTestCases: [updatedTestCase], destination: tmpDir, + partialSync: false, }); expect(fs.existsSync(oldFolderPath)).toBe(false); @@ -293,6 +295,7 @@ describe("yaml", () => { cleanupFilesystem({ remoteTestCases: [remoteTestCase], destination: tmpDir, + partialSync: false, }); expect(fs.existsSync(localFilePath)).toBe(false); @@ -328,6 +331,7 @@ describe("yaml", () => { cleanupFilesystem({ remoteTestCases: [parentTestCase], destination: tmpDir, + partialSync: false, }); expect(fs.existsSync(childFilePath)).toBe(false); @@ -373,12 +377,48 @@ describe("yaml", () => { cleanupFilesystem({ remoteTestCases: [parentTestCase, childTestCase2], destination: tmpDir, + partialSync: false, }); expect(fs.existsSync(childFilePath1)).toBe(false); expect(fs.existsSync(childFilePath2)).toBe(true); expect(fs.existsSync(parentDir)).toBe(true); }); + + it("should not remove local test cases when partialSync is true", () => { + const localTestCase = createMockSyncTestCase({ + id: crypto.randomUUID(), + description: "Local test case", + }); + const remoteTestCase = createMockSyncTestCase({ + id: crypto.randomUUID(), + description: "Remote test case", + }); + + const localFilePath = path.join( + tmpDir, + buildFilename(localTestCase, tmpDir), + ); + const remoteFilePath = path.join( + tmpDir, + buildFilename(remoteTestCase, tmpDir), + ); + + fs.writeFileSync(localFilePath, yaml.stringify(localTestCase)); + fs.writeFileSync(remoteFilePath, yaml.stringify(remoteTestCase)); + + expect(fs.existsSync(localFilePath)).toBe(true); + expect(fs.existsSync(remoteFilePath)).toBe(true); + + cleanupFilesystem({ + remoteTestCases: [remoteTestCase], + destination: tmpDir, + partialSync: true, + }); + + expect(fs.existsSync(localFilePath)).toBe(true); + expect(fs.existsSync(remoteFilePath)).toBe(true); + }); }); describe("removeEmptyDirectoriesRecursively", () => { From 3ff7349309c33286134c2abe02f2d21449bf87e5 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 09:05:40 +0100 Subject: [PATCH 4/4] test fix --- tests/tools/test-plans.spec.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/tools/test-plans.spec.ts b/tests/tools/test-plans.spec.ts index bea8aa5..510b70f 100644 --- a/tests/tools/test-plans.spec.ts +++ b/tests/tools/test-plans.spec.ts @@ -34,15 +34,22 @@ describe("test-plans", () => { response: mock(), }); - const result = await getTestPlan({ id: testPlanId }); + const result = await getTestPlan({ + id: testPlanId, + testTargetId: "test-target-123", + }); - expect(client.GET).toHaveBeenCalledWith("/apiKey/beta/test-plans/{id}", { - params: { - path: { - id: testPlanId, + expect(client.GET).toHaveBeenCalledWith( + "/apiKey/beta/test-targets/{testTargetId}/test-plans/{id}", + { + params: { + path: { + id: testPlanId, + testTargetId: "test-target-123", + }, }, }, - }); + ); expect(handleError).toHaveBeenCalledWith(undefined); expect(result).toEqual(mockTestPlan); }); @@ -56,9 +63,9 @@ describe("test-plans", () => { response: mock(), }); - await expect(getTestPlan({ id: testPlanId })).rejects.toThrow( - `No test plan with id ${testPlanId} found`, - ); + await expect( + getTestPlan({ id: testPlanId, testTargetId: "test-target-123" }), + ).rejects.toThrow(`No test plan with id ${testPlanId} found`); expect(handleError).toHaveBeenCalledWith({ message: "Not found" }); }); });