-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathcode-server.test.ts
More file actions
258 lines (220 loc) · 10.7 KB
/
Copy pathcode-server.test.ts
File metadata and controls
258 lines (220 loc) · 10.7 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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import type { CodeServerSharedState } from '../src/types'
import { mkdtempSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { PLUGIN_ID, STATE_KEY, TERMINAL_SESSION_ICON, TERMINAL_SESSION_TITLE } from '../src/constants'
import { setupCodeServer } from '../src/node/index'
import {
createFakeHubTerminals,
createTestContext,
writeFakeCodeServer,
writeFakeServeWeb,
writeFakeTunnel,
} from './_utils'
async function sharedState(ctx: Awaited<ReturnType<typeof createTestContext>>): Promise<CodeServerSharedState> {
const state = await ctx.rpc.sharedState.get(STATE_KEY)
return state.value() as CodeServerSharedState
}
/** Poll until `predicate` holds or the deadline elapses. */
async function waitUntil(predicate: () => boolean, timeout = 5000): Promise<void> {
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
if (predicate())
return
await new Promise(resolve => setTimeout(resolve, 25))
}
throw new Error('timed out waiting for condition')
}
describe('@devframes/plugin-code-server', () => {
const supervisors: { dispose: () => void }[] = []
afterEach(() => {
for (const s of supervisors.splice(0))
s.dispose()
})
it('detects a missing binary and refuses to start', async () => {
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin: '/no/such/code-server-xyz' })
supervisors.push(supervisor)
expect(supervisor.status().detection).toMatchObject({ checked: true, installed: false })
expect((await sharedState(ctx)).detection.installed).toBe(false)
await expect(supervisor.start()).rejects.toThrow(/binary|not installed/i)
})
it('detects an installed binary and reports its version + backend', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0 fakehash with Code 1.99.0' })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
const detection = supervisor.status().detection
expect(detection.installed).toBe(true)
expect(detection.version).toBe('4.99.0')
expect(detection.backend).toBe('code-server')
expect(detection.mode).toBe('local')
})
it('launches code-server, becomes ready, and hands off a valid session cookie', async () => {
const dumpEnvTo = join(mkdtempSync(join(tmpdir(), 'dcs-dump-')), 'hashed')
const bin = writeFakeCodeServer({ version: '4.99.0', dumpEnvTo })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.server.status).toBe('running')
expect(result.server.port).toBeGreaterThan(0)
expect(result.connect?.cookie?.name).toBe('code-server-session')
expect(result.connect?.cookie?.value).toMatch(/^[a-f0-9]{64}$/)
expect(result.connect?.path).toBe('/')
// The cookie value handed to the client must equal HASHED_PASSWORD the
// server was launched with — that is what makes the iframe auto-auth.
const hashed = readFileSync(dumpEnvTo, 'utf8')
expect(hashed).toBe(result.connect?.cookie?.value)
expect((await sharedState(ctx)).server.status).toBe('running')
// Shared state must never leak the connect material.
expect((await sharedState(ctx)) as any).not.toHaveProperty('connect')
})
it('stops a running server and resets to stopped', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
await supervisor.start()
const stopped = supervisor.stop()
expect(stopped.server.status).toBe('stopped')
expect(stopped.connect).toBeUndefined()
expect((await sharedState(ctx)).server.status).toBe('stopped')
})
it('is idempotent while running', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
const first = await supervisor.start()
const second = await supervisor.start()
expect(second.server.port).toBe(first.server.port)
})
it('allocates a dynamic port when serverPort is 0', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin, serverPort: 0 })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.server.status).toBe('running')
expect(result.server.port).toBeGreaterThan(0)
expect(result.server.port).not.toBe(8080)
})
it('starts on boot when startOnBoot is set', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { bin, serverPort: 0, startOnBoot: true })
supervisors.push(supervisor)
await waitUntil(() => supervisor.status().server.status === 'running')
expect(supervisor.status().server.port).toBeGreaterThan(0)
})
describe('code serve-web backend', () => {
it('launches `code serve-web` and hands off a connection token via query', async () => {
const dumpTokenTo = join(mkdtempSync(join(tmpdir(), 'dcs-tkn-')), 'token')
const bin = writeFakeServeWeb({ version: '1.99.0', dumpTokenTo })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { backend: 'ms-code-serve-web', bin, serverPort: 0 })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.server.status).toBe('running')
expect(result.detection.backend).toBe('ms-code-serve-web')
// No cookie for serve-web; the token rides on the URL query.
expect(result.connect?.cookie).toBeUndefined()
const token = readFileSync(dumpTokenTo, 'utf8')
expect(token).toMatch(/^[a-f0-9]{64}$/)
expect(result.connect?.path).toContain(`tkn=${token}`)
expect(result.connect?.path).toContain('folder=')
})
})
describe('reuseExistingServer', () => {
it('adopts an already-running server on the target port without spawning', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const running = await setupCodeServer(ctx, { bin, serverPort: 0 })
supervisors.push(running)
const first = await running.start()
const port = first.server.port!
const ctx2 = await createTestContext()
const adopter = await setupCodeServer(ctx2, { bin, serverPort: port, reuseExistingServer: true })
supervisors.push(adopter)
const result = await adopter.start()
expect(result.server.status).toBe('running')
expect(result.server.port).toBe(port)
expect(result.server.pid).toBeUndefined()
// We don't own the adopted server's secret, so no auth handoff.
expect(result.connect?.cookie).toBeUndefined()
expect(result.connect?.path).toBe('/')
})
})
describe('tunnel mode', () => {
it('surfaces the device-login prompt while authenticating', async () => {
const bin = writeFakeTunnel({ version: '1.99.0', printLogin: true })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { mode: 'tunnel', bin })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.detection.mode).toBe('tunnel')
expect(result.server.status).toBe('starting')
expect(result.server.login).toEqual({ url: 'https://github.com/login/device', code: 'ABCD-1234' })
})
it('becomes running and embeds the vscode.dev URL when the tunnel opens', async () => {
const url = 'https://vscode.dev/tunnel/testmachine/work'
const bin = writeFakeTunnel({ version: '1.99.0', printUrl: true, url })
const ctx = await createTestContext()
const supervisor = await setupCodeServer(ctx, { mode: 'tunnel', bin })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.server.status).toBe('running')
expect(result.connect?.url).toBe(url)
})
})
describe('hub terminals integration', () => {
it('spawns code-server through ctx.terminals as a read-only session', async () => {
const dumpEnvTo = join(mkdtempSync(join(tmpdir(), 'dcs-dump-')), 'hashed')
const bin = writeFakeCodeServer({ version: '4.99.0', dumpEnvTo })
const ctx = await createTestContext()
const terminals = createFakeHubTerminals()
;(ctx as unknown as { terminals: typeof terminals }).terminals = terminals
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
const result = await supervisor.start()
expect(result.server.status).toBe('running')
// Launched through the hub, surfaced as exactly one session.
expect(terminals.sessions.size).toBe(1)
const session = terminals.sessions.get(PLUGIN_ID)
expect(session).toBeDefined()
// Proper name + icon, and the workspace folder as description.
expect(session!.title).toBe(TERMINAL_SESSION_TITLE)
expect(session!.icon).toBe(TERMINAL_SESSION_ICON)
expect(session!.status).toBe('running')
expect(session!.type).toBe('child-process')
expect(session!.description).toBe(ctx.cwd)
// The hub-owned child is the one the supervisor reports as running.
expect(session!.getChildProcess()?.pid).toBe(result.server.pid)
// Auth still flows end to end through the hub: HASHED_PASSWORD the process
// was launched with equals the cookie handed to the client.
const hashed = readFileSync(dumpEnvTo, 'utf8')
expect(hashed).toBe(result.connect?.cookie?.value)
})
it('reflects stop on the hub session and re-registers on the next start', async () => {
const bin = writeFakeCodeServer({ version: '4.99.0' })
const ctx = await createTestContext()
const terminals = createFakeHubTerminals()
;(ctx as unknown as { terminals: typeof terminals }).terminals = terminals
const supervisor = await setupCodeServer(ctx, { bin })
supervisors.push(supervisor)
await supervisor.start()
const first = terminals.sessions.get(PLUGIN_ID)
supervisor.stop()
// The session stays visible, marked stopped.
expect(terminals.sessions.get(PLUGIN_ID)?.status).toBe('stopped')
// A fresh start replaces the stale session under the same stable id.
await supervisor.start()
expect(terminals.sessions.size).toBe(1)
const second = terminals.sessions.get(PLUGIN_ID)
expect(second).not.toBe(first)
expect(second?.status).toBe('running')
})
})
})