fix(workspace): keep the canvas link on the proxy mount

A resolver failure no longer stops the shell from mounting: the ComfyUI link falls back to the root read from the page's own mount path (never another origin). Tests cover a non-standard /ComfyBackendDirect mount, including that no request escapes it.
This commit is contained in:
Vito Sansevero
2026-10-19 22:48:39 -07:00
parent c858329f7d
commit 020a8df1e0
5 changed files with 100 additions and 4 deletions
+52
View File
@@ -630,3 +630,55 @@ test('the footer is the concept app footer with production copy', async ({ page
}
});
// #164: the maintainer serves ComfyUI behind a proxy prefix. Everything, the ComfyUI link included, stays on that mount.
const PROXY_MOUNT = '/ComfyBackendDirect';
async function serveUnderProxyMount(page) {
const escaped = [];
page.on('request', item => {
const { pathname } = new URL(item.url());
if (pathname.startsWith('/prompt_manager/')) escaped.push(pathname);
});
await page.route(url => url.pathname.startsWith(`${PROXY_MOUNT}/`), async route => {
const url = new URL(route.request().url());
url.pathname = url.pathname.slice(PROXY_MOUNT.length);
const response = await route.fetch({ url: url.href });
await route.fulfill({ response });
});
return escaped;
}
test('the ComfyUI link points to the root of a non-standard proxy mount', async ({ page }) => {
const escaped = await serveUnderProxyMount(page);
for (const route of ['overview', 'library', 'tags']) {
await page.goto(`${PROXY_MOUNT}${workspace}#/${route}`);
await expect(page.locator('#workspace-main > .heading h1')).toBeVisible();
const link = page.getByRole('link', { name: 'ComfyUI', exact: true });
await expect(link).toHaveAttribute('href', `${PROXY_MOUNT}/`);
expect(await link.evaluate(node => node.href)).toBe(new URL(`${PROXY_MOUNT}/`, page.url()).href);
}
await expect(navCount(page, 'library')).toHaveCount(1);
expect(escaped).toEqual([]);
});
test('a resolver failure for the ComfyUI link never stops the shell from mounting', async ({ page }) => {
await serveUnderProxyMount(page);
// Wrap the shared client as it loads so only the topbar's canvas-root lookup throws (the sender reads the same root).
await page.addInitScript(() => {
let client;
Object.defineProperty(window, 'ApiClient', { configurable: true, get: () => client, set(value) {
client = { ...value, resolveUrl: (url, ...rest) => {
if (url === '/prompt_manager/' && /topbar\.mjs/.test(new Error().stack)) throw new Error('resolver unavailable');
return value.resolveUrl(url, ...rest);
} };
} });
});
const warnings = [];
page.on('console', message => { if (message.type() === 'warning') warnings.push(message.text()); });
await page.goto(`${PROXY_MOUNT}${workspace}#/overview`);
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
await expect(sidebarLinks(page)).toHaveCount(routes.length);
expect(warnings.filter(text => text.includes('the ComfyUI link falls back to the page mount'))).toHaveLength(1);
await expect(page.getByRole('link', { name: 'ComfyUI', exact: true })).toHaveAttribute('href', `${PROXY_MOUNT}/`);
await sidebarLink(page, 'Prompt library').click();
await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library');
});
+28 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { canvasRoot, bindCanvasLink } from '../../web/js/workspace/topbar.mjs';
import { canvasRoot, canvasHref, bindCanvasLink } from '../../web/js/workspace/topbar.mjs';
// The shared mount-aware resolver (#164), as api-client.js implements it.
const resolverAt = pathname => url => {
@@ -27,6 +27,33 @@ test('the canvas root is the ComfyUI root under the page mount', () => {
assert.throws(() => canvasRoot(() => 'https://elsewhere.example/'), /canvas/i);
});
test('the canvas root follows a non-standard proxy mount', () => {
assert.equal(canvasRoot(resolverAt('/ComfyBackendDirect/prompt_manager/workspace')), '/ComfyBackendDirect/');
assert.equal(canvasHref(resolverAt('/ComfyBackendDirect/prompt_manager/workspace'), '/ComfyBackendDirect/prompt_manager/workspace'),
'/ComfyBackendDirect/');
});
// A4 review: a resolver failure must never stop the shell from mounting, and the fallback stays on the page's mount.
test('a resolver failure falls back to the page mount and never to another origin', () => {
const warn = console.warn;
const warnings = [];
console.warn = (...args) => warnings.push(args);
try {
const broken = () => { throw new Error('resolver unavailable'); };
assert.equal(canvasHref(broken, '/ComfyBackendDirect/prompt_manager/workspace'), '/ComfyBackendDirect/');
assert.equal(canvasHref(broken, '/ComfyBackendDirect/prompt_manager/workspace/'), '/ComfyBackendDirect/');
assert.equal(canvasHref(broken, '/prompt_manager/workspace'), '/');
assert.equal(canvasHref(() => 'https://elsewhere.example/prompt_manager/', '/a/prompt_manager/workspace'), '/a/');
assert.equal(canvasHref(broken, '//elsewhere.example/prompt_manager/workspace'), '/');
assert.equal(canvasHref(broken, '/workspace'), '/');
assert.equal(canvasHref(broken, undefined), '/');
assert.equal(canvasHref(undefined, '/a/prompt_manager/workspace'), '/a/');
assert.equal(warnings.length, 8);
} finally {
console.warn = warn;
}
});
test('ComfyUI link follows its href unless a resolved pair is selected', () => {
const node = link();
const opened = [];
+1 -1
View File
@@ -302,7 +302,7 @@
"workspace/topbar.mjs": {
"lines": 98,
"branches": 98,
"functions": 81,
"functions": 83,
"statements": 98
},
"workspace/viewer-state.mjs": {
+2 -2
View File
@@ -6,7 +6,7 @@ import { mountImages } from './images.mjs';
import { mountLibrary, mountNewPromptAction, onLibraryWrite } from './library.mjs';
import { mountVocabulary } from './vocabulary.mjs';
import { createWorkspaceIcon } from './icons.mjs';
import { bindCanvasLink, canvasRoot } from './topbar.mjs';
import { bindCanvasLink, canvasHref } from './topbar.mjs';
import { renderHeading } from './heading.mjs';
const { createApiClient, resolveUrl } = window.ApiClient;
@@ -42,7 +42,7 @@ function mountWorkspace() {
// D6: ComfyUI ↗ opens the canvas; on Images with a resolved selection it sends that pair (D5: no Activity).
let images = null;
scope.cleanup(bindCanvasLink(document.querySelector('.topbar .canvas-link'), {
href: canvasRoot(resolveUrl),
href: canvasHref(resolveUrl, window.location.pathname),
getSelectedPair: () => images?.selectedPair?.() ?? null,
sender: {open: (roles, trigger) => images?.sender.open(roles, trigger)},
}));
+17
View File
@@ -11,6 +11,23 @@ export function canvasRoot(resolve) {
return root.replace(PROMPT_MANAGER, '');
}
const PAGE_MOUNT = /^(.*\/)prompt_manager(?:\/|$)/;
/**
* The canvas link's href. A resolver failure must never stop the shell from mounting, so it falls back to the
* ComfyUI root read from the page's own path (the #164 proxy mount), and to the origin root when even that is not a
* same-origin root-relative path.
*/
export function canvasHref(resolve, pathname) {
try {
return canvasRoot(resolve);
} catch (error) {
console.warn('Prompt Manager: the ComfyUI link falls back to the page mount.', error);
const mount = typeof pathname === 'string' ? pathname.match(PAGE_MOUNT)?.[1] : undefined;
return mount && !mount.startsWith('//') ? mount : '/';
}
}
const plainClick = event => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
const isPair = roles => roles !== null && typeof roles === 'object' &&
typeof roles.positive === 'string' && typeof roles.negative === 'string';