From c1e1a30aefa5214584a8ae89a5e52ebc1ee6f58f Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 13:33:20 +0100 Subject: [PATCH 1/5] debug with bearer --- README.md | 5 +- package.json | 3 +- src/cli.ts | 8 +- src/debugtopus/index.ts | 3 + src/tools/environments.ts | 21 ++- src/tools/playwright.ts | 57 +++++++- src/tools/test-cases.ts | 18 ++- tests/tools/environments.spec.ts | 97 ++++++++++++++ tests/tools/playwright.spec.ts | 217 +++++++++++++++++++++++++++++++ 9 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 tests/tools/environments.spec.ts create mode 100644 tests/tools/playwright.spec.ts diff --git a/README.md b/README.md index 4e528f2..d5faed5 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.4.0. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 4.5.0. Additional documentation see https://octomind.dev/docs/api-reference/ **Usage:** `octomind [options] [command]` @@ -184,7 +184,7 @@ Delete an environment ## debug -run test cases against local build +run test cases against local build. can also be authenticated with a bearer token so no need to provide an api key **Usage:** `debug [options]` @@ -204,6 +204,7 @@ run test cases against local build | `--browser [CHROMIUM, FIREFOX, SAFARI]` | Browser type | No | CHROMIUM | | `--breakpoint [DESKTOP, MOBILE, TABLET]` | Breakpoint | No | DESKTOP | | `--run-status [ON, OFF]` | only run test cases that are either ON or OFF | No | | +| `-b, --bearer-token [token]` | Bearer token for authentication (instead of api key) | No | | ## execute diff --git a/package.json b/package.json index f645a66..bdc7f09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "4.4.0", + "version": "4.5.0", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316", @@ -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", + "typecheck": "tsc --project tsconfig.build.json --noEmit", "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", diff --git a/src/cli.ts b/src/cli.ts index 203c599..8143ca9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,7 +116,9 @@ export const buildCmd = async (): Promise => { .completer(testTargetIdCompleter) .completer(testCaseIdCompleter) .completer(optionsCompleter) - .description("run test cases against local build") + .description( + "run test cases against local build. can also be authenticated with a bearer token so no need to provide an api key", + ) .helpGroup("execute") .requiredOption("-u, --url ", "url the tests should run against") .option( @@ -147,6 +149,10 @@ export const buildCmd = async (): Promise => { "--run-status [ON, OFF]", "only run test cases that are either ON or OFF", ) + .option( + "-b, --bearer-token [token]", + "Bearer token for authentication (instead of api key)", + ) .action(addTestTargetWrapper(runDebugtopus)); createCommandWithCommonOptions(program, "execute") diff --git a/src/debugtopus/index.ts b/src/debugtopus/index.ts index 32e7e63..a3ae3e0 100644 --- a/src/debugtopus/index.ts +++ b/src/debugtopus/index.ts @@ -41,6 +41,7 @@ export type DebugtopusOptions = { browser?: "CHROMIUM" | "FIREFOX" | "SAFARI"; breakpoint?: "DESKTOP" | "MOBILE" | "TABLET"; runStatus?: "ON" | "OFF"; + bearerToken?: string; }; const getPackageRootLevel = (appDir: string): string => { @@ -224,6 +225,7 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { testTargetId: options.testTargetId, url: options.url, environmentId: options.environmentId, + bearerToken: options.bearerToken, }; let testCasesWithCode: TestCaseCodeWithId[] = []; @@ -293,6 +295,7 @@ export const runDebugtopus = async (options: DebugtopusOptions) => { bypassProxy: options.bypassProxy, browser: options.browser, breakpoint: options.breakpoint, + bearerToken: options.bearerToken, }); if (!config) { diff --git a/src/tools/environments.ts b/src/tools/environments.ts index c38925f..e6fee74 100644 --- a/src/tools/environments.ts +++ b/src/tools/environments.ts @@ -1,9 +1,11 @@ import type { components, paths } from "../api"; // generated by openapi-typescript import { logger } from "../logger"; -import { client, handleError, ListOptions, logJson } from "./client"; +import { BASE_URL, client, handleError, ListOptions, logJson } from "./client"; export type GetEnvironmentsOptions = - paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"]; + paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"] & { + bearerToken?: string; + }; export type PostEnvironmentOptions = paths["/apiKey/v3/test-targets/{testTargetId}/environments"]["post"]["requestBody"]["content"]["application/json"] & { testTargetId: string; @@ -39,6 +41,21 @@ export const listEnvironments = async ( export const getEnvironments = async ( options: GetEnvironmentsOptions, ): Promise => { + if (options.bearerToken) { + const res = await fetch( + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/environments`, + { + headers: { + Authorization: `Bearer ${options.bearerToken}`, + }, + }, + ); + if (res.ok) { + const environments = await res.json(); + return environments; + } + throw new Error(`no environments found. error: ${res.statusText}`); + } const { data, error } = await client.GET( "/apiKey/v3/test-targets/{testTargetId}/environments", { diff --git a/src/tools/playwright.ts b/src/tools/playwright.ts index 537f80b..aa93251 100644 --- a/src/tools/playwright.ts +++ b/src/tools/playwright.ts @@ -1,4 +1,4 @@ -import { client, handleError } from "./client"; +import { BASE_URL, client, handleError } from "./client"; export const getPlaywrightConfig = async (options: { testTargetId: string; @@ -9,7 +9,36 @@ export const getPlaywrightConfig = async (options: { bypassProxy?: boolean; browser?: "CHROMIUM" | "FIREFOX" | "SAFARI"; breakpoint?: "DESKTOP" | "MOBILE" | "TABLET"; + bearerToken?: string; }): Promise => { + if (options.bearerToken) { + const params = new URLSearchParams({ + ...(options.environmentId && { environmentId: options.environmentId }), + ...(options.url && { url: options.url }), + ...(options.outputDir && { outputDir: options.outputDir }), + ...(options.headless !== undefined && { + headless: options.headless.toString(), + }), + ...(options.bypassProxy !== undefined && { + bypassProxy: options.bypassProxy.toString(), + }), + ...(options.browser && { browser: options.browser }), + ...(options.breakpoint && { breakpoint: options.breakpoint }), + }); + + const response = await fetch( + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/config?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${options.bearerToken}`, + }, + }, + ); + if (response.ok) { + return await response.text(); + } + throw new Error(`no config found. error: ${response.statusText}`); + } const { data, error } = await client.GET( "/apiKey/v3/test-targets/{testTargetId}/config", { @@ -40,12 +69,38 @@ export const getPlaywrightConfig = async (options: { return data; }; +type PlaywrightCodeResponse = { + testCode: string; +}; + export const getPlaywrightCode = async (options: { testTargetId: string; testCaseId: string; environmentId?: string; executionUrl: string; + bearerToken?: string; }): Promise => { + if (options.bearerToken) { + const params = new URLSearchParams({ + source: "debugtopus", + executionUrl: options.executionUrl, + ...(options.environmentId && { environmentId: options.environmentId }), + }); + + const response = await fetch( + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases/${options.testCaseId}/code?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${options.bearerToken}`, + }, + }, + ); + if (response.ok) { + const res = (await response.json()) as PlaywrightCodeResponse; + return res.testCode; + } + throw new Error(`no test code found. error: ${response.statusText}`); + } const { data, error } = await client.GET( "/apiKey/v3/test-targets/{testTargetId}/test-cases/{testCaseId}/code", { diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index 54a3831..ffeffa4 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -5,7 +5,7 @@ import type { components, paths } from "../api"; // generated by openapi-typescr import { findOctomindFolder } from "../helpers"; import { logger } from "../logger"; import { getUrl } from "../url"; -import { client, handleError, ListOptions, logJson } from "./client"; +import { BASE_URL, client, handleError, ListOptions, logJson } from "./client"; import { getEnvironments } from "./environments"; import { buildFilename, readTestCasesFromDir } from "./sync/yaml"; @@ -121,10 +121,26 @@ export type GetTestCasesOptions = { testTargetId: string; status?: string; runStatus?: string; + bearerToken?: string; }; export const getTestCases = async ( options: GetTestCasesOptions, ): Promise => { + if (options.bearerToken) { + const res = await fetch( + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases`, + { + headers: { + Authorization: `Bearer ${options.bearerToken}`, + }, + }, + ); + if (res.ok) { + const response = await res.json(); + return response; + } + throw new Error(`no test cases found. error: ${res.statusText}`); + } const { data, error } = await client.GET( "/apiKey/v3/test-targets/{testTargetId}/test-cases", { diff --git a/tests/tools/environments.spec.ts b/tests/tools/environments.spec.ts new file mode 100644 index 0000000..1122ae3 --- /dev/null +++ b/tests/tools/environments.spec.ts @@ -0,0 +1,97 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { BASE_URL, client } from "../../src/tools/client"; +import { + getEnvironments, + type GetEnvironmentsOptions, +} from "../../src/tools/environments"; +import { createMockEnvironment } from "../mocks"; + +vi.mock("../../src/tools/client"); + +global.fetch = vi.fn(); + +describe("environments", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getEnvironments", () => { + const testTargetId = "test-target-id"; + const mockEnvironments = [ + createMockEnvironment({ id: "env-1", name: "Environment 1" }), + createMockEnvironment({ id: "env-2", name: "Environment 2" }), + ]; + + it("should fetch environments using API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: mockEnvironments, + error: undefined, + response: {} as Response, + }); + + const options: GetEnvironmentsOptions = { testTargetId }; + const result = await getEnvironments(options); + + expect(client.GET).toHaveBeenCalledWith( + "/apiKey/v3/test-targets/{testTargetId}/environments", + { + params: { + path: { testTargetId }, + }, + }, + ); + expect(result).toEqual(mockEnvironments); + }); + + it("should fetch environments using bearer token", async () => { + const bearerToken = "test-bearer-token"; + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => mockEnvironments, + } as Response); + + const options: GetEnvironmentsOptions = { testTargetId, bearerToken }; + const result = await getEnvironments(options); + + expect(fetch).toHaveBeenCalledWith( + `${BASE_URL}/bearer/v1/test-targets/${testTargetId}/environments`, + { + headers: { + Authorization: `Bearer ${bearerToken}`, + }, + }, + ); + expect(result).toEqual(mockEnvironments); + expect(client.GET).not.toHaveBeenCalled(); + }); + + it("should throw error when no environments found with API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }); + + const options: GetEnvironmentsOptions = { testTargetId }; + + await expect(getEnvironments(options)).rejects.toThrow( + "no environments found", + ); + }); + + it("should throw error when response is not ok with bearer token", async () => { + const bearerToken = "test-bearer-token"; + vi.mocked(fetch).mockResolvedValue({ + ok: false, + statusText: "Unauthorized", + } as Response); + + const options: GetEnvironmentsOptions = { testTargetId, bearerToken }; + + await expect(getEnvironments(options)).rejects.toThrow( + "no environments found. error: Unauthorized", + ); + }); + }); +}); diff --git a/tests/tools/playwright.spec.ts b/tests/tools/playwright.spec.ts new file mode 100644 index 0000000..7863e08 --- /dev/null +++ b/tests/tools/playwright.spec.ts @@ -0,0 +1,217 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { BASE_URL, client } from "../../src/tools/client"; +import { + getPlaywrightCode, + getPlaywrightConfig, +} from "../../src/tools/playwright"; + +vi.mock("../../src/tools/client"); + +global.fetch = vi.fn(); + +describe("playwright", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getPlaywrightConfig", () => { + const testTargetId = "test-target-id"; + const url = "https://example.com"; + const outputDir = "/output"; + const mockConfig = "export default { testDir: './tests' };"; + + it("should fetch playwright config using API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: mockConfig, + error: undefined, + response: {} as Response, + }); + + const result = await getPlaywrightConfig({ + testTargetId, + url, + outputDir, + }); + + expect(client.GET).toHaveBeenCalledWith( + "/apiKey/v3/test-targets/{testTargetId}/config", + { + params: { + path: { testTargetId }, + query: { + environmentId: undefined, + url, + outputDir, + headless: "false", + bypassProxy: "false", + browser: undefined, + breakpoint: undefined, + }, + }, + parseAs: "text", + }, + ); + expect(result).toEqual(mockConfig); + }); + + it("should fetch playwright config using bearer token", async () => { + const bearerToken = "test-bearer-token"; + const environmentId = "env-id"; + vi.mocked(fetch).mockResolvedValue({ + ok: true, + text: async () => mockConfig, + } as Response); + + const result = await getPlaywrightConfig({ + testTargetId, + url, + outputDir, + environmentId, + headless: true, + bypassProxy: true, + browser: "CHROMIUM", + breakpoint: "DESKTOP", + bearerToken, + }); + + const expectedParams = new URLSearchParams({ + environmentId, + url, + outputDir, + headless: "true", + bypassProxy: "true", + browser: "CHROMIUM", + breakpoint: "DESKTOP", + }); + + expect(fetch).toHaveBeenCalledWith( + `${BASE_URL}/bearer/v1/test-targets/${testTargetId}/config?${expectedParams.toString()}`, + { + headers: { + Authorization: `Bearer ${bearerToken}`, + }, + }, + ); + expect(result).toEqual(mockConfig); + expect(client.GET).not.toHaveBeenCalled(); + }); + + it("should throw error when no config found with API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }); + + await expect( + getPlaywrightConfig({ testTargetId, url, outputDir }), + ).rejects.toThrow("no config found"); + }); + + it("should throw error when response is not ok with bearer token", async () => { + const bearerToken = "test-bearer-token"; + vi.mocked(fetch).mockResolvedValue({ + ok: false, + statusText: "Unauthorized", + } as Response); + + await expect( + getPlaywrightConfig({ testTargetId, url, outputDir, bearerToken }), + ).rejects.toThrow("no config found. error: Unauthorized"); + }); + }); + + describe("getPlaywrightCode", () => { + const testTargetId = "test-target-id"; + const testCaseId = "test-case-id"; + const executionUrl = "https://example.com"; + const mockTestCode = "test('example', async ({ page }) => {});"; + + it("should fetch playwright code using API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: { testCode: mockTestCode }, + error: undefined, + response: {} as Response, + }); + + const result = await getPlaywrightCode({ + testTargetId, + testCaseId, + executionUrl, + }); + + expect(client.GET).toHaveBeenCalledWith( + "/apiKey/v3/test-targets/{testTargetId}/test-cases/{testCaseId}/code", + { + params: { + path: { testTargetId, testCaseId }, + query: { + environmentId: undefined, + executionUrl, + }, + }, + }, + ); + expect(result).toEqual(mockTestCode); + }); + + it("should fetch playwright code using bearer token", async () => { + const bearerToken = "test-bearer-token"; + const environmentId = "env-id"; + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ testCode: mockTestCode }), + } as Response); + + const result = await getPlaywrightCode({ + testTargetId, + testCaseId, + executionUrl, + environmentId, + bearerToken, + }); + + const expectedParams = new URLSearchParams({ + source: "debugtopus", + executionUrl, + environmentId, + }); + + expect(fetch).toHaveBeenCalledWith( + `${BASE_URL}/bearer/v1/test-targets/${testTargetId}/test-cases/${testCaseId}/code?${expectedParams.toString()}`, + { + headers: { + Authorization: `Bearer ${bearerToken}`, + }, + }, + ); + expect(result).toEqual(mockTestCode); + expect(client.GET).not.toHaveBeenCalled(); + }); + + it("should throw error when no test code found with API key", async () => { + vi.mocked(client.GET).mockResolvedValue({ + data: undefined, + error: undefined, + response: {} as Response, + }); + + await expect( + getPlaywrightCode({ testTargetId, testCaseId, executionUrl }), + ).rejects.toThrow("no test code found"); + }); + + it("should throw error when response is not ok with bearer token", async () => { + const bearerToken = "test-bearer-token"; + vi.mocked(fetch).mockResolvedValue({ + ok: false, + statusText: "Forbidden", + } as Response); + + await expect( + getPlaywrightCode({ testTargetId, testCaseId, executionUrl, bearerToken }), + ).rejects.toThrow("no test code found. error: Forbidden"); + }); + }); +}); From 3369c75f4755b4f2e3cc62fd3c57238821a7876a Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 13:40:37 +0100 Subject: [PATCH 2/5] logger --- src/logger.ts | 6 +++++- src/tools/environments.ts | 1 + src/tools/playwright.ts | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/logger.ts b/src/logger.ts index c2baaba..a722821 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -51,6 +51,10 @@ export const configureLogger = async (): Promise => sinks: ["console"], lowestLevel: "warning", }, - { category: "octomind", lowestLevel: "debug", sinks: ["console"] }, + { + category: "octomind", + lowestLevel: (process.env.LOG_LEVEL as LogLevel) || "warning", + sinks: ["console"], + }, ], }); diff --git a/src/tools/environments.ts b/src/tools/environments.ts index e6fee74..a5969f1 100644 --- a/src/tools/environments.ts +++ b/src/tools/environments.ts @@ -42,6 +42,7 @@ export const getEnvironments = async ( options: GetEnvironmentsOptions, ): Promise => { if (options.bearerToken) { + logger.debug("Using bearer token for environments"); const res = await fetch( `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/environments`, { diff --git a/src/tools/playwright.ts b/src/tools/playwright.ts index aa93251..ed9731a 100644 --- a/src/tools/playwright.ts +++ b/src/tools/playwright.ts @@ -1,3 +1,4 @@ +import { logger } from "../logger"; import { BASE_URL, client, handleError } from "./client"; export const getPlaywrightConfig = async (options: { @@ -12,6 +13,7 @@ export const getPlaywrightConfig = async (options: { bearerToken?: string; }): Promise => { if (options.bearerToken) { + logger.debug("Using bearer token for config"); const params = new URLSearchParams({ ...(options.environmentId && { environmentId: options.environmentId }), ...(options.url && { url: options.url }), @@ -81,6 +83,7 @@ export const getPlaywrightCode = async (options: { bearerToken?: string; }): Promise => { if (options.bearerToken) { + logger.debug("Using bearer token for test code"); const params = new URLSearchParams({ source: "debugtopus", executionUrl: options.executionUrl, From 802603a866dceb3da10f4b8829f178dd0c87c95c Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 13:42:48 +0100 Subject: [PATCH 3/5] format --- tests/tools/environments.spec.ts | 2 +- tests/tools/playwright.spec.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/tools/environments.spec.ts b/tests/tools/environments.spec.ts index 1122ae3..546876f 100644 --- a/tests/tools/environments.spec.ts +++ b/tests/tools/environments.spec.ts @@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { BASE_URL, client } from "../../src/tools/client"; import { - getEnvironments, type GetEnvironmentsOptions, + getEnvironments, } from "../../src/tools/environments"; import { createMockEnvironment } from "../mocks"; diff --git a/tests/tools/playwright.spec.ts b/tests/tools/playwright.spec.ts index 7863e08..2c50bea 100644 --- a/tests/tools/playwright.spec.ts +++ b/tests/tools/playwright.spec.ts @@ -210,7 +210,12 @@ describe("playwright", () => { } as Response); await expect( - getPlaywrightCode({ testTargetId, testCaseId, executionUrl, bearerToken }), + getPlaywrightCode({ + testTargetId, + testCaseId, + executionUrl, + bearerToken, + }), ).rejects.toThrow("no test code found. error: Forbidden"); }); }); From 9ab1b569cb86dd7c88158f2a1d5b4852381bfe0a Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 14:36:52 +0100 Subject: [PATCH 4/5] pr comment added filter --- src/tools/test-cases.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index ffeffa4..b5f670c 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -127,14 +127,23 @@ export const getTestCases = async ( options: GetTestCasesOptions, ): Promise => { if (options.bearerToken) { - const res = await fetch( + const url = new URL( `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases`, - { - headers: { - Authorization: `Bearer ${options.bearerToken}`, - }, - }, ); + if (options.status || options.runStatus) { + url.searchParams.set( + "filter", + JSON.stringify({ + status: options.status, + runStatus: options.runStatus, + }), + ); + } + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${options.bearerToken}`, + }, + }); if (res.ok) { const response = await res.json(); return response; From 42eaf17b45a3c2dbf139e0f7b22154245e38ca58 Mon Sep 17 00:00:00 2001 From: Stefan Rinke Date: Fri, 23 Jan 2026 15:02:12 +0100 Subject: [PATCH 5/5] pr comments --- src/logger.ts | 27 ++++++++++++++++- src/tools/playwright.ts | 44 ++++++++++++++++------------ tests/logger.spec.ts | 50 ++++++++++++++++++++++++++++++++ tests/tools/environments.spec.ts | 4 --- 4 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 tests/logger.spec.ts diff --git a/src/logger.ts b/src/logger.ts index a722821..7dfff1e 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -9,6 +9,31 @@ import { export const logger = getLogger("octomind"); +const validLogLevels: LogLevel[] = [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", +]; + +export const getLogLevel = ( + level: string | undefined, + defaultLevel: LogLevel = "warning", +): LogLevel => { + if (!level) { + return defaultLevel; + } + + const normalizedLevel = level.toLowerCase(); + if (validLogLevels.includes(normalizedLevel as LogLevel)) { + return normalizedLevel as LogLevel; + } + + return defaultLevel; +}; + const ansiColors = { red: "\x1B[31m", green: "\x1B[32m", @@ -53,7 +78,7 @@ export const configureLogger = async (): Promise => }, { category: "octomind", - lowestLevel: (process.env.LOG_LEVEL as LogLevel) || "warning", + lowestLevel: getLogLevel(process.env.LOG_LEVEL), sinks: ["console"], }, ], diff --git a/src/tools/playwright.ts b/src/tools/playwright.ts index ed9731a..ee021b8 100644 --- a/src/tools/playwright.ts +++ b/src/tools/playwright.ts @@ -14,22 +14,24 @@ export const getPlaywrightConfig = async (options: { }): Promise => { if (options.bearerToken) { logger.debug("Using bearer token for config"); - const params = new URLSearchParams({ - ...(options.environmentId && { environmentId: options.environmentId }), - ...(options.url && { url: options.url }), - ...(options.outputDir && { outputDir: options.outputDir }), - ...(options.headless !== undefined && { - headless: options.headless.toString(), - }), - ...(options.bypassProxy !== undefined && { - bypassProxy: options.bypassProxy.toString(), - }), - ...(options.browser && { browser: options.browser }), - ...(options.breakpoint && { breakpoint: options.breakpoint }), - }); + const params = { + environmentId: options.environmentId, + url: options.url, + outputDir: options.outputDir, + headless: options.headless?.toString(), + bypassProxy: options.bypassProxy?.toString(), + browser: options.browser, + breakpoint: options.breakpoint, + }; + + const filteredParams = Object.fromEntries( + Object.entries(params).filter(([, value]) => value !== undefined), + ) as Record; + + const searchParams = new URLSearchParams(filteredParams); const response = await fetch( - `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/config?${params.toString()}`, + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/config?${searchParams.toString()}`, { headers: { Authorization: `Bearer ${options.bearerToken}`, @@ -84,14 +86,20 @@ export const getPlaywrightCode = async (options: { }): Promise => { if (options.bearerToken) { logger.debug("Using bearer token for test code"); - const params = new URLSearchParams({ + const params = { source: "debugtopus", executionUrl: options.executionUrl, - ...(options.environmentId && { environmentId: options.environmentId }), - }); + environmentId: options.environmentId, + }; + + const filteredParams = Object.fromEntries( + Object.entries(params).filter(([, value]) => value !== undefined), + ) as Record; + + const searchParams = new URLSearchParams(filteredParams); const response = await fetch( - `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases/${options.testCaseId}/code?${params.toString()}`, + `${BASE_URL}/bearer/v1/test-targets/${options.testTargetId}/test-cases/${options.testCaseId}/code?${searchParams.toString()}`, { headers: { Authorization: `Bearer ${options.bearerToken}`, diff --git a/tests/logger.spec.ts b/tests/logger.spec.ts new file mode 100644 index 0000000..24cbf5b --- /dev/null +++ b/tests/logger.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.unmock("../src/logger"); + +import { getLogLevel } from "../src/logger"; + +describe("logger", () => { + describe("getLogLevel", () => { + it("should return default level when no level is provided", () => { + expect(getLogLevel(undefined)).toBe("warning"); + }); + + it("should return custom default level when provided", () => { + expect(getLogLevel(undefined, "info")).toBe("info"); + }); + + it("should return valid log level when provided", () => { + expect(getLogLevel("debug")).toBe("debug"); + expect(getLogLevel("info")).toBe("info"); + expect(getLogLevel("warning")).toBe("warning"); + expect(getLogLevel("error")).toBe("error"); + expect(getLogLevel("fatal")).toBe("fatal"); + expect(getLogLevel("trace")).toBe("trace"); + }); + + it("should handle uppercase log levels", () => { + expect(getLogLevel("DEBUG")).toBe("debug"); + expect(getLogLevel("INFO")).toBe("info"); + expect(getLogLevel("WARNING")).toBe("warning"); + expect(getLogLevel("ERROR")).toBe("error"); + }); + + it("should handle mixed case log levels", () => { + expect(getLogLevel("DeBuG")).toBe("debug"); + expect(getLogLevel("WaRnInG")).toBe("warning"); + }); + + it("should return default level for invalid log levels", () => { + expect(getLogLevel("invalid")).toBe("warning"); + expect(getLogLevel("")).toBe("warning"); + expect(getLogLevel("verbose")).toBe("warning"); + expect(getLogLevel("critical")).toBe("warning"); + }); + + it("should return custom default level for invalid log levels", () => { + expect(getLogLevel("invalid", "error")).toBe("error"); + expect(getLogLevel("", "debug")).toBe("debug"); + }); + }); +}); diff --git a/tests/tools/environments.spec.ts b/tests/tools/environments.spec.ts index 546876f..e36b7ad 100644 --- a/tests/tools/environments.spec.ts +++ b/tests/tools/environments.spec.ts @@ -12,10 +12,6 @@ vi.mock("../../src/tools/client"); global.fetch = vi.fn(); describe("environments", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - describe("getEnvironments", () => { const testTargetId = "test-target-id"; const mockEnvironments = [