mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Asked to link to a session, the agent had no way to know the @session reference syntax exists — every mention in the tool schema described consuming a link the user dropped, never writing one — so it answered with the title and timestamp as prose and the desktop had nothing to render. Every result now carries a ready-to-copy `link`, and the schema says to write it inline instead of restating the title around it. The profile segment is omitted when the active profile can't be named confidently; a bare id still resolves. Also skip linkifying a ref a model already wrapped in a markdown link, which would otherwise rewrite into a nested link.
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { preprocessMarkdown } from './markdown-preprocess'
|
|
import {
|
|
linkifySessionRefs,
|
|
parseSessionRefValue,
|
|
sessionMarkdownHref,
|
|
sessionRefCacheKey,
|
|
sessionRefFallbackLabel,
|
|
sessionRefFromMarkdownHref,
|
|
splitSessionRefValue
|
|
} from './session-refs'
|
|
|
|
describe('parseSessionRefValue', () => {
|
|
it('splits profile and session id', () => {
|
|
expect(parseSessionRefValue('work/20260101_abc123')).toEqual({
|
|
profile: 'work',
|
|
sessionId: '20260101_abc123'
|
|
})
|
|
})
|
|
|
|
it('treats bare values as session ids', () => {
|
|
expect(parseSessionRefValue('20260101_abc123')).toEqual({ sessionId: '20260101_abc123' })
|
|
})
|
|
})
|
|
|
|
describe('sessionRefFallbackLabel', () => {
|
|
it('truncates long ids', () => {
|
|
expect(sessionRefFallbackLabel('default/20260610_120000_abcdef')).toBe('20260610…')
|
|
})
|
|
|
|
it('leaves short ids alone', () => {
|
|
expect(sessionRefFallbackLabel('work/abc123')).toBe('abc123')
|
|
})
|
|
})
|
|
|
|
describe('sessionRefCacheKey', () => {
|
|
it('separates the same id across profiles', () => {
|
|
expect(sessionRefCacheKey('work/abc')).not.toBe(sessionRefCacheKey('home/abc'))
|
|
})
|
|
|
|
it('is empty for a valueless ref', () => {
|
|
expect(sessionRefCacheKey(' ')).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('splitSessionRefValue', () => {
|
|
it('peels prose punctuation off a bare value', () => {
|
|
expect(splitSessionRefValue('default/abc123.')).toEqual({ trailing: '.', value: 'default/abc123' })
|
|
})
|
|
|
|
it('leaves a quoted value fenced', () => {
|
|
expect(splitSessionRefValue('`my session`')).toEqual({ trailing: '', value: 'my session' })
|
|
})
|
|
})
|
|
|
|
describe('session markdown hrefs', () => {
|
|
it('round-trips a value through the fragment href', () => {
|
|
const href = sessionMarkdownHref('work/20260101_abc123')
|
|
|
|
expect(href).toBe('#session/work%2F20260101_abc123')
|
|
expect(sessionRefFromMarkdownHref(href)).toBe('work/20260101_abc123')
|
|
})
|
|
|
|
it('ignores hrefs that are not session fragments', () => {
|
|
expect(sessionRefFromMarkdownHref('#preview/foo')).toBeNull()
|
|
expect(sessionRefFromMarkdownHref('https://example.com')).toBeNull()
|
|
expect(sessionRefFromMarkdownHref(undefined)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('linkifySessionRefs', () => {
|
|
it('rewrites a bare ref into a session link', () => {
|
|
expect(linkifySessionRefs('see @session:work/20260101_abc123 next')).toBe(
|
|
'see [20260101…](#session/work%2F20260101_abc123) next'
|
|
)
|
|
})
|
|
|
|
it('keeps trailing prose punctuation outside the link', () => {
|
|
expect(linkifySessionRefs('see @session:default/abc123.')).toBe('see [abc123](#session/default%2Fabc123).')
|
|
})
|
|
|
|
it('leaves text without a ref untouched', () => {
|
|
const text = 'no references here'
|
|
|
|
expect(linkifySessionRefs(text)).toBe(text)
|
|
})
|
|
|
|
it('does not match an email-like or path-embedded @session', () => {
|
|
expect(linkifySessionRefs('me@session:abc')).toBe('me@session:abc')
|
|
expect(linkifySessionRefs('a/@session:abc')).toBe('a/@session:abc')
|
|
})
|
|
|
|
it('leaves a ref a model already wrapped in a markdown link alone', () => {
|
|
const text = '[that chat](@session:work/abc123)'
|
|
|
|
expect(linkifySessionRefs(text)).toBe(text)
|
|
})
|
|
})
|
|
|
|
// The agent-authored path: assistant markdown runs through preprocessMarkdown
|
|
// before Streamdown, so a bare ref must survive as a link — and must NOT be
|
|
// rewritten inside code, where it is being discussed rather than referenced.
|
|
describe('preprocessMarkdown session refs', () => {
|
|
it('linkifies a ref in prose', () => {
|
|
expect(preprocessMarkdown('Context is in @session:work/20260101_abc123 there.')).toContain(
|
|
'[20260101…](#session/work%2F20260101_abc123)'
|
|
)
|
|
})
|
|
|
|
it('leaves refs inside inline code alone', () => {
|
|
const out = preprocessMarkdown('Type `@session:work/20260101_abc123` to link a chat.')
|
|
|
|
expect(out).toContain('`@session:work/20260101_abc123`')
|
|
expect(out).not.toContain('#session/')
|
|
})
|
|
|
|
it('leaves refs inside a fenced block alone', () => {
|
|
const out = preprocessMarkdown(['```text', '@session:work/20260101_abc123', '```'].join('\n'))
|
|
|
|
expect(out).toContain('@session:work/20260101_abc123')
|
|
expect(out).not.toContain('#session/')
|
|
})
|
|
})
|