derived-artifact-storage-stress.test.mjs
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠ breaking
3 hours ago
| 1 | /** |
| 2 | * Tier 4 — STRESS: Phase 6 derived-artifact storage layer. |
| 3 | * |
| 4 | * Covers (§10 Stress obligations): |
| 5 | * - High-volume concurrent writes across all three stores |
| 6 | * - Large embedding batch writes through the writer |
| 7 | * - Many delegated-write authorization checks under concurrency |
| 8 | * - Deletion fan-out across stores under load |
| 9 | * - No partial/half-write under contention |
| 10 | */ |
| 11 | |
| 12 | import { describe, it } from 'node:test'; |
| 13 | import assert from 'node:assert/strict'; |
| 14 | |
| 15 | import { createDerivedArtifactWriter } from '../lib/companion-artifact-writer.mjs'; |
| 16 | import { buildConvenienceProvenance } from '../lib/companion-provenance-validator.mjs'; |
| 17 | import { TERMINAL_STATES } from '../lib/companion-tier-resolver.mjs'; |
| 18 | |
| 19 | const CONCURRENCY = 50; |
| 20 | const BATCH_SIZE = 200; |
| 21 | |
| 22 | function buildStressStores() { |
| 23 | const frontmatter = new Map(); |
| 24 | const vectors = []; |
| 25 | const insights = []; |
| 26 | const maintenance = []; |
| 27 | let writeCalls = 0; |
| 28 | let upsertCalls = 0; |
| 29 | |
| 30 | const writeNoteFn = (_vp, notePath, opts) => { |
| 31 | writeCalls++; |
| 32 | const prev = frontmatter.get(notePath) ?? {}; |
| 33 | frontmatter.set(notePath, { ...prev, ...opts.frontmatter }); |
| 34 | }; |
| 35 | |
| 36 | const vectorStore = { |
| 37 | upsert: async (points) => { |
| 38 | upsertCalls++; |
| 39 | vectors.push(...points); |
| 40 | }, |
| 41 | deleteByPath: async (notePath) => { |
| 42 | const idx = vectors.findIndex((v) => v.path === notePath); |
| 43 | if (idx >= 0) vectors.splice(idx, 1); |
| 44 | }, |
| 45 | }; |
| 46 | |
| 47 | const mm = { |
| 48 | store: (type, data) => { |
| 49 | if (type === 'insight') insights.push(data); |
| 50 | if (type === 'maintenance') maintenance.push(data); |
| 51 | return { id: `mem_${Date.now()}_${Math.random()}`, ts: new Date().toISOString() }; |
| 52 | }, |
| 53 | }; |
| 54 | |
| 55 | return { |
| 56 | frontmatter, vectors, insights, maintenance, |
| 57 | writeNoteFn, vectorStore, mm, |
| 58 | get writeCalls() { return writeCalls; }, |
| 59 | get upsertCalls() { return upsertCalls; }, |
| 60 | }; |
| 61 | } |
| 62 | |
| 63 | function makeProvenance(artifactType, notePath, idx) { |
| 64 | return buildConvenienceProvenance({ |
| 65 | generatedBy: `user-${idx}`, |
| 66 | source: 'companion', |
| 67 | model: 'stress-model', |
| 68 | modelVersion: '1.0', |
| 69 | lane: 'local', |
| 70 | artifactType, |
| 71 | sourceNotePath: notePath, |
| 72 | sourceEventId: `mem_${idx}`, |
| 73 | }); |
| 74 | } |
| 75 | |
| 76 | function selfCtx() { |
| 77 | return { |
| 78 | lane: 'local', |
| 79 | containsPrivateData: false, |
| 80 | isDelegate: false, |
| 81 | delegatedManagedAllowed: false, |
| 82 | enrichesDelegatedPartition: false, |
| 83 | delegatedEnrichmentAllowed: false, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | describe('stress — concurrent ai_summary writes', () => { |
| 88 | it(`${CONCURRENCY} concurrent writes all succeed without partial state`, async () => { |
| 89 | const stores = buildStressStores(); |
| 90 | const writer = createDerivedArtifactWriter({ |
| 91 | writeNoteFn: stores.writeNoteFn, |
| 92 | vaultPath: '/vault', |
| 93 | mm: stores.mm, |
| 94 | }); |
| 95 | |
| 96 | const tasks = Array.from({ length: CONCURRENCY }, (_, i) => { |
| 97 | const notePath = `notes/stress-${i}.md`; |
| 98 | const prov = makeProvenance('ai_summary', notePath, i); |
| 99 | return writer.write({ summary: `Summary for note ${i}` }, prov, selfCtx()); |
| 100 | }); |
| 101 | |
| 102 | const results = await Promise.all(tasks); |
| 103 | |
| 104 | const failed = results.filter((r) => !r.ok); |
| 105 | assert.equal(failed.length, 0, `All ${CONCURRENCY} writes must succeed`); |
| 106 | assert.equal(stores.writeCalls, CONCURRENCY, `writeNoteFn called exactly ${CONCURRENCY} times`); |
| 107 | }); |
| 108 | }); |
| 109 | |
| 110 | describe('stress — large embedding batch', () => { |
| 111 | it(`${BATCH_SIZE} sequential embedding writes all succeed`, async () => { |
| 112 | const stores = buildStressStores(); |
| 113 | const writer = createDerivedArtifactWriter({ |
| 114 | writeNoteFn: stores.writeNoteFn, |
| 115 | vaultPath: '/vault', |
| 116 | vectorStore: stores.vectorStore, |
| 117 | }); |
| 118 | |
| 119 | let successCount = 0; |
| 120 | for (let i = 0; i < BATCH_SIZE; i++) { |
| 121 | const notePath = `embeddings/note-${i}.md`; |
| 122 | const prov = makeProvenance('embedding', notePath, i); |
| 123 | const r = await writer.write( |
| 124 | { vector: [i * 0.01, i * 0.02, i * 0.03], payload: { path: notePath } }, |
| 125 | prov, |
| 126 | selfCtx(), |
| 127 | ); |
| 128 | if (r.ok) successCount++; |
| 129 | } |
| 130 | |
| 131 | assert.equal(successCount, BATCH_SIZE); |
| 132 | assert.equal(stores.vectors.length, BATCH_SIZE); |
| 133 | assert.equal(stores.upsertCalls, BATCH_SIZE); |
| 134 | }); |
| 135 | }); |
| 136 | |
| 137 | describe('stress — delegated writes all denied under load (D6.3.6)', () => { |
| 138 | it(`${CONCURRENCY} concurrent delegated writes all return SELF_PARTITION_ONLY`, async () => { |
| 139 | const stores = buildStressStores(); |
| 140 | const writer = createDerivedArtifactWriter({ |
| 141 | writeNoteFn: stores.writeNoteFn, |
| 142 | vaultPath: '/vault', |
| 143 | }); |
| 144 | |
| 145 | const tasks = Array.from({ length: CONCURRENCY }, (_, i) => { |
| 146 | const prov = makeProvenance('ai_summary', `owner/note-${i}.md`, i); |
| 147 | return writer.write({ summary: `delegated ${i}` }, prov, { |
| 148 | lane: 'local', |
| 149 | containsPrivateData: false, |
| 150 | isDelegate: true, |
| 151 | delegatedManagedAllowed: false, |
| 152 | enrichesDelegatedPartition: true, |
| 153 | delegatedEnrichmentAllowed: false, |
| 154 | }); |
| 155 | }); |
| 156 | |
| 157 | const results = await Promise.all(tasks); |
| 158 | for (const r of results) { |
| 159 | assert.equal(r.ok, false); |
| 160 | assert.equal(r.reason, 'writer_self_partition_only'); |
| 161 | } |
| 162 | // No writes |
| 163 | assert.equal(stores.writeCalls, 0); |
| 164 | }); |
| 165 | }); |
| 166 | |
| 167 | describe('stress — concurrent insight writes', () => { |
| 168 | it(`${CONCURRENCY} concurrent insight writes all store without collision`, async () => { |
| 169 | const stores = buildStressStores(); |
| 170 | const writer = createDerivedArtifactWriter({ |
| 171 | writeNoteFn: stores.writeNoteFn, |
| 172 | vaultPath: '/vault', |
| 173 | mm: stores.mm, |
| 174 | }); |
| 175 | |
| 176 | const tasks = Array.from({ length: CONCURRENCY }, (_, i) => { |
| 177 | const prov = buildConvenienceProvenance({ |
| 178 | generatedBy: `u${i}`, |
| 179 | source: 'companion', |
| 180 | model: 'm', |
| 181 | modelVersion: '1', |
| 182 | lane: 'local', |
| 183 | artifactType: 'insight', |
| 184 | sourceNotePath: null, |
| 185 | sourceEventId: [`mem_c${i}`, `mem_d${i}`], |
| 186 | }); |
| 187 | return writer.write( |
| 188 | { connections: [`conn-${i}`], contradictions: [], open_questions: [], topic_count: 2 }, |
| 189 | prov, |
| 190 | selfCtx(), |
| 191 | ); |
| 192 | }); |
| 193 | |
| 194 | const results = await Promise.all(tasks); |
| 195 | const succeeded = results.filter((r) => r.ok); |
| 196 | assert.equal(succeeded.length, CONCURRENCY); |
| 197 | assert.equal(stores.insights.length, CONCURRENCY); |
| 198 | }); |
| 199 | }); |
| 200 | |
| 201 | describe('stress — deletion fan-out under load', () => { |
| 202 | it('concurrent deletes of different notes all succeed without corruption', async () => { |
| 203 | const stores = buildStressStores(); |
| 204 | const writer = createDerivedArtifactWriter({ |
| 205 | writeNoteFn: stores.writeNoteFn, |
| 206 | vaultPath: '/vault', |
| 207 | vectorStore: stores.vectorStore, |
| 208 | mm: stores.mm, |
| 209 | }); |
| 210 | |
| 211 | // Pre-write notes |
| 212 | for (let i = 0; i < CONCURRENCY; i++) { |
| 213 | const notePath = `notes/del-stress-${i}.md`; |
| 214 | stores.frontmatter.set(notePath, { ai_summary: `old-${i}` }); |
| 215 | stores.vectors.push({ path: notePath, vector: [i] }); |
| 216 | } |
| 217 | |
| 218 | const tasks = Array.from({ length: CONCURRENCY }, (_, i) => |
| 219 | writer.deleteArtifacts({ notePath: `notes/del-stress-${i}.md` }), |
| 220 | ); |
| 221 | |
| 222 | const results = await Promise.all(tasks); |
| 223 | const failed = results.filter((r) => !r.ok); |
| 224 | assert.equal(failed.length, 0, 'All concurrent deletes must succeed'); |
| 225 | |
| 226 | // Vectors purged |
| 227 | const remaining = stores.vectors.filter((v) => v.path?.startsWith('notes/del-stress-')); |
| 228 | assert.equal(remaining.length, 0, 'All stress vectors must be purged'); |
| 229 | }); |
| 230 | }); |
| 231 | |
| 232 | describe('stress — no partial write on store failure under load', () => { |
| 233 | it('when writeNoteFn throws, no partial state leaks', async () => { |
| 234 | let callCount = 0; |
| 235 | const failOnThird = (_vp, notePath, _opts) => { |
| 236 | callCount++; |
| 237 | if (callCount % 3 === 0) throw new Error('disk full'); |
| 238 | }; |
| 239 | |
| 240 | const writer = createDerivedArtifactWriter({ |
| 241 | writeNoteFn: failOnThird, |
| 242 | vaultPath: '/vault', |
| 243 | }); |
| 244 | |
| 245 | const tasks = Array.from({ length: 30 }, (_, i) => { |
| 246 | const prov = makeProvenance('ai_summary', `notes/partial-${i}.md`, i); |
| 247 | return writer.write({ summary: `partial ${i}` }, prov, selfCtx()); |
| 248 | }); |
| 249 | |
| 250 | const results = await Promise.all(tasks); |
| 251 | // Every result must be either ok:true or ok:false — never a thrown exception |
| 252 | for (const r of results) { |
| 253 | assert.ok(r.ok === true || r.ok === false, 'Result must always be ok:true or ok:false'); |
| 254 | assert.ok(typeof r.reason === 'string' || r.ok === true, 'Failed results must have reason'); |
| 255 | } |
| 256 | }); |
| 257 | }); |
File History
1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠
3 hours ago