From e1e356cab4a9425fa287e4b01efd15187b8a9450 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 30 Jan 2026 06:48:49 +0000 Subject: [PATCH 1/9] release: v1.1.45 --- bun.lock | 36 ++++++++++++-------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 ++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 38 insertions(+), 42 deletions(-) diff --git a/bun.lock b/bun.lock index 99d35168f30..538712601d5 100644 --- a/bun.lock +++ b/bun.lock @@ -18,14 +18,12 @@ "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.17.23", - "stackback": "0.0.2", "turbo": "2.5.6", - "why-is-node-running": "2.2.2", }, }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +213,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +242,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +258,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.43", + "version": "1.1.45", "bin": { "opencode": "./bin/opencode", }, @@ -364,7 +362,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -384,7 +382,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.43", + "version": "1.1.45", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -395,7 +393,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -408,7 +406,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -450,7 +448,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "zod": "catalog:", }, @@ -461,7 +459,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -3901,7 +3899,7 @@ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - "why-is-node-running": ["why-is-node-running@2.2.2", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA=="], + "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], @@ -4389,8 +4387,6 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - "opencode/why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], - "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], diff --git a/packages/app/package.json b/packages/app/package.json index 87ca7931c55..8459172e7cf 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.43", + "version": "1.1.45", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index b10593a8fa5..84d45d3a7b1 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 111390efbe7..38811d4de29 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index c26b4584ded..50acf1567c6 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.43", + "version": "1.1.45", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 564a7140cc1..902dd447bed 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.43", + "version": "1.1.45", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index c1f330ce930..1b01f8a6f2b 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 03016c1c58f..2091e685749 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index eaa882b65bd..283036bf5b1 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.43" +version = "1.1.45" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.45/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 4a9e82def5e..9746d11dccf 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.43", + "version": "1.1.45", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bd77afa2e8c..ab28687d358 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.43", + "version": "1.1.45", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 19077d786d6..9e9232a4da7 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e929d628943..6b550512dea 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index decfd834a74..fbcccac399a 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 2e7a2cfefdf..5fde0960aca 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.43", + "version": "1.1.45", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index f76106701b2..9a3eb8f7d33 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.43", + "version": "1.1.45", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 0f601ba248f..06ab5ef4e95 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.1.43", + "version": "1.1.45", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 1ffe553c2cd..7c160a454fb 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.1.43", + "version": "1.1.45", "publisher": "sst-dev", "repository": { "type": "git", From 00637c0269312455e55e4977a7a8f55c728e93bd Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:56:47 -0600 Subject: [PATCH 2/9] fix: rm ai sdk middleware that was preventing blocks from being sent back as assistant message content (#11270) Co-authored-by: opencode-agent[bot] --- packages/opencode/src/session/llm.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 4c4f4114a28..0c765210452 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,4 +1,3 @@ -import os from "os" import { Installation } from "@/installation" import { Provider } from "@/provider/provider" import { Log } from "@/util/log" @@ -9,7 +8,6 @@ import { type StreamTextResult, type Tool, type ToolSet, - extractReasoningMiddleware, tool, jsonSchema, } from "ai" @@ -261,7 +259,6 @@ export namespace LLM { return args.params }, }, - extractReasoningMiddleware({ tagName: "think", startWithReasoning: false }), ], }), experimental_telemetry: { From 71d59bfa3d1031758570ca63cb62d79ee53eb636 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 01:12:36 +0100 Subject: [PATCH 3/9] test(app): add session management e2e tests and refactor reusable utilities Add new e2e tests for session actions: - Rename, archive, delete sessions via sidebar menu - Share, unshare, copy link via header button Add reusable utility functions to actions.ts: - withSession: lifecycle wrapper for session creation/cleanup - openSessionMoreMenu: hover and click session more options - clickMenuItem: click any menu item by text/regex - confirmDialog: click confirm button in dialog - openSharePopover: open share popover in header - clickPopoverButton: click button in popover - clickListItem: click list item with key or text filter Add new selectors to selectors.ts: - titlebarRightSelector, popoverBodySelector - dropdownMenuTriggerSelector, dropdownMenuContentSelector - inlineInputSelector, sessionItemSelector - listItemSelector, listItemKeySelector, listItemKeyStartsWithSelector Refactor existing tests to use new utilities: - session.spec.ts, context.spec.ts, prompt.spec.ts - titlebar-history.spec.ts, sidebar-session-links.spec.ts - file-open.spec.ts, file-viewer.spec.ts - model-picker.spec.ts, models-visibility.spec.ts - settings-providers.spec.ts, server-default.spec.ts --- packages/app/e2e/actions.ts | 100 ++++++++++++++++++ packages/app/e2e/app/server-default.spec.ts | 30 ++---- packages/app/e2e/app/session.spec.ts | 13 +-- packages/app/e2e/app/titlebar-history.spec.ts | 56 +++++----- packages/app/e2e/files/file-open.spec.ts | 6 +- packages/app/e2e/files/file-viewer.spec.ts | 10 +- packages/app/e2e/models/model-picker.spec.ts | 5 +- .../app/e2e/models/models-visibility.spec.ts | 2 +- .../app/e2e/projects/project-edit.spec.ts | 7 +- packages/app/e2e/prompt/context.spec.ts | 17 ++- packages/app/e2e/prompt/prompt.spec.ts | 2 +- packages/app/e2e/selectors.ts | 18 ++++ packages/app/e2e/session/session.spec.ts | 89 ++++++++++++++++ .../e2e/settings/settings-providers.spec.ts | 2 +- .../e2e/sidebar/sidebar-session-links.spec.ts | 2 +- 15 files changed, 267 insertions(+), 92 deletions(-) create mode 100644 packages/app/e2e/session/session.spec.ts diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index 3da16d3171f..706fe15087c 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -4,6 +4,17 @@ import os from "node:os" import path from "node:path" import { execSync } from "node:child_process" import { modKey, serverUrl } from "./utils" +import { + sessionItemSelector, + dropdownMenuTriggerSelector, + dropdownMenuContentSelector, + titlebarRightSelector, + popoverBodySelector, + listItemSelector, + listItemKeySelector, + listItemKeyStartsWithSelector, +} from "./selectors" +import type { createSdk } from "./utils" export async function defocus(page: Page) { await page.mouse.click(5, 5) @@ -158,3 +169,92 @@ export function sessionIDFromUrl(url: string) { const match = /\/session\/([^/?#]+)/.exec(url) return match?.[1] } + +export async function hoverSessionItem(page: Page, sessionID: string) { + const sessionEl = page.locator(sessionItemSelector(sessionID)).first() + await expect(sessionEl).toBeVisible() + await sessionEl.hover() + return sessionEl +} + +export async function openSessionMoreMenu(page: Page, sessionID: string) { + const sessionEl = await hoverSessionItem(page, sessionID) + + const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first() + await expect(menuTrigger).toBeVisible() + await menuTrigger.click() + + const menu = page.locator(dropdownMenuContentSelector).first() + await expect(menu).toBeVisible() + return menu +} + +export async function clickMenuItem(menu: Locator, itemName: string | RegExp) { + const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first() + await expect(item).toBeVisible() + await item.click() +} + +export async function confirmDialog(page: Page, buttonName: string | RegExp) { + const dialog = page.getByRole("dialog").first() + await expect(dialog).toBeVisible() + + const button = dialog.getByRole("button").filter({ hasText: buttonName }).first() + await expect(button).toBeVisible() + await button.click() +} + +export async function openSharePopover(page: Page) { + const rightSection = page.locator(titlebarRightSelector) + const shareButton = rightSection.getByRole("button", { name: "Share" }).first() + await expect(shareButton).toBeVisible() + await shareButton.click() + + const popoverBody = page.locator(popoverBodySelector).first() + await expect(popoverBody).toBeVisible() + return { rightSection, popoverBody } +} + +export async function clickPopoverButton(page: Page, buttonName: string | RegExp) { + const button = page.getByRole("button").filter({ hasText: buttonName }).first() + await expect(button).toBeVisible() + await button.click() +} + +export async function clickListItem( + container: Locator | Page, + filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string }, +): Promise { + let item: Locator + + if (typeof filter === "string" || filter instanceof RegExp) { + item = container.locator(listItemSelector).filter({ hasText: filter }).first() + } else if (filter.keyStartsWith) { + item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first() + } else if (filter.key) { + item = container.locator(listItemKeySelector(filter.key)).first() + } else if (filter.text) { + item = container.locator(listItemSelector).filter({ hasText: filter.text }).first() + } else { + throw new Error("Invalid filter provided to clickListItem") + } + + await expect(item).toBeVisible() + await item.click() + return item +} + +export async function withSession( + sdk: ReturnType, + title: string, + callback: (session: { id: string; title: string }) => Promise, +): Promise { + const session = await sdk.session.create({ title }).then((r) => r.data) + if (!session?.id) throw new Error("Session create did not return an id") + + try { + return await callback(session) + } finally { + await sdk.session.delete({ sessionID: session.id }).catch(() => undefined) + } +} diff --git a/packages/app/e2e/app/server-default.spec.ts b/packages/app/e2e/app/server-default.spec.ts index 6f44ded1a28..adbc83473be 100644 --- a/packages/app/e2e/app/server-default.spec.ts +++ b/packages/app/e2e/app/server-default.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "../fixtures" import { serverName, serverUrl } from "../utils" +import { clickListItem, closeDialog, clickMenuItem } from "../actions" const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" @@ -33,31 +34,18 @@ test("can set a default server on web", async ({ page, gotoSession }) => { const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first() await expect(row).toBeVisible() - const menu = row.locator('[data-component="icon-button"]').last() - await menu.click() - await page.getByRole("menuitem", { name: "Set as default" }).click() + const menuTrigger = row.locator('[data-slot="dropdown-menu-trigger"]').first() + await expect(menuTrigger).toBeVisible() + await menuTrigger.click({ force: true }) + + const menu = page.locator('[data-component="dropdown-menu-content"]').first() + await expect(menu).toBeVisible() + await clickMenuItem(menu, /set as default/i) await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl) await expect(row.getByText("Default", { exact: true })).toBeVisible() - await page.keyboard.press("Escape") - const closed = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (!closed) { - await page.keyboard.press("Escape") - const closedSecond = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (!closedSecond) { - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(dialog).toHaveCount(0) - } - } + await closeDialog(page, dialog) await ensurePopoverOpen() diff --git a/packages/app/e2e/app/session.spec.ts b/packages/app/e2e/app/session.spec.ts index d35af7ef778..c7fdfdc542b 100644 --- a/packages/app/e2e/app/session.spec.ts +++ b/packages/app/e2e/app/session.spec.ts @@ -1,21 +1,16 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" +import { withSession } from "../actions" test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { const title = `e2e smoke ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { - await gotoSession(sessionID) + await withSession(sdk, title, async (session) => { + await gotoSession(session.id) const prompt = page.locator(promptSelector) await prompt.click() await page.keyboard.type("hello from e2e") await expect(prompt).toContainText("hello from e2e") - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } + }) }) diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts index c7ff6566c14..ec65dca0b35 100644 --- a/packages/app/e2e/app/titlebar-history.spec.ts +++ b/packages/app/e2e/app/titlebar-history.spec.ts @@ -1,48 +1,42 @@ import { test, expect } from "../fixtures" -import { openSidebar } from "../actions" +import { openSidebar, withSession } from "../actions" import { promptSelector } from "../selectors" test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => { await page.setViewportSize({ width: 1400, height: 800 }) const stamp = Date.now() - const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data) - const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data) - if (!one?.id) throw new Error("Session create did not return an id") - if (!two?.id) throw new Error("Session create did not return an id") + await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => { + await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => { + await gotoSession(one.id) - try { - await gotoSession(one.id) + await openSidebar(page) - await openSidebar(page) + const link = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(link).toBeVisible() + await link.scrollIntoViewIfNeeded() + await link.click() - const link = page.locator(`[data-session-id="${two.id}"] a`).first() - await expect(link).toBeVisible() - await link.scrollIntoViewIfNeeded() - await link.click() + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() + const back = page.getByRole("button", { name: "Back" }) + const forward = page.getByRole("button", { name: "Forward" }) - const back = page.getByRole("button", { name: "Back" }) - const forward = page.getByRole("button", { name: "Forward" }) + await expect(back).toBeVisible() + await expect(back).toBeEnabled() + await back.click() - await expect(back).toBeVisible() - await expect(back).toBeEnabled() - await back.click() + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() + await expect(forward).toBeVisible() + await expect(forward).toBeEnabled() + await forward.click() - await expect(forward).toBeVisible() - await expect(forward).toBeEnabled() - await forward.click() - - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() - } finally { - await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) - await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) - } + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) }) diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts index dea35d25baa..3c636d748a7 100644 --- a/packages/app/e2e/files/file-open.spec.ts +++ b/packages/app/e2e/files/file-open.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openPalette } from "../actions" +import { openPalette, clickListItem } from "../actions" test("can open a file tab from the search palette", async ({ page, gotoSession }) => { await gotoSession() @@ -9,9 +9,7 @@ test("can open a file tab from the search palette", async ({ page, gotoSession } const input = dialog.getByRole("textbox").first() await input.fill("package.json") - const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() - await expect(fileItem).toBeVisible() - await fileItem.click() + await clickListItem(dialog, { keyStartsWith: "file:" }) await expect(dialog).toHaveCount(0) diff --git a/packages/app/e2e/files/file-viewer.spec.ts b/packages/app/e2e/files/file-viewer.spec.ts index 3dc0dead2d8..52838449759 100644 --- a/packages/app/e2e/files/file-viewer.spec.ts +++ b/packages/app/e2e/files/file-viewer.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openPalette } from "../actions" +import { openPalette, clickListItem } from "../actions" test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { await gotoSession() @@ -12,13 +12,7 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession } const input = dialog.getByRole("textbox").first() await input.fill(file) - const fileItem = dialog - .locator( - '[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]', - ) - .first() - await expect(fileItem).toBeVisible() - await fileItem.click() + await clickListItem(dialog, { text: /packages.*app.*package.json/ }) await expect(dialog).toHaveCount(0) diff --git a/packages/app/e2e/models/model-picker.spec.ts b/packages/app/e2e/models/model-picker.spec.ts index df95e04d222..01e72464cc5 100644 --- a/packages/app/e2e/models/model-picker.spec.ts +++ b/packages/app/e2e/models/model-picker.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" +import { clickListItem } from "../actions" test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => { await gotoSession() @@ -32,9 +33,7 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession } await input.fill(model) - const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`) - await expect(item).toBeVisible() - await item.click() + await clickListItem(dialog, { key }) await expect(dialog).toHaveCount(0) diff --git a/packages/app/e2e/models/models-visibility.spec.ts b/packages/app/e2e/models/models-visibility.spec.ts index 36f14596dd3..c6991117937 100644 --- a/packages/app/e2e/models/models-visibility.spec.ts +++ b/packages/app/e2e/models/models-visibility.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" -import { closeDialog, openSettings } from "../actions" +import { closeDialog, openSettings, clickListItem } from "../actions" test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/projects/project-edit.spec.ts b/packages/app/e2e/projects/project-edit.spec.ts index 22d053f3d92..93e18ec86e0 100644 --- a/packages/app/e2e/projects/project-edit.spec.ts +++ b/packages/app/e2e/projects/project-edit.spec.ts @@ -14,7 +14,12 @@ test("dialog edit project updates name and startup script", async ({ page, gotoS await expect(trigger).toBeVisible() await trigger.click({ force: true }) - await page.getByRole("menuitem", { name: "Edit" }).click() + const menu = page.locator('[data-component="dropdown-menu-content"]').first() + await expect(menu).toBeVisible() + + const editItem = menu.getByRole("menuitem", { name: "Edit" }).first() + await expect(editItem).toBeVisible() + await editItem.click() const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts index 9e8f998f27f..80aa9ea334d 100644 --- a/packages/app/e2e/prompt/context.spec.ts +++ b/packages/app/e2e/prompt/context.spec.ts @@ -1,16 +1,13 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" +import { withSession } from "../actions" test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { const title = `e2e smoke context ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { + await withSession(sdk, title, async (session) => { await sdk.session.promptAsync({ - sessionID, + sessionID: session.id, noReply: true, parts: [ { @@ -22,12 +19,12 @@ test("context panel can be opened from the prompt", async ({ page, sdk, gotoSess await expect .poll(async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) + const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? []) return messages.length }) .toBeGreaterThan(0) - await gotoSession(sessionID) + await gotoSession(session.id) const contextButton = page .locator('[data-component="button"]') @@ -39,7 +36,5 @@ test("context panel can be opened from the prompt", async ({ page, sdk, gotoSess const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } + }) }) diff --git a/packages/app/e2e/prompt/prompt.spec.ts b/packages/app/e2e/prompt/prompt.spec.ts index 33f8d7ebc3a..07d242c6342 100644 --- a/packages/app/e2e/prompt/prompt.spec.ts +++ b/packages/app/e2e/prompt/prompt.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" -import { sessionIDFromUrl } from "../actions" +import { sessionIDFromUrl, withSession } from "../actions" test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => { test.setTimeout(120_000) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 9179a6fd570..8beade57303 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -15,3 +15,21 @@ export const projectMenuTriggerSelector = (slug: string) => `${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]` export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]` + +export const titlebarRightSelector = "#opencode-titlebar-right" + +export const popoverBodySelector = '[data-slot="popover-body"]' + +export const dropdownMenuTriggerSelector = '[data-slot="dropdown-menu-trigger"]' + +export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]' + +export const inlineInputSelector = '[data-component="inline-input"]' + +export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]` + +export const listItemSelector = '[data-slot="list-item"]' + +export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]` + +export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]` diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts new file mode 100644 index 00000000000..dd95046bcf5 --- /dev/null +++ b/packages/app/e2e/session/session.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from "../fixtures" +import { + openSidebar, + openSessionMoreMenu, + clickMenuItem, + confirmDialog, + openSharePopover, + clickPopoverButton, + withSession, +} from "../actions" +import { sessionItemSelector, inlineInputSelector } from "../selectors" + +test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + const originalTitle = `e2e rename test ${stamp}` + const newTitle = `e2e renamed ${stamp}` + + await withSession(sdk, originalTitle, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const menu = await openSessionMoreMenu(page, session.id) + await clickMenuItem(menu, /rename/i) + + const input = page.locator(sessionItemSelector(session.id)).locator(inlineInputSelector).first() + await expect(input).toBeVisible() + await input.fill(newTitle) + await input.press("Enter") + + await expect(page.locator(sessionItemSelector(session.id)).locator("a").first()).toContainText(newTitle) + }) +}) + +test("sidebar session can be archived", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + const title = `e2e archive test ${stamp}` + + await withSession(sdk, title, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sessionEl = page.locator(sessionItemSelector(session.id)) + const menu = await openSessionMoreMenu(page, session.id) + await clickMenuItem(menu, /archive/i) + + await expect(sessionEl).not.toBeVisible() + }) +}) + +test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + const title = `e2e delete test ${stamp}` + + await withSession(sdk, title, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sessionEl = page.locator(sessionItemSelector(session.id)) + const menu = await openSessionMoreMenu(page, session.id) + await clickMenuItem(menu, /delete/i) + await confirmDialog(page, /delete/i) + + await expect(sessionEl).not.toBeVisible() + }) +}) + +test("session can be shared and unshared via header button", async ({ page, sdk, gotoSession }) => { + const stamp = Date.now() + const title = `e2e share test ${stamp}` + + await withSession(sdk, title, async (session) => { + await gotoSession(session.id) + + const { rightSection } = await openSharePopover(page) + await clickPopoverButton(page, "Publish") + await expect(page.locator("input[readonly]").first()).toBeVisible() + + const copyButton = rightSection.locator('button[aria-label="Copy link"]').first() + await expect(copyButton).toBeVisible() + await copyButton.click() + await page.waitForTimeout(100) + + await openSharePopover(page) + await clickPopoverButton(page, "Unpublish") + await expect(page.getByRole("button", { name: "Publish" }).first()).toBeVisible() + + await expect(rightSection.locator('button[aria-label="Copy link"]')).not.toBeVisible() + }) +}) diff --git a/packages/app/e2e/settings/settings-providers.spec.ts b/packages/app/e2e/settings/settings-providers.spec.ts index 4b3b178cc59..2bd2616bc9a 100644 --- a/packages/app/e2e/settings/settings-providers.spec.ts +++ b/packages/app/e2e/settings/settings-providers.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "../fixtures" import { promptSelector } from "../selectors" -import { closeDialog, openSettings } from "../actions" +import { closeDialog, openSettings, clickListItem } from "../actions" test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts index 1c0f4fa71dd..cda2278a950 100644 --- a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openSidebar } from "../actions" +import { openSidebar, withSession } from "../actions" import { promptSelector } from "../selectors" test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => { From 649ab84d3ed2897be8736a021e976f436f3def85 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 01:52:11 +0100 Subject: [PATCH 4/9] test(app): fix flaky tests in session share and project edit - Add force click to project edit menu item to prevent detachment issues - Add wait timeouts and popover reopening to session share test to handle async share/unshare operations --- packages/app/e2e/projects/project-edit.spec.ts | 2 +- packages/app/e2e/session/session.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/projects/project-edit.spec.ts b/packages/app/e2e/projects/project-edit.spec.ts index 93e18ec86e0..772c259517f 100644 --- a/packages/app/e2e/projects/project-edit.spec.ts +++ b/packages/app/e2e/projects/project-edit.spec.ts @@ -19,7 +19,7 @@ test("dialog edit project updates name and startup script", async ({ page, gotoS const editItem = menu.getByRole("menuitem", { name: "Edit" }).first() await expect(editItem).toBeVisible() - await editItem.click() + await editItem.click({ force: true }) const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index dd95046bcf5..6e35417df7f 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -73,6 +73,10 @@ test("session can be shared and unshared via header button", async ({ page, sdk, const { rightSection } = await openSharePopover(page) await clickPopoverButton(page, "Publish") + + await page.waitForTimeout(500) + + await openSharePopover(page) await expect(page.locator("input[readonly]").first()).toBeVisible() const copyButton = rightSection.locator('button[aria-label="Copy link"]').first() @@ -82,6 +86,10 @@ test("session can be shared and unshared via header button", async ({ page, sdk, await openSharePopover(page) await clickPopoverButton(page, "Unpublish") + + await page.waitForTimeout(500) + + await openSharePopover(page) await expect(page.getByRole("button", { name: "Publish" }).first()).toBeVisible() await expect(rightSection.locator('button[aria-label="Copy link"]')).not.toBeVisible() From b89b510d416c00b08fcf66a6c5991efd07e4a15a Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 02:00:00 +0100 Subject: [PATCH 5/9] test(app): stabilize session share/unshare e2e --- packages/app/e2e/session/session.spec.ts | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index 6e35417df7f..0ddb3463047 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -74,24 +74,32 @@ test("session can be shared and unshared via header button", async ({ page, sdk, const { rightSection } = await openSharePopover(page) await clickPopoverButton(page, "Publish") - await page.waitForTimeout(500) + await expect + .poll(async () => { + const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) + return data?.share?.url + }) + .not.toBeUndefined() - await openSharePopover(page) - await expect(page.locator("input[readonly]").first()).toBeVisible() - - const copyButton = rightSection.locator('button[aria-label="Copy link"]').first() - await expect(copyButton).toBeVisible() - await copyButton.click() - await page.waitForTimeout(100) + await gotoSession(session.id) await openSharePopover(page) + await expect(page.getByRole("button", { name: "Unpublish" }).first()).toBeVisible() + await expect(rightSection.locator('button[aria-label="Copy link"]').first()).toBeVisible() + await clickPopoverButton(page, "Unpublish") - await page.waitForTimeout(500) + await expect + .poll(async () => { + const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) + return data?.share?.url + }) + .toBeUndefined() + + await gotoSession(session.id) await openSharePopover(page) await expect(page.getByRole("button", { name: "Publish" }).first()).toBeVisible() - await expect(rightSection.locator('button[aria-label="Copy link"]')).not.toBeVisible() }) }) From a63bc127d0f4c8b894a3bf22bead9204d9f9456a Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 02:08:39 +0100 Subject: [PATCH 6/9] test(app): reduce share and project menu flakiness - Make openSharePopover target the share popover body and avoid toggling - In share/unshare e2e, wait for Copy link button before asserting Unpublish - In projects-close header menu test, click Close via dropdown menu content --- packages/app/e2e/actions.ts | 21 +++++++-- .../app/e2e/projects/projects-close.spec.ts | 19 +++----- packages/app/e2e/session/session.spec.ts | 47 +++++++++++-------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index 706fe15087c..7308adc7e69 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -189,10 +189,10 @@ export async function openSessionMoreMenu(page: Page, sessionID: string) { return menu } -export async function clickMenuItem(menu: Locator, itemName: string | RegExp) { +export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) { const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first() await expect(item).toBeVisible() - await item.click() + await item.click({ force: options?.force }) } export async function confirmDialog(page: Page, buttonName: string | RegExp) { @@ -208,10 +208,21 @@ export async function openSharePopover(page: Page) { const rightSection = page.locator(titlebarRightSelector) const shareButton = rightSection.getByRole("button", { name: "Share" }).first() await expect(shareButton).toBeVisible() - await shareButton.click() - const popoverBody = page.locator(popoverBodySelector).first() - await expect(popoverBody).toBeVisible() + const popoverBody = page + .locator(popoverBodySelector) + .filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) }) + .first() + + const opened = await popoverBody + .isVisible() + .then((x) => x) + .catch(() => false) + + if (!opened) { + await shareButton.click() + await expect(popoverBody).toBeVisible() + } return { rightSection, popoverBody } } diff --git a/packages/app/e2e/projects/projects-close.spec.ts b/packages/app/e2e/projects/projects-close.spec.ts index c3618740dd9..bd323b90c6b 100644 --- a/packages/app/e2e/projects/projects-close.spec.ts +++ b/packages/app/e2e/projects/projects-close.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { createTestProject, seedProjects, cleanupTestProject, openSidebar } from "../actions" +import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions" import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors" import { dirSlug } from "../utils" @@ -33,7 +33,7 @@ test("can close a project via project header more options menu", async ({ page, await page.setViewportSize({ width: 1400, height: 800 }) const other = await createTestProject() - const otherName = other.split("/").pop() + const otherName = other.split("/").pop() ?? other const otherSlug = dirSlug(other) await seedProjects(page, { directory, extra: [other] }) @@ -59,17 +59,10 @@ test("can close a project via project header more options menu", async ({ page, await trigger.focus() await page.keyboard.press("Enter") - const close = page - .locator(projectCloseMenuSelector(otherSlug)) - .or(page.getByRole("menuitem", { name: "Close" })) - .or( - page - .locator('[data-component="dropdown-menu-content"] [data-slot="dropdown-menu-item"]') - .filter({ hasText: "Close" }), - ) - .first() - await expect(close).toBeVisible({ timeout: 10_000 }) - await close.click({ force: true }) + const menu = page.locator('[data-component="dropdown-menu-content"]').first() + await expect(menu).toBeVisible({ timeout: 10_000 }) + + await clickMenuItem(menu, /^Close$/i, { force: true }) await expect(otherButton).toHaveCount(0) } finally { await cleanupTestProject(other) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index 0ddb3463047..80be5ffbb12 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -71,35 +71,42 @@ test("session can be shared and unshared via header button", async ({ page, sdk, await withSession(sdk, title, async (session) => { await gotoSession(session.id) - const { rightSection } = await openSharePopover(page) - await clickPopoverButton(page, "Publish") + const { rightSection, popoverBody } = await openSharePopover(page) + await popoverBody.getByRole("button", { name: "Publish" }).first().click() await expect - .poll(async () => { - const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url - }) + .poll( + async () => { + const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) + return data?.share?.url + }, + { timeout: 30_000 }, + ) .not.toBeUndefined() - await gotoSession(session.id) - - await openSharePopover(page) - await expect(page.getByRole("button", { name: "Unpublish" }).first()).toBeVisible() - await expect(rightSection.locator('button[aria-label="Copy link"]').first()).toBeVisible() + const copyButton = rightSection.locator('button[aria-label="Copy link"]').first() + await expect(copyButton).toBeVisible({ timeout: 30_000 }) - await clickPopoverButton(page, "Unpublish") + const sharedPopover = await openSharePopover(page) + const unpublish = sharedPopover.popoverBody.getByRole("button", { name: "Unpublish" }).first() + await expect(unpublish).toBeVisible({ timeout: 30_000 }) + await unpublish.click() await expect - .poll(async () => { - const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url - }) + .poll( + async () => { + const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) + return data?.share?.url + }, + { timeout: 30_000 }, + ) .toBeUndefined() - await gotoSession(session.id) + await expect(copyButton).not.toBeVisible({ timeout: 30_000 }) - await openSharePopover(page) - await expect(page.getByRole("button", { name: "Publish" }).first()).toBeVisible() - await expect(rightSection.locator('button[aria-label="Copy link"]')).not.toBeVisible() + const unsharedPopover = await openSharePopover(page) + await expect(unsharedPopover.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ + timeout: 30_000, + }) }) }) From de6b3427ebc25e7b93be70d606ae352554ea8035 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 02:28:05 +0100 Subject: [PATCH 7/9] test(app): skip share test when OPENCODE_DISABLE_SHARE is set --- packages/app/e2e/session/session.spec.ts | 9 ++++++--- packages/app/script/e2e-local.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts index 80be5ffbb12..05984bbeee4 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -5,11 +5,12 @@ import { clickMenuItem, confirmDialog, openSharePopover, - clickPopoverButton, withSession, } from "../actions" import { sessionItemSelector, inlineInputSelector } from "../selectors" +const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1" + test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => { const stamp = Date.now() const originalTitle = `e2e rename test ${stamp}` @@ -65,6 +66,8 @@ test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => { }) test("session can be shared and unshared via header button", async ({ page, sdk, gotoSession }) => { + test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") + const stamp = Date.now() const title = `e2e share test ${stamp}` @@ -78,7 +81,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk, .poll( async () => { const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url + return data?.share?.url || undefined }, { timeout: 30_000 }, ) @@ -96,7 +99,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk, .poll( async () => { const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url + return data?.share?.url || undefined }, { timeout: 30_000 }, ) diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts index 2c7be2ad952..df2107f76d9 100644 --- a/packages/app/script/e2e-local.ts +++ b/packages/app/script/e2e-local.ts @@ -58,7 +58,7 @@ const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-")) const serverEnv = { ...process.env, - OPENCODE_DISABLE_SHARE: "true", + OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true", OPENCODE_DISABLE_LSP_DOWNLOAD: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true", From a074849b261a995f005e9fbc5ef501b682b556a4 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 02:40:18 +0100 Subject: [PATCH 8/9] ci(test): pass through e2e env vars in turbo --- turbo.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/turbo.json b/turbo.json index 5de1b8d7517..dd43f5f12ee 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,41 @@ { "$schema": "https://turborepo.com/schema.json", + "globalEnv": [ + "CI", + "OPENCODE_DISABLE_SHARE", + "OPENCODE_DISABLE_LSP_DOWNLOAD", + "OPENCODE_DISABLE_DEFAULT_PLUGINS", + "OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER", + "OPENCODE_TEST_HOME", + "XDG_DATA_HOME", + "XDG_CACHE_HOME", + "XDG_CONFIG_HOME", + "XDG_STATE_HOME", + "PLAYWRIGHT_SERVER_HOST", + "PLAYWRIGHT_SERVER_PORT", + "VITE_OPENCODE_SERVER_HOST", + "VITE_OPENCODE_SERVER_PORT", + "PLAYWRIGHT_PORT", + "PLAYWRIGHT_BASE_URL" + ], + "globalPassThroughEnv": [ + "CI", + "OPENCODE_DISABLE_SHARE", + "OPENCODE_DISABLE_LSP_DOWNLOAD", + "OPENCODE_DISABLE_DEFAULT_PLUGINS", + "OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER", + "OPENCODE_TEST_HOME", + "XDG_DATA_HOME", + "XDG_CACHE_HOME", + "XDG_CONFIG_HOME", + "XDG_STATE_HOME", + "PLAYWRIGHT_SERVER_HOST", + "PLAYWRIGHT_SERVER_PORT", + "VITE_OPENCODE_SERVER_HOST", + "VITE_OPENCODE_SERVER_PORT", + "PLAYWRIGHT_PORT", + "PLAYWRIGHT_BASE_URL" + ], "tasks": { "typecheck": {}, "build": { From c71a0152b1db6bec626215ebab38fb38faa697b6 Mon Sep 17 00:00:00 2001 From: neriousy Date: Sat, 31 Jan 2026 02:45:22 +0100 Subject: [PATCH 9/9] ci(test): limit turbo env passthrough to share flag --- turbo.json | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/turbo.json b/turbo.json index dd43f5f12ee..f06ddb0e8b7 100644 --- a/turbo.json +++ b/turbo.json @@ -1,41 +1,7 @@ { "$schema": "https://turborepo.com/schema.json", - "globalEnv": [ - "CI", - "OPENCODE_DISABLE_SHARE", - "OPENCODE_DISABLE_LSP_DOWNLOAD", - "OPENCODE_DISABLE_DEFAULT_PLUGINS", - "OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER", - "OPENCODE_TEST_HOME", - "XDG_DATA_HOME", - "XDG_CACHE_HOME", - "XDG_CONFIG_HOME", - "XDG_STATE_HOME", - "PLAYWRIGHT_SERVER_HOST", - "PLAYWRIGHT_SERVER_PORT", - "VITE_OPENCODE_SERVER_HOST", - "VITE_OPENCODE_SERVER_PORT", - "PLAYWRIGHT_PORT", - "PLAYWRIGHT_BASE_URL" - ], - "globalPassThroughEnv": [ - "CI", - "OPENCODE_DISABLE_SHARE", - "OPENCODE_DISABLE_LSP_DOWNLOAD", - "OPENCODE_DISABLE_DEFAULT_PLUGINS", - "OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER", - "OPENCODE_TEST_HOME", - "XDG_DATA_HOME", - "XDG_CACHE_HOME", - "XDG_CONFIG_HOME", - "XDG_STATE_HOME", - "PLAYWRIGHT_SERVER_HOST", - "PLAYWRIGHT_SERVER_PORT", - "VITE_OPENCODE_SERVER_HOST", - "VITE_OPENCODE_SERVER_PORT", - "PLAYWRIGHT_PORT", - "PLAYWRIGHT_BASE_URL" - ], + "globalEnv": ["CI", "OPENCODE_DISABLE_SHARE"], + "globalPassThroughEnv": ["CI", "OPENCODE_DISABLE_SHARE"], "tasks": { "typecheck": {}, "build": {