Skip to content

feat(extensions-overhaul): zip with ext, e2e tests, developer sdk with agent/skills#43

Open
zalos wants to merge 5 commits intomainfrom
feature/extensions-package-e2e
Open

feat(extensions-overhaul): zip with ext, e2e tests, developer sdk with agent/skills#43
zalos wants to merge 5 commits intomainfrom
feature/extensions-package-e2e

Conversation

@zalos
Copy link
Copy Markdown
Collaborator

@zalos zalos commented Apr 22, 2026

This pull request adds a comprehensive, fully automated end-to-end (e2e) testing workflow and developer documentation for extension development, centered around the SDK example extension. The SDK example is now a first-class reference implementation, with scripts and configs for local development, bundling, packaging, and e2e testing. Key documentation is added to guide developers through building, packaging, and testing extensions, and the example extension is updated to cover all current SDK features.

The most important changes are:


Extension Example Structure and Tooling

  • The SDK example extension is moved to extension-sdk/example/com.clearpathai.sdk-example/, with updated references and a .gitignore for build artifacts. [1] [2]
  • A new build.mjs script is added for esbuild-based bundling, supporting development and distribution builds with proper SDK aliasing.
  • package.json is updated with scripts for building, bundling, packaging, cleaning, and development, and now includes all required devDependencies (esbuild, TypeScript, React, etc.). [1] [2]

End-to-End Testing and Developer Documentation

  • A new EXTENSION_TESTING.md guide is added, detailing the full workflow for building, packaging, and running automated e2e tests for extensions. It documents all test coverage, build steps, and TypeScript config usage.
  • The developer agent and SDK README are updated to reference the new example location and testing workflow, including the single-command e2e test runner (npm run e2e:extensions). [1] [2]

Example Extension Manifest and Features

  • The example extension manifest (clearpath-extension.json) is expanded to declare all available permissions, allowed domains, and feature flags, ensuring it exercises the full SDK surface. [1] [2]

E2E Test Utilities

  • New WebdriverIO helpers are added to e2e/helpers/app.ts for reliably interacting with extension UI elements, such as sidebar navigation, iframes, and tab content, improving e2e test robustness.

zalos added 4 commits April 20, 2026 22:33
Add a complete SDK example extension and end-to-end test workflow to validate consumer-built extensions. Introduces extension-sdk/example/com.clearpathai.sdk-example (source, components, build.mjs, tsconfigs, packaging), a build script for producing a dist-packaged .clear.ext (scripts/build-sdk-for-testing.js), and a WebdriverIO config for running extension e2e tests (wdio.extensions.conf.ts). Adds EXTENSION_TESTING.md with developer guidance, updates README/agent docs to reference the new test command, extends e2e tests (extensions-integration.spec.ts) and test helpers (e2e/helpers/app.ts) to cover install, IPC handler verification, iframe mounting, tab navigation and restart flows, and adds supporting package.json changes and scripts to wire the workflow. Also updates the example extension manifest to expose additional permissions and feature flags so the example exercises more SDK APIs.
Copilot AI review requested due to automatic review settings April 22, 2026 01:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR overhauls the extension developer workflow by making the SDK example extension a first-class, fully packaged reference and adding a reproducible end-to-end (WebdriverIO) test path that exercises the extension system via a packaged .clear.ext.

Changes:

  • Adds an automated “consumer-like” SDK build + pack + install + bundle + package pipeline (scripts/build-sdk-for-testing.js) and wires it into npm run e2e:extensions.
  • Improves extension runtime plumbing: new clearpath-ext:// protocol bootstrap page, extension change notifications (extension:changed), theme/feature-flag event fanout, and an sdk.http.fetch proxy.
  • Expands the SDK example extension to exercise most SDK namespaces with many new UI tabs/components and updated manifest permissions/domains.

Reviewed changes

Copilot reviewed 50 out of 52 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
wdio.extensions.conf.ts Adds a dedicated WDIO config targeting only the extension integration spec.
wdio.conf.ts Updates baseline WDIO config; unsets ELECTRON_RUN_AS_NODE for stability when launched from VS Code terminals.
src/renderer/src/hooks/useExtensions.ts Refreshes extension list on a new extension:changed renderer event.
src/renderer/src/hooks/useExtensions.test.ts Updates mocks for electronAPI.on() to return an unsubscribe function.
src/renderer/src/components/extensions/ExtensionManager.test.tsx Updates electronAPI.on() mock to return an unsubscribe function.
src/renderer/src/components/extensions/ExtensionHost.tsx Moves iframe bootstrap to clearpath-ext://.../__host__.html, changes timing to useLayoutEffect, adds event wiring and SDK method routing updates.
src/preload/index.ts Allows new invoke channel extension:http-fetch and new receive channel extension:changed.
src/main/ipc/integrationHandlers.ts Adds owner field to GitHub repo mapping.
src/main/ipc/featureFlagHandlers.ts Emits extension:event (feature-flags-changed) when flags are modified.
src/main/ipc/extensionHandlers.ts Adds extension:changed notifications, auto-permission granting on install, and new extension:http-fetch IPC proxy.
src/main/ipc/extensionHandlers.test.ts Adds mocks for adm-zip and adds/extends tests for install and requirements checks.
src/main/ipc/brandingHandlers.ts Emits extension:event (theme-changed) when branding changes and adds theme mapping helper.
src/main/index.ts Registers clearpath-ext privileged scheme; migrates to protocol.handle and adds __host__.html dynamic bootstrap; adjusts CSP header handling for extension protocol.
scripts/build-sdk-for-testing.js New script to build SDK, npm pack, install into example, bundle dist mode, and package .clear.ext for e2e.
package.json Rewires pretest:e2e:extensions to use the new build script and uses new WDIO config for extension e2e.
package-lock.json Lockfile updates from dependency/script changes.
extension-sdk/src/client.ts Makes sdk.theme.onChange() manage theme-changed subscription lifecycle via events.
extension-sdk/package.json Adds convenience scripts for building/bundling/packaging/testing the example extension.
extension-sdk/example/com.clearpathai.sdk-example/tsconfig.json Adds example TS config with local SDK source path aliases for dev.
extension-sdk/example/com.clearpathai.sdk-example/tsconfig.dist.json Adds dist TS config that clears path aliases for consumer-like builds.
extension-sdk/example/com.clearpathai.sdk-example/src/widgets/SidebarWidget.tsx Adds sidebar widget contribution example.
extension-sdk/example/com.clearpathai.sdk-example/src/widgets/HomeWidget.tsx Adds home dashboard widget contribution example.
extension-sdk/example/com.clearpathai.sdk-example/src/renderer.tsx New renderer entry with manual React bootstrap + SDK exploration UI registration.
extension-sdk/example/com.clearpathai.sdk-example/src/providers/ContextProvider.tsx Adds a context provider preview UI component.
extension-sdk/example/com.clearpathai.sdk-example/src/main.ts New main-process entry implementing example handlers and lifecycle hooks.
extension-sdk/example/com.clearpathai.sdk-example/src/components/shared-styles.ts Adds shared inline styles for sandboxed iframe UI.
extension-sdk/example/com.clearpathai.sdk-example/src/components/ThemeTab.tsx New Theme tab demonstrating theme get + live updates.
extension-sdk/example/com.clearpathai.sdk-example/src/components/StorageTab.tsx New Storage tab demonstrating storage APIs.
extension-sdk/example/com.clearpathai.sdk-example/src/components/SessionsTab.tsx New Sessions tab demonstrating sessions APIs.
extension-sdk/example/com.clearpathai.sdk-example/src/components/OverviewTab.tsx New Overview tab demonstrating extension identity and theme.
extension-sdk/example/com.clearpathai.sdk-example/src/components/NotificationsTab.tsx New Notifications tab demonstrating notifications emit.
extension-sdk/example/com.clearpathai.sdk-example/src/components/NavigationTab.tsx New Navigation tab demonstrating host navigation.
extension-sdk/example/com.clearpathai.sdk-example/src/components/LocalModelsTab.tsx New Local models tab demonstrating local model detection/chat.
extension-sdk/example/com.clearpathai.sdk-example/src/components/HttpTab.tsx New HTTP tab demonstrating sdk.http.fetch.
extension-sdk/example/com.clearpathai.sdk-example/src/components/GitHubTab.tsx New GitHub tab demonstrating GitHub integration APIs.
extension-sdk/example/com.clearpathai.sdk-example/src/components/FeatureFlagsTab.tsx New Feature flags tab demonstrating get/set and change events.
extension-sdk/example/com.clearpathai.sdk-example/src/components/EventsTab.tsx New Events tab demonstrating event subscriptions and round-trip demo.
extension-sdk/example/com.clearpathai.sdk-example/src/components/EnvironmentTab.tsx New Environment tab demonstrating env APIs.
extension-sdk/example/com.clearpathai.sdk-example/src/components/CostTab.tsx New Cost tab demonstrating cost APIs.
extension-sdk/example/com.clearpathai.sdk-example/src/components/ContextTab.tsx New Context tab demonstrating token estimation.
extension-sdk/example/com.clearpathai.sdk-example/src/App.tsx New main UI shell with a 14-tab SDK explorer.
extension-sdk/example/com.clearpathai.sdk-example/package.json Adds example build/bundle/package scripts and devDependencies; updates repository paths.
extension-sdk/example/com.clearpathai.sdk-example/package-lock.json Adds lockfile for the example extension’s devDependencies.
extension-sdk/example/com.clearpathai.sdk-example/clearpath-extension.json Expands permissions, allowed domains, and feature flags for full surface-area coverage.
extension-sdk/example/com.clearpathai.sdk-example/build.mjs Adds esbuild bundler supporting dev/watch and dist builds with correct aliasing behavior.
extension-sdk/example/com.clearpathai.sdk-example/.gitignore Ignores example build artifacts and packaged extensions.
extension-sdk/agents/extension-developer.md Updates developer agent docs with new example path and e2e workflow.
extension-sdk/README.md Adds example extension documentation and points to EXTENSION_TESTING.md.
e2e/helpers/app.ts Adds robust helpers for extension UI navigation, iframe switching, and tab interaction.
e2e/extensions-integration.spec.ts Expands extension e2e coverage substantially across install/navigation/tab/IPC flows.
EXTENSION_TESTING.md New end-to-end developer guide documenting extension build/package/test workflows.
.claude/agents/extension-developer.md Mirrors the extension developer agent doc updates under .claude/.
Files not reviewed (1)
  • extension-sdk/example/com.clearpathai.sdk-example/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +122 to +123
themeListeners.add(callback)
return () => themeListeners.delete(callback)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example extension re-implements its own buildSDKClient(). Its theme.onChange() only adds/removes callbacks and never subscribes to the host theme-changed event via events.subscribe. Since the host only forwards events that have been subscribed to, these callbacks will never fire (unless some other code separately subscribes to theme-changed). Mirror the SDK's createSDKClient behavior here (subscribe on first listener, unsubscribe on last), or expose/reuse the SDK's real client factory to avoid drift.

Suggested change
themeListeners.add(callback)
return () => themeListeners.delete(callback)
const wasEmpty = themeListeners.size === 0
themeListeners.add(callback)
if (wasEmpty) {
void request('events.subscribe', { event: 'theme-changed' })
}
return () => {
themeListeners.delete(callback)
if (themeListeners.size === 0) {
void request('events.unsubscribe', { event: 'theme-changed' })
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +72
console.log(`[ClearPath:ExtHost] sending ext:port to extension "${extId}"`)
iframe.contentWindow.postMessage(
{ type: 'ext:port', extensionId: extId },
'*',
[channel.port2],
)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The port transfer uses postMessage(..., '*', [channel.port2]). Since clearpath-ext is registered as a standard+secure scheme, the iframe has a stable origin (e.g. clearpath-ext://<extensionId>). Using '*' weakens origin checks and makes it easier to accidentally send the MessagePort to the wrong target if this code is refactored to support more frames. Use the iframe's actual origin (or new URL(iframe.src).origin) as the targetOrigin, and consider gating the added console.log statements behind a debug flag to avoid noisy logs in production.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +165
// Refresh when the main process fires `extension:changed` (after install, toggle, uninstall).
// This ensures all hook instances — including the Sidebar's — update immediately
// without requiring a page refresh.
useEffect(() => {
// Guard: window.electronAPI.on may be absent in test environments or non-Electron
// contexts where this hook is used but the preload isn't loaded.
if (!window.electronAPI?.on) return
const unsubscribe = window.electronAPI.on('extension:changed', () => {
void refresh()
})
return () => {
if (typeof unsubscribe === 'function') unsubscribe()
}
}, [refresh])
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new extension:changed subscription will typically cause duplicate refreshes because toggle(), install(), and uninstall() already call refresh() directly, and the main process now also emits extension:changed for those operations. Consider de-duplicating by relying on the event for cross-component updates (and removing the explicit refresh() calls), or add a simple in-flight debounce so one operation doesn't trigger two identical list loads.

Copilot uses AI. Check for mistakes.
Comment thread wdio.extensions.conf.ts
Comment on lines +1 to +3
import { config as baseConfig } from './wdio.conf.js'
import path from 'path'
import { fileURLToPath } from 'url'
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdio.extensions.conf.ts imports the base config from ./wdio.conf.js, but the base config file in this repo is wdio.conf.ts (no wdio.conf.js in the root). Running wdio run wdio.extensions.conf.ts will fail module resolution under Node ESM/ts-node. Import the TypeScript config (or use an extensionless import that matches your loader) so the extensions config can be executed.

Copilot uses AI. Check for mistakes.
Comment thread src/main/index.ts
Comment on lines +517 to +520
window.addEventListener('message', function onInit(event) {
if (event.data && event.data.type !== 'ext:port') return;
window.removeEventListener('message', onInit);

Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the generated __host__.html bootstrap, the message filter is inverted: if (event.data && event.data.type !== 'ext:port') return; will not return when event.data is null/undefined, and then event.data.extensionId is accessed below (crashes on unrelated/empty postMessages). Change the guard to require event.data and event.data.type === 'ext:port' before proceeding.

Copilot uses AI. Check for mistakes.
Comment thread src/main/index.ts
Comment on lines 571 to 576
const resolved = assertPathWithinRoots(
join(ext.installPath, filePath),
[ext.installPath],
)
callback({ path: resolved })
return net.fetch(`file://${resolved}`)
} catch (err) {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protocol.handle('clearpath-ext', ...) serves static files via net.fetch(file://${resolved}). resolved is a filesystem path and may contain spaces or characters that need URL encoding; interpolating it into a file:// string can produce invalid URLs on some paths/platforms. Prefer constructing the file URL via pathToFileURL(resolved).toString() (or equivalent) before calling net.fetch.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +120
// Auto-grant all permissions declared in the manifest so the extension works
// out of the box. Users can review/revoke permissions in Configure > Extensions.
if (ext.manifest.permissions.length > 0) {
registry.grantPermissions(ext.manifest.id, ext.manifest.permissions)
}

Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extension:install now automatically grants all permissions declared in the extension manifest (registry.grantPermissions(...)). This removes any meaningful permission boundary for user-installed extensions (e.g., an extension can request http:fetch, integration:*, etc. and gets it immediately on install). Please gate permission granting behind explicit user consent / a permission prompt, or restrict auto-granting to trusted/bundled extensions only.

Copilot uses AI. Check for mistakes.
Comment on lines +405 to +446
// ── HTTP fetch proxy (for extensions calling sdk.http.fetch) ─────────────

ipcMain.handle(
'extension:http-fetch',
async (_e, args: { extensionId: string; url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
try {
if (!registry.hasPermission(args.extensionId, 'http:fetch' as ExtensionPermission)) {
return { success: false, error: 'http:fetch permission not granted' }
}

const ext = registry.get(args.extensionId)
const allowedDomains = ext?.manifest.allowedDomains ?? []

let hostname: string
try {
hostname = new URL(args.url).hostname
} catch {
return { success: false, error: 'Invalid URL' }
}

if (!allowedDomains.includes(hostname)) {
return { success: false, error: `Domain "${hostname}" is not in allowedDomains for this extension` }
}

const fetchFn = getSystemFetch()
const response = await fetchFn(args.url, {
method: args.method ?? 'GET',
headers: args.headers,
body: args.body,
})

const body = await response.text()
const headers: Record<string, string> = {}
response.headers.forEach((value: string, key: string) => { headers[key] = value })

return { success: true, data: { status: response.status, headers, body } }
} catch (err) {
log.error('[ext-handlers] http-fetch failed for "%s": %s', args.extensionId, err)
return { success: false, error: String(err) }
}
},
)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New IPC surface extension:http-fetch is added here (permission checks, allowedDomains enforcement, and network call behavior), but src/main/ipc/extensionHandlers.test.ts does not include any coverage for it. Add unit tests for at least: (1) missing http:fetch permission denied, (2) invalid URL rejected, (3) domain not in allowedDomains rejected, and (4) allowed request returns { success: true, data: {status, headers, body} } with fetch mocked.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants