Files
ycode/scripts/resource-bridge-regression.ts
T

119 lines
8.5 KiB
TypeScript

import assert from 'node:assert/strict'
import { testTerminalHostPlugin } from './test-terminal-host-plugin'
import { createServer } from 'node:http'
import { mkdtemp, mkdir, writeFile, readFile, rm, symlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import { classifyWorkspacePath, decodeDownload, imageDownloadPayload, readWorkspaceResource, readWorkspaceText, resolveWorkspaceImage, resolveWorkspacePath } from '../src/main/resources'
async function main(): Promise<void> {
const root = await mkdtemp(join(tmpdir(), 'ycode-resource-bridge-'))
const workspace = join(root, 'workspace')
await mkdir(workspace)
const state = { workspace, handlers: new Map<string, (...args: unknown[]) => Promise<unknown>>(), savePath: undefined as string | undefined, dialogCalls: 0 }
const globals = globalThis as typeof globalThis & { __YCODE_RESOURCE_TEST__?: typeof state }
globals.__YCODE_RESOURCE_TEST__ = state
const server = createServer((_req, res) => { res.setHeader('content-type', 'image/png'); res.end(Buffer.from([137, 80, 78, 71])) })
try {
await writeFile(join(workspace, 'example.ts'), Array.from({ length: 1000 }, (_, index) => `line ${index + 1}`).join('\n'))
await writeFile(join(workspace, 'preview.png'), Buffer.from([137, 80, 78, 71]))
await writeFile(join(workspace, 'binary.bin'), Buffer.from([0, 1, 2]))
await mkdir(join(workspace, 'folder'))
await writeFile(join(root, 'outside.txt'), 'must stay outside')
await symlink(root, join(workspace, 'outside-link'), process.platform === 'win32' ? 'junction' : 'dir')
assert.equal(await resolveWorkspacePath(workspace, 'example.ts'), join(workspace, 'example.ts'))
assert.equal(await classifyWorkspacePath(workspace, 'example.ts'), 'file')
assert.equal(await classifyWorkspacePath(workspace, 'folder'), 'directory')
assert.equal(await classifyWorkspacePath(workspace, 'missing'), 'unknown')
await assert.rejects(resolveWorkspacePath(workspace, '../outside.txt'), /超出工作目录/)
await assert.rejects(resolveWorkspacePath(workspace, 'outside-link/outside.txt'), /超出工作目录/)
assert.equal(await resolveWorkspaceImage(workspace, '../outside.txt'), undefined)
const file = await readWorkspaceResource(workspace, 'example.ts', { start: 600, end: 605 })
assert.equal(file.kind, 'text')
if (file.kind !== 'text') throw new Error('Expected text')
assert.equal(file.firstLine, 580)
assert.equal(file.totalLines, 1000)
assert.deepEqual(file.range, { start: 600, end: 605 })
assert.equal(file.content.split('\n')[20], 'line 600')
assert.equal(file.truncated, true)
const fullText = await readWorkspaceText(workspace, 'example.ts')
assert.equal(fullText.split('\n').length, 1000, 'Artifact content must not use the 400-line window')
await assert.rejects(readWorkspaceText(workspace, '../outside.txt'), /超出工作目录/)
await assert.rejects(readWorkspaceText(workspace, 'outside-link/outside.txt'), /超出工作目录/)
await assert.rejects(readWorkspaceText(workspace, 'binary.bin'), /暂不支持文本预览/)
const directory = await readWorkspaceResource(workspace, '.')
assert.equal(directory.kind, 'directory')
await assert.rejects(readWorkspaceResource(workspace, 'binary.bin'), /暂不支持文本预览/)
const image = await resolveWorkspaceImage(workspace, 'preview.png')
assert.equal(image, 'data:image/png;base64,iVBORw==')
assert.equal(await resolveWorkspaceImage(workspace, pathToFileURL(join(workspace, 'preview.png')).href), image)
assert.equal(await resolveWorkspaceImage(workspace, pathToFileURL(join(workspace, 'preview.png')).href.replace('file:', 'local-file:')), image)
const payload = await imageDownloadPayload(workspace, image!)
assert.equal(payload.encoding, 'base64')
assert.deepEqual(decodeDownload(payload).bytes, Buffer.from([137, 80, 78, 71]))
assert.equal(decodeDownload({ ...payload, filename: '../nested/result.png' }).filename, 'result.png')
assert.throws(() => decodeDownload({ ...payload, mimeType: 'application/javascript' }), /不支持/)
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
assert.ok(address && typeof address !== 'string')
const remote = await imageDownloadPayload(workspace, `http://127.0.0.1:${address.port}/remote.png`)
assert.equal(remote.filename, 'remote.png')
assert.deepEqual(decodeDownload(remote).bytes, Buffer.from([137, 80, 78, 71]))
const outfile = join(root, 'ipc.mjs')
await build({ entryPoints: ['src/main/ipc.ts'], outfile, bundle: true, external: ['node-pty'], platform: 'node', format: 'esm', logLevel: 'silent',
banner: { js: `import { createRequire as createTestRequire } from 'node:module'; const require = createTestRequire(import.meta.url);` },
plugins: [testTerminalHostPlugin, { name: 'resource-ipc-adapters', setup(builder) {
builder.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'resource-test' }))
builder.onLoad({ filter: /.*/, namespace: 'resource-test' }, () => ({ loader: 'js', contents: `
const state=globalThis.__YCODE_RESOURCE_TEST__;
export const app={getPath:()=>state.workspace,getName:()=> 'YCode',getVersion:()=> 'test'};
export const ipcMain={handle:(name,handler)=>state.handlers.set(name,handler)};
export const dialog={showSaveDialog:async()=>{state.dialogCalls++;return {canceled:!state.savePath,filePath:state.savePath}},showOpenDialog:async()=>({canceled:true,filePaths:[]})};
export const shell={openPath:async()=>''};
export const powerSaveBlocker={start:()=>1,stop:()=>{}};
export const safeStorage={isEncryptionAvailable:()=>false};
export class BrowserWindow{}
export class WebContentsView{}
` }))
builder.onResolve({ filter: /^\.\/config$/ }, () => ({ path: 'config', namespace: 'resource-config' }))
builder.onLoad({ filter: /.*/, namespace: 'resource-config' }, () => ({ loader: 'js', contents: `
export const getWorkspaceRoot=async()=>globalThis.__YCODE_RESOURCE_TEST__.workspace;
export const getConfigView=async()=>({}); export const updateConfig=async()=>({});
export const getGatewayConfig=async()=>({}); export const setWorkspaceRoot=async()=>{};
export const getStoredAgentSettings=async()=>({}); export const setStoredAgentSettings=async()=>{};
export const getStoredMemoryPreferences=async()=>({}); export const setStoredMemoryPreferences=async()=>{};
export const getStoredPersonalization=async()=>({}); export const setStoredPersonalization=async()=>{};
` }))
} }]
})
const { registerIpc } = await import(pathToFileURL(outfile).href)
registerIpc(() => null)
const invoke = (name: string, ...args: unknown[]): Promise<unknown> => state.handlers.get(name)!({}, ...args)
assert.equal(await invoke('resources:classify', 'folder'), 'directory')
assert.equal(await invoke('resources:read-text', 'example.ts'), fullText)
assert.equal(await invoke('resources:resolveImage', 'preview.png'), image)
// Match the restored Mermaid export contract, including its charset parameter.
const svg = { filename: 'diagram.svg', mimeType: 'image/svg+xml;charset=utf-8', data: '<svg xmlns="http://www.w3.org/2000/svg"></svg>', encoding: 'utf8' }
assert.deepEqual(await invoke('resources:download', svg), { saved: false })
state.savePath = join(root, 'saved.svg')
assert.deepEqual(await invoke('resources:download', svg), { saved: true, path: state.savePath })
assert.equal(await readFile(state.savePath, 'utf8'), svg.data)
state.savePath = join(root, 'saved.png')
await invoke('resources:downloadImage', image)
assert.deepEqual(await readFile(state.savePath), Buffer.from([137, 80, 78, 71]))
state.savePath = join(root, 'plan.md')
await invoke('resources:download', { filename: 'plan.md', mimeType: 'text/markdown', data: '# Plan\n\n- [ ] Work', encoding: 'utf8' })
assert.equal(await readFile(state.savePath, 'utf8'), '# Plan\n\n- [ ] Work')
assert.equal(state.dialogCalls, 4)
console.log('resource-bridge-regression: workspace paths, ranges, images, downloads and actual IPC checks passed')
} finally {
delete globals.__YCODE_RESOURCE_TEST__
await new Promise<void>((resolve) => server.close(() => resolve()))
await rm(root, { recursive: true, force: true })
}
}
void main().catch((error) => { console.error(error); process.exitCode = 1 })