chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,170 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
import { parse } from '@babel/parser'
|
||||
import { testTerminalHostPlugin } from './test-terminal-host-plugin'
|
||||
import type { WorkspaceAttachResult, YCodeApi } from '../src/shared/ipc'
|
||||
import type { useChatStore as ChatStore } from '../src/renderer/src/stores/chat'
|
||||
import type { useSettingsStore as SettingsStore } from '../src/renderer/src/stores/settings'
|
||||
|
||||
type DialogResult = { canceled: boolean; filePaths: string[] }
|
||||
const key = '__YCODE_WORKSPACE_ATTACH_REGRESSION__'
|
||||
const globals = globalThis as typeof globalThis & Record<string, any>
|
||||
const previous = globals[key], previousWindow = globals.window
|
||||
const temp = await mkdtemp(join(tmpdir(), 'ycode-workspace-attach-'))
|
||||
const state = {
|
||||
userData: join(temp, 'data'), home: join(temp, 'home'),
|
||||
handlers: new Map<string, (...args: any[]) => any>(),
|
||||
nextDialog: () => Promise.resolve<DialogResult>({ canceled: true, filePaths: [] }),
|
||||
dialogCalls: 0, changes: 0, api: undefined as YCodeApi | undefined,
|
||||
invoke: async (channel: string, ...args: any[]): Promise<any> => state.handlers.get(channel)!({}, ...args)
|
||||
}
|
||||
globals[key] = state
|
||||
try {
|
||||
const source = await readFile('src/renderer/src/components/ChatPane.tsx', 'utf8')
|
||||
const tree = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] })
|
||||
const declarations = new Map<string, string>()
|
||||
function visit(node: any): void {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'VariableDeclaration') for (const declaration of node.declarations) {
|
||||
if (['bindDraftWorkspace', 'attachDraftWorkspace'].includes(declaration.id?.name)) {
|
||||
declarations.set(declaration.id.name, source.slice(node.start, node.end))
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
visit(tree)
|
||||
assert.equal(declarations.size, 2, 'test must execute the production ChatPane callbacks')
|
||||
const outfile = join(temp, 'attach.mjs')
|
||||
const modulePath = (file: string): string => JSON.stringify(resolve(file))
|
||||
await build({
|
||||
stdin: {
|
||||
contents: `export { registerIpc } from ${modulePath('src/main/ipc.ts')};
|
||||
import ${modulePath('src/preload/index.ts')};
|
||||
import { useSettingsStore } from ${modulePath('src/renderer/src/stores/settings.ts')};
|
||||
import { useChatStore } from ${modulePath('src/renderer/src/stores/chat.ts')};
|
||||
${declarations.get('bindDraftWorkspace')}
|
||||
${declarations.get('attachDraftWorkspace')}
|
||||
export { useSettingsStore, useChatStore, attachDraftWorkspace };`,
|
||||
resolveDir: process.cwd(), sourcefile: 'workspace-attach-fixture.ts', loader: 'ts'
|
||||
},
|
||||
outfile, bundle: true, platform: 'node', format: 'esm', external: ['node-pty'], logLevel: 'silent',
|
||||
plugins: [testTerminalHostPlugin, { name: 'workspace-attach-electron-boundary', setup(plugin) {
|
||||
plugin.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'attach-test' }))
|
||||
plugin.onLoad({ filter: /.*/, namespace: 'attach-test' }, () => ({ loader: 'js', contents: `
|
||||
const state = globalThis.${key};
|
||||
export const app = {getPath: name => name === 'home' ? state.home : state.userData,getName: ()=>'YCode',getVersion: ()=>'0.1.0'};
|
||||
export const safeStorage = {isEncryptionAvailable:()=>false,encryptString: value=>Buffer.from(value),decryptString:value=>value.toString()};
|
||||
export const ipcMain = {handle:(channel,handler)=>state.handlers.set(channel,handler)};
|
||||
export const ipcRenderer = {invoke:(...args)=>state.invoke(...args),on:()=>{},removeListener:()=>{}};
|
||||
export const contextBridge = {exposeInMainWorld:(_name,api)=>state.api=api};
|
||||
export const dialog = {showOpenDialog:async()=>{state.dialogCalls++;return state.nextDialog()}};
|
||||
export const shell = {openPath:async()=>''};
|
||||
export class BrowserWindow { static getAllWindows(){return [{webContents:{send:()=>state.changes++}}]} }
|
||||
export class WebContentsView {}
|
||||
` }))
|
||||
plugin.onResolve({ filter: /^(?:\.\/|\.\.\/)omp$/ }, () => ({ path: 'omp', namespace: 'attach-omp' }))
|
||||
plugin.onLoad({ filter: /.*/, namespace: 'attach-omp' }, () => ({ loader: 'js', contents: `
|
||||
export class OmpKernel {}
|
||||
export const detectOmpPath=()=>null,getOmpInfo=()=>({available:false,exePath:null,profile:'ycode',model:null});
|
||||
` }))
|
||||
} }]
|
||||
})
|
||||
const fixture = await import(pathToFileURL(outfile).href) as {
|
||||
registerIpc(getWindow: () => null): void
|
||||
useSettingsStore: typeof SettingsStore
|
||||
useChatStore: typeof ChatStore
|
||||
attachDraftWorkspace(): Promise<void>
|
||||
}
|
||||
fixture.registerIpc(() => null)
|
||||
globals.window = { ycode: state.api! }
|
||||
const api = state.api!, settings = fixture.useSettingsStore, chat = fixture.useChatStore
|
||||
const firstPath = join(temp, 'existing'), secondPath = join(temp, 'new')
|
||||
await Promise.all([mkdir(firstPath), mkdir(secondPath)])
|
||||
const original = await api.workspace.list()
|
||||
assert.equal(original.workspaces.length, 0)
|
||||
const first = await api.workspace.attach(firstPath)
|
||||
assert.ok(first.attachedWorkspaceId)
|
||||
assert.equal(first.selectedWorkspaceId, first.attachedWorkspaceId)
|
||||
assert.equal(first.workspaces.at(-1)?.root, firstPath)
|
||||
assert.equal(state.dialogCalls, 0, 'programmatic attach must bypass the OS picker')
|
||||
const repeated = await api.workspace.attach(firstPath)
|
||||
assert.equal(repeated.attachedWorkspaceId, first.attachedWorkspaceId)
|
||||
assert.equal(repeated.workspaces.length, first.workspaces.length)
|
||||
await assert.rejects(() => api.workspace.attach(''), /路径无效/)
|
||||
const countBeforeCancel = state.changes
|
||||
const cancelled = await api.workspace.attach()
|
||||
assert.equal(cancelled.attachedWorkspaceId, undefined)
|
||||
assert.deepEqual(cancelled, { workspaces: repeated.workspaces, selectedWorkspaceId: repeated.selectedWorkspaceId })
|
||||
assert.equal(state.changes, countBeforeCancel, 'cancellation must not signal a registry mutation')
|
||||
state.nextDialog = async () => ({ canceled: false, filePaths: [] })
|
||||
assert.equal((await api.workspace.attach()).attachedWorkspaceId, undefined, 'empty picker result also cancels')
|
||||
const second = await settings.getState().attachWorkspace(secondPath)
|
||||
assert.ok(second)
|
||||
assert.equal(settings.getState().selectedWorkspaceId, second)
|
||||
assert.equal('attachedWorkspaceId' in settings.getState(), false, 'result metadata is not persisted in the settings store')
|
||||
state.nextDialog = async () => ({ canceled: false, filePaths: [firstPath] })
|
||||
chat.getState().startTask({ cwd: 'old-draft-root', modelId: 'keep-model' })
|
||||
await fixture.attachDraftWorkspace()
|
||||
assert.deepEqual(chat.getState().draftContext, { workspaceId: first.attachedWorkspaceId, cwd: firstPath, modelId: 'keep-model' })
|
||||
assert.equal(settings.getState().workspaces.length, first.workspaces.length + 1)
|
||||
// Selecting the same globally selected directory must still bind an unrelated draft.
|
||||
chat.getState().startTask({ cwd: 'another-root' })
|
||||
await fixture.attachDraftWorkspace()
|
||||
assert.equal(chat.getState().draftContext.workspaceId, first.attachedWorkspaceId)
|
||||
assert.equal(chat.getState().draftContext.cwd, firstPath)
|
||||
const realAttach = settings.getState().attachWorkspace
|
||||
settings.setState({ attachWorkspace: async () => {
|
||||
const attachedId = await realAttach()
|
||||
// A separate settings window may select another default before this callback binds.
|
||||
settings.setState({ selectedWorkspaceId: second })
|
||||
return attachedId
|
||||
} })
|
||||
chat.getState().startTask()
|
||||
await fixture.attachDraftWorkspace()
|
||||
assert.equal(chat.getState().draftContext.workspaceId, first.attachedWorkspaceId, 'bind the returned ID, not the later global default')
|
||||
settings.setState({ attachWorkspace: realAttach })
|
||||
state.nextDialog = async () => ({ canceled: true, filePaths: [] })
|
||||
chat.getState().startTask({ cwd: 'cancel-preserved', modelId: 'model' })
|
||||
const before = chat.getState().draftContext, draftId = chat.getState().draftId
|
||||
await fixture.attachDraftWorkspace()
|
||||
assert.strictEqual(chat.getState().draftContext, before)
|
||||
assert.equal(chat.getState().draftId, draftId)
|
||||
assert.equal(chat.getState().currentSessionId, null)
|
||||
let complete!: (result: DialogResult) => void
|
||||
state.nextDialog = () => new Promise(resolve => { complete = resolve })
|
||||
const pending = fixture.attachDraftWorkspace()
|
||||
chat.getState().startTask({ cwd: 'newer-draft', modelId: 'next' })
|
||||
complete({ canceled: false, filePaths: [secondPath] })
|
||||
await pending
|
||||
assert.deepEqual(chat.getState().draftContext, { cwd: 'newer-draft', modelId: 'next' })
|
||||
const pendingSessionSwitch = fixture.attachDraftWorkspace()
|
||||
chat.setState({ currentSessionId: 'existing-session' })
|
||||
complete({ canceled: false, filePaths: [firstPath] })
|
||||
await pendingSessionSwitch
|
||||
assert.deepEqual(chat.getState().draftContext, { cwd: 'newer-draft', modelId: 'next' })
|
||||
const callsBeforeSession = state.dialogCalls
|
||||
await fixture.attachDraftWorkspace()
|
||||
assert.equal(state.dialogCalls, callsBeforeSession, 'existing sessions cannot open the draft picker')
|
||||
assert.equal(chat.getState().sessions.length, 0, 'attach and cancel never create sessions')
|
||||
const thirdPath = join(temp, 'new-through-picker')
|
||||
await mkdir(thirdPath)
|
||||
state.nextDialog = async () => ({ canceled: false, filePaths: [thirdPath] })
|
||||
const newFromPicker = await api.workspace.attach()
|
||||
assert.ok(newFromPicker.attachedWorkspaceId)
|
||||
assert.equal(newFromPicker.workspaces.at(-1)?.root, thirdPath)
|
||||
assert.equal(newFromPicker.workspaces.length, first.workspaces.length + 2)
|
||||
const unchangedApi = await api.workspace.select(null)
|
||||
assert.equal('attachedWorkspaceId' in unchangedApi, false, 'other workspace APIs preserve their registry-only contract')
|
||||
const disk = JSON.parse(await readFile(join(state.userData, 'workspaces.json'), 'utf8'))
|
||||
assert.equal('attachedWorkspaceId' in disk, false)
|
||||
console.log('workspace-attach-regression: cancel, repeated/new paths, preload/store, and draft/session races passed (Electron dialog stub; OS picker not driven)')
|
||||
} finally {
|
||||
if (previous === undefined) delete globals[key]; else globals[key] = previous
|
||||
if (previousWindow === undefined) delete globals.window; else globals.window = previousWindow
|
||||
await rm(temp, { recursive: true, force: true })
|
||||
}
|
||||
Reference in new issue
Block a user