/ src / lib / server / run-context.test.ts
run-context.test.ts
  1  import assert from 'node:assert/strict'
  2  import { test } from 'node:test'
  3  
  4  import {
  5    ensureRunContext,
  6    dedup,
  7    pruneRunContext,
  8    foldReflectionIntoRunContext,
  9    buildRunContextSection,
 10    extractFactsFromMessages,
 11  } from './run-context'
 12  import type { RunContext, RunReflection, Message } from '@/types'
 13  
 14  // ---------------------------------------------------------------------------
 15  // ensureRunContext
 16  // ---------------------------------------------------------------------------
 17  
 18  test('ensureRunContext returns fresh context for null input', () => {
 19    const ctx = ensureRunContext(null)
 20    assert.equal(ctx.version, 0)
 21    assert.equal(ctx.objective, null)
 22    assert.deepEqual(ctx.keyFacts, [])
 23    assert.deepEqual(ctx.discoveries, [])
 24    assert.deepEqual(ctx.failedApproaches, [])
 25    assert.deepEqual(ctx.constraints, [])
 26    assert.deepEqual(ctx.currentPlan, [])
 27    assert.deepEqual(ctx.completedSteps, [])
 28    assert.deepEqual(ctx.blockers, [])
 29    assert.equal(ctx.parentContext, null)
 30  })
 31  
 32  test('ensureRunContext returns fresh context for undefined input', () => {
 33    const ctx = ensureRunContext(undefined)
 34    assert.equal(ctx.version, 0)
 35    assert.deepEqual(ctx.keyFacts, [])
 36  })
 37  
 38  test('ensureRunContext passes through a valid RunContext', () => {
 39    const existing: RunContext = {
 40      objective: 'Ship it',
 41      constraints: ['No breaking changes'],
 42      keyFacts: ['Fact A'],
 43      discoveries: [],
 44      failedApproaches: [],
 45      currentPlan: ['Step 1'],
 46      completedSteps: [],
 47      blockers: [],
 48      parentContext: null,
 49      updatedAt: 1000,
 50      version: 5,
 51    }
 52  
 53    const result = ensureRunContext(existing)
 54    assert.equal(result, existing) // same reference
 55    assert.equal(result.version, 5)
 56    assert.deepEqual(result.keyFacts, ['Fact A'])
 57  })
 58  
 59  test('ensureRunContext backfills missing arrays on malformed object with version', () => {
 60    // Simulates persisted data where some array fields were stripped/corrupted
 61    const malformed = { version: 3, objective: 'Fix it', updatedAt: 1 } as unknown as RunContext
 62  
 63    const result = ensureRunContext(malformed)
 64    assert.equal(result.version, 3) // preserves version
 65    assert.equal(result.objective, 'Fix it')
 66    assert.deepEqual(result.constraints, [])
 67    assert.deepEqual(result.keyFacts, [])
 68    assert.deepEqual(result.discoveries, [])
 69    assert.deepEqual(result.failedApproaches, [])
 70    assert.deepEqual(result.currentPlan, [])
 71    assert.deepEqual(result.completedSteps, [])
 72    assert.deepEqual(result.blockers, [])
 73  })
 74  
 75  test('ensureRunContext returns fresh context for object without version field', () => {
 76    const noVersion = { objective: 'Something', keyFacts: ['A'] } as unknown as RunContext
 77    const result = ensureRunContext(noVersion)
 78    assert.equal(result.version, 0)
 79    assert.deepEqual(result.keyFacts, [])
 80    assert.notEqual(result, noVersion) // new object
 81  })
 82  
 83  // ---------------------------------------------------------------------------
 84  // dedup
 85  // ---------------------------------------------------------------------------
 86  
 87  test('dedup removes case-insensitive duplicates', () => {
 88    assert.deepEqual(dedup(['Hello World', 'hello world', 'HELLO WORLD']), ['Hello World'])
 89  })
 90  
 91  test('dedup normalizes whitespace', () => {
 92    assert.deepEqual(dedup(['too   many   spaces', 'too many spaces']), ['too many spaces'])
 93  })
 94  
 95  test('dedup filters out empty and blank strings', () => {
 96    assert.deepEqual(dedup(['valid', '', '   ', '\t\n', 'also valid']), ['valid', 'also valid'])
 97  })
 98  
 99  test('dedup preserves order of first occurrence', () => {
100    assert.deepEqual(dedup(['B', 'A', 'b', 'C', 'a']), ['B', 'A', 'C'])
101  })
102  
103  // ---------------------------------------------------------------------------
104  // pruneRunContext
105  // ---------------------------------------------------------------------------
106  
107  test('pruneRunContext enforces array caps keeping most recent entries', () => {
108    const ctx = ensureRunContext(null)
109    // keyFacts cap is 20 — fill with 25
110    ctx.keyFacts = Array.from({ length: 25 }, (_, i) => `fact-${i}`)
111    // blockers cap is 8 — fill with 12
112    ctx.blockers = Array.from({ length: 12 }, (_, i) => `blocker-${i}`)
113  
114    const pruned = pruneRunContext(ctx)
115    assert.equal(pruned.keyFacts.length, 20)
116    assert.equal(pruned.keyFacts[0], 'fact-5') // sliced from end
117    assert.equal(pruned.keyFacts[19], 'fact-24')
118    assert.equal(pruned.blockers.length, 8)
119    assert.equal(pruned.blockers[0], 'blocker-4')
120  })
121  
122  test('pruneRunContext leaves arrays under cap unchanged', () => {
123    const ctx = ensureRunContext(null)
124    ctx.keyFacts = ['a', 'b', 'c']
125    const pruned = pruneRunContext(ctx)
126    assert.deepEqual(pruned.keyFacts, ['a', 'b', 'c'])
127  })
128  
129  // ---------------------------------------------------------------------------
130  // foldReflectionIntoRunContext
131  // ---------------------------------------------------------------------------
132  
133  test('foldReflectionIntoRunContext creates context from null and maps reflection fields', () => {
134    const reflection = {
135      id: 'r1',
136      runId: 'run1',
137      sessionId: 's1',
138      source: 'test',
139      status: 'completed' as const,
140      summary: 'Test reflection',
141      invariantNotes: ['Invariant A'],
142      lessonNotes: ['Lesson B'],
143      derivedNotes: ['Derived C'],
144      significantEventNotes: ['Event D'],
145      failureNotes: ['Failure E'],
146      openLoopNotes: ['Open loop F'],
147      boundaryNotes: ['Boundary G'],
148      createdAt: 1,
149      updatedAt: 1,
150    } satisfies Partial<RunReflection> as RunReflection
151  
152    const ctx = foldReflectionIntoRunContext(null, reflection)
153    assert.equal(ctx.version, 1)
154    assert.ok(ctx.keyFacts.includes('Invariant A'))
155    assert.ok(ctx.keyFacts.includes('Lesson B'))
156    assert.ok(ctx.discoveries.includes('Derived C'))
157    assert.ok(ctx.discoveries.includes('Event D'))
158    assert.ok(ctx.failedApproaches.includes('Failure E'))
159    assert.ok(ctx.blockers.includes('Open loop F'))
160    assert.ok(ctx.constraints.includes('Boundary G'))
161  })
162  
163  test('foldReflectionIntoRunContext deduplicates when folding', () => {
164    const existing: RunContext = {
165      objective: null,
166      constraints: [],
167      keyFacts: ['Already known fact'],
168      discoveries: [],
169      failedApproaches: [],
170      currentPlan: [],
171      completedSteps: [],
172      blockers: [],
173      parentContext: null,
174      updatedAt: 1,
175      version: 2,
176    }
177  
178    const reflection = {
179      id: 'r2',
180      runId: 'run2',
181      sessionId: 's2',
182      source: 'test',
183      status: 'completed' as const,
184      summary: 'Dup test',
185      invariantNotes: ['already known fact'], // same content, different case
186      derivedNotes: [],
187      failureNotes: [],
188      lessonNotes: [],
189      createdAt: 1,
190      updatedAt: 1,
191    } satisfies Partial<RunReflection> as RunReflection
192  
193    const ctx = foldReflectionIntoRunContext(existing, reflection)
194    assert.equal(ctx.keyFacts.length, 1)
195    assert.equal(ctx.keyFacts[0], 'Already known fact') // keeps original casing
196  })
197  
198  test('foldReflectionIntoRunContext increments version', () => {
199    const existing = ensureRunContext(null)
200    existing.version = 4
201  
202    const reflection = {
203      id: 'r3',
204      runId: 'run3',
205      sessionId: 's3',
206      source: 'test',
207      status: 'completed' as const,
208      summary: 'Version test',
209      invariantNotes: [],
210      derivedNotes: [],
211      failureNotes: [],
212      lessonNotes: [],
213      createdAt: 1,
214      updatedAt: 1,
215    } satisfies Partial<RunReflection> as RunReflection
216  
217    const ctx = foldReflectionIntoRunContext(existing, reflection)
218    assert.equal(ctx.version, 5)
219  })
220  
221  // ---------------------------------------------------------------------------
222  // buildRunContextSection
223  // ---------------------------------------------------------------------------
224  
225  test('buildRunContextSection returns null for null context', () => {
226    assert.equal(buildRunContextSection(null, false), null)
227  })
228  
229  test('buildRunContextSection returns null for minimal prompt', () => {
230    const ctx = ensureRunContext(null)
231    ctx.keyFacts = ['Something important']
232    assert.equal(buildRunContextSection(ctx, true), null)
233  })
234  
235  test('buildRunContextSection returns null for empty context', () => {
236    const ctx = ensureRunContext(null)
237    assert.equal(buildRunContextSection(ctx, false), null)
238  })
239  
240  test('buildRunContextSection renders all non-empty fields', () => {
241    const ctx: RunContext = {
242      objective: 'Fix the pipeline',
243      constraints: ['No downtime'],
244      keyFacts: ['Build passes locally'],
245      discoveries: ['Staging uses different auth'],
246      failedApproaches: ['Restart did not help'],
247      currentPlan: ['Investigate auth', 'Deploy fix'],
248      completedSteps: ['Investigate auth'],
249      blockers: ['Waiting on credentials'],
250      parentContext: 'Coordinator wants a contained fix',
251      updatedAt: 1,
252      version: 3,
253    }
254  
255    const section = buildRunContextSection(ctx, false)
256    assert.ok(section)
257    assert.match(section, /Working Memory \(RunContext\)/)
258    assert.match(section, /Coordinator Context/)
259    assert.match(section, /Coordinator wants a contained fix/)
260    assert.match(section, /Current Objective/)
261    assert.match(section, /Fix the pipeline/)
262    assert.match(section, /Constraints/)
263    assert.match(section, /No downtime/)
264    assert.match(section, /Key Facts/)
265    assert.match(section, /Build passes locally/)
266    assert.match(section, /Already Tried \(Failed\)/)
267    assert.match(section, /Restart did not help/)
268    assert.match(section, /Current Plan/)
269    assert.match(section, /Blockers/)
270    assert.match(section, /Waiting on credentials/)
271    assert.match(section, /Discoveries/)
272    assert.match(section, /Staging uses different auth/)
273  })
274  
275  test('buildRunContextSection renders plan with checkboxes for completed steps', () => {
276    const ctx: RunContext = {
277      objective: null,
278      constraints: [],
279      keyFacts: [],
280      discoveries: [],
281      failedApproaches: [],
282      currentPlan: ['Step A', 'Step B', 'Step C'],
283      completedSteps: ['step a', 'Step C'], // case-insensitive match
284      blockers: [],
285      parentContext: null,
286      updatedAt: 1,
287      version: 1,
288    }
289  
290    const section = buildRunContextSection(ctx, false)
291    assert.ok(section)
292    assert.match(section, /\[x\] Step A/)
293    assert.match(section, /\[ \] Step B/)
294    assert.match(section, /\[x\] Step C/)
295  })
296  
297  test('buildRunContextSection respects budget cap', () => {
298    const ctx: RunContext = {
299      objective: 'A'.repeat(2000),
300      constraints: ['B'.repeat(2000)],
301      keyFacts: ['C'.repeat(2000)],
302      discoveries: [],
303      failedApproaches: [],
304      currentPlan: [],
305      completedSteps: [],
306      blockers: [],
307      parentContext: null,
308      updatedAt: 1,
309      version: 1,
310    }
311  
312    const section = buildRunContextSection(ctx, false)
313    assert.ok(section)
314    // The section should exist but be bounded. At 3000 char budget,
315    // not all fields can fit with 2000-char values.
316    assert.ok(section.length < 4000) // header + budget
317  })
318  
319  // ---------------------------------------------------------------------------
320  // extractFactsFromMessages
321  // ---------------------------------------------------------------------------
322  
323  test('extractFactsFromMessages extracts facts matching keyword patterns', () => {
324    const messages = [
325      { text: 'I discovered that the API key must always be rotated monthly for compliance reasons.' },
326      { text: 'Note: the staging environment uses a separate auth service from production.' },
327    ] as Message[]
328  
329    const result = extractFactsFromMessages(messages)
330    assert.ok(result.keyFacts.length > 0)
331  })
332  
333  test('extractFactsFromMessages categorizes error patterns as failedApproaches', () => {
334    const messages = [
335      { text: "The migration failed: the schema was incompatible with the target version due to column ordering." },
336    ] as Message[]
337  
338    const result = extractFactsFromMessages(messages)
339    assert.ok(result.failedApproaches.length > 0)
340    assert.ok(result.failedApproaches.some((f) => /schema/i.test(f)))
341  })
342  
343  test('extractFactsFromMessages deduplicates results', () => {
344    const messages = [
345      { text: 'Important: always validate the input before processing the request payload.' },
346      { text: 'important: always validate the input before processing the request payload.' },
347    ] as Message[]
348  
349    const result = extractFactsFromMessages(messages)
350    // Same fact stated twice (different case) should be deduped
351    const matching = result.keyFacts.filter((f) => /validate the input/i.test(f))
352    assert.ok(matching.length <= 1)
353  })
354  
355  test('extractFactsFromMessages ignores short messages', () => {
356    const messages = [
357      { text: 'OK' },
358      { text: 'Sure, noted.' },
359      { text: '' },
360    ] as Message[]
361  
362    const result = extractFactsFromMessages(messages)
363    assert.equal(result.keyFacts.length, 0)
364    assert.equal(result.failedApproaches.length, 0)
365  })