fix(links): resolve ambiguous hard wraps through guarded lookup

This commit is contained in:
Devvyn committed 2026-10-28 21:45:49 +08:00
1 parent 5ce8213726
commit 97d53f6621
10 files changed
+250 -103

No files matched your search

+55 -3
View File
@@ -241,6 +241,8 @@ function loadApp() {
app.sessions = new Map([['s1', { workingDir: '/mnt/build/neon_getaway-mgr-hebel' }]]);
app.openFilePreview = vi.fn();
app.openLogViewerWindow = vi.fn();
app._registerExternalPreview = vi.fn(async () => ({ attachmentId: 'att-fixture' }));
app.showToast = vi.fn();
app._isExternalPreviewPath = (p: string) => p.startsWith('~/');
return app;
}
@@ -257,7 +259,30 @@ function linksOn(lines: string[], row: number, cols = 80, wrappedRows = new Set<
}
describe('terminal link provider', () => {
it('retains the filename space when the CLI word-wraps a parenthesised path', () => {
it.each([
[
['Saved the screenshot (/home/me/proj/out/ghost1b_game', ' play.png).'],
'/home/me/proj/out/ghost1b_gameplay.png',
],
[
[' ⎿ Wrote (/mnt/build/neon/builds/captures/ghost1b_gamepl', ' ay.png) ok'],
'/mnt/build/neon/builds/captures/ghost1b_gameplay.png',
],
[
['Saved the screenshot (/home/me/proj/out/ghost1b_', ' very_long_game', ' play.png).'],
'/home/me/proj/out/ghost1b_very_long_gameplay.png',
],
])('preserves a parenthesised filename without spaces across hard wraps: %s', async (rows, path) => {
for (let row = 1; row <= rows.length; row++) {
const { app, links } = linksOn(rows, row, 120);
expect(links.map((link) => link.text)).toEqual([path]);
await links[0].activate({}, links[0].text);
expect(app.openFilePreview).toHaveBeenCalledWith(path, 's1');
expect(app._registerExternalPreview.mock.calls).toEqual([[path, 's1']]);
}
});
it('uses the spaced filename only when the unspaced hard-wrap target is missing', async () => {
const path = '/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo eReader.conf';
const rows = [
'Found it: I prepared a corrected configuration (/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo',
@@ -265,13 +290,40 @@ describe('terminal link provider', () => {
];
for (const row of [1, 2]) {
const { app, links } = linksOn(rows, row, 120);
expect(links.map((link) => link.text)).toEqual([path]);
const unspaced = path.replace('Kobo eReader', 'KoboeReader');
expect(links.map((link) => link.text)).toEqual([unspaced]);
expect(links[0].range.end).toEqual({ x: ' eReader.conf'.length + 1, y: 2 });
links[0].activate({}, links[0].text);
app._registerExternalPreview.mockImplementation(async (candidate: string) =>
candidate === path ? { attachmentId: 'att-fixture' } : { status: 404, error: 'File not found' }
);
await links[0].activate({}, links[0].text);
expect(app.openLogViewerWindow).toHaveBeenCalledWith(path, 's1');
expect(app._registerExternalPreview.mock.calls).toEqual([
[unspaced, 's1'],
[path, 's1'],
]);
}
});
it.each([403, 500, 503])('does not try another path after a guarded lookup returns HTTP %s', async (status) => {
const { app, links } = linksOn(['Saved (/mnt/data/Kobo', ' eReader.conf).'], 1, 120);
app._registerExternalPreview.mockResolvedValue({ status, error: 'Lookup refused' });
await links[0].activate({}, links[0].text);
expect(app._registerExternalPreview).toHaveBeenCalledTimes(1);
expect(app.openFilePreview).not.toHaveBeenCalled();
expect(app.openLogViewerWindow).not.toHaveBeenCalled();
expect(app.showToast).toHaveBeenCalledWith('Lookup refused', 'error');
});
it('keeps the session selected at click time while a path lookup is pending', async () => {
const { app, links } = linksOn(['Saved (/mnt/data/Kobo', ' eReader.conf).'], 1, 120);
const pending = links[0].activate({}, links[0].text);
app.activeSessionId = 'another-session';
await pending;
expect(app._registerExternalPreview).toHaveBeenCalledWith('/mnt/data/KoboeReader.conf', 's1');
expect(app.openLogViewerWindow).toHaveBeenCalledWith('/mnt/data/KoboeReader.conf', 's1');
});
it('opens the complete parenthesised configuration path from either wrapped row', () => {
const path = '/mnt/user/Scratch/Kobo/Set-KoboCovers/Kobo eReader.conf';
const text = `Found it: I prepared a corrected configuration (${path}).`;
+21
View File
@@ -212,6 +212,27 @@ function loadApp(fetchImpl: (url: string, init?: { body?: string }) => unknown)
return { app, elements, keyHandlers, fetch: context.fetch as ReturnType<typeof vi.fn> };
}
describe('guarded attachment registration results', () => {
it.each([404, 403, 500, 503])('preserves HTTP %s for the activation decision', async (status) => {
const { app, fetch } = loadApp(async () => ({
ok: false,
status,
json: async () => ({ error: 'Cannot read file' }),
}));
const result = await Object.getPrototypeOf(app)._registerExternalPreview.call(app, '/mnt/data/a.conf', 's1');
expect(result).toEqual({ error: 'Cannot read file', status });
expect(JSON.parse(fetch.mock.calls[0][1].body)).toEqual({ path: '/mnt/data/a.conf', notify: false });
});
it('does not turn a connection failure into a missing file', async () => {
const { app } = loadApp(async () => {
throw new Error('Connection lost');
});
const result = await Object.getPrototypeOf(app)._registerExternalPreview.call(app, '/mnt/data/a.conf', 's1');
expect(result).toEqual({ error: 'Connection lost' });
});
});
const LISTING = {
folderId: 'fld_1',
path: '/mnt/raid1/styleframes',
+4
View File
@@ -42,6 +42,10 @@ describe('spaced file paths', () => {
expect(paths('Do not link (/mnt/data/../private files/settings.conf).')).toEqual([]);
});
it('does not take a comma-separated list inside parentheses as one spaced filename', () => {
expect(paths('Updated (/home/me/proj, config.json)')).toEqual(['/home/me/proj', 'config.json']);
});
it('links an own-line absolute path with spaces whole', () => {
expect(paths(LONG)).toEqual([LONG]);
expect(paths('/mnt/c/proj/My Big Docs/report.docx')).toEqual(['/mnt/c/proj/My Big Docs/report.docx']);
+93 -60
View File
@@ -16,67 +16,100 @@ describe('wrapped configuration file links (Chromium)', () => {
await browser.close();
});
it.each(['soft', 'hard'])('opens the complete file from both rows of a %s wrap', async (wrap) => {
const page = await browser.newPage({ viewport: { width: 1920, height: 600 } });
try {
await page.setContent('<div id="terminal"></div>');
await page.addStyleTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/css/xterm.css') });
await page.addScriptTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/lib/xterm.js') });
await page.addScriptTag({ content: 'window.CodemanApp = class CodemanApp {};' });
for (const file of ['constants.js', 'terminal-ui.js']) {
await page.addScriptTag({ content: readFileSync(resolve(publicDir, file), 'utf8') });
}
await page.evaluate(
async ({ path, wrap }) => {
const w = window as any;
const first = `Found it: prepared a corrected configuration (${path.slice(0, -' eReader.conf'.length)}`;
const terminal = new w.Terminal({ cols: first.length + (wrap === 'soft' ? 1 : 20), rows: 6 });
terminal.open(document.getElementById('terminal'));
w.opened = [];
w.app = Object.assign(new w.CodemanApp(), {
terminal,
activeSessionId: 'fixture',
sessions: new Map([['fixture', { workingDir: '/workspace' }]]),
_isExternalPreviewPath: () => true,
openFilePreview: (file: string) => w.opened.push(file),
openLogViewerWindow: (file: string) => w.opened.push(`log:${file}`),
openFolderGrid: (file: string) => w.opened.push(`folder:${file}`),
});
w.app.registerFilePathLinkProvider();
const text = `${first}${wrap === 'soft' ? ' ' : '\r\n '}eReader.conf).`;
await new Promise<void>((done) => terminal.write(text, done));
await new Promise<void>((done) => requestAnimationFrame(() => requestAnimationFrame(() => done())));
},
{ path, wrap }
);
for (const row of [0, 1]) {
const point = await page.evaluate(
async ({ path, row }) => {
const app = (window as any).app;
const terminal = app.terminal;
const links = await new Promise<any[]>((done) => app._terminalLinkProvider.provideLinks(row + 1, done));
const link = links.find((candidate) => candidate.text === path);
if (!link) throw new Error(`The complete filename is not linked on row ${row + 1}`);
const text = terminal.buffer.active.getLine(row).translateToString(true);
const col = row === 0 ? link.range.start.x : text.indexOf('eReader') + 1;
const box = terminal.element.querySelector('.xterm-screen').getBoundingClientRect();
return {
x: box.left + (col - 0.5) * (box.width / terminal.cols),
y: box.top + (row + 0.5) * (box.height / terminal.rows),
};
},
{ path, row }
it.each([
{ wrap: 'soft', originalExists: false },
{ wrap: 'hard', originalExists: false },
{ wrap: 'hard', originalExists: true },
])(
'opens the existing file from both rows: $wrap, original exists: $originalExists',
async ({ wrap, originalExists }) => {
const original = path.replace('Kobo eReader', 'KoboeReader');
const expected = wrap === 'hard' && originalExists ? original : path;
const lookups: string[] = [];
const page = await browser.newPage({ viewport: { width: 1920, height: 600 } });
try {
await page.route('http://fixture.test/', (route) =>
route.fulfill({
contentType: 'text/html',
body: '<div id="terminal"></div>',
})
);
await page.mouse.move(point.x, point.y);
await page.waitForFunction(() => (window as any).app._linkHovered === true);
await page.mouse.click(point.x, point.y);
await page.waitForFunction((count) => (window as any).opened.length === count, row + 1);
expect(await page.evaluate(() => (window as any).opened)).toEqual(Array(row + 1).fill(path));
await page.mouse.move(0, 0);
await page.route('http://fixture.test/api/sessions/fixture/attachments', async (route) => {
const body = route.request().postDataJSON();
expect(body.notify).toBe(false);
lookups.push(body.path);
const exists = body.path === path || (originalExists && body.path === original);
await route.fulfill({
status: exists ? 200 : 404,
json: exists
? { success: true, data: { attachmentId: 'att-fixture' } }
: { success: false, error: 'File not found' },
});
});
await page.goto('http://fixture.test/');
await page.addStyleTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/css/xterm.css') });
await page.addScriptTag({ path: resolve(publicDir, '../../../node_modules/@xterm/xterm/lib/xterm.js') });
await page.addScriptTag({ content: 'window.CodemanApp = class CodemanApp {};' });
for (const file of ['constants.js', 'terminal-ui.js', 'panels-ui.js']) {
await page.addScriptTag({ content: readFileSync(resolve(publicDir, file), 'utf8') });
}
await page.evaluate(
async ({ path, wrap }) => {
const w = window as any;
const first = `Found it: prepared a corrected configuration (${path.slice(0, -' eReader.conf'.length)}`;
const terminal = new w.Terminal({ cols: first.length + (wrap === 'soft' ? 1 : 20), rows: 6 });
terminal.open(document.getElementById('terminal'));
w.opened = [];
w.app = Object.assign(new w.CodemanApp(), {
terminal,
activeSessionId: 'fixture',
sessions: new Map([['fixture', { workingDir: '/workspace' }]]),
_isExternalPreviewPath: () => true,
openFilePreview: (file: string) => w.opened.push(file),
openLogViewerWindow: (file: string) => w.opened.push(`log:${file}`),
openFolderGrid: (file: string) => w.opened.push(`folder:${file}`),
showToast: (message: string) => w.opened.push(`error:${message}`),
});
w.app.registerFilePathLinkProvider();
const text = `${first}${wrap === 'soft' ? ' ' : '\r\n '}eReader.conf).`;
await new Promise<void>((done) => terminal.write(text, done));
await new Promise<void>((done) => requestAnimationFrame(() => requestAnimationFrame(() => done())));
},
{ path, wrap }
);
expect(lookups).toEqual([]);
for (const row of [0, 1]) {
const point = await page.evaluate(
async ({ path, row }) => {
const app = (window as any).app;
const terminal = app.terminal;
const links = await new Promise<any[]>((done) => app._terminalLinkProvider.provideLinks(row + 1, done));
const link = links.find((candidate) => candidate.text === path);
if (!link) throw new Error(`The complete filename is not linked on row ${row + 1}`);
const text = terminal.buffer.active.getLine(row).translateToString(true);
const col = row === 0 ? link.range.start.x : text.indexOf('eReader') + 1;
const box = terminal.element.querySelector('.xterm-screen').getBoundingClientRect();
return {
x: box.left + (col - 0.5) * (box.width / terminal.cols),
y: box.top + (row + 0.5) * (box.height / terminal.rows),
};
},
{ path: wrap === 'hard' ? original : path, row }
);
await page.mouse.move(point.x, point.y);
await page.waitForFunction(() => (window as any).app._linkHovered === true);
await page.mouse.click(point.x, point.y);
await page.waitForFunction((count) => (window as any).opened.length === count, row + 1);
expect(await page.evaluate(() => (window as any).opened)).toEqual(Array(row + 1).fill(expected));
await page.mouse.move(0, 0);
}
expect(lookups).toEqual(
wrap === 'soft' ? [] : originalExists ? [original, original] : [original, path, original, path]
);
} finally {
await page.close();
}
} finally {
await page.close();
}
});
);
});