fix: harden session query authorization

This commit is contained in:
Hypatia May
2026-07-24 20:50:53 +08:00
parent 04c8a17de5
commit ba6fc1cfa3
7 changed files with 758 additions and 99 deletions

View File

@@ -363,6 +363,7 @@ describe('input validation and translation', () => {
it('normalizes the query and compiles inclusive session/event filters with one parent OR clause', async () => {
const mounted = await mount()
createSession(mounted.ctx, 'parent', '/work')
await mounted.call('session_search', {
query: ' alpha beta ',
session_ids: ['a', 'b'],
@@ -388,8 +389,8 @@ describe('input validation and translation', () => {
from: Date.parse('2026-07-24T00:00:00+08:00'),
to: Date.parse('2026-07-24T01:00:00+08:00'),
},
{ kind: 'parent', values: ['parent', null] },
{ kind: 'availability', values: ['live'] },
{ kind: 'parent', values: ['parent', null] },
{ kind: 'cwd', values: ['/work'] },
],
eventFilters: [
@@ -538,6 +539,7 @@ describe('input validation and translation', () => {
it('compiles one-sided timestamps and independent root/parent clauses', async () => {
const mounted = await mount()
createSession(mounted.ctx, 'parent', '/work')
await mounted.call('session_search', {
query: 'q',
created_at_from: '2024-02-29T00:00Z',
@@ -596,6 +598,191 @@ describe('workspace authority and lineage redaction', () => {
.toBe('SESSION_QUERY_TOOL_UNAUTHORIZED')
})
it('makes hidden and nonexistent parent guesses indistinguishable without calling search', async () => {
const mounted = await mount()
const hiddenParent = createSession(mounted.ctx, 'guessed-hidden-parent-secret', '/outside')
const visibleChild = createSession(
mounted.ctx,
'visible-child-of-hidden-parent',
'/work',
20,
hiddenParent.id,
)
FakeQuery.sessionSearch = () => Promise.resolve({
items: [sessionHit(visibleChild.id, '/work', 'must not be discoverable', hiddenParent.id)],
})
const hidden = await mounted.call('session_search', {
query: 'needle',
parent_session_ids: [hiddenParent.id],
})
const missing = await mounted.call('session_search', {
query: 'needle',
parent_session_ids: ['guessed-missing-parent'],
})
expect(hidden).toEqual(missing)
expect(text(hidden)).toBe('No prior session matches found.')
expect(JSON.stringify(hidden)).not.toContain(visibleChild.id)
expect(FakeQuery.sessionRequests).toEqual([])
})
it('deduplicates parent guesses and sends only authorized parents plus the root marker', async () => {
const mounted = await mount()
const visible = createSession(mounted.ctx, 'visible-parent', '/work')
const hidden = createSession(mounted.ctx, 'hidden-parent-filter-secret', '/outside')
await mounted.call('session_search', {
query: 'needle',
parent_session_ids: [visible.id, hidden.id, visible.id, 'missing-parent'],
include_root_sessions: true,
})
await mounted.call('session_search', {
query: 'needle',
parent_session_ids: [hidden.id],
include_root_sessions: true,
})
await mounted.call('session_search', {
query: 'needle',
parent_session_ids: ['missing-parent'],
include_root_sessions: true,
})
const parentValues = FakeQuery.sessionRequests.map(request =>
request.sessionFilters?.find(filter => filter.kind === 'parent'))
expect(parentValues).toEqual([
{ kind: 'parent', values: [visible.id, null] },
{ kind: 'parent', values: [null] },
{ kind: 'parent', values: [null] },
])
})
it('rejects unrequested or unauthorized records returned during parent preauthorization', async () => {
const mounted = await mount()
const requested = SessionId('requested-parent')
vi.spyOn(mounted.ctx.sessionQuery, 'filterSessions').mockResolvedValueOnce([
{ header: header('unrequested-parent', '/work'), live: true, persisted: false },
{ header: header(requested, '/outside'), live: true, persisted: false },
])
const result = await mounted.call('session_search', {
query: 'needle',
parent_session_ids: [requested],
})
expect(text(result)).toBe('No prior session matches found.')
expect(FakeQuery.sessionRequests).toEqual([])
})
it('validates every other search filter before parent preauthorization', async () => {
const mounted = await mount()
const filterSessions = vi.spyOn(mounted.ctx.sessionQuery, 'filterSessions')
const result = await mounted.call('session_search', {
query: 'needle',
parent_session_ids: ['guessed-parent'],
event_seq_from: -1,
})
expect(errorCode(result)).toBe('SESSION_QUERY_INVALID_FILTER')
expect(filterSessions).not.toHaveBeenCalled()
expect(FakeQuery.sessionRequests).toEqual([])
})
it('sanitizes parent preauthorization failures without calling search', async () => {
const mounted = await mount()
const secret = 'conflict at hidden-parent-preauthorization-secret'
vi.spyOn(mounted.ctx.sessionQuery, 'filterSessions').mockRejectedValueOnce(
new SessionQueryError(secret, 'SESSION_QUERY_SOURCE_CONFLICT'),
)
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', {
query: 'needle',
parent_session_ids: ['guessed-parent'],
})
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
expect(FakeQuery.sessionRequests).toEqual([])
})
it('sanitizes direct-target authorization failures before event search', async () => {
const mounted = await mount()
const target = createSession(mounted.ctx, 'authorization-failure-target', '/work')
const secret = 'conflict with hidden-authorization-session-secret'
vi.spyOn(mounted.ctx.sessionQuery, 'filterSessions').mockRejectedValueOnce(
new SessionQueryError(secret, 'SESSION_QUERY_SOURCE_CONFLICT'),
)
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_event_search', {
session_id: target.id,
query: 'needle',
})
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
expect(FakeQuery.eventRequests).toEqual([])
})
it('preserves parent-preauthorization cancellation and waits for cleanup without logging it', async () => {
const mounted = await mount()
const controller = new AbortController()
const cancellation = new SessionQueryError(
'parent preauthorization cancelled',
'SESSION_QUERY_ABORTED',
)
const started = Promise.withResolvers<undefined>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
let active = false
vi.spyOn(mounted.ctx.sessionQuery, 'filterSessions')
.mockImplementation(async (_filters, signal) => {
if (signal === undefined) throw new Error('expected parent-authorization signal')
active = true
const aborted = new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
started.resolve(undefined)
await aborted
abortObserved.resolve(undefined)
await cleanup.promise
active = false
signal.throwIfAborted()
return []
})
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const pending = mounted.call('session_search', {
query: 'needle',
parent_session_ids: ['guessed-parent'],
}, { signal: controller.signal })
let settled = false
void pending.then(
() => { settled = true },
() => { settled = true },
)
await started.promise
controller.abort(cancellation)
await abortObserved.promise
expect(settled).toBe(false)
expect(active).toBe(true)
expect(FakeQuery.sessionRequests).toEqual([])
cleanup.resolve(undefined)
const result = await pending
expect(active).toBe(false)
expect(errorCode(result)).toBe('SESSION_QUERY_ABORTED')
expect(text(result)).toBe('Error: parent preauthorization cancelled')
expect(warn).not.toHaveBeenCalled()
})
it('redacts an unauthorized ancestor and prunes unauthorized descendant subtrees without hidden ids', async () => {
const mounted = await mount()
const hiddenParent = createSession(mounted.ctx, 'hidden-parent-secret', '/outside')
@@ -635,6 +822,16 @@ describe('workspace authority and lineage redaction', () => {
})
it.each([
{
name: 'sensitive source conflict',
makeError: () => new SessionQueryError(
'conflict with hidden-lineage-session-secret',
'SESSION_QUERY_SOURCE_CONFLICT',
),
code: 'SESSION_QUERY_TOOL_FAILED',
message: 'session query operation failed',
secret: 'hidden-lineage-session-secret',
},
{
name: 'typed query error',
makeError: () => new SessionQueryError(
@@ -642,23 +839,56 @@ describe('workspace authority and lineage redaction', () => {
'SESSION_QUERY_PERSISTENCE_FAILED',
),
code: 'SESSION_QUERY_PERSISTENCE_FAILED',
message: 'unrelated persistence failure',
message: 'session history storage is unavailable',
secret: 'unrelated persistence failure',
},
{
name: 'plain error',
makeError: () => new Error('unrelated plain trace failure'),
code: undefined,
message: 'unrelated plain trace failure',
code: 'SESSION_QUERY_TOOL_FAILED',
message: 'session query operation failed',
secret: 'unrelated plain trace failure',
},
])('preserves an unrelated $name from lineage tracing', async ({ makeError, code, message }) => {
])('sanitizes an unrelated $name from lineage tracing', async ({ makeError, code, message, secret }) => {
const mounted = await mount()
const target = createSession(mounted.ctx, 'trace-failure-target', '/work')
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
vi.spyOn(mounted.ctx.sessionQuery, 'traceSession').mockRejectedValueOnce(makeError())
const result = await mounted.call('session_trace', { session_id: target.id })
expect(errorCode(result)).toBe(code)
expect(text(result)).toBe(`Error: ${message}`)
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
})
it.each([
'session_event_trace',
'session_event_read',
] as const)('sanitizes typed service diagnostics from %s', async (toolName) => {
const mounted = await mount()
const target = createSession(mounted.ctx, `${toolName}-failure-target`, '/work')
target.append(
'user/message',
{ content: [{ type: 'text', text: 'event' }], source: { kind: 'user' } },
{ surfaceOp: 'append' },
)
const secret = `event missing beside hidden-${toolName}-secret`
const failure = new SessionQueryError(secret, 'SESSION_QUERY_EVENT_NOT_FOUND')
if (toolName === 'session_event_trace') {
vi.spyOn(mounted.ctx.sessionQuery, 'traceEvent').mockRejectedValueOnce(failure)
} else {
vi.spyOn(mounted.ctx.sessionQuery, 'readEvent').mockRejectedValueOnce(failure)
}
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call(toolName, { session_id: target.id, seq: 0 })
expect(errorCode(result)).toBe('SESSION_QUERY_EVENT_NOT_FOUND')
expect(text(result)).toBe('Error: session event was not found')
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
})
it.each([
@@ -681,6 +911,7 @@ describe('workspace authority and lineage redaction', () => {
const started = Promise.withResolvers<undefined>()
const abortObserved = Promise.withResolvers<undefined>()
const cleanup = Promise.withResolvers<undefined>()
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
let observedSignal: AbortSignal | undefined
let active = false
const holdExactRead = async (signal?: AbortSignal): Promise<never> => {
@@ -731,6 +962,7 @@ describe('workspace authority and lineage redaction', () => {
expect(active).toBe(false)
expect(errorCode(result)).toBe('SESSION_QUERY_ABORTED')
expect(text(result)).toBe(`Error: ${toolName} cancelled`)
expect(warn).not.toHaveBeenCalled()
})
it('preserves caller cancellation while a lineage trace is pending', async () => {
@@ -1052,6 +1284,239 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
expect(output).not.toContain('Result cap reached')
})
it.each([
{
toolName: 'session_search',
args: { query: 'needle' },
secrets: [
'session source conflict at hidden-search-session-secret',
'hidden-search-cause-secret',
],
failure: () => new SessionQueryError(
'session source conflict at hidden-search-session-secret',
'SESSION_QUERY_SOURCE_CONFLICT',
{ cause: new Error('hidden-search-cause-secret') },
),
},
{
toolName: 'session_event_search',
args: { query: 'needle' },
secrets: [
'plain event provider failure at hidden-event-session-secret',
'hidden-event-cause-secret',
],
failure: () => new Error(
'plain event provider failure at hidden-event-session-secret',
{ cause: 'hidden-event-cause-secret' },
),
},
] as const)('sanitizes $toolName provider diagnostics', async ({ toolName, args, secrets, failure }) => {
const mounted = await mount()
if (toolName === 'session_search') {
FakeQuery.sessionSearch = () => Promise.reject(failure())
} else {
FakeQuery.eventSearch = () => Promise.reject(failure())
}
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call(toolName, args)
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
for (const secret of secrets) {
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
}
})
it.each([
{
name: 'a hostile prototype trap',
secrets: ['proxy payload secret', 'getPrototypeOf secondary secret'],
diagnostic: '[unprintable session query failure]',
failure: (): unknown => new Proxy(
{ payload: 'proxy payload secret' },
{
getPrototypeOf() {
throw new Error('getPrototypeOf secondary secret')
},
},
),
},
{
name: 'a throwing stack getter',
secrets: ['stack primary secret', 'stack getter secondary secret'],
diagnostic: '[unprintable session query failure]',
failure: (): unknown => {
const error = new Error('stack primary secret')
Object.defineProperty(error, 'stack', {
get() {
throw new Error('stack getter secondary secret')
},
})
return error
},
},
{
name: 'a throwing cause getter',
secrets: ['cause primary secret', 'cause getter secondary secret'],
diagnostic: '[unprintable session query failure]',
failure: (): unknown => {
const error = new Error('cause primary secret')
Object.defineProperty(error, 'cause', {
get() {
throw new Error('cause getter secondary secret')
},
})
return error
},
},
{
name: 'throwing string coercion',
secrets: ['string payload secret', 'string coercion secondary secret'],
diagnostic: '[unprintable session query failure]',
failure: (): unknown => ({
payload: 'string payload secret',
[Symbol.toPrimitive]() {
throw new Error('string coercion secondary secret')
},
}),
},
{
name: 'a throwing code getter',
secrets: ['code primary secret', 'code getter secondary secret'],
diagnostic: 'code primary secret',
failure: (): unknown => {
const error = new SessionQueryError(
'code primary secret',
'SESSION_QUERY_PERSISTENCE_FAILED',
)
Object.defineProperty(error, 'code', {
get() {
throw new Error('code getter secondary secret')
},
})
return error
},
},
{
name: 'an unknown string code',
secrets: ['unknown code primary secret', '__proto__'],
diagnostic: 'unknown code primary secret',
failure: (): unknown => {
const error = new SessionQueryError(
'unknown code primary secret',
'SESSION_QUERY_PERSISTENCE_FAILED',
)
Object.defineProperty(error, 'code', { value: '__proto__' })
return error
},
},
{
name: 'a non-string code',
secrets: ['non-string code primary secret', 'non-string code secondary secret'],
diagnostic: 'non-string code primary secret',
failure: (): unknown => {
const error = new SessionQueryError(
'non-string code primary secret',
'SESSION_QUERY_PERSISTENCE_FAILED',
)
Object.defineProperty(error, 'code', {
value: {
toString() {
throw new Error('non-string code secondary secret')
},
},
})
return error
},
},
])('fails generic when inspecting $name is unsafe', async ({ secrets, diagnostic, failure }) => {
const mounted = await mount()
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- hostile unknown rejection is the scenario
FakeQuery.sessionSearch = () => Promise.reject(failure())
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
for (const secret of secrets) expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(diagnostic))
})
it('retains a fixed safe typed failure when only its nested diagnostic is unprintable', async () => {
const mounted = await mount()
const primary = 'typed outer diagnostic secret'
const nested = 'nested prototype secondary secret'
const cause = new Proxy(
{},
{
getPrototypeOf() {
throw new Error(nested)
},
},
)
FakeQuery.sessionSearch = () => Promise.reject(
new SessionQueryError(
primary,
'SESSION_QUERY_PERSISTENCE_FAILED',
{ cause },
),
)
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_PERSISTENCE_FAILED')
expect(text(result)).toBe('Error: session history storage is unavailable')
expect(JSON.stringify(result)).not.toContain(primary)
expect(JSON.stringify(result)).not.toContain(nested)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('[unprintable session query failure]'))
})
it('logs an inspectable cyclic cause chain without exposing it', async () => {
const mounted = await mount()
const outer = new Error('cyclic outer secret')
const inner = new Error('cyclic inner secret')
Object.defineProperty(outer, 'cause', { value: inner })
Object.defineProperty(inner, 'cause', { value: outer })
FakeQuery.sessionSearch = () => Promise.reject(outer)
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
expect(JSON.stringify(result)).not.toContain('cyclic outer secret')
expect(JSON.stringify(result)).not.toContain('cyclic inner secret')
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cyclic outer secret'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cyclic inner secret'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('[circular error cause]'))
})
it('fails generic when internal warning logging throws', async () => {
const mounted = await mount()
const primary = 'typed persistence primary secret'
const secondary = 'logger warning secondary secret'
FakeQuery.sessionSearch = () => Promise.reject(
new SessionQueryError(primary, 'SESSION_QUERY_PERSISTENCE_FAILED'),
)
const warn = vi.spyOn(mounted.ctx.logger, 'warn')
.mockImplementation(() => undefined)
.mockImplementationOnce(() => {
throw new Error(secondary)
})
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
expect(JSON.stringify(result)).not.toContain(primary)
expect(JSON.stringify(result)).not.toContain(secondary)
expect(warn).toHaveBeenCalledTimes(1)
})
it('preserves stale-cursor diagnostics without transparently restarting', async () => {
const mounted = await mount({ maxSearchResults: 2 })
const cursor = SessionSearchCursor('stale-next')
@@ -1070,6 +1535,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
FakeQuery.sessionSearch = () => Promise.resolve({ items: [], nextCursor: cursor })
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_INVALID_CURSOR')
expect(text(result)).toBe('Error: session-search provider repeated a continuation cursor')
expect(FakeQuery.sessionRequests).toHaveLength(2)
})
@@ -1166,7 +1632,8 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(result.isError).toBe(false)
expect(text(result)).toContain('untitled (title unavailable: TITLE_BACKEND)')
expect(text(result)).toContain('untitled (title unavailable: SESSION_QUERY_TOOL_FAILED)')
expect(JSON.stringify(result)).not.toContain('title backend failed')
expect(warn).toHaveBeenCalledWith(expect.stringContaining('title backend failed'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('HarnessError'))
})
@@ -1174,7 +1641,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
it('reports unknown title failures and preserves an Error without a stack', async () => {
const mounted = await mount()
const first = createSession(mounted.ctx, 'unknown-title', '/work')
const second = createSession(mounted.ctx, 'stackless-title', '/work')
const second = createSession(mounted.ctx, 'second-title-failure', '/work')
const stackless = new Error('stackless')
Object.defineProperty(stackless, 'stack', { value: undefined })
const readTitles = vi.spyOn(mounted.ctx.sessionQuery, 'readTitleSnapshots')
@@ -1190,13 +1657,62 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
})
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(text(result)).toContain('title unavailable: UNKNOWN')
expect(text(result)).toContain('title unavailable: SESSION_QUERY_TOOL_FAILED')
expect(JSON.stringify(result)).not.toContain('string failure')
expect(JSON.stringify(result)).not.toContain('stackless')
expect(readTitles).toHaveBeenCalledTimes(1)
expect(readTitles.mock.calls[0]?.[0]).toEqual([first.id, second.id])
expect(warn).toHaveBeenCalledWith(expect.stringContaining('string failure'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Error: stackless'))
})
it('isolates an unprintable per-title failure behind the generic unavailable marker', async () => {
const mounted = await mount()
const hit = createSession(mounted.ctx, 'hostile-title-failure', '/work')
const primary = 'per-title proxy payload secret'
const secondary = 'per-title prototype secondary secret'
const reason = new Proxy(
{ payload: primary },
{
getPrototypeOf() {
throw new Error(secondary)
},
},
)
FakeQuery.sessionSearch = () => Promise.resolve({ items: [sessionHit(hit.id, '/work')] })
vi.spyOn(mounted.ctx.sessionQuery, 'readTitleSnapshots').mockResolvedValueOnce([{
sessionId: hit.id,
status: 'rejected',
reason,
}])
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(result.isError).toBe(false)
expect(text(result)).toContain('untitled (title unavailable: SESSION_QUERY_TOOL_FAILED)')
expect(JSON.stringify(result)).not.toContain(primary)
expect(JSON.stringify(result)).not.toContain(secondary)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('[unprintable session query failure]'))
})
it('sanitizes a thrown batch-title service failure instead of rendering its diagnostic', async () => {
const mounted = await mount()
const hit = createSession(mounted.ctx, 'thrown-title-failure', '/work')
const secret = 'title batch failed beside hidden-title-session-secret'
FakeQuery.sessionSearch = () => Promise.resolve({ items: [sessionHit(hit.id, '/work')] })
vi.spyOn(mounted.ctx.sessionQuery, 'readTitleSnapshots')
.mockRejectedValueOnce(new Error(secret))
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_FAILED')
expect(text(result)).toBe('Error: session query operation failed')
expect(JSON.stringify(result)).not.toContain(secret)
expect(warn).toHaveBeenCalledWith(expect.stringContaining(secret))
})
it('does not downgrade cancellation during title enrichment', async () => {
const mounted = await mount()
const hit = createSession(mounted.ctx, 'abort-title', '/work')
@@ -1237,6 +1753,8 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
const result = await mounted.call('session_search', { query: 'needle' })
expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_UNAUTHORIZED')
expect(text(result)).toBe('Error: session target is outside the caller workspace')
expect(JSON.stringify(result)).not.toContain('title observation became unauthorized')
expect(text(result)).not.toContain('title unavailable')
})
@@ -1360,6 +1878,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
it('passes the exact execution signal to every FTS page and stops on cancellation', async () => {
const mounted = await mount()
const controller = new AbortController()
const warn = vi.spyOn(mounted.ctx.logger, 'warn').mockImplementation(() => undefined)
let started!: () => void
const bodyStarted = new Promise<void>((resolve) => { started = resolve })
FakeQuery.sessionSearch = (_request, exec) => new Promise((_resolve, reject) => {
@@ -1368,13 +1887,15 @@ describe('search paging, prior-history bounds, titles, and cancellation', () =>
reject(new SessionQueryError('aborted', 'SESSION_QUERY_ABORTED'))
}, { once: true })
})
const cancellation = new SessionQueryError('aborted', 'SESSION_QUERY_ABORTED')
const pending = mounted.call('session_search', { query: 'needle' }, { signal: controller.signal })
await bodyStarted
controller.abort()
controller.abort(cancellation)
const result = await pending
expect(result.isError).toBe(true)
expect(errorCode(result)).toBe('SESSION_QUERY_ABORTED')
expect(FakeQuery.searchSignals).toEqual([controller.signal])
expect(warn).not.toHaveBeenCalled()
})
})