feat(web): inject workspace instructions
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
// and theme after, reload recovery last. Tests run sequentially in-file.
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createServer } from 'node:http'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -57,6 +58,25 @@ function waitForReadyLine(child: ChildProcess): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
async function rpc<T>(baseUrl: string, method: string, payload: unknown): Promise<T> {
|
||||
const response = await fetch(`${baseUrl}/api/${method}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request',
|
||||
rpcId: `smoke-${method}`,
|
||||
method,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error(`${method} failed over HTTP ${response.status}: ${await response.text()}`)
|
||||
const body = await response.json() as {
|
||||
result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } }
|
||||
}
|
||||
if (!body.result.ok) throw new Error(`${method} failed: ${body.result.error.code}: ${body.result.error.message}`)
|
||||
return body.result.value
|
||||
}
|
||||
|
||||
/** W5 screenshot: evidence for the figma comparison, not a failure artifact. */
|
||||
async function screen(page: Page, name: string): Promise<void> {
|
||||
await page.screenshot({ path: join(REPO_ROOT, '.artifacts', `w5-${name}.png`) })
|
||||
@@ -116,6 +136,91 @@ describe('dsh web keyless CLI smoke', () => {
|
||||
rmSync(sessionsDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('injects the invoking workspace AGENTS.md into the provider request', async () => {
|
||||
requireDist()
|
||||
const workspace = mkdtempSync(join(tmpdir(), 'dsh-web-workspace-'))
|
||||
mkdirSync(join(workspace, '.git'))
|
||||
writeFileSync(join(workspace, 'AGENTS.md'), 'web-workspace-context-probe\n')
|
||||
|
||||
let resolveProviderRequest!: (request: { messages?: { role?: string; content?: string }[] }) => void
|
||||
const providerRequest = new Promise<{ messages?: { role?: string; content?: string }[] }>((resolve) => {
|
||||
resolveProviderRequest = resolve
|
||||
})
|
||||
const provider = createServer((request, response) => {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk: string) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
resolveProviderRequest(JSON.parse(body) as { messages?: { role?: string; content?: string }[] })
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end([
|
||||
'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}',
|
||||
'data: {"choices":[{"delta":{"content":"done"}}]}',
|
||||
'data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
|
||||
'data: [DONE]',
|
||||
'',
|
||||
].join('\n\n'))
|
||||
})
|
||||
})
|
||||
await new Promise<void>(resolve => provider.listen(0, '127.0.0.1', resolve))
|
||||
const address = provider.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('mock provider did not bind a TCP port')
|
||||
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
|
||||
{
|
||||
cwd: workspace,
|
||||
env: {
|
||||
...process.env,
|
||||
DEEPSEEK_API_KEY: 'keyless-web-workspace',
|
||||
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
DSH_HOME: join(workspace, '.dsh'),
|
||||
TSX_TSCONFIG_PATH: join(REPO_ROOT, 'tsconfig.json'),
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
try {
|
||||
const baseUrl = await waitForReadyLine(child)
|
||||
const created = await rpc<{ sessionId: string }>(baseUrl, 'session.create', {})
|
||||
await rpc<{ accepted: true }>(baseUrl, 'session.prompt', {
|
||||
sessionId: created.sessionId,
|
||||
mode: 'queue',
|
||||
content: [{ type: 'text', text: 'go' }],
|
||||
})
|
||||
const captured = await Promise.race([
|
||||
providerRequest,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
setTimeout(() => { reject(new Error('provider request not received in 10s')) }, 10_000).unref()
|
||||
}),
|
||||
])
|
||||
const workspaceMessage = captured.messages?.find(message =>
|
||||
message.role === 'user' && message.content?.includes('web-workspace-context-probe'))
|
||||
expect(workspaceMessage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"content": "<system-reminder>
|
||||
The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.
|
||||
|
||||
Instructions from: AGENTS.md
|
||||
|
||||
web-workspace-context-probe
|
||||
|
||||
</system-reminder>",
|
||||
"role": "user",
|
||||
}
|
||||
`)
|
||||
} finally {
|
||||
const closed = child.exitCode === null
|
||||
? new Promise<void>((resolveClose) => { child.once('close', () => { resolveClose() }) })
|
||||
: Promise.resolve()
|
||||
if (child.exitCode === null) child.kill('SIGTERM')
|
||||
await closed
|
||||
await new Promise<void>(resolveClose => provider.close(() => { resolveClose() }))
|
||||
rmSync(workspace, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke (real host, real key, W5)', () => {
|
||||
|
||||
Reference in New Issue
Block a user