upload.spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import { afterEach, describe, expect, expectTypeOf, it } from 'vitest'
  2. import { Context } from '@deepseek-ai/cordis'
  3. import SessionStore, {
  4. SESSION_FORMAT_VERSION,
  5. Session,
  6. SessionId,
  7. SessionLogOffset,
  8. SessionSeq,
  9. type CreateSessionOptions,
  10. type SessionEvent,
  11. } from '@deepseek-ai/dsh-session'
  12. import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
  13. import { createAssistantMessage, createSystemMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
  14. import type { JsonValue } from '@deepseek-ai/dsh-util-values'
  15. import * as SessionLogDeepSeek from '../src/index.ts'
  16. import type { DeepSeekSessionLogExtension, DeepSeekSessionLogWireEvent, DeepSeekSessionLogWireSurfaceOp } from '../src/types.ts'
  17. const contexts: Context[] = []
  18. const SIGNAL = new AbortController().signal
  19. afterEach(async () => {
  20. await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
  21. })
  22. async function harness(
  23. id: string,
  24. seed?: readonly SessionEvent[],
  25. creation?: Omit<CreateSessionOptions, 'seed'>,
  26. ): Promise<{
  27. ctx: Context
  28. session: Session
  29. disposeUpload: () => Promise<void>
  30. }> {
  31. const ctx = new Context()
  32. contexts.push(ctx)
  33. await ctx.plugin(SessionStore)
  34. await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
  35. const upload = ctx.plugin(SessionLogDeepSeek, { enabled: true })
  36. await upload
  37. const options = seed === undefined
  38. ? undefined
  39. : { seed, ...creation }
  40. const session = ctx.sessions.create(SessionId(id), options)
  41. return { ctx, session, disposeUpload: () => upload.dispose() }
  42. }
  43. function body(text = 'x'.repeat(300)) {
  44. return { messages: [{ role: 'user', content: text }] }
  45. }
  46. describe('incremental DeepSeek session-log upload', () => {
  47. it('publishes raw numeric sequence fields on its external wire DTO', () => {
  48. expectTypeOf<DeepSeekSessionLogExtension['sessionFormatVersion']>().toEqualTypeOf<number>()
  49. expectTypeOf<DeepSeekSessionLogExtension['afterSeq']>().toEqualTypeOf<number>()
  50. expectTypeOf<DeepSeekSessionLogExtension['throughSeq']>().toEqualTypeOf<number>()
  51. expectTypeOf<DeepSeekSessionLogExtension['events'][number]['seq']>().toEqualTypeOf<number>()
  52. expectTypeOf<DeepSeekSessionLogExtension['events'][number]['data']>().toEqualTypeOf<JsonValue>()
  53. expectTypeOf<DeepSeekSessionLogExtension['session']['seedLength']>()
  54. .toEqualTypeOf<number | undefined>()
  55. })
  56. it('requires surface placement and restricts sources to non-assistant surface wire events', () => {
  57. type System = Extract<DeepSeekSessionLogWireEvent, { type: 'system/message' }>
  58. type Assistant = Extract<DeepSeekSessionLogWireEvent, { type: 'assistant/message' }>
  59. type User = Extract<DeepSeekSessionLogWireEvent, { type: 'user/message' }>
  60. type Tool = Extract<DeepSeekSessionLogWireEvent, { type: 'tool/result' }>
  61. type LogOnly = Extract<DeepSeekSessionLogWireEvent, { type: 'turn/start' }>
  62. type Replace = Exclude<DeepSeekSessionLogWireSurfaceOp, 'append'>
  63. expectTypeOf<System['surfaceOp']>().toEqualTypeOf<DeepSeekSessionLogWireSurfaceOp>()
  64. expectTypeOf<System['sourceEventSeqs']>().toEqualTypeOf<readonly number[] | undefined>()
  65. expectTypeOf<Assistant['surfaceOp']>().toEqualTypeOf<DeepSeekSessionLogWireSurfaceOp>()
  66. expectTypeOf<User['surfaceOp']>().toEqualTypeOf<DeepSeekSessionLogWireSurfaceOp>()
  67. expectTypeOf<Tool['surfaceOp']>().toEqualTypeOf<DeepSeekSessionLogWireSurfaceOp>()
  68. expectTypeOf<Assistant['sourceEventSeqs']>().toEqualTypeOf<undefined>()
  69. expectTypeOf<LogOnly['sourceEventSeqs']>().toEqualTypeOf<undefined>()
  70. expectTypeOf<LogOnly['surfaceOp']>().toEqualTypeOf<undefined>()
  71. expectTypeOf<User['sourceEventSeqs']>().toEqualTypeOf<readonly number[] | undefined>()
  72. expectTypeOf<Tool['sourceEventSeqs']>().toEqualTypeOf<readonly number[] | undefined>()
  73. expectTypeOf<Replace>().toEqualTypeOf<{
  74. readonly op: 'replace'
  75. readonly startSeq: number
  76. readonly endSeq: number
  77. }>()
  78. })
  79. it('uploads Assistant provider metadata only through its embedded stream', async () => {
  80. const { ctx, session } = await harness('wire-assistant')
  81. const assistant = session.append('assistant/message', {
  82. turn: 1,
  83. step: 1,
  84. message: createAssistantMessage({
  85. content: [{ type: 'text', text: 'Answer' }],
  86. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  87. }),
  88. stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['Answer'] }],
  89. }, { surfaceOp: 'append' })
  90. const prepared = await ctx.deepseekLlmApiExtensions.prepare({
  91. body: body(), signal: SIGNAL, sessionId: session.id,
  92. })
  93. expect(prepared.fields.dsh_session_log?.events).toEqual([{
  94. type: assistant.type,
  95. seq: Number(assistant.seq),
  96. time: assistant.time,
  97. data: assistant.data,
  98. surfaceOp: 'append',
  99. }])
  100. })
  101. it('uploads system append and replacement placement with unchanged data and source-event references', async () => {
  102. const { ctx, session } = await harness('wire-system')
  103. const headData = { turn: 1, step: 1, message: createSystemMessage('head', 'fixture'), extra: { retained: true } }
  104. const head = session.append('system/message', headData, { surfaceOp: 'append' })
  105. session.append('system/message', {
  106. turn: 1, step: 2, message: createSystemMessage('later', 'fixture'),
  107. }, { surfaceOp: 'append' })
  108. session.append('system/message', {
  109. turn: 1, step: 3, message: createSystemMessage('new head', 'fixture'),
  110. }, { surfaceOp: { op: 'replace', startSeq: head.seq, endSeq: head.seq }, sourceEventSeqs: [head.seq] })
  111. const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  112. expect(prepared.fields.dsh_session_log?.events).toEqual(session.snapshotEvents())
  113. })
  114. it.each(['extension/event', 'tool/code-dispatch', 'tool/code-dispatch-start'])('uploads opaque ignorable %s without interpreting its metadata', async (type) => {
  115. for (const metadata of [
  116. {},
  117. { surfaceOp: null },
  118. { sourceEventSeqs: null },
  119. { surfaceOp: { opaque: ['retained'] }, sourceEventSeqs: { opaque: [null] } },
  120. ]) {
  121. const event = {
  122. type, seq: SessionSeq(0), time: 1, data: { nested: [null, true] }, ignorable: true, ...metadata,
  123. } as unknown as SessionEvent
  124. const { ctx, session } = await harness('wire-opaque', [event])
  125. const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  126. expect(prepared.fields.dsh_session_log?.events[0]).toStrictEqual(event)
  127. expect(session.deriveMessages()).toEqual([])
  128. }
  129. })
  130. it.each(['turn/start', 'assistant/attempt', 'request/context', 'tool/ptc-dispatch'])('rejects known log-only %s metadata before uploading', async (type) => {
  131. for (const metadata of [{ surfaceOp: 'append' }, { sourceEventSeqs: [0] }]) {
  132. const event = {
  133. type, seq: SessionSeq(0), time: 1, data: { turn: 1, step: 1, stream: [] }, ignorable: true, ...metadata,
  134. } as unknown as SessionEvent
  135. await expect(harness('wire-invalid', [event])).rejects.toThrow(/not surface-eligible/)
  136. }
  137. })
  138. it('does not contribute the session log when explicitly disabled', async () => {
  139. const ctx = new Context()
  140. contexts.push(ctx)
  141. await ctx.plugin(SessionStore)
  142. await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
  143. await ctx.plugin(SessionLogDeepSeek, { enabled: false })
  144. const session = ctx.sessions.create(SessionId('explicit-off'))
  145. session.append('turn/start', { turn: 1 })
  146. const prepared = await ctx.deepseekLlmApiExtensions.prepare({
  147. body: body(), signal: SIGNAL, sessionId: session.id,
  148. })
  149. expect(prepared.fields).not.toHaveProperty('dsh_session_log')
  150. })
  151. it('uploads the full first prefix, records acceptance, then sends only the appended suffix', async () => {
  152. const { ctx, session } = await harness('incremental')
  153. session.append('turn/start', { turn: 1 })
  154. session.append('step/start', { turn: 1, step: 1 })
  155. const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  156. const firstPayload = first.fields.dsh_session_log
  157. expect(firstPayload).toMatchObject({
  158. sessionFormatVersion: SESSION_FORMAT_VERSION,
  159. afterSeq: -1,
  160. throughSeq: 1,
  161. })
  162. expect(firstPayload?.events).toHaveLength(2)
  163. await first.accept()
  164. expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1)
  165. expect(session.snapshotEvents().at(-1)?.data).toEqual({
  166. sessionId: session.id,
  167. throughSeq: 1,
  168. sessionFormatVersion: SESSION_FORMAT_VERSION,
  169. })
  170. session.append('step/end', { turn: 1, step: 1 })
  171. const second = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  172. expect(second.fields.dsh_session_log).toMatchObject({ afterSeq: 1, throughSeq: 3 })
  173. expect(second.fields.dsh_session_log?.events).toHaveLength(2)
  174. expect(second.fields.dsh_session_log?.events[0]).toMatchObject({
  175. type: 'session-log-deepseek/delivery-accepted',
  176. seq: 2,
  177. })
  178. })
  179. it('reconstructs a persisted cursor and ignores an inherited parent watermark in a fork', async () => {
  180. const first = await harness('parent')
  181. first.session.append('turn/start', { turn: 1 })
  182. const prepared = await first.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: first.session.id })
  183. await prepared.accept()
  184. const seed = first.session.snapshotEvents()
  185. const resumed = await harness('parent', seed)
  186. expect(SessionLogDeepSeek.acceptedThrough(resumed.session)).toBe(0)
  187. const resumedPayload = await resumed.ctx.deepseekLlmApiExtensions.prepare({
  188. body: body(), signal: SIGNAL, sessionId: resumed.session.id,
  189. })
  190. expect(resumedPayload.fields.dsh_session_log?.afterSeq).toBe(0)
  191. const fork = await harness('child', seed, {
  192. inheritedEventCount: SessionLogOffset(seed.length),
  193. meta: { parentSession: first.session.id, isSeeded: true },
  194. })
  195. expect(SessionLogDeepSeek.acceptedThrough(fork.session)).toBe(-1)
  196. const forkPayload = await fork.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: fork.session.id })
  197. expect(forkPayload.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: fork.session.seq - 1 })
  198. })
  199. it('takes the maximum watermark when concurrent acceptances settle out of order', async () => {
  200. const { ctx, session } = await harness('concurrent')
  201. session.append('turn/start', { turn: 1 })
  202. const earlier = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  203. session.append('step/start', { turn: 1, step: 1 })
  204. const later = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  205. await later.accept()
  206. await earlier.accept()
  207. expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1)
  208. })
  209. it('folds only events appended after the cached acceptance scan', () => {
  210. const id = SessionId('incremental-fold')
  211. const events: SessionEvent[] = [
  212. { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
  213. {
  214. type: 'session-log-deepseek/delivery-accepted',
  215. seq: SessionSeq(1),
  216. time: 2,
  217. data: {
  218. sessionId: id,
  219. throughSeq: SessionSeq(0),
  220. sessionFormatVersion: SESSION_FORMAT_VERSION,
  221. },
  222. },
  223. ]
  224. let reads = 0
  225. const session = {
  226. id,
  227. header: { version: SESSION_FORMAT_VERSION },
  228. get seq() { return SessionLogOffset(events.length) },
  229. eventAt(seq: ReturnType<typeof SessionSeq>) {
  230. reads++
  231. return events[seq]
  232. },
  233. } as unknown as Session
  234. expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
  235. expect(reads).toBe(2)
  236. reads = 0
  237. expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
  238. expect(reads).toBe(0)
  239. events.push(
  240. { type: 'step/start', seq: SessionSeq(2), time: 3, data: { turn: 1, step: 1 } },
  241. {
  242. type: 'session-log-deepseek/delivery-accepted',
  243. seq: SessionSeq(3),
  244. time: 4,
  245. data: {
  246. sessionId: id,
  247. throughSeq: SessionSeq(2),
  248. sessionFormatVersion: SESSION_FORMAT_VERSION,
  249. },
  250. },
  251. )
  252. expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(2)
  253. expect(reads).toBe(2)
  254. })
  255. it('rejects a missing event below the captured Session length', () => {
  256. const session = {
  257. id: SessionId('missing-event'),
  258. header: { version: SESSION_FORMAT_VERSION },
  259. seq: SessionLogOffset(1),
  260. eventAt: () => undefined,
  261. } as unknown as Session
  262. expect(() => SessionLogDeepSeek.acceptedThrough(session))
  263. .toThrow('session-log-deepseek: missing event 0 below captured length 1')
  264. })
  265. it('ignores another format generation before interpreting its frozen sequence', () => {
  266. const id = SessionId('migrated-generation')
  267. const events = [
  268. { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
  269. {
  270. type: 'session-log-deepseek/delivery-accepted',
  271. seq: SessionSeq(1),
  272. time: 2,
  273. data: { sessionId: id, throughSeq: SessionSeq(99) },
  274. },
  275. { type: 'step/start', seq: SessionSeq(2), time: 3, data: { turn: 1, step: 1 } },
  276. {
  277. type: 'session-log-deepseek/delivery-accepted',
  278. seq: SessionSeq(3),
  279. time: 4,
  280. data: {
  281. sessionId: id,
  282. throughSeq: SessionSeq(2),
  283. sessionFormatVersion: SESSION_FORMAT_VERSION,
  284. },
  285. },
  286. ] as SessionEvent[]
  287. const migrated = {
  288. id,
  289. header: { version: SESSION_FORMAT_VERSION },
  290. get seq() { return SessionLogOffset(events.length) },
  291. eventAt: (seq: ReturnType<typeof SessionSeq>) => events[seq],
  292. } as unknown as Session
  293. expect(SessionLogDeepSeek.acceptedThrough(migrated)).toBe(2)
  294. })
  295. it.each([-1, -0, 0.5])('rejects malformed acceptance format version %s', (sessionFormatVersion) => {
  296. const id = SessionId(`malformed-format-${sessionFormatVersion}`)
  297. const events = [{
  298. type: 'session-log-deepseek/delivery-accepted',
  299. seq: SessionSeq(0),
  300. time: 1,
  301. data: { sessionId: id, throughSeq: SessionSeq(0), sessionFormatVersion },
  302. }] as unknown as SessionEvent[]
  303. const session = {
  304. id,
  305. header: { version: SESSION_FORMAT_VERSION },
  306. get seq() { return SessionLogOffset(events.length) },
  307. eventAt: (seq: ReturnType<typeof SessionSeq>) => events[seq],
  308. } as unknown as Session
  309. expect(() => SessionLogDeepSeek.acceptedThrough(session)).toThrow(/malformed acceptance format version/)
  310. })
  311. it('omits the field for direct or stale requests and uploads the prior acceptance marker next', async () => {
  312. const { ctx, session } = await harness('edges')
  313. await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL }))
  314. .resolves.toMatchObject({ fields: {} })
  315. await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: 'missing' }))
  316. .resolves.toMatchObject({ fields: {} })
  317. await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }))
  318. .resolves.toMatchObject({ fields: {} })
  319. session.append('turn/start', { turn: 1 })
  320. const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  321. await first.accept()
  322. const current = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
  323. expect(current.fields.dsh_session_log).toMatchObject({
  324. afterSeq: 0,
  325. throughSeq: 1,
  326. events: [{ type: 'session-log-deepseek/delivery-accepted' }],
  327. })
  328. })
  329. it('contributes complete events without reading request messages', async () => {
  330. const { ctx, session } = await harness('direct-events')
  331. session.append('turn/start', { turn: 1 })
  332. const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: {}, signal: SIGNAL, sessionId: session.id })
  333. expect(prepared.fields.dsh_session_log?.events).toEqual(session.snapshotEvents())
  334. })
  335. it('translates logical brands and isSeeded into the raw upload DTO', async () => {
  336. const firstMessage = createUserMessage({
  337. content: [{ type: 'text', text: 'first' }],
  338. source: { kind: 'user' },
  339. })
  340. const replacementMessage = createUserMessage({
  341. content: [{ type: 'text', text: 'replacement' }],
  342. source: { kind: 'user' },
  343. })
  344. const seed = [
  345. {
  346. type: 'user/message',
  347. seq: SessionSeq(0),
  348. time: 1,
  349. data: firstMessage,
  350. ignorable: true,
  351. surfaceOp: 'append',
  352. },
  353. {
  354. type: 'user/message',
  355. seq: SessionSeq(1),
  356. time: 2,
  357. data: replacementMessage,
  358. sourceEventSeqs: [SessionSeq(0)],
  359. surfaceOp: { op: 'replace', startSeq: SessionSeq(0), endSeq: SessionSeq(0) },
  360. },
  361. ] satisfies SessionEvent[]
  362. const { ctx, session } = await harness('wire-child', seed, {
  363. inheritedEventCount: SessionLogOffset(seed.length),
  364. meta: {
  365. cwd: '/wire-workspace',
  366. parentSession: SessionId('wire-parent'),
  367. isSeeded: true,
  368. origin: 'subagent',
  369. delegationDepth: 1,
  370. agentPreset: 'minimal',
  371. },
  372. })
  373. const prepared = await ctx.deepseekLlmApiExtensions.prepare({
  374. body: body(), signal: SIGNAL, sessionId: session.id,
  375. })
  376. const wire = JSON.parse(JSON.stringify(prepared.fields.dsh_session_log)) as Record<string, unknown>
  377. expect(wire.session).toMatchObject({
  378. version: SESSION_FORMAT_VERSION,
  379. id: 'wire-child',
  380. cwd: '/wire-workspace',
  381. parentSession: 'wire-parent',
  382. seedLength: seed.length,
  383. origin: 'subagent',
  384. delegationDepth: 1,
  385. agentPreset: 'minimal',
  386. })
  387. expect(wire.session).not.toHaveProperty('isSeeded')
  388. expect(typeof wire.afterSeq).toBe('number')
  389. expect(typeof wire.throughSeq).toBe('number')
  390. expect(Array.isArray(wire.events)).toBe(true)
  391. const events = Array.isArray(wire.events) ? wire.events : []
  392. expect(events[0]).toMatchObject({
  393. seq: 0,
  394. ignorable: true,
  395. surfaceOp: 'append',
  396. })
  397. expect(events[0]).not.toHaveProperty('sourceEventSeqs')
  398. expect(events[1]).toMatchObject({
  399. seq: 1,
  400. sourceEventSeqs: [0],
  401. surfaceOp: { op: 'replace', startSeq: 0, endSeq: 0 },
  402. })
  403. })
  404. it('translates ignorable and surface event envelopes to raw wire values', async () => {
  405. const seed: SessionEvent[] = [
  406. { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
  407. {
  408. type: 'user/message',
  409. seq: SessionSeq(1),
  410. time: 2,
  411. data: createUserMessage({
  412. content: [{ type: 'text', text: 'first' }],
  413. source: { kind: 'user' },
  414. }),
  415. ignorable: true,
  416. sourceEventSeqs: [SessionSeq(0)],
  417. surfaceOp: 'append',
  418. },
  419. {
  420. type: 'user/message',
  421. seq: SessionSeq(2),
  422. time: 3,
  423. data: createUserMessage({
  424. content: [{ type: 'text', text: 'replacement' }],
  425. source: { kind: 'user' },
  426. }),
  427. sourceEventSeqs: [SessionSeq(1)],
  428. surfaceOp: { op: 'replace', startSeq: SessionSeq(1), endSeq: SessionSeq(1) },
  429. },
  430. ]
  431. const { ctx, session } = await harness('wire-events', seed)
  432. const prepared = await ctx.deepseekLlmApiExtensions.prepare({
  433. body: body(), signal: SIGNAL, sessionId: session.id,
  434. })
  435. const events = prepared.fields.dsh_session_log?.events ?? []
  436. expect(events[0]).not.toHaveProperty('surfaceOp')
  437. expect(events[0]).not.toHaveProperty('sourceEventSeqs')
  438. expect(events[1]).toMatchObject({
  439. type: 'user/message',
  440. ignorable: true,
  441. sourceEventSeqs: [0],
  442. surfaceOp: 'append',
  443. })
  444. expect(events[2]).toMatchObject({
  445. type: 'user/message',
  446. sourceEventSeqs: [1],
  447. surfaceOp: { op: 'replace', startSeq: 1, endSeq: 1 },
  448. })
  449. })
  450. it('fails closed on a malformed persisted acceptance watermark', async () => {
  451. const malformed = [{
  452. type: 'session-log-deepseek/delivery-accepted',
  453. seq: 0,
  454. time: 1,
  455. data: {
  456. sessionId: 'malformed',
  457. throughSeq: 0,
  458. sessionFormatVersion: SESSION_FORMAT_VERSION,
  459. },
  460. }] as unknown as SessionEvent[]
  461. const session = Session.create(SessionId('malformed'), malformed)
  462. expect(() => SessionLogDeepSeek.acceptedThrough(session)).toThrow(/malformed acceptance watermark/)
  463. })
  464. it('rejects a negative persisted acceptance watermark before comparing it', () => {
  465. const id = SessionId('negative-watermark')
  466. const events = [{
  467. type: 'session-log-deepseek/delivery-accepted',
  468. seq: SessionSeq(0),
  469. time: 1,
  470. data: {
  471. sessionId: id,
  472. throughSeq: -1,
  473. sessionFormatVersion: SESSION_FORMAT_VERSION,
  474. },
  475. }] as unknown as SessionEvent[]
  476. const session = {
  477. id,
  478. header: { version: SESSION_FORMAT_VERSION },
  479. get seq() { return SessionLogOffset(events.length) },
  480. eventAt: (seq: ReturnType<typeof SessionSeq>) => events[seq],
  481. } as unknown as Session
  482. expect(() => SessionLogDeepSeek.acceptedThrough(session)).toThrow(/malformed acceptance watermark/)
  483. })
  484. it('withdraws its request field when the contributing plugin reloads', async () => {
  485. const { ctx, session, disposeUpload } = await harness('hmr')
  486. session.append('turn/start', { turn: 1 })
  487. expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields)
  488. .toHaveProperty('dsh_session_log')
  489. await disposeUpload()
  490. expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields)
  491. .not.toHaveProperty('dsh_session_log')
  492. })
  493. })