diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9cf83ca8d..9510aee94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,29 @@ on: workflow_dispatch: jobs: test: - runs-on: blacksmith-4vcpu-ubuntu-2404 + name: test (${{ matrix.settings.name }}) + strategy: + fail-fast: false + matrix: + settings: + - name: linux + host: blacksmith-4vcpu-ubuntu-2404 + playwright: bunx playwright install --with-deps + workdir: . + command: | + git config --global user.email "bot@opencode.ai" + git config --global user.name "opencode" + bun turbo typecheck + bun turbo test + - name: windows + host: windows-latest + playwright: bunx playwright install + workdir: packages/app + command: bun test:e2e + runs-on: ${{ matrix.settings.host }} + defaults: + run: + shell: bash steps: - name: Checkout repository uses: actions/checkout@v4 @@ -20,7 +42,7 @@ jobs: - name: Install Playwright browsers working-directory: packages/app - run: bunx playwright install --with-deps + run: ${{ matrix.settings.playwright }} - name: Seed opencode data working-directory: packages/opencode @@ -67,11 +89,8 @@ jobs: exit 1 - name: run - run: | - git config --global user.email "bot@opencode.ai" - git config --global user.name "opencode" - bun turbo typecheck - bun turbo test + working-directory: ${{ matrix.settings.workdir }} + run: ${{ matrix.settings.command }} env: CI: true MODELS_DEV_API_JSON: ${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json diff --git a/packages/app/e2e/prompt.spec.ts b/packages/app/e2e/prompt.spec.ts new file mode 100644 index 000000000..26cab5a38 --- /dev/null +++ b/packages/app/e2e/prompt.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from "./fixtures" +import { promptSelector } from "./utils" + +function sessionIDFromUrl(url: string) { + const match = /\/session\/([^/?#]+)/.exec(url) + return match?.[1] +} + +test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + const pageErrors: string[] = [] + const onPageError = (err: Error) => { + pageErrors.push(err.message) + } + page.on("pageerror", onPageError) + + await gotoSession() + + const token = `E2E_OK_${Date.now()}` + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + + const sessionID = (() => { + const id = sessionIDFromUrl(page.url()) + if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`) + return id + })() + + try { + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + const assistant = messages + .slice() + .reverse() + .find((m) => m.info.role === "assistant") + + return ( + assistant?.parts + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") ?? "" + ) + }, + { timeout: 90_000 }, + ) + + .toContain(token) + + const reply = page.locator('[data-component="text-part"]').filter({ hasText: token }).first() + await expect(reply).toBeVisible({ timeout: 90_000 }) + } finally { + page.off("pageerror", onPageError) + await sdk.session.delete({ sessionID }).catch(() => undefined) + } + + if (pageErrors.length > 0) { + throw new Error(`Page error(s):\n${pageErrors.join("\n")}`) + } +})