9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
129 lines
5.8 KiB
JavaScript
129 lines
5.8 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
import { parseQianwenSessionId, waitForAnswer } from './utils.js';
|
|
|
|
describe('qwen waitForAnswer baseline anchoring', () => {
|
|
// page.evaluate serves both getMessageBubbles (data-chat-question-wrap) and
|
|
// hasLoginGate (alert-biz-modal); route by inspecting the injected JS.
|
|
const fakePage = (bubbles) => ({
|
|
wait: () => new Promise((r) => setTimeout(r, 5)),
|
|
evaluate: (js) => Promise.resolve(
|
|
String(js).includes('alert-biz-modal') ? false : bubbles,
|
|
),
|
|
});
|
|
|
|
it('does not return the pre-send assistant turn (it is skipped via baseline id)', async () => {
|
|
// Only the previous, already-complete answer is present. With the
|
|
// baseline anchored to its id, waitForAnswer must skip it and time out
|
|
// rather than return the stale answer as the new reply.
|
|
const bubbles = [{ id: 'a1', role: 'Assistant', text: 'old answer', html: '' }];
|
|
const result = await waitForAnswer(fakePage(bubbles), 'new question', 0.05, 'a1');
|
|
expect(result.status).toBe('timeout');
|
|
expect(result.assistant).toBeUndefined();
|
|
});
|
|
|
|
it('returns only an assistant turn after the newly sent prompt', async () => {
|
|
const bubbles = [
|
|
{ id: 'u1', role: 'User', text: 'old question', html: '' },
|
|
{ id: 'a1', role: 'Assistant', text: 'old answer', html: '' },
|
|
{ id: 'u2', role: 'User', text: 'new question', html: '' },
|
|
{ id: 'a2', role: 'Assistant', text: 'new answer', html: '<p>new answer</p>' },
|
|
];
|
|
const result = await waitForAnswer(
|
|
fakePage(bubbles),
|
|
'new question',
|
|
0.2,
|
|
{ lastBubbleId: 'a1', lastAssistantId: 'a1' },
|
|
);
|
|
expect(result.status).toBe('partial');
|
|
expect(result.assistant).toEqual(bubbles[3]);
|
|
});
|
|
|
|
it('does not match a repeated prompt that existed before the pre-send anchor', async () => {
|
|
const bubbles = [
|
|
{ id: 'u1', role: 'User', text: 'same prompt', html: '' },
|
|
{ id: 'a1', role: 'Assistant', text: 'old answer', html: '' },
|
|
];
|
|
const result = await waitForAnswer(
|
|
fakePage(bubbles),
|
|
'same prompt',
|
|
0.05,
|
|
{ lastBubbleId: 'a1', lastAssistantId: 'a1' },
|
|
);
|
|
expect(result.status).toBe('timeout');
|
|
expect(result.assistant).toBeUndefined();
|
|
});
|
|
|
|
it('does not scan the visible transcript when a non-empty baseline anchor is missing', async () => {
|
|
const bubbles = [
|
|
{ id: 'u1-new-visible-id', role: 'User', text: 'same prompt', html: '' },
|
|
{ id: 'a1-new-visible-id', role: 'Assistant', text: 'old answer', html: '' },
|
|
];
|
|
const result = await waitForAnswer(
|
|
fakePage(bubbles),
|
|
'same prompt',
|
|
0.05,
|
|
{ lastBubbleId: 'a-anchor-missing', lastAssistantId: 'a-anchor-missing' },
|
|
);
|
|
expect(result.status).toBe('timeout');
|
|
expect(result.assistant).toBeUndefined();
|
|
});
|
|
|
|
it('supports a fresh chat with no baseline anchor', async () => {
|
|
const bubbles = [
|
|
{ id: 'u1', role: 'User', text: 'fresh question', html: '' },
|
|
{ id: 'a1', role: 'Assistant', text: 'fresh answer', html: '' },
|
|
];
|
|
const result = await waitForAnswer(
|
|
fakePage(bubbles),
|
|
'fresh question',
|
|
0.2,
|
|
{ lastBubbleId: '', lastAssistantId: '' },
|
|
);
|
|
expect(result.status).toBe('partial');
|
|
expect(result.assistant).toEqual(bubbles[1]);
|
|
});
|
|
});
|
|
|
|
describe('qwen parseQianwenSessionId', () => {
|
|
const id = 'abcd1234ef567890abcd1234ef567890';
|
|
|
|
it('returns a bare 32-char hex ID unchanged', () => {
|
|
expect(parseQianwenSessionId(id)).toBe(id);
|
|
});
|
|
|
|
it('lowercases an upper-case ID', () => {
|
|
expect(parseQianwenSessionId(id.toUpperCase())).toBe(id);
|
|
});
|
|
|
|
it('extracts the session ID from a full qianwen.com chat URL', () => {
|
|
expect(parseQianwenSessionId(`https://www.qianwen.com/chat/${id}`)).toBe(id);
|
|
expect(parseQianwenSessionId(`https://www.qianwen.com/chat/${id}?from=share`)).toBe(id);
|
|
expect(parseQianwenSessionId(`http://qianwen.com/chat/${id}`)).toBe(id);
|
|
});
|
|
|
|
it('throws ArgumentError on empty input', () => {
|
|
expect(() => parseQianwenSessionId('')).toThrow(ArgumentError);
|
|
expect(() => parseQianwenSessionId(null)).toThrow(ArgumentError);
|
|
expect(() => parseQianwenSessionId(undefined)).toThrow(ArgumentError);
|
|
expect(() => parseQianwenSessionId(' ')).toThrow(ArgumentError);
|
|
});
|
|
|
|
it('throws ArgumentError on non-hex input', () => {
|
|
expect(() => parseQianwenSessionId('not-an-id')).toThrow(ArgumentError);
|
|
expect(() => parseQianwenSessionId('123')).toThrow(ArgumentError);
|
|
// 32 chars but not all hex
|
|
expect(() => parseQianwenSessionId('zbcd1234ef567890abcd1234ef567890')).toThrow(ArgumentError);
|
|
// 31 hex chars — too short
|
|
expect(() => parseQianwenSessionId('abcd1234ef567890abcd1234ef56789')).toThrow(ArgumentError);
|
|
// 33 hex chars — too long
|
|
expect(() => parseQianwenSessionId('abcd1234ef567890abcd1234ef5678900')).toThrow(ArgumentError);
|
|
// URL with the wrong path shape must not silently fall through.
|
|
expect(() => parseQianwenSessionId('https://www.qianwen.com/somewhere/else')).toThrow(ArgumentError);
|
|
// URL embedding a 33+ hex tail must not silently truncate to 32 chars
|
|
// and open the wrong conversation.
|
|
expect(() => parseQianwenSessionId(`https://www.qianwen.com/chat/${id}0`)).toThrow(ArgumentError);
|
|
expect(() => parseQianwenSessionId(`https://www.qianwen.com/chat/${id}abc`)).toThrow(ArgumentError);
|
|
});
|
|
});
|