sqlite-integration.spec.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { afterEach, describe, expect, it } from 'vitest'
  2. import { Context } from '@deepseek-ai/cordis'
  3. import { mkdtemp, rm } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import type { Agent } from '@deepseek-ai/dsh-agent'
  7. import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
  8. import SessionStore, {
  9. SESSION_FORMAT_VERSION,
  10. SessionId,
  11. SessionSeq,
  12. type Session,
  13. } from '@deepseek-ai/dsh-session'
  14. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  15. import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
  16. import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
  17. import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
  18. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  19. import ToolRuntime from '@deepseek-ai/dsh-tools'
  20. import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
  21. const temporaryDirectories: string[] = []
  22. const contexts: Context[] = []
  23. afterEach(async () => {
  24. for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
  25. for (const directory of temporaryDirectories.splice(0)) {
  26. await rm(directory, { recursive: true, force: true })
  27. }
  28. })
  29. function fakeAgent(session: Session): Agent {
  30. return { id: session.id, session } as unknown as Agent
  31. }
  32. function registerTurnBoundary(ctx: Context): void {
  33. ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
  34. }
  35. describe('tool-session-query with the real SQLite provider', () => {
  36. it('searches live prior-step history and a persisted same-workspace log', { timeout: 20_000 }, async () => {
  37. const root = await mkdtemp(join(tmpdir(), 'dsh-tool-session-query-'))
  38. temporaryDirectories.push(root)
  39. const ctx = new Context()
  40. contexts.push(ctx)
  41. await ctx.plugin(SessionStore)
  42. await ctx.plugin(SessionProjectionRegistry)
  43. registerTurnBoundary(ctx)
  44. await ctx.plugin(SystemPrompt)
  45. await ctx.plugin(ToolRuntime)
  46. await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  47. await ctx.plugin(SqliteSessionQueryEngine, { path: join(root, 'session-query.db') })
  48. await ctx.plugin(ToolSessionQuery)
  49. const persisted = SessionId('persisted')
  50. const writer = await ctx.sessionPersistence.create({
  51. version: SESSION_FORMAT_VERSION,
  52. id: persisted,
  53. createdAt: 1,
  54. cwd: '/work',
  55. isSeeded: false,
  56. })
  57. await writer.append([{
  58. type: 'user/message',
  59. seq: SessionSeq(0),
  60. time: 2,
  61. data: createUserMessage({
  62. content: [{ type: 'text', text: 'persisted integration needle' }],
  63. source: { kind: 'user' },
  64. }),
  65. surfaceOp: 'append',
  66. }])
  67. await writer.close()
  68. const caller = ctx.sessions.create(SessionId('caller'), {
  69. meta: { createdAt: 10, cwd: '/work' },
  70. })
  71. caller.append('turn/start', { turn: 1 })
  72. caller.append(
  73. 'user/message',
  74. createUserMessage({
  75. content: [{ type: 'text', text: 'live integration needle' }], source: { kind: 'user' },
  76. }),
  77. { surfaceOp: 'append' },
  78. )
  79. caller.append('step/start', { turn: 1, step: 1 })
  80. let call = 0
  81. const execute = (name: string, args: unknown) => ctx.tools.execute({
  82. name,
  83. arguments: args,
  84. callId: ToolCallId(`integration-${++call}`),
  85. signal: new AbortController().signal,
  86. agent: fakeAgent(caller),
  87. })
  88. const sessions = await execute('session_search', { query: 'persisted integration needle' })
  89. expect(sessions.isError).toBe(false)
  90. expect(sessions.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
  91. .toContain('Session persisted')
  92. const persistedEvents = await execute('session_event_search', {
  93. session_id: persisted,
  94. query: 'persisted integration needle',
  95. })
  96. expect(persistedEvents.isError).toBe(false)
  97. expect(persistedEvents.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
  98. .toContain('seq 0')
  99. const liveEvents = await execute('session_event_search', { query: 'live integration needle' })
  100. expect(liveEvents.isError).toBe(false)
  101. expect(liveEvents.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
  102. .toContain('seq 1')
  103. })
  104. it('passes finite fractional epoch-millisecond bounds through SQLite comparisons', async () => {
  105. const root = await mkdtemp(join(tmpdir(), 'dsh-tool-session-query-fractional-'))
  106. temporaryDirectories.push(root)
  107. const ctx = new Context()
  108. contexts.push(ctx)
  109. await ctx.plugin(SessionStore)
  110. await ctx.plugin(SessionProjectionRegistry)
  111. registerTurnBoundary(ctx)
  112. await ctx.plugin(SystemPrompt)
  113. await ctx.plugin(ToolRuntime)
  114. await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
  115. await ctx.plugin(SqliteSessionQueryEngine, { path: join(root, 'session-query.db') })
  116. await ctx.plugin(ToolSessionQuery)
  117. const base = Date.parse('2026-07-24T00:00:00.000Z')
  118. const persisted = SessionId('fractional-persisted')
  119. const writer = await ctx.sessionPersistence.create({
  120. version: SESSION_FORMAT_VERSION,
  121. id: persisted,
  122. createdAt: base,
  123. cwd: '/work',
  124. isSeeded: false,
  125. })
  126. await writer.append([
  127. {
  128. type: 'user/message',
  129. seq: SessionSeq(0),
  130. time: base + 123,
  131. data: createUserMessage({
  132. content: [{ type: 'text', text: 'fractional integration needle' }],
  133. source: { kind: 'user' },
  134. }),
  135. surfaceOp: 'append',
  136. },
  137. {
  138. type: 'user/message',
  139. seq: SessionSeq(1),
  140. time: base + 124,
  141. data: createUserMessage({
  142. content: [{ type: 'text', text: 'fractional integration needle' }],
  143. source: { kind: 'user' },
  144. }),
  145. surfaceOp: 'append',
  146. },
  147. {
  148. type: 'user/message',
  149. seq: SessionSeq(2),
  150. time: -124,
  151. data: createUserMessage({
  152. content: [{ type: 'text', text: 'pre-epoch fractional needle' }],
  153. source: { kind: 'user' },
  154. }),
  155. surfaceOp: 'append',
  156. },
  157. {
  158. type: 'user/message',
  159. seq: SessionSeq(3),
  160. time: -123,
  161. data: createUserMessage({
  162. content: [{ type: 'text', text: 'pre-epoch fractional needle' }],
  163. source: { kind: 'user' },
  164. }),
  165. surfaceOp: 'append',
  166. },
  167. ])
  168. await writer.close()
  169. const caller = ctx.sessions.create(SessionId('fractional-caller'), {
  170. meta: { createdAt: base + 1_000, cwd: '/work' },
  171. })
  172. let call = 0
  173. const execute = (args: unknown) => ctx.tools.execute({
  174. name: 'session_event_search',
  175. arguments: args,
  176. callId: ToolCallId(`fractional-integration-${++call}`),
  177. signal: new AbortController().signal,
  178. agent: fakeAgent(caller),
  179. })
  180. const lowerBound = await execute({
  181. session_id: persisted,
  182. query: 'fractional integration needle',
  183. time_from: '2026-07-24T00:00:00.12300001Z',
  184. })
  185. expect(lowerBound.isError).toBe(false)
  186. const lowerText = lowerBound.content.map(block => block.type === 'text' ? block.text : '').join('\n')
  187. expect(lowerText).toContain('seq 1')
  188. expect(lowerText).not.toContain('seq 0')
  189. const upperBound = await execute({
  190. session_id: persisted,
  191. query: 'fractional integration needle',
  192. time_to: '2026-07-24T08:00:00.1239999+08:00',
  193. })
  194. expect(upperBound.isError).toBe(false)
  195. const upperText = upperBound.content.map(block => block.type === 'text' ? block.text : '').join('\n')
  196. expect(upperText).toContain('seq 0')
  197. expect(upperText).not.toContain('seq 1')
  198. const emptySameMillisecond = await execute({
  199. session_id: persisted,
  200. query: 'fractional integration needle',
  201. time_from: '2026-07-24T00:00:00.12300001Z',
  202. time_to: '2026-07-24T08:00:00.1239999+08:00',
  203. })
  204. expect(emptySameMillisecond.isError).toBe(false)
  205. expect(emptySameMillisecond.content.map(block => block.type === 'text' ? block.text : '').join('\n'))
  206. .toContain('No prior event matches found.')
  207. const preEpochLower = await execute({
  208. session_id: persisted,
  209. query: 'pre-epoch fractional needle',
  210. time_from: '1969-12-31T23:59:59.87600001Z',
  211. })
  212. expect(preEpochLower.isError).toBe(false)
  213. const preEpochLowerText = preEpochLower.content
  214. .map(block => block.type === 'text' ? block.text : '').join('\n')
  215. expect(preEpochLowerText).toContain('seq 3')
  216. expect(preEpochLowerText).not.toContain('seq 2')
  217. const preEpochUpper = await execute({
  218. session_id: persisted,
  219. query: 'pre-epoch fractional needle',
  220. time_to: '1969-12-31T19:59:59.8769999-04:00',
  221. })
  222. expect(preEpochUpper.isError).toBe(false)
  223. const preEpochUpperText = preEpochUpper.content
  224. .map(block => block.type === 'text' ? block.text : '').join('\n')
  225. expect(preEpochUpperText).toContain('seq 2')
  226. expect(preEpochUpperText).not.toContain('seq 3')
  227. })
  228. })