diff --git a/.github/workflows/phase0-qa.yml b/.github/workflows/phase0-qa.yml new file mode 100644 index 000000000000..e04ecfd57ec3 --- /dev/null +++ b/.github/workflows/phase0-qa.yml @@ -0,0 +1,140 @@ +name: Phase 0 QA + +on: + push: + branches: [main] + pull_request: + branches: + - main + - 'release/*' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: phase0-qa-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + phase0-unit: + name: Phase 0 unit regression + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + lfs: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci --ignore-scripts + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install build dependencies + run: npm ci + working-directory: build + + - name: Transpile source + run: npm run transpile-client + + - name: Run Phase 0 QA unit tests + run: npm run test-phase0-qa + + phase0-cdp-macos: + name: Phase 0 CDP smoke (macOS) + if: github.event_name == 'workflow_dispatch' + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + lfs: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install build dependencies + run: npm ci + working-directory: build + + - name: Build React UI + run: npm run buildreact + + - name: Build Electron dev app + run: node build/lib/preLaunch.ts + + - name: Run Phase 0 QA (unit + CDP) + run: test/cortexide-smoke/run-phase0-qa.sh --cdp + env: + CX_CDP_PORT: 9222 + CX_WS: /tmp/cx-phase0-qa-ws + CX_PROFILE: /tmp/cx-phase0-qa-profile + + - name: Upload CDP screenshot on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: phase0-qa-screenshot + path: /tmp/cortexide-phase0-qa.png + if-no-files-found: ignore + + phase0-cdp-windows: + name: Phase 0 CDP smoke (Windows — menubar dropdown gate) + if: github.event_name == 'workflow_dispatch' + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + lfs: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install build dependencies + run: npm ci + working-directory: build + + - name: Build React UI + run: npm run buildreact + + - name: Build Electron dev app + run: node build/lib/preLaunch.ts + + - name: Run Phase 0 QA (unit + CDP) + shell: bash + run: test/cortexide-smoke/run-phase0-qa.sh --cdp + env: + CX_CDP_PORT: 9222 + CX_WS: C:/Temp/cx-phase0-qa-ws + CX_PROFILE: C:/Temp/cx-phase0-qa-profile + + - name: Upload CDP screenshot on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: phase0-qa-windows-screenshot + path: ${{ runner.temp }}/cortexide-phase0-qa.png + if-no-files-found: ignore diff --git a/package.json b/package.json index 2209b1d32592..66cc20161081 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test-browser": "npx playwright install && node test/unit/browser/index.js", "test-browser-no-install": "node test/unit/browser/index.js", "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --timeout=5000 --exit", + "test-phase0-qa": "npm run test-node -- --runGlob \"**/cortexide/test/common/{providerSettingsValidation,onboardingHelpers,attachFileToChat,chatThreadStorageReviver,providerToolFormat,phase0ClaimVerification,modelCapabilities,menubarStackingFix}.test.js\"", "test-extension": "vscode-test", "test-build-scripts": "cd build && npm run test", "check-cyclic-dependencies": "node build/lib/checkCyclicDependencies.ts out", diff --git a/src/vs/workbench/contrib/cortexide/test/common/menubarStackingFix.test.ts b/src/vs/workbench/contrib/cortexide/test/common/menubarStackingFix.test.ts new file mode 100644 index 000000000000..a2ad4aab0ea2 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/test/common/menubarStackingFix.test.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { suite, test } from 'mocha'; + +/** + * CSS contract for issues #8 / #68 — menubar dropdowns must not be clipped on Windows/Linux. + * Runs in node (no Electron) so CI can gate Windows release behaviour without a Windows runner. + */ +const cssPath = join(dirname(fileURLToPath(import.meta.url)), '../../browser/media/cortexide.css'); +const css = readFileSync(cssPath, 'utf8'); + +const hasRule = (pattern: RegExp) => pattern.test(css); + +suite('menubarStackingFix (#8 / #68 — Windows/Linux dropdown)', () => { + + test('titlebar backdrop-filter override exists (prevents stacking-context clip)', () => { + assert.ok( + hasRule(/\.monaco-workbench\s+\.part\.titlebar\s*\{[^}]*backdrop-filter:\s*none/s), + 'expected .monaco-workbench .part.titlebar { backdrop-filter: none; }', + ); + assert.ok( + hasRule(/\.monaco-workbench\s+\.part\.titlebar\s*\{[^}]*-webkit-backdrop-filter:\s*none/s), + 'expected webkit backdrop-filter none on titlebar', + ); + }); + + test('menubar menu container z-index is raised above title bar chrome', () => { + assert.ok( + hasRule(/\.monaco-workbench\s+\.monaco-menu-container[^}]*z-index:\s*2500/s), + 'expected .monaco-menu-container { z-index: 2500; }', + ); + }); + + test('status bar keeps blur (titlebar override must not remove status bar styling)', () => { + assert.ok( + hasRule(/\.monaco-workbench\s+\.part\.statusbar[^}]*backdrop-filter:\s*blur/s), + 'status bar should retain backdrop blur', + ); + }); + + test('titlebar blur rule is scoped separately from the none override', () => { + // Both rules exist: shared blur on titlebar+statusbar, then titlebar-only reset. + const titlebarNoneIdx = css.indexOf('.monaco-workbench .part.titlebar {\n\tbackdrop-filter: none'); + const sharedBlurIdx = css.indexOf('.monaco-workbench .part.titlebar,\n.monaco-workbench .part.statusbar'); + assert.ok(sharedBlurIdx >= 0, 'shared titlebar/statusbar blur block missing'); + assert.ok(titlebarNoneIdx >= 0, 'titlebar backdrop-filter:none block missing'); + assert.ok(titlebarNoneIdx > sharedBlurIdx, 'titlebar none override must come AFTER shared blur rule'); + }); +}); diff --git a/test/cortexide-smoke/README.md b/test/cortexide-smoke/README.md index 1985052efa1d..21ca1b16bb9c 100644 --- a/test/cortexide-smoke/README.md +++ b/test/cortexide-smoke/README.md @@ -10,6 +10,9 @@ Chrome DevTools Protocol (CDP). Used to confirm the editor actually boots and it otherwise makes Electron run as plain Node. Uses throwaway user-data/extensions dirs. - `cdp-smoke.mjs` — connects with Playwright `connectOverCDP`, finds the workbench page (`workbench-dev.html` in dev builds), and asserts core + CortexIDE surfaces exist. +- `phase0-qa-verify.mjs` — CDP regression for merged Phase 0 fixes (PR #69): provider + validation, local tool format, attach-file commands, menubar stacking, theme scoping. +- `run-phase0-qa.sh` — runs `npm run test-phase0-qa` (unit) and optionally `--cdp` live verify. ## Usage ```bash @@ -24,6 +27,23 @@ node test/cortexide-smoke/cdp-smoke.mjs --port 9222 ``` Exit code 0 = all checks passed. A screenshot is written to the OS temp dir. +### Phase 0 QA (PR #69 regression) + +```bash +# Fast — unit tests only (CI runs this on every PR): +npm run test-phase0-qa + +# Full — unit tests + live CDP verify (needs a built dev app): +node build/lib/preLaunch.ts +npm run buildreact # if you touched React UI +test/cortexide-smoke/run-phase0-qa.sh --cdp +``` + +CI: `.github/workflows/phase0-qa.yml` runs unit tests on every PR/push to `main`. +Trigger CDP jobs manually via **Actions → Phase 0 QA → Run workflow**: +- **Windows** job clicks the in-window menubar and verifies the dropdown is visible (release gate for #8). +- **macOS** job runs module + CSS checks (native menu bar — no live click). + ## Why CDP and not Playwright `_electron.launch()` VS Code / CortexIDE manages its own (re)launching, so Playwright's stdout-handshake Electron launcher fails with "Process failed to launch!". Launching the app ourselves diff --git a/test/cortexide-smoke/launch-dev.sh b/test/cortexide-smoke/launch-dev.sh index 2eb92c3b95ea..1230bb443027 100755 --- a/test/cortexide-smoke/launch-dev.sh +++ b/test/cortexide-smoke/launch-dev.sh @@ -22,9 +22,11 @@ WS="${2:-/tmp/cx-ws-cdp}" APP="$ROOT/.build/electron/CortexIDE.app/Contents/MacOS/CortexIDE" if [[ ! -x "$APP" ]]; then - # Linux / other: fall back to applicationName binary. APP="$ROOT/.build/electron/cortexide" fi +if [[ ! -x "$APP" ]] && [[ -f "$ROOT/.build/electron/cortexide.exe" ]]; then + APP="$ROOT/.build/electron/cortexide.exe" +fi if [[ ! -x "$APP" ]]; then echo "ERROR: built app not found. Run: node build/lib/preLaunch.ts" >&2 exit 1 @@ -43,6 +45,13 @@ UDD="$PROFILE/user-data" EXT="$PROFILE/extensions" mkdir -p "$UDD" "$EXT" +# Ensure in-window menubar on Windows/Linux (required for #8 dropdown QA). +SETTINGS="$UDD/User/settings.json" +if [[ ! -f "$SETTINGS" ]]; then + mkdir -p "$(dirname "$SETTINGS")" + printf '%s\n' '{"window.menuBarVisibility":"classic","window.titleBarStyle":"custom"}' > "$SETTINGS" +fi + echo "Launching CortexIDE (port $PORT, ws $WS, profile $PROFILE)" # --password-store=basic: the ad-hoc-signed dev build (Identifier "Electron", no Team ID) can't be # stably attributed to its macOS Keychain item, so safeStorage prompts for the login password on diff --git a/test/cortexide-smoke/phase0-qa-verify.mjs b/test/cortexide-smoke/phase0-qa-verify.mjs new file mode 100644 index 000000000000..eb81f133fab1 --- /dev/null +++ b/test/cortexide-smoke/phase0-qa-verify.mjs @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Phase 0 QA verification — exercises merged PR #69 fixes against an ALREADY-RUNNING dev build +// over CDP. Combines live module evaluation (pure helpers shipped in the renderer bundle) with +// UI checks for menubar stacking, attach-file commands, and theme scoping. +// +// Issues covered: #8/#68 menubar, #54 attach file, #45 duplicate tools, #67 llama-server, +// #36 extension paths, #1 v0 multimodal, #27/#32 where testable in CDP. +// +// Usage: node test/cortexide-smoke/phase0-qa-verify.mjs [--port 9222] +import { chromium } from 'playwright-core'; +import { writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const argPort = process.argv.indexOf('--port'); +const PORT = argPort !== -1 ? process.argv[argPort + 1] : '9222'; +const log = (...a) => console.log('[phase0-qa]', ...a); +const results = []; +const rec = (name, ok, detail = '') => { + results.push({ name, ok, detail }); + log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' - ' + detail : ''}`); +}; + +const isWorkbench = (u) => + !u.startsWith('devtools://') && + /workbench(-dev|-monkey-patch)?\.html(\?|#|$)/.test(u) && + (u.startsWith('vscode-file://') || u.startsWith('file://')); +const sleep = (ms) => new Promise(r => setTimeout(r, ms)); + +const connDeadline = Date.now() + 120_000; +let browser = null; +let win = null; +while (Date.now() < connDeadline) { + try { + if (!browser || !browser.isConnected()) { + browser = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`, { timeout: 5000 }); + } + for (const ctx of browser.contexts()) { + for (const p of ctx.pages()) { + if (isWorkbench(p.url())) { win = p; break; } + } + if (win) { break; } + } + if (win) { break; } + } catch { + browser = null; + } + await sleep(3000); +} +if (!win) { + log('FATAL: workbench not reachable in 120s — launch with test/cortexide-smoke/launch-dev.sh'); + process.exit(2); +} +rec('app booted + workbench rendered', true); + +async function runCommand(title) { + await win.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P'); + await win.waitForSelector('.quick-input-widget', { timeout: 8000 }); + await win.keyboard.type(title); + await sleep(900); +} + +async function countPaletteRows() { + return win.locator('.quick-input-list .monaco-list-row').count().catch(() => 0); +} + +async function testMenubarDropdown() { + const platform = await win.evaluate(() => { + const wb = document.querySelector('.monaco-workbench'); + return { + isWindows: !!wb?.classList.contains('windows'), + isLinux: !!wb?.classList.contains('linux'), + isMac: !!wb?.classList.contains('mac'), + }; + }); + + // Windows/Linux ship in-window menubar — dropdown MUST open (release gate). + const needsLiveDropdown = platform.isWindows || platform.isLinux; + const menubarBtn = win.locator('.part.titlebar .menubar .menubar-menu-button').first(); + const hasInWindowMenubar = await menubarBtn.count().then(c => c > 0).catch(() => false); + + if (hasInWindowMenubar) { + // Click "File" (first top-level menu on Windows/Linux). + await menubarBtn.click({ timeout: 5000 }); + await sleep(700); + const menu = win.locator('.monaco-menu-container:visible').first(); + const menuVisible = await menu.count().then(c => c > 0).catch(() => false); + rec('#8: menubar opens visible dropdown', menuVisible, needsLiveDropdown ? 'Windows/Linux release gate' : 'in-window menubar'); + + if (menuVisible) { + const box = await menu.boundingBox().catch(() => null); + const titlebar = await win.locator('.part.titlebar').first().boundingBox().catch(() => null); + const menuBelowTitlebar = box && titlebar ? box.y >= titlebar.y : false; + rec('#8: dropdown renders outside titlebar (not clipped)', menuBelowTitlebar, + box ? `menu y=${Math.round(box.y)} titlebar y=${titlebar ? Math.round(titlebar.y) : '?'}` : 'no bbox'); + + const itemCount = await menu.locator('.action-menu-item').count().catch(() => 0); + rec('#8: dropdown contains menu items', itemCount > 0, `items=${itemCount}`); + } + await win.keyboard.press('Escape').catch(() => {}); + await sleep(300); + } else if (needsLiveDropdown) { + rec('#8: in-window menubar present (Windows/Linux)', false, 'no .menubar-menu-button in titlebar'); + } else { + // macOS dev: simulate Windows class + verify CSS contract still holds in live DOM. + const sim = await win.evaluate(() => { + const wb = document.querySelector('.monaco-workbench'); + if (!wb) { return { ok: false, detail: 'no workbench' }; } + wb.classList.add('windows'); + const titlebar = document.querySelector('.part.titlebar'); + const style = titlebar ? getComputedStyle(titlebar) : null; + const bf = (style?.backdropFilter || style?.webkitBackdropFilter || '').trim(); + const noBackdrop = !bf || bf === 'none' || bf === 'initial'; + let menuZ = 0; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules || [])) { + const text = rule.cssText || ''; + if (text.includes('.monaco-menu-container') && text.includes('z-index')) { + const m = text.match(/z-index:\s*(\d+)/); + if (m) { menuZ = Math.max(menuZ, Number(m[1])); } + } + } + } catch { /* cross-origin */ } + } + return { ok: noBackdrop && menuZ >= 2500, noBackdrop, menuZ, detail: `windows-sim backdrop=${bf || 'off'} menuZ=${menuZ}` }; + }); + rec('#8: Windows CSS contract in live DOM (macOS sim)', !!sim.ok, sim.detail || sim); + rec('#8: live menubar dropdown (skipped — native macOS menu bar)', true, 'run on Windows CI for click test'); + } +} + +try { + await win.waitForSelector('.monaco-workbench', { timeout: 60_000 }); + + // ===== Pure modules (real transpiled artifacts in the live renderer) ===== + const mod = await win.evaluate(async () => { + const base = window.location.href.replace(/out\/vs\/code\/.*$/, 'out/'); + const url = (rel) => base + rel; + const r = {}; + try { + const psv = await import(url('vs/workbench/contrib/cortexide/common/providerSettingsValidation.js')); + const ptf = await import(url('vs/workbench/contrib/cortexide/common/providerToolFormat.js')); + const attach = await import(url('vs/workbench/contrib/cortexide/common/attachFileToChat.js')); + const paths = await import(url('vs/workbench/contrib/cortexide/common/extensionTransferPaths.js')); + const caps = await import(url('vs/workbench/contrib/cortexide/common/modelCapabilities.js')); + const onboard = await import(url('vs/workbench/contrib/cortexide/common/onboardingHelpers.js')); + + // #67 — local OpenAI-compatible without apiKey is complete + r.llamaComplete = psv.isProviderSettingsComplete('openAICompatible', { + endpoint: 'http://127.0.0.1:8080/v1', + apiKey: '', + headersJSON: '{}', + models: [{ modelName: 'local', isHidden: false }], + _didFillInProviderSettings: true, + }); + + // #45 — local inference drops native tool format (XML/text path only) + r.localNoNativeTools = ptf.effectiveSpecialToolFormat('openai-style', true) === undefined; + r.cloudKeepsNativeTools = ptf.effectiveSpecialToolFormat('openai-style', false) === 'openai-style'; + + // #54 — attach helper accepts file URIs + const uriMod = await import(url('vs/base/common/uri.js')); + const fileUri = uriMod.URI.file('/tmp/hello.ts'); + const uris = attach.collectAttachableUris([fileUri]); + r.attachCollectsFile = uris.length === 1 && uris[0].fsPath.endsWith('hello.ts'); + + // #36 — CortexIDE data paths (not Void) + r.dataFolder = paths.CORTEXIDE_DATA_FOLDER; + r.appDataDir = paths.CORTEXIDE_APP_DATA_DIR; + + // #1 — v0 multimodal uses openai-style + const v0 = caps.getModelCapabilities('openAICompatible', 'v0-1.5-md'); + r.v0ToolFormat = v0.specialToolFormat; + + // #67 — llama-server preset endpoint + r.llamaEndpoint = onboard.LLAMA_SERVER_DEFAULT_ENDPOINT; + } catch (e) { + r.moduleError = String(e && e.stack || e).slice(0, 500); + } + return r; + }); + + if (mod.moduleError) { + rec('load Phase 0 modules in renderer', false, mod.moduleError); + } else { + rec('load Phase 0 modules in renderer', true); + rec('#67: openAICompatible without apiKey is complete', !!mod.llamaComplete); + rec('#45: local inference disables native tool format', !!mod.localNoNativeTools); + rec('#45: cloud keeps native tool format', !!mod.cloudKeepsNativeTools); + rec('#54: collectAttachableUris accepts file URIs', !!mod.attachCollectsFile); + rec('#36: data folder is .cortexide', mod.dataFolder === '.cortexide', mod.dataFolder); + rec('#36: app data dir is CortexIDE', mod.appDataDir === 'CortexIDE', mod.appDataDir); + rec('#1: v0 uses openai-style tool format', mod.v0ToolFormat === 'openai-style', mod.v0ToolFormat); + rec('#67: llama-server default endpoint', mod.llamaEndpoint === 'http://127.0.0.1:8080/v1', mod.llamaEndpoint); + } + + // ===== #54 — attach commands registered in palette ===== + try { + await runCommand('Add File to Chat'); + const attachRows = await countPaletteRows(); + rec('#54: "Add File to Chat" command in palette', attachRows > 0, `rows=${attachRows}`); + await win.keyboard.press('Escape'); + await sleep(400); + } catch (e) { + rec('#54: "Add File to Chat" command in palette', false, String(e).slice(0, 120)); + } + + try { + await runCommand('Add Selection to Chat'); + const selRows = await countPaletteRows(); + rec('#54: "Add Selection to Chat" (Ctrl+L) in palette', selRows > 0, `rows=${selRows}`); + await win.keyboard.press('Escape'); + await sleep(400); + } catch (e) { + rec('#54: "Add Selection to Chat" in palette', false, String(e).slice(0, 120)); + } + + // ===== #8/#68 — titlebar stacking + menubar dropdown ===== + const css = await win.evaluate(() => { + const titlebar = document.querySelector('.part.titlebar'); + const style = titlebar ? getComputedStyle(titlebar) : null; + const voidScope = document.querySelector('.void-scope'); + let menuZ = 0; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules || [])) { + const text = rule.cssText || ''; + if (text.includes('.monaco-menu-container') && text.includes('z-index')) { + const m = text.match(/z-index:\s*(\d+)/); + if (m) { menuZ = Math.max(menuZ, Number(m[1])); } + } + } + } catch { /* cross-origin sheets */ } + } + const bf = style ? (style.backdropFilter || style.webkitBackdropFilter || '').trim() : ''; + return { + titlebarBackdrop: style ? `${style.backdropFilter || '(empty)'} / ${style.webkitBackdropFilter || '(empty)'}` : '(no titlebar)', + titlebarBackdropOff: !bf || bf === 'none' || bf === 'initial', + voidScopePresent: !!voidScope, + menuContainerZIndex: menuZ, + }; + }); + rec('#8: titlebar backdrop-filter disabled', !!css.titlebarBackdropOff, css.titlebarBackdrop); + rec('#8: menu container z-index >= 2500', css.menuContainerZIndex >= 2500, `z-index=${css.menuContainerZIndex}`); + rec('#32: void-scope present (theme tokens scoped)', !!css.voidScopePresent); + + await testMenubarDropdown(); + + const shot = join(tmpdir(), 'cortexide-phase0-qa.png'); + await win.screenshot({ path: shot }).catch(() => {}); + rec('screenshot captured', true, shot); + + const passed = results.filter(r => r.ok).length; + log(`\n==== Phase 0 QA: ${passed}/${results.length} checks passed ====`); + writeFileSync(join(tmpdir(), 'cortexide-phase0-qa-results.json'), JSON.stringify(results, null, 2)); + await browser.close().catch(() => {}); + process.exit(passed === results.length ? 0 : 1); +} catch (e) { + log('ERROR', String(e).slice(0, 300)); + await browser?.close().catch(() => {}); + process.exit(3); +} diff --git a/test/cortexide-smoke/run-phase0-qa.sh b/test/cortexide-smoke/run-phase0-qa.sh new file mode 100755 index 000000000000..17ca03002e48 --- /dev/null +++ b/test/cortexide-smoke/run-phase0-qa.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +#--------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +#--------------------------------------------------------------------------------------------- +# +# Phase 0 QA runner — unit tests (always) + optional CDP smoke against a dev build. +# +# Usage: +# test/cortexide-smoke/run-phase0-qa.sh # unit tests only +# test/cortexide-smoke/run-phase0-qa.sh --cdp # unit tests + CDP verify (port 9222) +# CX_CDP_PORT=9333 test/cortexide-smoke/run-phase0-qa.sh --cdp +#--------------------------------------------------------------------------------------------- +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +RUN_CDP=0 +for arg in "$@"; do + if [[ "$arg" == "--cdp" ]]; then RUN_CDP=1; fi +done +if [[ "${CX_CDP:-0}" == "1" ]]; then RUN_CDP=1; fi + +PORT="${CX_CDP_PORT:-9222}" +WS="${CX_WS:-/tmp/cx-phase0-qa-ws}" +PROFILE="${CX_PROFILE:-/tmp/cx-phase0-qa-profile}" + +echo "== Phase 0 QA: unit tests ==" +npm run test-phase0-qa + +if [[ "$RUN_CDP" -ne 1 ]]; then + echo "" + echo "Unit tests passed. For live CDP verification, re-run with: $0 --cdp" + echo " (requires: node build/lib/preLaunch.ts && npm run buildreact)" + exit 0 +fi + +APP="$ROOT/.build/electron/CortexIDE.app/Contents/MacOS/CortexIDE" +if [[ ! -x "$APP" ]]; then APP="$ROOT/.build/electron/cortexide"; fi +if [[ ! -x "$APP" ]] && [[ -f "$ROOT/.build/electron/cortexide.exe" ]]; then APP="$ROOT/.build/electron/cortexide.exe"; fi +if [[ ! -x "$APP" ]]; then + echo "ERROR: built app not found. Run: node build/lib/preLaunch.ts" >&2 + exit 1 +fi + +echo "" +echo "== Phase 0 QA: CDP verify (port $PORT) ==" + +# Kill any stale instance on our profile/port. +pkill -f "remote-debugging-port=$PORT" 2>/dev/null || true +sleep 1 + +mkdir -p "$WS" +[[ -f "$WS/hello.txt" ]] || printf 'phase0 qa\n' > "$WS/hello.txt" + +test/cortexide-smoke/launch-dev.sh "$PORT" "$WS" "$PROFILE" & +LAUNCH_PID=$! +cleanup() { + kill "$LAUNCH_PID" 2>/dev/null || true + pkill -f "remote-debugging-port=$PORT" 2>/dev/null || true +} +trap cleanup EXIT + +# Wait for CDP endpoint. +for i in {1..60}; do + if curl -sf "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; then break; fi + sleep 2 +done + +node test/cortexide-smoke/phase0-qa-verify.mjs --port "$PORT" +echo "Phase 0 QA (unit + CDP) complete."