| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- /** Keyless assembled-Web evidence for GitHub ready-for-review Session creation. */
- import { createHmac } from 'node:crypto'
- import { createServer } from 'node:http'
- import type { AddressInfo } from 'node:net'
- import { fileURLToPath } from 'node:url'
- import type { Browser, Page } from 'playwright'
- import { chromium } from 'playwright'
- import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest'
- import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
- import { LlmAdapter } from '@deepseek-ai/dsh-llm'
- import type {} from '@deepseek-ai/dsh-webhook'
- import {
- captureStableAria,
- compareOrRefreshGolden,
- launchWebScaffold,
- watchConsole,
- webSnapshotMode,
- type WebScaffold,
- } from './scaffold.ts'
- import { saveFailureShot } from './support.ts'
- const MODE = webSnapshotMode()
- const OVERLAY = fileURLToPath(new URL('../../cli/config/examples/github-review/cordis.yml', import.meta.url))
- const EXPECTED = fileURLToPath(new URL('./expected/github-ready-review/conversation.expected.md', import.meta.url))
- const PROVIDER = 'github-webhook-review-test'
- const MODEL = 'reply'
- const SECRET = 'github-webhook-review-secret'
- const TITLE = 'Review deepseek-harness/deepseek-harness#314'
- const REPLY = 'Review complete: no actionable findings.'
- /** Deterministic model response for the webhook-created Session. */
- class ReviewAdapter extends LlmAdapter {
- readonly requests: GenerateOptions[] = []
- override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
- this.requests.push(options)
- yield { type: 'block-start', index: 0, blockType: 'text' }
- yield { type: 'block-end', index: 0, block: { type: 'text', text: REPLY } }
- yield { type: 'finish', reason: { kind: 'stop' } }
- }
- }
- /** Reserve one currently free loopback port for the isolated WebServer. */
- async function freePort(): Promise<number> {
- const server = createServer()
- await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
- const port = (server.address() as AddressInfo).port
- await new Promise<void>(resolve => server.close(() => { resolve() }))
- return port
- }
- /** Sign one exact GitHub JSON body. */
- function signature(body: string): string {
- return `sha256=${createHmac('sha256', SECRET).update(body).digest('hex')}`
- }
- /** Send one signed GitHub delivery to a selected origin. */
- async function send(origin: string, delivery: string, body: object, event = 'pull_request'): Promise<Response> {
- const text = JSON.stringify(body)
- return await fetch(`${origin}/github`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- 'x-hub-signature-256': signature(text),
- 'x-github-event': event,
- 'x-github-delivery': delivery,
- },
- body: text,
- })
- }
- describe.skipIf(MODE === 'record')('web e2e: GitHub ready-for-review', () => {
- let scaffold: WebScaffold
- let browser: Browser
- let page: Page
- let webhookOrigin: string
- let tripwire: ReturnType<typeof watchConsole>
- let previousPort: string | undefined
- let previousSecret: string | undefined
- const adapter = new ReviewAdapter()
- beforeAll(async () => {
- previousPort = process.env.DSH_GITHUB_WEBHOOK_PORT
- previousSecret = process.env.DSH_GITHUB_WEBHOOK_SECRET
- const port = await freePort()
- process.env.DSH_GITHUB_WEBHOOK_PORT = String(port)
- process.env.DSH_GITHUB_WEBHOOK_SECRET = SECRET
- webhookOrigin = `http://127.0.0.1:${String(port)}`
- scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
- scaffold.ctx.effect(
- () => scaffold.ctx.llm.registerAdapter([PROVIDER], adapter),
- 'GitHub webhook review adapter',
- )
- await scaffold.ctx.agentDefaultModel.saveSelection({ provider: PROVIDER, model: MODEL })
- browser = await chromium.launch()
- page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' })
- await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
- tripwire = watchConsole(page)
- await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
- await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
- }, 60_000)
- afterAll(async () => {
- await browser?.close()
- await scaffold?.close()
- if (previousPort === undefined) Reflect.deleteProperty(process.env, 'DSH_GITHUB_WEBHOOK_PORT')
- else process.env.DSH_GITHUB_WEBHOOK_PORT = previousPort
- if (previousSecret === undefined) Reflect.deleteProperty(process.env, 'DSH_GITHUB_WEBHOOK_SECRET')
- else process.env.DSH_GITHUB_WEBHOOK_SECRET = previousSecret
- })
- it('isolates ingress and creates a browsable Workspace Session', async () => {
- onTestFailed(async () => { await saveFailureShot(page, 'github-ready-review') })
- const before = scaffold.ctx.agents.list().length
- expect((await fetch(`${webhookOrigin}/api`)).status).toBe(404)
- expect((await send(scaffold.baseUrl, 'wrong-port', { zen: 'ping' }, 'ping')).status).not.toBe(202)
- expect(scaffold.ctx.agents.list()).toHaveLength(before)
- expect((await send(webhookOrigin, 'ping', { zen: 'keep it logically awesome' }, 'ping')).status).toBe(202)
- await vi.waitFor(() => { expect(scaffold.ctx.agents.list()).toHaveLength(before) })
- const payload = {
- action: 'ready_for_review',
- number: 314,
- repository: { full_name: 'deepseek-harness/deepseek-harness' },
- pull_request: {
- title: 'Fix session replay',
- html_url: 'https://github.com/deepseek-harness/deepseek-harness/pull/314',
- draft: false,
- user: { login: 'octocat' },
- base: { ref: 'master', sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
- head: { ref: 'fix-session-replay', sha: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' },
- },
- }
- expect((await send(webhookOrigin, 'ready', payload)).status).toBe(202)
- await vi.waitFor(() => { expect(scaffold.ctx.agents.list()).toHaveLength(before + 1) })
- await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
- const agent = scaffold.ctx.agents.list().find(candidate => candidate.session.header.cwd === scaffold.workspaceCwd)
- expect(agent).toBeDefined()
- const workspace = await scaffold.ctx.workspaceRegistry.resolveByPath(scaffold.workspaceCwd)
- expect(workspace?.sessionIds).toContain(agent?.id)
- const webhookMessage = adapter.requests[0]?.messages.find(message => message.source.kind === 'webhook')
- expect(webhookMessage?.content).toHaveLength(1)
- const [content] = webhookMessage?.content ?? []
- expect(content?.type).toBe('text')
- if (content?.type !== 'text') throw new Error('webhook prompt was not text')
- expect(content.text).toContain('exact head SHA bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')
- const workspaceRow = page.locator('[role="treeitem"]').first()
- if (await workspaceRow.getAttribute('aria-expanded') !== 'true') await workspaceRow.click()
- await page.getByText(TITLE, { exact: true }).click()
- await page.getByText(REPLY, { exact: true }).waitFor({ state: 'visible', timeout: 30_000 })
- const tree = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd)
- const conversation = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
- await compareOrRefreshGolden(EXPECTED, `${tree}\n\n---\n\n${conversation}`, MODE)
- expect(tripwire.pageErrors).toEqual([])
- expect(tripwire.warnings).toEqual([])
- }, 60_000)
- })
|