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.
160 lines
7.1 KiB
JavaScript
160 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { execFile } from 'node:child_process'
|
|
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import test from 'node:test'
|
|
import { promisify } from 'node:util'
|
|
import { MutationStore } from '../lib/mutation-store.js'
|
|
import { createMutationHandlers } from '../lib/mutation-routes.js'
|
|
|
|
const execute = promisify(execFile)
|
|
|
|
async function fixture(context, { inGit = true } = {}) {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-mutation-test-'))
|
|
context.after(() => rm(root, { recursive: true, force: true }))
|
|
const workspace = join(root, 'workspace')
|
|
await mkdir(workspace)
|
|
if (inGit) await execute('git', ['-C', workspace, 'init', '--initial-branch=main'])
|
|
const filePath = join(workspace, 'file #雪.txt')
|
|
await writeFile(filePath, 'original content')
|
|
const mutations = new MutationStore(join(root, 'records.json'))
|
|
await mutations.record({ sessionId: 's1', callId: 'c1', toolName: 'write', filePath, repository: inGit ? workspace : null, warning: inGit ? null : 'WARNING: outside Git' })
|
|
const entries = inGit ? [{ ok: true, repository: workspace, workingDirectory: workspace }] : []
|
|
const opened = []
|
|
const store = { list: async () => entries }
|
|
const handlers = createMutationHandlers({
|
|
mutations,
|
|
store,
|
|
editor: { openFile: async (...argumentsList) => { opened.push(argumentsList); return { delivery: 'existing', directory: argumentsList[0] } } },
|
|
resolveDirectoryRepository: async directory => ({ repository: workspace, workingDirectory: directory }),
|
|
isLoopbackRequest: request => request.headers.origin !== 'https://outside.invalid',
|
|
writeJson: (response, status, body) => { response.status = status; response.body = body },
|
|
})
|
|
return { root, workspace, filePath, mutations, entries, opened, ...handlers }
|
|
}
|
|
|
|
function request(method = 'POST', query = '') {
|
|
return {
|
|
method,
|
|
url: `/touched-git/editor/open-file?sessionId=s1&callId=c1${query}`,
|
|
headers: { host: '127.0.0.1:3080', 'x-dsh-editor-action': 'open' },
|
|
}
|
|
}
|
|
|
|
function response() {
|
|
return { headers: {}, setHeader(name, value) { this.headers[name] = value } }
|
|
}
|
|
|
|
test('describing a recorded mutation never launches an editor', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const result = response()
|
|
await fixtureData.describeHandler(request('GET'), result)
|
|
assert.equal(result.status, 200)
|
|
assert.equal(result.body.value.canOpen, true)
|
|
assert.equal(result.body.value.filePath, fixtureData.filePath)
|
|
assert.equal(result.headers['cache-control'], 'no-store')
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('explicit open uses the recorded physical file and forwards existing-workspace safety', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const result = response()
|
|
await fixtureData.openFileHandler(request('POST', '&requireExisting=true'), result)
|
|
assert.equal(result.status, 200)
|
|
assert.deepEqual(fixtureData.opened, [[fixtureData.workspace, fixtureData.filePath, { requireExisting: true }]])
|
|
assert.equal(result.body.value.delivery, 'existing')
|
|
assert.equal(await readFile(fixtureData.filePath, 'utf8'), 'original content')
|
|
})
|
|
|
|
test('GET, cross-origin, and missing action header cannot open editors', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const cases = [
|
|
[request('GET'), 405],
|
|
[{ ...request(), headers: { ...request().headers, origin: 'https://outside.invalid' } }, 403],
|
|
[{ ...request(), headers: { host: '127.0.0.1:3080' } }, 403],
|
|
]
|
|
for (const [input, status] of cases) {
|
|
const result = response()
|
|
await fixtureData.openFileHandler(input, result)
|
|
assert.equal(result.status, status)
|
|
}
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('unknown and other-session calls cannot supply arbitrary target paths', async context => {
|
|
const fixtureData = await fixture(context)
|
|
for (const url of ['/route?sessionId=s1&callId=unknown&path=/etc/passwd', '/route?sessionId=s2&callId=c1']) {
|
|
const result = response()
|
|
await fixtureData.openFileHandler({ ...request(), url }, result)
|
|
assert.equal(result.status, 403)
|
|
}
|
|
const description = response()
|
|
await fixtureData.describeHandler({ ...request('GET'), url: '/route?sessionId=s1&callId=old' }, description)
|
|
assert.equal(description.body.value.canOpen, false)
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('unregistering a repository disables its recorded file action', async context => {
|
|
const fixtureData = await fixture(context)
|
|
fixtureData.entries.length = 0
|
|
const result = response()
|
|
await fixtureData.openFileHandler(request(), result)
|
|
assert.equal(result.status, 403)
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('outside-Git files open only on click in their parent folder and retain warning', async context => {
|
|
const fixtureData = await fixture(context, { inGit: false })
|
|
const description = response()
|
|
await fixtureData.describeHandler(request('GET'), description)
|
|
assert.equal(description.body.value.canOpen, true)
|
|
assert.match(description.body.value.warning, /outside Git/)
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
const result = response()
|
|
await fixtureData.openFileHandler(request(), result)
|
|
assert.equal(result.status, 200)
|
|
assert.equal(fixtureData.opened[0][0], dirname(fixtureData.filePath))
|
|
assert.match(result.body.value.warning, /outside Git/)
|
|
assert.deepEqual(fixtureData.entries, [])
|
|
})
|
|
|
|
test('changed symlink targets cannot redirect recorded file actions', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const other = join(fixtureData.root, 'other.txt')
|
|
await writeFile(other, 'unrelated')
|
|
await rm(fixtureData.filePath)
|
|
await symlink(other, fixtureData.filePath)
|
|
const result = response()
|
|
await fixtureData.openFileHandler(request(), result)
|
|
assert.equal(result.status, 503)
|
|
assert.match(result.body.error.message, /symlink/)
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('a deleted recorded file produces an error rather than creating an editor', async context => {
|
|
const fixtureData = await fixture(context)
|
|
await rm(fixtureData.filePath)
|
|
const result = response()
|
|
await fixtureData.openFileHandler(request(), result)
|
|
assert.equal(result.status, 503)
|
|
assert.deepEqual(fixtureData.opened, [])
|
|
})
|
|
|
|
test('mutation records survive reload and remain isolated by session and call', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const reloaded = new MutationStore(join(fixtureData.root, 'records.json'))
|
|
assert.equal((await reloaded.get('s1', 'c1')).filePath, fixtureData.filePath)
|
|
assert.equal(await reloaded.get('s2', 'c1'), null)
|
|
assert.equal(await reloaded.get('s1', 'c2'), null)
|
|
})
|
|
|
|
test('corrupt mutation records are surfaced and not silently overwritten', async context => {
|
|
const fixtureData = await fixture(context)
|
|
const path = join(fixtureData.root, 'corrupt.json')
|
|
await writeFile(path, 'broken JSON')
|
|
const store = new MutationStore(path)
|
|
await assert.rejects(store.get('s1', 'c1'))
|
|
await assert.rejects(store.record({ sessionId: 's1', callId: 'c1', filePath: fixtureData.filePath }))
|
|
assert.equal(await readFile(path, 'utf8'), 'broken JSON')
|
|
})
|