DSH-Arch-Setup/plugins/dsh-touched-git/test/mutation-routes.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

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')
})