message-feedback-protocol.snapshot.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { readFile } from 'node:fs/promises'
  2. import { join } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { afterAll, beforeAll, describe, expect, it } from 'vitest'
  5. import {
  6. assertFixtureInventory,
  7. compareOrRefreshGolden,
  8. launchWebScaffold,
  9. seedSession,
  10. type WebScaffold,
  11. } from './scaffold.ts'
  12. const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url))
  13. const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
  14. const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json')
  15. const SESSION_ID = 'message-feedback-protocol'
  16. const MESSAGE_ID = '11111111-1111-4111-8111-111111111111'
  17. interface ProtocolExchange {
  18. readonly endpoint: string
  19. readonly request: unknown
  20. readonly status: number
  21. readonly response: unknown
  22. }
  23. function isRecord(value: unknown): value is Record<string, unknown> {
  24. return typeof value === 'object' && value !== null
  25. }
  26. /** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */
  27. function createdVersion(response: unknown): string {
  28. if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true
  29. || !isRecord(response.result.value) || response.result.value.ok !== true
  30. || !isRecord(response.result.value.value)
  31. || typeof response.result.value.value.version !== 'string') {
  32. throw new Error('messageFeedback.put did not return a successful versioned item')
  33. }
  34. return response.result.value.value.version
  35. }
  36. /** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */
  37. function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string {
  38. return JSON.stringify(exchanges, (key, value: unknown) => {
  39. if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}'
  40. if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}'
  41. return value
  42. }, 2)
  43. }
  44. describe('message feedback Host Remote protocol', () => {
  45. let scaffold: WebScaffold
  46. beforeAll(async () => {
  47. scaffold = await launchWebScaffold()
  48. await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID)
  49. })
  50. afterAll(async () => {
  51. await scaffold?.close()
  52. })
  53. it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => {
  54. const exchanges: ProtocolExchange[] = []
  55. const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise<unknown> => {
  56. const payload = { args: { request } }
  57. const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, {
  58. method: 'POST',
  59. headers: { 'content-type': 'application/json' },
  60. body: JSON.stringify({
  61. type: 'client-request',
  62. rpcId,
  63. method: endpoint,
  64. payload,
  65. }),
  66. })
  67. const body: unknown = await response.json()
  68. exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body })
  69. return body
  70. }
  71. await invoke('feedback-invalid', 'messageFeedback/put', {
  72. sessionId: SESSION_ID,
  73. messageId: MESSAGE_ID,
  74. rating: 'invalid-rating',
  75. ifVersion: null,
  76. })
  77. await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID })
  78. const created = await invoke('feedback-put', 'messageFeedback/put', {
  79. sessionId: SESSION_ID,
  80. messageId: MESSAGE_ID,
  81. rating: 'positive',
  82. note: 'Useful answer',
  83. ifVersion: null,
  84. })
  85. const version = createdVersion(created)
  86. expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
  87. await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID })
  88. await invoke('feedback-conflict', 'messageFeedback/put', {
  89. sessionId: SESSION_ID,
  90. messageId: MESSAGE_ID,
  91. rating: 'negative',
  92. ifVersion: null,
  93. })
  94. await invoke('feedback-delete', 'messageFeedback/delete', {
  95. sessionId: SESSION_ID,
  96. messageId: MESSAGE_ID,
  97. ifVersion: version,
  98. })
  99. await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID })
  100. expect(exchanges.every(exchange => exchange.status === 200)).toBe(true)
  101. await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode)
  102. await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl'])
  103. })
  104. })