DSH-Arch-Setup/plugins/dsh-touched-git/test/client.test.js
Шурупов Илья Викторович cf9443753e Add one-command Arch DSH setup
Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
2026-09-12 19:13:36 +03:00

769 lines
30 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import vm from 'node:vm'
const clientPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'client.js')
const clientSource = readFileSync(clientPath, 'utf8')
function createRenderer() {
const state = { component: null, props: null, hooks: [], hookIndex: 0, pending: [], tree: null, rendering: false }
function render() {
state.rendering = true
state.hookIndex = 0
state.pending = []
try {
state.tree = state.component(state.props)
} finally {
state.rendering = false
}
for (const effect of state.pending) {
const hook = state.hooks[effect.index]
if (typeof hook.cleanup === 'function') hook.cleanup()
hook.deps = effect.deps
const result = effect.fn()
hook.cleanup = typeof result === 'function' ? result : undefined
}
}
function schedule() {
if (state.rendering) {
Promise.resolve().then(render)
return
}
render()
}
const react = {
useState(initial) {
const index = state.hookIndex++
if (!(index in state.hooks)) {
state.hooks[index] = { value: typeof initial === 'function' ? initial() : initial }
}
const hook = state.hooks[index]
const setValue = (next) => {
const resolved = typeof next === 'function' ? next(hook.value) : next
if (Object.is(resolved, hook.value)) return
hook.value = resolved
schedule()
}
return [hook.value, setValue]
},
useRef(initial) {
const index = state.hookIndex++
if (!(index in state.hooks)) state.hooks[index] = { current: initial }
return state.hooks[index]
},
useEffect(fn, deps) {
const index = state.hookIndex++
let hook = state.hooks[index]
if (!hook) hook = state.hooks[index] = { initialized: false, deps: undefined, cleanup: undefined }
const changed = !hook.initialized || !deps || !hook.deps ||
deps.length !== hook.deps.length || deps.some((value, i) => !Object.is(value, hook.deps[i]))
if (changed) {
hook.initialized = true
state.pending.push({ index, fn, deps })
}
},
}
const element = (type, props) => ({ type, props: props || {} })
const jsxRuntime = { jsx: element, jsxs: element, Fragment: Symbol('Fragment') }
return {
react,
jsxRuntime,
mount(component, props) {
state.component = component
state.props = props
state.hooks = []
render()
},
update(props) {
state.props = props
render()
},
unmount() {
for (const hook of state.hooks) {
if (hook && typeof hook.cleanup === 'function') {
hook.cleanup()
hook.cleanup = undefined
}
}
},
tree: () => state.tree,
}
}
function makeEditorWindow() {
return {
closed: false,
focusCount: 0,
closeCount: 0,
location: { href: 'about:blank' },
focus() { this.focusCount++ },
close() { this.closeCount++; this.closed = true },
}
}
function makeWindow() {
const win = {
openCalls: [],
openImpl: () => null,
intervals: [],
nextIntervalId: 1,
open(url, name) {
win.openCalls.push({ url, name })
return win.openImpl(url, name)
},
setInterval(callback, ms) {
const id = win.nextIntervalId++
win.intervals.push({ id, callback, ms })
return id
},
clearInterval(id) {
win.intervals = win.intervals.filter(entry => entry.id !== id)
},
triggerIntervals() {
for (const entry of win.intervals) entry.callback()
},
}
return win
}
function makeDocument() {
return {
visibilityState: 'visible',
getElementById: () => null,
createElement: () => ({}),
head: { appendChild() {} },
}
}
function loadClient() {
const renderer = createRenderer()
const win = makeWindow()
const doc = makeDocument()
const captured = {}
win.__ModuleLoader__ = { load: (module) => { captured.module = module } }
const fetchCalls = []
const sandbox = { window: win, document: doc, console, URL, encodeURIComponent, fetch: async (path, options) => {
fetchCalls.push({ path, options })
return { json: async () => ({ ok: true, value: {} }) }
} }
vm.createContext(sandbox)
vm.runInContext(clientSource, sandbox)
const DiffViewer = Symbol('SplitDiffViewer')
const require = (name) => {
if (name === 'react') return renderer.react
if (name === 'react/jsx-runtime') return renderer.jsxRuntime
if (name === '@dsh-external/dsh-diff-viewer') return { DiffViewer }
throw new Error(`Unexpected module request: ${name}`)
}
const api = captured.module.factory(require)
const registrations = []
const slots = {
inject: (name, build) => {
const result = build()
if (typeof result?.[Symbol.iterator] === 'function') return [...result]
return result
},
register: (config, component) => {
const registration = { config, component }
registrations.push(registration)
return registration
},
}
api.apply({ slots })
const workingDirs = registrations.find(entry => entry.config.name === 'conversation.session.header.actions')
const mutation = registrations.find(entry => entry.config.name === 'tool.call.toolview')
return { GitWorkingDirsMenu: workingDirs.component, MutationToolCard: mutation.component, renderer, window: win, document: doc, config: workingDirs.config, registrations, DiffViewer, fetchCalls }
}
async function flush() {
for (let i = 0; i < 5; i++) await new Promise(resolve => setImmediate(resolve))
}
function collectText(node, out = []) {
if (node == null) return out
if (Array.isArray(node)) {
for (const child of node) collectText(child, out)
return out
}
if (typeof node === 'string' || typeof node === 'number') {
out.push(String(node))
return out
}
if (typeof node === 'object') collectText(node.props?.children, out)
return out
}
function findAll(node, predicate, out = []) {
if (node == null) return out
if (Array.isArray(node)) {
for (const child of node) findAll(child, predicate, out)
return out
}
if (typeof node === 'object') {
if (predicate(node)) out.push(node)
findAll(node.props?.children, predicate, out)
}
return out
}
function visibleText(renderer) {
return collectText(renderer.tree()).join(' ')
}
function buttons(renderer) {
return findAll(renderer.tree(), node => node.type === 'button')
}
function trackedEntry(overrides = {}) {
return { ok: true, id: 'e1', repository: '/repo/.git', workingDirectory: '/repo/work', ...overrides }
}
function openGitMenu(renderer) {
buttons(renderer).find(button => button.props.children === 'Git Working Dirs').props.onClick()
}
function gitOpenButton(renderer) {
return buttons(renderer).find(button => button.props.children === 'Open VS Code')
}
test('mounting only lists tracked directories and never opens an editor', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
let listCalls = 0
let openCalls = 0
const list = async () => { listCalls++; return { ok: true, value: { entries: [trackedEntry()] } } }
const open = async () => { openCalls++; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
assert.equal(listCalls, 1)
assert.equal(openCalls, 0)
assert.equal(window.intervals.length, 1)
})
test('header trigger opens and dismisses the Git Working Dirs menu', async () => {
const { GitWorkingDirsMenu, renderer } = loadClient()
renderer.mount(GitWorkingDirsMenu, {
list: async () => ({ ok: true, value: { entries: [trackedEntry()] } }),
open: async () => ({ ok: true, value: {} }),
})
await flush()
assert.equal(buttons(renderer)[0].props.children, 'Git Working Dirs')
assert.equal(buttons(renderer)[0].props['aria-expanded'], false)
assert.doesNotMatch(visibleText(renderer), /repo\/work/)
openGitMenu(renderer)
assert.equal(buttons(renderer)[0].props['aria-expanded'], true)
assert.match(visibleText(renderer), /repo\/work/)
buttons(renderer).find(button => button.props['aria-label'] === 'Close Git Working Dirs').props.onClick()
assert.equal(buttons(renderer)[0].props['aria-expanded'], false)
})
test('interval polling keeps listing without ever opening an editor', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
let listCalls = 0
let openCalls = 0
const list = async () => { listCalls++; return { ok: true, value: { entries: [trackedEntry()] } } }
const open = async () => { openCalls++; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
window.triggerIntervals()
await flush()
window.triggerIntervals()
await flush()
assert.equal(listCalls, 3)
assert.equal(openCalls, 0)
})
test('an explicit click opens the editor and navigates the popup to the local URL', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
const editorWindow = makeEditorWindow()
window.openImpl = () => editorWindow
let openArgument = null
const list = async () => ({ ok: true, value: { entries: [trackedEntry()] } })
const open = async (id) => { openArgument = id; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
openGitMenu(renderer)
gitOpenButton(renderer).props.onClick()
await flush()
assert.equal(openArgument, 'e1')
assert.equal(window.openCalls.length, 1)
assert.equal(editorWindow.location.href, 'http://127.0.0.1:40000/')
assert.match(visibleText(renderer), /VS Code opened/)
})
test('a blocked popup surfaces guidance and never opens an editor', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
window.openImpl = () => null
let openCalls = 0
const list = async () => ({ ok: true, value: { entries: [trackedEntry()] } })
const open = async () => { openCalls++; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
openGitMenu(renderer)
gitOpenButton(renderer).props.onClick()
await flush()
assert.equal(window.openCalls.length, 1)
assert.equal(openCalls, 0)
assert.match(visibleText(renderer), /Allow popups/)
})
test('a still-open editor window is focused instead of reopened', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
const editorWindow = makeEditorWindow()
window.openImpl = () => editorWindow
let openCalls = 0
const list = async () => ({ ok: true, value: { entries: [trackedEntry()] } })
const open = async () => { openCalls++; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
openGitMenu(renderer)
gitOpenButton(renderer).props.onClick()
await flush()
gitOpenButton(renderer).props.onClick()
await flush()
assert.equal(window.openCalls.length, 1)
assert.equal(openCalls, 1)
assert.equal(editorWindow.focusCount, 1)
assert.match(visibleText(renderer), /Focused VS Code/)
})
test('a failed list request is reported and never opens an editor', async () => {
const { GitWorkingDirsMenu, renderer } = loadClient()
let openCalls = 0
const list = async () => ({ ok: false, error: { message: 'list request failed' } })
const open = async () => { openCalls++; return { ok: true, value: { url: 'http://127.0.0.1:40000' } } }
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
openGitMenu(renderer)
assert.equal(openCalls, 0)
assert.match(visibleText(renderer), /list request failed/)
})
test('a failed open request reports the error and closes the popup', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
const editorWindow = makeEditorWindow()
window.openImpl = () => editorWindow
const list = async () => ({ ok: true, value: { entries: [trackedEntry()] } })
const open = async () => ({ ok: false, error: { message: 'editor is unavailable' } })
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
openGitMenu(renderer)
gitOpenButton(renderer).props.onClick()
await flush()
assert.equal(editorWindow.closeCount, 1)
assert.equal(editorWindow.location.href, 'about:blank')
assert.match(visibleText(renderer), /editor is unavailable/)
})
test('unmounting stops the polling interval', async () => {
const { GitWorkingDirsMenu, renderer, window } = loadClient()
const list = async () => ({ ok: true, value: { entries: [trackedEntry()] } })
const open = async () => ({ ok: true, value: { url: 'http://127.0.0.1:40000' } })
renderer.mount(GitWorkingDirsMenu, { list, open })
await flush()
assert.equal(window.intervals.length, 1)
renderer.unmount()
assert.equal(window.intervals.length, 0)
})
function mutationBlock(overrides = {}) {
return {
kind: 'tool-result',
callId: 'mutation-1',
call: { name: 'write', argsRaw: JSON.stringify({ file_path: '/workspace/file.txt' }) },
isError: false,
content: [{ type: 'text', text: 'File updated.\nWARNING: model-visible note' }],
resultView: { card: 'diff', diffs: [
{ path: '/workspace/file.txt', oldText: 'before', newText: Array.from({ length: 45 }, (_, i) => `after ${i}`).join('\n') },
{ path: '/workspace/file.txt', oldText: 'other context', newText: 'other updated context' },
] },
...overrides,
}
}
function mutationMetadata(overrides = {}) {
return { filePath: '/workspace/file.txt', repository: '/workspace', workspaceDirectory: '/workspace', canOpen: true, warning: null, ...overrides }
}
function mutationProps(overrides = {}) {
return {
callId: 'mutation-1',
toolName: 'write',
block: mutationBlock(),
cwd: '/workspace',
home: '/home/user',
mutation: async () => ({ ok: true, value: mutationMetadata() }),
openMutation: async () => ({ ok: true, value: { delivery: 'new', directory: '/workspace', filePath: '/workspace/file.txt', url: 'http://127.0.0.1:40000/?file=file.txt' } }),
openFile: () => {},
...overrides,
}
}
function openFileButton(renderer) {
return buttons(renderer).find(node => node.props.children === 'Open in VS Code' || node.props.children === 'Opening…')
}
function nativeOpenButton(renderer) {
return buttons(renderer).find(node => node.props.children === 'Open')
}
test('registers the working-directory menu as the final header action and removes the Git tab', async () => {
const { registrations, fetchCalls } = loadClient()
assert.deepEqual(registrations.map(entry => [entry.config.name, entry.config.key]), [
['conversation.session.header.actions', undefined], ['tool.call.toolview', 'write'], ['tool.call.toolview', 'edit'],
])
assert.equal(registrations[0].config.id, 'touched-git-working-dirs')
assert.equal(registrations[0].config.order, 10000)
assert.equal(registrations.some(entry => entry.config.name === 'conversation.view'), false)
assert.match(clientSource, /_headerActions.*:has\(\.dsh-git-working-dirs\)\{flex:1\}/)
assert.match(clientSource, /\.dsh-git-working-dirs\{position:relative;margin-left:auto/)
assert.match(clientSource, /\.dsh-git-working-dirs__menu\{[^}]*right:0/)
assert.equal(registrations[1].component, registrations[2].component)
for (const registration of registrations.slice(1)) {
const api = registration.config.inject('session /1')
assert.equal(fetchCalls.length % 3, 0)
await api.mutation('call /1')
await api.openMutation('call /1', false)
await api.openMutation('call /1', true)
const requests = fetchCalls.slice(-3)
assert.equal(requests[0].path, '/touched-git/mutation?sessionId=session%20%2F1&callId=call%20%2F1')
assert.equal(requests[0].options, undefined)
assert.equal(requests[1].path, '/touched-git/editor/open-file?sessionId=session%20%2F1&callId=call%20%2F1')
assert.equal(requests[2].path, `${requests[1].path}&requireExisting=true`)
assert.equal(requests[2].options.method, 'POST')
assert.equal(requests[2].options.headers['x-dsh-editor-action'], 'open')
}
})
for (const toolName of ['write', 'edit']) {
test(`${toolName} defaults all result diffs to expanded and never launches during metadata preload`, async () => {
const { MutationToolCard, renderer, window, DiffViewer } = loadClient()
let metadataCalls = 0
let openCalls = 0
const props = mutationProps({ toolName,
mutation: async callId => { assert.equal(callId, 'mutation-1'); metadataCalls++; return { ok: true, value: mutationMetadata() } },
openMutation: async () => { openCalls++; return { ok: true, value: {} } },
})
renderer.mount(MutationToolCard, props)
await flush()
const diffs = findAll(renderer.tree(), node => node.type === DiffViewer)
assert.equal(diffs.length, 1)
assert.equal(diffs[0].props.maxLines, Infinity)
assert.equal(diffs[0].props.viewMode, 'split')
assert.equal(diffs[0].props.diffs, props.block.resultView.diffs)
assert.equal(diffs[0].props.diffs.length, 2)
assert.match(diffs[0].props.diffs[0].newText, /after 44$/)
assert.equal(buttons(renderer).find(node => node.props.children === 'Collapse diff').props['aria-expanded'], true)
assert.equal(renderer.tree().props['data-state'], 'ok')
assert.match(visibleText(renderer), /Success/)
assert.match(visibleText(renderer), /WARNING: model-visible note/)
assert.equal(openFileButton(renderer).props.disabled, false)
assert.equal(metadataCalls, 1)
assert.equal(openCalls, 0)
assert.equal(window.openCalls.length, 0)
})
}
test('native Open follows Open in VS Code and forwards the recorded path to the host', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const paths = []
let editorCalls = 0
renderer.mount(MutationToolCard, mutationProps({
openFile: path => paths.push(path),
openMutation: async () => { editorCalls++; return { ok: true, value: {} } },
}))
await flush()
const allButtons = buttons(renderer)
const editorIndex = allButtons.indexOf(openFileButton(renderer))
assert.equal(allButtons[editorIndex + 1], nativeOpenButton(renderer))
assert.equal(nativeOpenButton(renderer).props.disabled, false)
nativeOpenButton(renderer).props.onClick()
assert.deepEqual(paths, ['/workspace/file.txt'])
assert.equal(editorCalls, 0)
assert.equal(window.openCalls.length, 0)
})
test('running mutation shows proposed diff and disabled file-opening buttons without metadata requests', async () => {
const { MutationToolCard, renderer, window, DiffViewer } = loadClient()
let metadataCalls = 0
const intended = [{ path: '/workspace/file.txt', oldText: 'old', newText: 'new' }]
renderer.mount(MutationToolCard, mutationProps({
block: { name: 'edit', argsRaw: '{}', callView: { card: 'diff', diffs: intended } },
mutation: async () => { metadataCalls++; return { ok: true, value: {} } },
}))
await flush()
assert.equal(renderer.tree().props['data-state'], 'running')
assert.equal(findAll(renderer.tree(), node => node.type === DiffViewer)[0].props.diffs, intended)
assert.match(visibleText(renderer), /Proposed changes/)
assert.doesNotMatch(visibleText(renderer), /Applied changes/)
assert.equal(openFileButton(renderer).props.disabled, true)
assert.equal(nativeOpenButton(renderer).props.disabled, true)
openFileButton(renderer).props.onClick()
await flush()
assert.equal(window.openCalls.length, 0)
assert.equal(metadataCalls, 0)
})
test('settled errors retain error text without presenting attempted diffs as applied', async () => {
const { MutationToolCard, renderer, DiffViewer, window } = loadClient()
let opens = 0
renderer.mount(MutationToolCard, mutationProps({
block: mutationBlock({ isError: true, content: [{ type: 'text', text: 'Error: write blocked; unregistered repository' }] }),
openMutation: async () => { opens++; return { ok: true, value: {} } },
}))
await flush()
assert.equal(renderer.tree().props['data-state'], 'error')
assert.match(visibleText(renderer), /Error: write blocked; unregistered repository/)
assert.doesNotMatch(visibleText(renderer), /Applied changes/)
assert.equal(findAll(renderer.tree(), node => node.type === DiffViewer).length, 0)
assert.equal(openFileButton(renderer).props.disabled, true)
assert.equal(nativeOpenButton(renderer).props.disabled, true)
openFileButton(renderer).props.onClick()
await flush()
assert.equal(opens, 0)
assert.equal(window.openCalls.length, 0)
})
test('historical calls without successful mutation records cannot invent editor provenance', async () => {
const { MutationToolCard, renderer, window } = loadClient()
renderer.mount(MutationToolCard, mutationProps({ mutation: async () => ({ ok: true, value: { canOpen: false } }) }))
await flush()
assert.equal(openFileButton(renderer).props.disabled, true)
assert.match(visibleText(renderer), /No successful mutation record.*Historical calls/)
openFileButton(renderer).props.onClick()
await flush()
assert.equal(window.openCalls.length, 0)
})
test('a new delivery navigates only the reserved blank and a later click never reloads it', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const editorWindow = makeEditorWindow()
window.openImpl = () => editorWindow
const requests = []
renderer.mount(MutationToolCard, mutationProps({ openMutation: async (callId, requireExisting) => {
requests.push({ callId, requireExisting, focusCount: editorWindow.focusCount })
return { ok: true, value: requireExisting
? { delivery: 'existing', directory: '/workspace', filePath: '/workspace/file.txt' }
: { delivery: 'new', directory: '/workspace', filePath: '/workspace/file.txt', url: 'http://127.0.0.1:40000/?file=file.txt' } }
} }))
await flush()
openFileButton(renderer).props.onClick()
assert.equal(window.openCalls[0].url, 'about:blank')
await flush()
const firstLocation = editorWindow.location.href
openFileButton(renderer).props.onClick()
assert.equal(editorWindow.focusCount, 1)
await flush()
assert.deepEqual(requests, [
{ callId: 'mutation-1', requireExisting: false, focusCount: 0 },
{ callId: 'mutation-1', requireExisting: true, focusCount: 1 },
])
assert.equal(editorWindow.location.href, firstLocation)
assert.equal(editorWindow.closeCount, 0)
assert.equal(window.openCalls.length, 1)
assert.match(visibleText(renderer), /without reloading/)
})
test('popup failure never sends a mutation open POST', async () => {
const { MutationToolCard, renderer, window } = loadClient()
let opens = 0
renderer.mount(MutationToolCard, mutationProps({ openMutation: async () => { opens++; return { ok: true, value: {} } } }))
await flush()
openFileButton(renderer).props.onClick()
await flush()
assert.equal(window.openCalls.length, 1)
assert.equal(opens, 0)
assert.match(visibleText(renderer), /No editor request was sent/)
})
test('existing delivery after losing browser references closes only the reserved blank', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const blank = makeEditorWindow()
window.openImpl = () => blank
renderer.mount(MutationToolCard, mutationProps({ openMutation: async () => ({ ok: true, value: { delivery: 'existing', directory: '/workspace', filePath: '/workspace/file.txt' } }) }))
await flush()
openFileButton(renderer).props.onClick()
await flush()
assert.equal(blank.closeCount, 1)
assert.equal(blank.location.href, 'about:blank')
assert.match(visibleText(renderer), /existing VS Code workspace.*no longer has a window reference/)
})
test('existing-window request failures leave that editor window open and unchanged', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const editorWindow = makeEditorWindow()
window.openImpl = () => editorWindow
renderer.mount(MutationToolCard, mutationProps({ openMutation: async (callId, requireExisting) => requireExisting
? { ok: false, error: { message: 'Editor socket unavailable' } }
: { ok: true, value: { delivery: 'new', directory: '/workspace', filePath: '/workspace/file.txt', url: 'http://127.0.0.1:40000/' } },
}))
await flush()
openFileButton(renderer).props.onClick()
await flush()
const originalLocation = editorWindow.location.href
openFileButton(renderer).props.onClick()
await flush()
assert.equal(editorWindow.closeCount, 0)
assert.equal(editorWindow.location.href, originalLocation)
assert.equal(window.openCalls.length, 1)
assert.match(visibleText(renderer), /Editor socket unavailable/)
})
test('non-Git recorded edits remain openable and show their warning', async () => {
const { MutationToolCard, renderer, window } = loadClient()
window.openImpl = () => makeEditorWindow()
let openCalls = 0
renderer.mount(MutationToolCard, mutationProps({ toolName: 'edit',
mutation: async () => ({ ok: true, value: mutationMetadata({ repository: null, workspaceDirectory: '/workspace', warning: 'WARNING: outside Git; parent-folder workspace' }) }),
openMutation: async () => { openCalls++; return { ok: true, value: { delivery: 'existing', directory: '/workspace', filePath: '/workspace/file.txt' } } },
}))
await flush()
assert.equal(openFileButton(renderer).props.disabled, false)
assert.match(visibleText(renderer), /outside Git; parent-folder workspace/)
openFileButton(renderer).props.onClick()
await flush()
assert.equal(openCalls, 1)
})
test('non-local new delivery is refused and closes only its blank popup', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const blank = makeEditorWindow()
window.openImpl = () => blank
renderer.mount(MutationToolCard, mutationProps({ openMutation: async () => ({ ok: true, value: { delivery: 'new', directory: '/workspace', url: 'https://untrusted.example/editor' } }) }))
await flush()
openFileButton(renderer).props.onClick()
await flush()
assert.equal(blank.location.href, 'about:blank')
assert.equal(blank.closeCount, 1)
assert.match(visibleText(renderer), /unexpected non-local URL/)
})
test('Git-menu workspace references are reused by mutation file buttons', async () => {
const { GitWorkingDirsMenu, MutationToolCard, renderer, window } = loadClient()
const existing = makeEditorWindow()
window.openImpl = () => existing
renderer.mount(GitWorkingDirsMenu, {
list: async () => ({ ok: true, value: { entries: [trackedEntry({ workingDirectory: '/workspace' })] } }),
open: async () => ({ ok: true, value: { url: 'http://127.0.0.1:40000/' } }),
})
await flush()
openGitMenu(renderer)
gitOpenButton(renderer).props.onClick()
await flush()
renderer.unmount()
const calls = []
renderer.mount(MutationToolCard, mutationProps({ openMutation: async (callId, requireExisting) => {
calls.push(requireExisting)
return { ok: true, value: { delivery: 'existing', directory: '/workspace', filePath: '/workspace/file.txt' } }
} }))
await flush()
openFileButton(renderer).props.onClick()
assert.equal(existing.focusCount, 1)
await flush()
assert.deepEqual(calls, [true])
assert.equal(window.openCalls.length, 1)
assert.equal(existing.location.href, 'http://127.0.0.1:40000/')
})
test('a running call preloads metadata once it settles and uses only authoritative result diffs', async () => {
const { MutationToolCard, renderer, DiffViewer } = loadClient()
let calls = 0
const props = mutationProps({ mutation: async () => { calls++; return { ok: true, value: mutationMetadata() } } })
renderer.mount(MutationToolCard, { ...props, block: { name: 'write', callView: { card: 'diff', diffs: [{ path: '/attempted', oldText: null, newText: 'attempt' }] } } })
await flush()
assert.equal(calls, 0)
renderer.update(props)
await flush()
assert.equal(calls, 1)
assert.equal(findAll(renderer.tree(), node => node.type === DiffViewer)[0].props.diffs, props.block.resultView.diffs)
assert.equal(openFileButton(renderer).props.disabled, false)
})
test('interrupted and malformed settled calls do not render an attempted applied diff', async () => {
const { MutationToolCard, renderer, DiffViewer } = loadClient()
const props = mutationProps({ block: mutationBlock({ error: { name: 'AbortError', code: 'interrupted' }, content: [], isError: true }) })
renderer.mount(MutationToolCard, props)
await flush()
assert.equal(renderer.tree().props['data-state'], 'stopped')
assert.match(visibleText(renderer), /Interrupted.*AbortError: interrupted/)
assert.equal(openFileButton(renderer).props.disabled, true)
assert.equal(findAll(renderer.tree(), node => node.type === DiffViewer).length, 0)
renderer.update({ ...props, block: mutationBlock({ resultView: { card: 'diff', diffs: [{ path: '/file', oldText: 12, newText: 'new' }] } }) })
await flush()
assert.equal(findAll(renderer.tree(), node => node.type === DiffViewer).length, 0)
assert.doesNotMatch(visibleText(renderer), /Applied changes/)
})
test('an unexpected new delivery never navigates or closes a remembered editor', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const existing = makeEditorWindow()
window.openImpl = () => existing
renderer.mount(MutationToolCard, mutationProps())
await flush()
openFileButton(renderer).props.onClick()
await flush()
const location = existing.location.href
openFileButton(renderer).props.onClick()
await flush()
assert.equal(existing.closeCount, 0)
assert.equal(existing.location.href, location)
assert.equal(window.openCalls.length, 1)
assert.match(visibleText(renderer), /window was left unchanged/)
})
test('a browser focus failure is visible without disturbing the existing editor', async () => {
const { MutationToolCard, renderer, window } = loadClient()
const existing = makeEditorWindow()
window.openImpl = () => existing
let calls = 0
const props = mutationProps()
renderer.mount(MutationToolCard, { ...props, openMutation: (...args) => { calls++; return props.openMutation(...args) } })
await flush()
openFileButton(renderer).props.onClick()
await flush()
existing.focus = () => { throw new Error('Browser refused focus') }
openFileButton(renderer).props.onClick()
await flush()
assert.equal(calls, 1)
assert.equal(existing.closeCount, 0)
assert.equal(window.openCalls.length, 1)
assert.match(visibleText(renderer), /could not focus.*left unchanged/)
})
test('metadata errors disable opening and remain visible', async () => {
const { MutationToolCard, renderer, window } = loadClient()
renderer.mount(MutationToolCard, mutationProps({ mutation: async () => ({ ok: false, error: { message: 'No mutation record exists for this call' } }) }))
await flush()
assert.equal(openFileButton(renderer).props.disabled, true)
assert.match(visibleText(renderer), /No mutation record exists/)
assert.equal(window.openCalls.length, 0)
})