Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
125 lines
5.7 KiB
TypeScript
125 lines
5.7 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import { isInternalHref } from '../markdown-links'
|
||
import { ANNOUNCEMENT_LIMIT, announceableAnswer } from '../AgentChat'
|
||
import { intentLabel } from '../conversation-display'
|
||
|
||
describe('isInternalHref', () => {
|
||
it('routes app paths client-side', () => {
|
||
expect(isInternalHref('/invoices/abc-123')).toBe(true)
|
||
expect(isInternalHref('/bookkeeping/year-end?step=2')).toBe(true)
|
||
expect(isInternalHref('/kpi#marginal')).toBe(true)
|
||
})
|
||
|
||
it('does not treat a protocol-relative url as internal', () => {
|
||
// The href comes out of a model that reads customer documents. "//host"
|
||
// starts with a slash but leaves the site, and handing it to the router as
|
||
// an app path is the one failure mode worth engineering against.
|
||
expect(isInternalHref('//evil.example/login')).toBe(false)
|
||
expect(isInternalHref('/\\evil.example')).toBe(false)
|
||
})
|
||
|
||
it('leaves absolute and non-http schemes external', () => {
|
||
expect(isInternalHref('https://skatteverket.se')).toBe(false)
|
||
expect(isInternalHref('http://example.com')).toBe(false)
|
||
expect(isInternalHref('mailto:a@b.se')).toBe(false)
|
||
expect(isInternalHref('javascript:alert(1)')).toBe(false)
|
||
expect(isInternalHref('')).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('announceableAnswer', () => {
|
||
const msg = (role: 'user' | 'assistant', text: string) =>
|
||
({ role, text }) as Parameters<typeof announceableAnswer>[0][number]
|
||
|
||
it('announces the finished answer, not the question', () => {
|
||
expect(
|
||
announceableAnswer([msg('user', 'Hur gick juli?'), msg('assistant', 'Juli gick 12 % bättre.')]),
|
||
).toBe('Juli gick 12 % bättre.')
|
||
})
|
||
|
||
it('reads the LAST assistant turn when the thread has several', () => {
|
||
expect(
|
||
announceableAnswer([
|
||
msg('assistant', 'Första svaret.'),
|
||
msg('user', 'Och augusti?'),
|
||
msg('assistant', 'Andra svaret.'),
|
||
]),
|
||
).toBe('Andra svaret.')
|
||
})
|
||
|
||
it('caps the WHOLE announcement, suffix included, and says where the rest is', () => {
|
||
// A screen reader reads a live region straight through: a full bokslut
|
||
// explanation announced in one uninterruptible burst is worse than not
|
||
// announcing at all. Asserted against the constant, and at the real limit
|
||
// rather than a looser number the suffix could sneak past.
|
||
const out = announceableAnswer([msg('assistant', 'a'.repeat(1000))])
|
||
expect(out.length).toBeLessThanOrEqual(ANNOUNCEMENT_LIMIT)
|
||
expect(out).toContain('Svaret fortsätter i meddelandet')
|
||
})
|
||
|
||
it('does not read an interrupted answer as a finished one', () => {
|
||
// Stop leaves the partial text with a visible marker. Announcing it as the
|
||
// answer tells a screen-reader user the opposite of what everyone else sees.
|
||
const out = announceableAnswer([
|
||
{ role: 'assistant', text: 'Momsen för juli blev 12 4', interrupted: true } as Parameters<
|
||
typeof announceableAnswer
|
||
>[0][number],
|
||
])
|
||
expect(out).toContain('avbröts')
|
||
expect(out).not.toContain('12 4')
|
||
})
|
||
|
||
it('never re-announces an earlier answer for a turn that produced no text', () => {
|
||
// The caller passes only THIS turn's messages. Given the whole thread, a
|
||
// tool-only turn would find the previous answer and read it out again as
|
||
// though it were new: the user hears a stale answer to a new question.
|
||
const thisTurn = [msg('user', 'Boka detta')]
|
||
expect(announceableAnswer(thisTurn)).toBe('Assistenten är klar.')
|
||
})
|
||
|
||
it('still says something when the turn produced no text', () => {
|
||
// Silence would be indistinguishable from the request never having been
|
||
// sent. An assistant bubble that only ever held whitespace counts as none.
|
||
expect(announceableAnswer([msg('assistant', ' ')])).toBe('Assistenten är klar.')
|
||
expect(announceableAnswer([])).toBe('Assistenten är klar.')
|
||
})
|
||
})
|
||
|
||
describe('intentLabel', () => {
|
||
it('never shows the raw intent id', () => {
|
||
// It used to return the id itself, so an unmapped intent put
|
||
// "bokslut.step" in front of the user as the name of their conversation.
|
||
expect(intentLabel('some.unmapped.intent')).toBe('Fråga din assistent')
|
||
expect(intentLabel('some.unmapped.intent', 'Anna')).toBe('Fråga Anna')
|
||
})
|
||
|
||
it('gives the panel and the history list the SAME name for a thread', () => {
|
||
// The two maps had drifted: the panel titled a bokslut thread "Fråga Anna"
|
||
// while the history list called it "Hjälp med bokslut".
|
||
expect(intentLabel('bokslut.step')).toBe('Hjälp med bokslut')
|
||
expect(intentLabel('bokslut.step', 'Anna')).toBe('Hjälp med bokslut')
|
||
expect(intentLabel('kpi.explain', 'Anna')).toBe('Förklara nyckeltal')
|
||
})
|
||
|
||
it('personalises general help and falls back without a name', () => {
|
||
expect(intentLabel('general.help', 'Anna')).toBe('Fråga Anna')
|
||
expect(intentLabel('general.help')).toBe('Fråga din assistent')
|
||
expect(intentLabel('general.help', ' ')).toBe('Fråga din assistent')
|
||
})
|
||
|
||
it('does not resolve inherited Object keys as labels', () => {
|
||
// A plain object literal inherits from Object.prototype, so a lookup of
|
||
// 'toString' returned a FUNCTION, passed the truthiness check and reached
|
||
// React as a conversation title. intent_id comes from the database.
|
||
expect(intentLabel('toString')).toBe('Fråga din assistent')
|
||
expect(intentLabel('constructor', 'Anna')).toBe('Fråga Anna')
|
||
expect(intentLabel('__proto__')).toBe('Fråga din assistent')
|
||
})
|
||
|
||
it('keeps momsdeklaration spelled correctly through the soft hyphen', () => {
|
||
// The label carries a U+00AD so it can break across the narrow panel.
|
||
// Stripping it must leave a real word.
|
||
expect(intentLabel('vat.review').replace(//g, '')).toBe('Granska momsdeklaration')
|
||
})
|
||
})
|