derived-artifact-storage-stress.test.mjs
257 lines 8.2 KB
Raw
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