qwen-code/packages/web-shell/client/utils/splitUrl.test.ts
ytahdn 2ab3681157
fix(web-shell): preserve token and base path in session URLs (#7926)
* fix(web-shell): preserve session URL context

* fix(web-shell): keep daemon token out of the session URL (#7926)

Restore the daemon token stripping that was dropped alongside the
base-path fix: removeDaemonTokenFromUrl() on startup and the ?token=
delete in replaceStandaloneSessionUrl. The ?token= query path is still
supported for backward compatibility, so without stripping it leaks into
the address bar, history, access logs, and Referer headers.

Also extract the session pathname building into buildSessionPathname()
with unit tests covering root/sub-path deployments, the no-session case,
trailing slashes, and id encoding.

* fix(web-shell): anchor session URL parser to agree with writer (#7926)

Extract parseSessionId() next to buildSessionPathname() and anchor it to the last /session/<id> segment so the parser agrees with the greedy writer. Previously a base path ending in a session segment produced /app/session/session/<id>, which the first-match parser read back as the literal id "session". Add round-trip and trailing-slash coverage.

* test(web-shell): cover parseSessionId malformed-encoding catch branch (#7926)

* fix(web-shell): preserve base path in split-view URL (#7926)

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-29 06:12:37 +00:00

118 lines
3.7 KiB
TypeScript

// @vitest-environment jsdom
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, expect, it } from 'vitest';
import {
buildSplitUrl,
clearSplitSessions,
loadSplitSessions,
parseSplitSessionIds,
saveSplitSessions,
} from './splitUrl';
describe('buildSplitUrl', () => {
it('opens the split for the given sessions on the same origin', () => {
expect(
buildSplitUrl(['a', 'b'], 'https://host:7777/session/other?x=1'),
).toBe('https://host:7777/?x=1&split=a%2Cb');
});
it('preserves the daemon/token query a dev deployment relies on', () => {
const url = buildSplitUrl(
['s1', 's2'],
'http://localhost:5173/?daemon=http://localhost:9000&token=secret',
);
expect(url).toContain('daemon=http');
expect(url).toContain('token=secret');
expect(url).toContain('split=s1%2Cs2');
});
it('strips the session deep-link so no single session competes', () => {
expect(
new URL(buildSplitUrl(['a'], 'https://host/session/x')).pathname,
).toBe('/');
});
it('preserves the deployment base path while stripping the session', () => {
expect(
new URL(buildSplitUrl(['a'], 'https://host/app/session/x')).pathname,
).toBe('/app');
});
it('carries the daemon token in the fragment when provided', () => {
const url = new URL(
buildSplitUrl(['a', 'b'], 'https://host/', 'secret-tok'),
);
expect(url.searchParams.get('split')).toBe('a,b');
// In the hash, not the query — never sent to the server / logs.
expect(url.search).not.toContain('secret-tok');
expect(new URLSearchParams(url.hash.slice(1)).get('token')).toBe(
'secret-tok',
);
});
it('adds no fragment when no token is given', () => {
expect(buildSplitUrl(['a'], 'https://host/')).not.toContain('#');
});
});
describe('parseSplitSessionIds', () => {
it('reads the comma-separated ids', () => {
expect(parseSplitSessionIds('?split=a,b,c')).toEqual(['a', 'b', 'c']);
});
it('round-trips with buildSplitUrl', () => {
const url = new URL(buildSplitUrl(['s1', 's2'], 'https://host/'));
expect(parseSplitSessionIds(url.search)).toEqual(['s1', 's2']);
});
it('returns an empty array when the param is absent or empty', () => {
expect(parseSplitSessionIds('')).toEqual([]);
expect(parseSplitSessionIds('?split=')).toEqual([]);
expect(parseSplitSessionIds('?other=1')).toEqual([]);
});
it('trims and drops blank ids', () => {
expect(parseSplitSessionIds('?split=a,,%20b%20,')).toEqual(['a', 'b']);
});
});
describe('split session persistence (sessionStorage)', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('round-trips the saved session set', () => {
saveSplitSessions(['s1', 's2', 's3']);
expect(loadSplitSessions()).toEqual(['s1', 's2', 's3']);
});
it('returns an empty array when nothing is saved', () => {
expect(loadSplitSessions()).toEqual([]);
});
it('dedupes, drops blanks, and caps at MAX_SPLIT_PANES (6)', () => {
saveSplitSessions(['a', 'a', '', 'b', 'c', 'd', 'e', 'f', 'g']);
expect(loadSplitSessions()).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
});
it('clears the saved set', () => {
saveSplitSessions(['s1', 's2']);
clearSplitSessions();
expect(loadSplitSessions()).toEqual([]);
});
it('falls back to [] on malformed stored JSON', () => {
sessionStorage.setItem('qwen-webshell-split-sessions', '{not json');
expect(loadSplitSessions()).toEqual([]);
});
it('falls back to [] when the stored value is not an array', () => {
sessionStorage.setItem('qwen-webshell-split-sessions', '"s1"');
expect(loadSplitSessions()).toEqual([]);
});
});