Skip to content
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
11 changes: 9 additions & 2 deletions src/lib/version-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
* Else if exact match, returns that
* Else picks the numerically closest (or first)
*/
const PRERELEASE_CHANNEL = /\b(beta|dev|alpha|canary|nightly|preview)\b/i;

export function resolveVersion(requested: string, available: string[]): string {
// strip duplicates & sort
const uniq = Array.from(new Set(available));

// pick min/max
if (requested === "latest" || requested === "oldest") {
// Prefer stable releases: BrowserStack lists pre-release channels such as
// "154.0 beta" / "155.0 dev" alongside stable versions, and "latest"
// should never resolve to one of those while a stable version exists.
const stable = uniq.filter((v) => !PRERELEASE_CHANNEL.test(v));
const candidates = stable.length > 0 ? stable : uniq;
// try numeric
const nums = uniq
const nums = candidates
.map((v) => ({ v, n: parseFloat(v) }))
.filter((x) => !isNaN(x.n))
.sort((a, b) => a.n - b.n);
if (nums.length) {
return requested === "latest" ? nums[nums.length - 1].v : nums[0].v;
}
// fallback lex
const lex = uniq.slice().sort();
const lex = candidates.slice().sort();
return requested === "latest" ? lex[lex.length - 1] : lex[0];
}

Expand Down
2 changes: 1 addition & 1 deletion src/tools/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ export default function addAccessibilityTools(
{
title: "Start Accessibility Scan",
readOnlyHint: false,
openWorldHint: false,
openWorldHint: true,
destructiveHint: false,
idempotentHint: false,
},
Expand Down
3 changes: 2 additions & 1 deletion src/tools/sdk-utils/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const IMPORTANT_SETUP_WARNING =
"IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup.";
"IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup. " +
"If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them.";

export const SETUP_PERCY_DESCRIPTION =
"Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}";
Expand Down
2 changes: 1 addition & 1 deletion src/tools/selfheal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ export default function addSelfHealTools(
},
{
title: "Prepare Self-Healing Plan",
readOnlyHint: true,
readOnlyHint: false,
openWorldHint: false,
destructiveHint: false,
idempotentHint: true,
Expand Down
28 changes: 28 additions & 0 deletions tests/lib/version-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from "vitest";
import { resolveVersion } from "../../src/lib/version-resolver";

describe("resolveVersion", () => {
const versions = ["152.0", "153.0", "154.0 beta", "155.0 dev"];

it("resolves 'latest' to the newest stable version, skipping beta/dev channels", () => {
expect(resolveVersion("latest", versions)).toBe("153.0");
});

it("resolves 'oldest' to the oldest stable version", () => {
expect(resolveVersion("oldest", versions)).toBe("152.0");
});

it("falls back to pre-release channels when no stable version exists", () => {
expect(resolveVersion("latest", ["154.0 beta", "155.0 dev"])).toBe(
"155.0 dev",
);
});

it("still returns exact matches, including pre-release channels", () => {
expect(resolveVersion("154.0 beta", versions)).toBe("154.0 beta");
});

it("matches by major version", () => {
expect(resolveVersion("152", versions)).toBe("152.0");
});
});
53 changes: 53 additions & 0 deletions tests/tools/tool-annotations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect, vi } from "vitest";

vi.mock("../../src/logger", () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}));
vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() }));

import addAccessibilityTools from "../../src/tools/accessibility";
import addSelfHealTools from "../../src/tools/selfheal";

const mockConfig = {
"browserstack-username": "fake-user",
"browserstack-access-key": "fake-key",
};

// Captures the annotations object passed as the 4th argument to server.tool().
function collectAnnotations(register: (server: any, config: any) => unknown) {
const annotations: Record<string, any> = {};
const serverMock = {
tool: vi.fn((...toolArgs: any[]) => {
annotations[toolArgs[0]] = toolArgs[3];
}),
server: {
getClientVersion: vi.fn().mockReturnValue({ version: "1.0" }),
getClientCapabilities: vi.fn().mockReturnValue({}),
elicitInput: vi.fn(),
},
};
register(serverMock, mockConfig);
return annotations;
}

// These values were flagged in the OpenAI marketplace review of v1.3.0; the
// assertions pin the corrected hints so they cannot silently regress.
describe("tool annotations flagged in marketplace review", () => {
it("startAccessibilityScan is open-world: it loads an arbitrary public URL (and can submit login forms)", () => {
const annotations = collectAnnotations(addAccessibilityTools);
expect(annotations.startAccessibilityScan).toMatchObject({
readOnlyHint: false,
openWorldHint: true,
destructiveHint: false,
});
});

it("prepareSelfHealingPlan is not read-only: it drives a code-edit workflow", () => {
const annotations = collectAnnotations(addSelfHealTools);
expect(annotations.prepareSelfHealingPlan).toMatchObject({
readOnlyHint: false,
openWorldHint: false,
destructiveHint: false,
});
});
});
Loading