-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmarketplace-install-regression.e2e.ts
More file actions
180 lines (164 loc) · 5.57 KB
/
Copy pathmarketplace-install-regression.e2e.ts
File metadata and controls
180 lines (164 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { _electron, type Page } from '@playwright/test'
import { test, expect } from '../fixtures/electron-app'
import { dispatchAction } from '../helpers/redux'
const SPARSE_GUI_PATH = '/usr/bin:/bin:/usr/sbin:/sbin'
const MARKETPLACE_SKILL = {
rank: 1,
name: 'find-skills',
repo: 'vercel-labs/skills',
url: 'https://skills.sh/vercel-labs/skills/find-skills',
installCount: 1,
}
/**
* Install a fake `npx` binary into a user-level directory that Finder-launched
* macOS apps do not receive in PATH by default.
*
* @param home - Isolated E2E HOME used by the Electron process
* @returns Path to the marker file written by the fake `npx`
* @example
* const markerPath = stageFallbackNpx('/tmp/home')
* // Electron can only execute it when skillsCliService extends PATH.
*/
function stageFallbackNpx(home: string): string {
const markerPath = join(home, '.e2e-fake-npx-args')
const fakeNpxPath = join(home, 'Library', 'pnpm', 'npx')
mkdirSync(dirname(fakeNpxPath), { recursive: true })
writeFileSync(
fakeNpxPath,
`#!/bin/sh
set -eu
printf '%s\\n' "$*" > "$HOME/.e2e-fake-npx-args"
repo=""
skill=""
next=""
for arg in "$@"; do
if [ "$next" = "source" ]; then
repo="$arg"
next=""
continue
fi
if [ "$next" = "skill" ]; then
skill="$arg"
next=""
continue
fi
case "$arg" in
add) next="source" ;;
--skill) next="skill" ;;
esac
done
if [ "$repo" != "vercel-labs/skills" ] || [ "$skill" != "find-skills" ]; then
echo "unexpected fake npx invocation: $*" >&2
exit 64
fi
mkdir -p "$HOME/.agents/skills/$skill" "$HOME/.claude/skills"
cat > "$HOME/.agents/skills/$skill/SKILL.md" <<'EOF_SKILL'
---
name: find-skills
description: E2E fake skill for Marketplace install regression.
---
# find-skills
EOF_SKILL
rm -f "$HOME/.claude/skills/$skill"
ln -s "../../.agents/skills/$skill" "$HOME/.claude/skills/$skill"
echo "Installation complete"
`,
)
chmodSync(fakeNpxPath, 0o755)
return markerPath
}
/**
* Wait until the renderer store has scanned the staged Claude Code agent dir.
*
* @param appWindow - Main Electron renderer window
* @returns Promise that resolves when Claude Code is installable in the modal
* @example
* await waitForClaudeCodeAgent(appWindow)
*/
async function waitForClaudeCodeAgent(appWindow: Page): Promise<void> {
await appWindow.waitForFunction(() => {
const store = window.__store__
const state = store?.getState() as
| { agents?: { items?: Array<{ id: string; exists: boolean }> } }
| undefined
return state?.agents?.items?.some(
(agent) => agent.id === 'claude-code' && agent.exists,
)
})
}
test('Marketplace Install works when Electron starts with sparse macOS GUI PATH', async ({
isolatedHome,
}) => {
// Stage the agent dir before Electron boots so the install modal can offer
// Claude Code without touching the developer's real ~/.claude directory.
mkdirSync(join(isolatedHome, '.agents', 'skills'), { recursive: true })
mkdirSync(join(isolatedHome, '.claude', 'skills'), { recursive: true })
const markerPath = stageFallbackNpx(isolatedHome)
const repoRoot = resolve(__dirname, '..', '..')
const mainEntry = resolve(repoRoot, 'out', 'main', 'index.mjs')
const electronApp = await _electron.launch({
args: [mainEntry],
env: {
...process.env,
HOME: isolatedHome,
PATH: SPARSE_GUI_PATH,
E2E_USERDATA_DIR: resolve(isolatedHome, 'userData'),
E2E_DISABLE_UPDATE: '1',
E2E_BACKGROUND_LAUNCH: process.env['E2E_BACKGROUND_LAUNCH'] ?? '1',
},
})
try {
// Arrange — boot the window and wait until Claude Code is installable.
const appWindow = await electronApp.firstWindow()
await appWindow.waitForLoadState('domcontentloaded')
await waitForClaudeCodeAgent(appWindow)
// Seed the Marketplace search results without hitting skills.sh. The
// Install button below still goes through IPC, skillsCliService, and
// child_process.spawn.
await dispatchAction(appWindow, {
type: 'marketplace/setMarketplaceSearchQuery',
payload: 'find-skills',
})
await dispatchAction(appWindow, {
type: 'marketplace/search/fulfilled',
payload: [MARKETPLACE_SKILL],
meta: {
arg: 'find-skills',
requestId: 'e2e-marketplace-install-path',
requestStatus: 'fulfilled',
},
})
// Act — open the Marketplace tab and confirm the install through the dialog.
await appWindow.getByRole('tab', { name: 'Marketplace' }).click()
await appWindow
.getByRole('button', { name: 'Install', exact: true })
.first()
.click()
const dialog = appWindow.getByRole('dialog', { name: 'Install Skill' })
await expect(dialog.getByText('find-skills')).toBeVisible()
await dialog.getByRole('button', { name: 'Install', exact: true }).click()
// Assert — the fake npx ran with the pinned CLI version (SKILLS_CLI_VERSION
// is '1.5.13' in src/shared/constants.ts; hardcoded here so this test pins
// the literal command rather than computing the expectation from the same
// constant the production command builds from) and the installed badge shows.
await expect
.poll(() => existsSync(markerPath), { timeout: 10_000 })
.toBe(true)
expect(readFileSync(markerPath, 'utf-8')).toContain(
'skills@1.5.13 add vercel-labs/skills',
)
await expect(
appWindow.getByRole('img', { name: /find-skills is installed/i }),
).toBeVisible()
} finally {
await electronApp.close()
}
})