sqlite-integration.spec.ts 8.7 KB

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