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 })