mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
164 lines
7.3 KiB
TypeScript
164 lines
7.3 KiB
TypeScript
/** 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('../../../examples/web-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)
|
|
})
|