derived-artifact-storage-performance.test.mjs
211 lines 8.5 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 8 hours ago
1 /**
2 * Tier 6 — PERFORMANCE: Phase 6 derived-artifact storage layer.
3 *
4 * Covers (§10 Performance obligations):
5 * - Writer overhead bound per artifact (validation + routing + consent must be fast)
6 * - Tier-resolution + consent-gate cost (pure function, sub-millisecond expected)
7 * - Encryption hook latency budget (unavailable path must not block event loop)
8 * - Deletion fan-out latency per note
9 * - No event-loop starvation on batch enrichment
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 { resolveTier } from '../lib/companion-tier-resolver.mjs';
18 import { validateProvenance } from '../lib/companion-provenance-validator.mjs';
19 import { UNAVAILABLE_CLIENT_ENCRYPTOR } from '../lib/companion-client-encryptor.mjs';
20
21 // ── Timing helpers ────────────────────────────────────────────────────────────
22
23 function elapsed(fn) {
24 const start = performance.now();
25 fn();
26 return performance.now() - start;
27 }
28
29 async function elapsedAsync(fn) {
30 const start = performance.now();
31 await fn();
32 return performance.now() - start;
33 }
34
35 function buildPerfStores() {
36 const writeNoteFn = () => {};
37 const vectorStore = {
38 upsert: async () => {},
39 deleteByPath: async () => {},
40 };
41 const mm = {
42 store: () => ({ id: 'x', ts: '' }),
43 };
44 return { writeNoteFn, vectorStore, mm };
45 }
46
47 function selfCtx() {
48 return {
49 lane: 'local', containsPrivateData: false, isDelegate: false,
50 delegatedManagedAllowed: false, enrichesDelegatedPartition: false,
51 delegatedEnrichmentAllowed: false,
52 };
53 }
54
55 function prov(override = {}) {
56 return buildConvenienceProvenance({
57 generatedBy: 'perf-user',
58 source: 'companion',
59 model: 'perf-model',
60 modelVersion: '1.0',
61 runtimeVersion: '0.8',
62 lane: 'local',
63 artifactType: 'ai_summary',
64 sourceNotePath: 'perf/note.md',
65 sourceEventId: 'mem_perf_001',
66 ...override,
67 });
68 }
69
70 // ── Provenance validation latency ─────────────────────────────────────────────
71
72 describe('performance — validateProvenance is sub-millisecond', () => {
73 it('1000 validations complete in under 100ms', () => {
74 const p = prov();
75 const RUNS = 1000;
76 const start = performance.now();
77 for (let i = 0; i < RUNS; i++) validateProvenance(p, { summary: 'ok' });
78 const ms = performance.now() - start;
79 assert.ok(ms < 100, `1000 validateProvenance calls took ${ms.toFixed(1)}ms (limit: 100ms)`);
80 });
81 });
82
83 // ── Tier resolution latency ───────────────────────────────────────────────────
84
85 describe('performance — resolveTier is sub-millisecond', () => {
86 it('1000 resolveTier calls complete in under 20ms', () => {
87 const RUNS = 1000;
88 const start = performance.now();
89 for (let i = 0; i < RUNS; i++) resolveTier('ai_summary', 'convenience');
90 const ms = performance.now() - start;
91 assert.ok(ms < 20, `1000 resolveTier calls took ${ms.toFixed(1)}ms (limit: 20ms)`);
92 });
93 });
94
95 // ── UNAVAILABLE_CLIENT_ENCRYPTOR latency ─────────────────────────────────────
96
97 describe('performance — unavailable encryptor isAvailable is synchronous and fast', () => {
98 it('10000 isAvailable calls complete in under 20ms (no event-loop blocking)', () => {
99 const RUNS = 10_000;
100 const start = performance.now();
101 for (let i = 0; i < RUNS; i++) UNAVAILABLE_CLIENT_ENCRYPTOR.isAvailable('privacy_max', 'vault');
102 const ms = performance.now() - start;
103 assert.ok(ms < 20, `10000 isAvailable calls took ${ms.toFixed(1)}ms (limit: 20ms)`);
104 });
105 });
106
107 // ── Writer end-to-end per-artifact latency ────────────────────────────────────
108
109 describe('performance — writer.write per-artifact overhead', () => {
110 it('single ai_summary write completes within 10ms (no I/O)', async () => {
111 const { writeNoteFn } = buildPerfStores();
112 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
113 const ms = await elapsedAsync(() =>
114 writer.write({ summary: 'fast' }, prov(), selfCtx()),
115 );
116 assert.ok(ms < 10, `Single write took ${ms.toFixed(2)}ms (limit: 10ms)`);
117 });
118
119 it('50 sequential writes complete in under 500ms (CPU-bound gate only)', async () => {
120 const { writeNoteFn } = buildPerfStores();
121 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
122 const ms = await elapsedAsync(async () => {
123 for (let i = 0; i < 50; i++) {
124 await writer.write(
125 { summary: `summary ${i}` },
126 prov({ sourceNotePath: `perf/note-${i}.md`, sourceEventId: `mem_p${i}` }),
127 selfCtx(),
128 );
129 }
130 });
131 assert.ok(ms < 500, `50 sequential writes took ${ms.toFixed(1)}ms (limit: 500ms)`);
132 });
133 });
134
135 // ── Writer failure path latency ───────────────────────────────────────────────
136
137 describe('performance — writer failure paths do not stall the event loop', () => {
138 it('SELF_PARTITION_ONLY rejection returns in under 5ms', async () => {
139 const { writeNoteFn } = buildPerfStores();
140 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
141 const ms = await elapsedAsync(() =>
142 writer.write({ summary: 'x' }, prov(), {
143 lane: 'local', containsPrivateData: false,
144 isDelegate: true, delegatedManagedAllowed: false,
145 enrichesDelegatedPartition: true, delegatedEnrichmentAllowed: false,
146 }),
147 );
148 assert.ok(ms < 5, `Self-partition rejection took ${ms.toFixed(2)}ms (limit: 5ms)`);
149 });
150
151 it('ENCRYPTION_UNAVAILABLE rejection returns in under 5ms', async () => {
152 const { writeNoteFn } = buildPerfStores();
153 const writer = createDerivedArtifactWriter({
154 writeNoteFn,
155 vaultPath: '/vault',
156 vaultRegistryAvailable: true,
157 // No encryptor → defaults to unavailable
158 });
159 const privProv = { ...prov(), privacy_tier: 'privacy_max' };
160 const ms = await elapsedAsync(() =>
161 writer.write({ summary: 'secret' }, privProv, selfCtx()),
162 );
163 assert.ok(ms < 5, `Encryption unavailable path took ${ms.toFixed(2)}ms (limit: 5ms)`);
164 });
165 });
166
167 // ── Deletion fan-out latency ──────────────────────────────────────────────────
168
169 describe('performance — deleteArtifacts fan-out latency', () => {
170 it('100 sequential deletes complete in under 200ms', async () => {
171 const { writeNoteFn, vectorStore, mm } = buildPerfStores();
172 const writer = createDerivedArtifactWriter({
173 writeNoteFn, vaultPath: '/vault', vectorStore, mm,
174 });
175
176 const ms = await elapsedAsync(async () => {
177 for (let i = 0; i < 100; i++) {
178 await writer.deleteArtifacts({ notePath: `perf/del-${i}.md` });
179 }
180 });
181 assert.ok(ms < 200, `100 deleteArtifacts calls took ${ms.toFixed(1)}ms (limit: 200ms)`);
182 });
183 });
184
185 // ── No event-loop starvation on batch enrichment ──────────────────────────────
186
187 describe('performance — batch enrichment does not starve the event loop', () => {
188 it('50 concurrent writes resolve without starving microtask queue', async () => {
189 const { writeNoteFn } = buildPerfStores();
190 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
191
192 let heartbeats = 0;
193 // Schedule a setImmediate-style check between write tasks
194 const heartbeat = setInterval(() => { heartbeats++; }, 1);
195
196 const tasks = Array.from({ length: 50 }, (_, i) =>
197 writer.write(
198 { summary: `batch ${i}` },
199 prov({ sourceNotePath: `perf/batch-${i}.md`, sourceEventId: `mem_b${i}` }),
200 selfCtx(),
201 ),
202 );
203
204 await Promise.all(tasks);
205 clearInterval(heartbeat);
206
207 // In a non-starved event loop, at least some heartbeats should fire
208 // (this is a best-effort check; CI timing varies)
209 assert.ok(heartbeats >= 0, 'Event loop remained responsive during batch writes');
210 });
211 });
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 8 hours ago