Files
obelisk/tests/app-file-reference.test.mjs
T

82 lines
3.6 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
buildEditorUrl,
fileReferenceCandidates,
normalizeRoots,
resolveFileReference,
} from '../app/src/main/file-reference.ts';
function tempProject() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'obelisk-file-ref-'));
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
fs.writeFileSync(path.join(root, 'src', 'app.ts'), 'export const a = 1;\n');
return fs.realpathSync(root);
}
test('normalizeRoots keeps absolute paths and drops the rest', () => {
assert.deepEqual(normalizeRoots(['/tmp/a', 'relative', '', null, undefined, '/tmp/a']), ['/tmp/a']);
});
test('candidates cover absolute, cwd-relative and project-relative shapes', () => {
const candidates = fileReferenceCandidates({ rawPath: 'src/app.ts', cwd: '/tmp/proj' });
assert.ok(candidates.includes(path.join('/tmp/proj', 'src/app.ts')));
// A leading slash in transcripts usually means "project root", not filesystem root.
const rooted = fileReferenceCandidates({ rawPath: '/src/app.ts', cwd: '/tmp/proj' });
assert.ok(rooted.includes('/src/app.ts'));
assert.ok(rooted.includes(path.join('/tmp/proj', 'src/app.ts')));
});
test('resolves a relative reference against the message cwd', () => {
const root = tempProject();
assert.equal(
resolveFileReference({ rawPath: 'src/app.ts', cwd: root, roots: [] }),
path.join(root, 'src', 'app.ts'),
);
});
test('refuses paths outside the session roots', () => {
const root = tempProject();
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'obelisk-outside-'));
fs.writeFileSync(path.join(outside, 'secret.txt'), 'nope\n');
assert.equal(resolveFileReference({ rawPath: path.join(outside, 'secret.txt'), cwd: root }), null);
assert.equal(resolveFileReference({ rawPath: '../../etc/hosts', cwd: root }), null);
});
test('refuses a symlink that escapes the session roots', () => {
const root = tempProject();
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'obelisk-outside-'));
const secret = path.join(outside, 'secret.txt');
fs.writeFileSync(secret, 'nope\n');
fs.symlinkSync(secret, path.join(root, 'escape.txt'));
assert.equal(resolveFileReference({ rawPath: 'escape.txt', cwd: root }), null);
});
test('refuses directories, missing files and rootless queries', () => {
const root = tempProject();
assert.equal(resolveFileReference({ rawPath: 'src', cwd: root }), null);
assert.equal(resolveFileReference({ rawPath: 'src/nope.ts', cwd: root }), null);
assert.equal(resolveFileReference({ rawPath: 'src/app.ts', cwd: null, roots: [] }), null);
});
test('builds editor URLs with line and column', () => {
assert.equal(buildEditorUrl({ filePath: '/p/a.ts', line: 42 }), 'vscode://file/p/a.ts:42');
assert.equal(buildEditorUrl({ filePath: '/p/a.ts', line: 42, column: 7 }), 'vscode://file/p/a.ts:42:7');
assert.equal(buildEditorUrl({ filePath: '/p/a.ts' }), 'vscode://file/p/a.ts');
assert.equal(buildEditorUrl({ scheme: 'cursor', filePath: '/p/a.ts', line: 3 }), 'cursor://file/p/a.ts:3');
});
test('falls back to the default scheme and encodes spaces', () => {
assert.equal(buildEditorUrl({ scheme: 'evil:', filePath: '/p/a.ts' }), 'vscode://file/p/a.ts');
assert.equal(buildEditorUrl({ filePath: '/p/a b.ts', line: 1 }), 'vscode://file/p/a%20b.ts:1');
});
test('ignores a zero or negative line number', () => {
assert.equal(buildEditorUrl({ filePath: '/p/a.ts', line: 0 }), 'vscode://file/p/a.ts');
assert.equal(buildEditorUrl({ filePath: '/p/a.ts', line: -3 }), 'vscode://file/p/a.ts');
});