derived-artifact-storage-e2e.test.mjs
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠ breaking
7 hours ago
| 1 | /** |
| 2 | * Tier 3 — END-TO-END: Phase 6 derived-artifact storage layer. |
| 3 | * |
| 4 | * Covers (§10 E2E obligations): |
| 5 | * - Self-partition: local inference produces summary → writer persists per owner tier |
| 6 | * - Delegated enrichment: denied without delegatedEnrichmentAllowed, allowed with it |
| 7 | * - Convenience vs privacy_max branch (encryptor stub) |
| 8 | * - Note delete removes summary + vector + stale-flags insight |
| 9 | * - runDiscoverPass produces an insight event end-to-end through the writer |
| 10 | */ |
| 11 | |
| 12 | import { describe, it } from 'node:test'; |
| 13 | import assert from 'node:assert/strict'; |
| 14 | |
| 15 | import { |
| 16 | createDerivedArtifactWriter, |
| 17 | WRITER_REASONS, |
| 18 | } from '../lib/companion-artifact-writer.mjs'; |
| 19 | |
| 20 | import { |
| 21 | createClientEncryptor, |
| 22 | } from '../lib/companion-client-encryptor.mjs'; |
| 23 | |
| 24 | import { |
| 25 | buildConvenienceProvenance, |
| 26 | PROVENANCE_SCHEMA_VERSION, |
| 27 | } from '../lib/companion-provenance-validator.mjs'; |
| 28 | |
| 29 | import { TERMINAL_STATES } from '../lib/companion-tier-resolver.mjs'; |
| 30 | import { runDiscoverPass } from '../lib/memory-consolidate.mjs'; |
| 31 | |
| 32 | // ── E2E test double infrastructure ─────────────────────────────────────────── |
| 33 | |
| 34 | function buildE2EStores() { |
| 35 | const noteStore = new Map(); // notePath → frontmatter fields |
| 36 | const vectorStore = []; |
| 37 | const insightStore = []; |
| 38 | const maintenanceLog = []; |
| 39 | |
| 40 | const writeNoteFn = (_vaultPath, notePath, opts) => { |
| 41 | const prev = noteStore.get(notePath) ?? {}; |
| 42 | // Merge, treating null values as explicit nulls (deletion) |
| 43 | const merged = { ...prev }; |
| 44 | for (const [k, v] of Object.entries(opts.frontmatter ?? {})) { |
| 45 | merged[k] = v; |
| 46 | } |
| 47 | noteStore.set(notePath, merged); |
| 48 | }; |
| 49 | |
| 50 | const vs = { |
| 51 | upsert: async (points) => { vectorStore.push(...points); }, |
| 52 | deleteByPath: async (notePath) => { |
| 53 | const idx = vectorStore.findIndex((v) => v.path === notePath); |
| 54 | if (idx >= 0) vectorStore.splice(idx, 1); |
| 55 | }, |
| 56 | }; |
| 57 | |
| 58 | const mm = { |
| 59 | store: (type, data) => { |
| 60 | if (type === 'insight') insightStore.push(data); |
| 61 | if (type === 'maintenance') maintenanceLog.push(data); |
| 62 | return { id: `mem_${Date.now()}`, ts: new Date().toISOString() }; |
| 63 | }, |
| 64 | }; |
| 65 | |
| 66 | return { noteStore, vectorStore, insightStore, maintenanceLog, writeNoteFn, vs, mm }; |
| 67 | } |
| 68 | |
| 69 | // ── E2E: self-partition convenience write ───────────────────────────────────── |
| 70 | |
| 71 | describe('E2E — self-partition summary write at convenience tier', () => { |
| 72 | it('full flow: local inference → writer → host-readable frontmatter', async () => { |
| 73 | const { noteStore, writeNoteFn } = buildE2EStores(); |
| 74 | const writer = createDerivedArtifactWriter({ |
| 75 | writeNoteFn, |
| 76 | vaultPath: '/vault', |
| 77 | vaultRegistryAvailable: false, |
| 78 | }); |
| 79 | |
| 80 | const prov = buildConvenienceProvenance({ |
| 81 | generatedBy: 'user-self', |
| 82 | source: 'companion', |
| 83 | model: 'llama-3', |
| 84 | modelVersion: '3.1', |
| 85 | runtimeVersion: '0.9.0', |
| 86 | lane: 'local', |
| 87 | artifactType: 'ai_summary', |
| 88 | sourceNotePath: 'research/quantum.md', |
| 89 | sourceEventId: 'mem_q001', |
| 90 | }); |
| 91 | |
| 92 | const result = await writer.write( |
| 93 | { summary: 'Quantum entanglement is a physical phenomenon.' }, |
| 94 | prov, |
| 95 | { |
| 96 | lane: 'local', |
| 97 | containsPrivateData: false, |
| 98 | isDelegate: false, |
| 99 | delegatedManagedAllowed: false, |
| 100 | enrichesDelegatedPartition: false, |
| 101 | delegatedEnrichmentAllowed: false, |
| 102 | }, |
| 103 | ); |
| 104 | |
| 105 | assert.equal(result.ok, true); |
| 106 | assert.equal(result.terminalState, TERMINAL_STATES.HOST_READABLE); |
| 107 | |
| 108 | const fm = noteStore.get('research/quantum.md'); |
| 109 | assert.ok(fm); |
| 110 | assert.equal(fm.ai_summary, 'Quantum entanglement is a physical phenomenon.'); |
| 111 | assert.ok(fm.ai_summary_provenance); |
| 112 | assert.equal(fm.ai_summary_provenance.generated_by, 'user-self'); |
| 113 | assert.equal(fm.ai_summary_provenance.schema_version, PROVENANCE_SCHEMA_VERSION); |
| 114 | }); |
| 115 | }); |
| 116 | |
| 117 | // ── E2E: delegated enrichment default-OFF ──────────────────────────────────── |
| 118 | |
| 119 | describe('E2E — delegated enrichment: denied by default (D6.3.3)', () => { |
| 120 | it('rejects delegated local-lane write without owner opt-in', async () => { |
| 121 | const { writeNoteFn, noteStore } = buildE2EStores(); |
| 122 | const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' }); |
| 123 | |
| 124 | const prov = buildConvenienceProvenance({ |
| 125 | generatedBy: 'delegate-user', |
| 126 | source: 'companion', |
| 127 | model: 'llama-3', |
| 128 | modelVersion: '3.1', |
| 129 | lane: 'local', |
| 130 | artifactType: 'ai_summary', |
| 131 | sourceNotePath: 'owner/note.md', |
| 132 | sourceEventId: 'mem_d001', |
| 133 | }); |
| 134 | |
| 135 | // D6.3.6: cross-partition write → self-partition only until tenancy gate |
| 136 | const result = await writer.write({ summary: 'delegate summary' }, prov, { |
| 137 | lane: 'local', |
| 138 | containsPrivateData: false, |
| 139 | isDelegate: true, |
| 140 | delegatedManagedAllowed: false, |
| 141 | enrichesDelegatedPartition: true, // actor ≠ owner |
| 142 | delegatedEnrichmentAllowed: false, // owner has NOT opted in |
| 143 | }); |
| 144 | |
| 145 | assert.equal(result.ok, false); |
| 146 | assert.equal(result.reason, WRITER_REASONS.SELF_PARTITION_ONLY); |
| 147 | assert.equal(noteStore.size, 0, 'No write should occur'); |
| 148 | }); |
| 149 | }); |
| 150 | |
| 151 | // ── E2E: privacy_max branch (encryptor stub) ────────────────────────────────── |
| 152 | |
| 153 | describe('E2E — privacy_max branch with stub encryptor', () => { |
| 154 | it('stores only ciphertext when encryptor is available', async () => { |
| 155 | const { noteStore, writeNoteFn } = buildE2EStores(); |
| 156 | |
| 157 | const stubEncryptor = createClientEncryptor({ |
| 158 | isAvailable: () => true, |
| 159 | encrypt: (bytes, _opts) => ({ |
| 160 | ciphertext: new Uint8Array(bytes.length).fill(0xab), |
| 161 | wrappedDekRef: 'wrapped-dek-for-vault', |
| 162 | alg: 'STUB-AES-256', |
| 163 | }), |
| 164 | }); |
| 165 | |
| 166 | const writer = createDerivedArtifactWriter({ |
| 167 | writeNoteFn, |
| 168 | vaultPath: '/vault', |
| 169 | vaultRegistryAvailable: true, |
| 170 | encryptor: stubEncryptor, |
| 171 | }); |
| 172 | |
| 173 | const prov = buildConvenienceProvenance({ |
| 174 | generatedBy: 'priv-user', |
| 175 | source: 'companion', |
| 176 | model: 'llama-3', |
| 177 | modelVersion: '3.1', |
| 178 | lane: 'local', |
| 179 | artifactType: 'ai_summary', |
| 180 | sourceNotePath: 'private/note.md', |
| 181 | sourceEventId: 'mem_p001', |
| 182 | }); |
| 183 | // Override privacy_tier after building (buildConvenienceProvenance always sets convenience) |
| 184 | const privProv = { ...prov, privacy_tier: 'privacy_max' }; |
| 185 | |
| 186 | const result = await writer.write( |
| 187 | { summary: 'My private content' }, |
| 188 | privProv, |
| 189 | { lane: 'local', containsPrivateData: true, isDelegate: false, |
| 190 | delegatedManagedAllowed: false }, |
| 191 | ); |
| 192 | |
| 193 | assert.equal(result.ok, true); |
| 194 | assert.equal(result.terminalState, TERMINAL_STATES.CLIENT_ENCRYPTED); |
| 195 | |
| 196 | const fm = noteStore.get('private/note.md'); |
| 197 | assert.ok(fm.ai_summary_ciphertext, 'Ciphertext must be stored'); |
| 198 | assert.equal(fm.ai_summary, undefined, 'Plaintext ai_summary must NOT be stored'); |
| 199 | assert.equal(fm.ai_summary_provenance.wrapped_dek_ref, 'wrapped-dek-for-vault'); |
| 200 | }); |
| 201 | |
| 202 | it('fails closed when encryptor unavailable — nothing written', async () => { |
| 203 | const { noteStore, writeNoteFn } = buildE2EStores(); |
| 204 | const writer = createDerivedArtifactWriter({ |
| 205 | writeNoteFn, |
| 206 | vaultPath: '/vault', |
| 207 | vaultRegistryAvailable: true, |
| 208 | // Default encryptor (unavailable) |
| 209 | }); |
| 210 | |
| 211 | const prov = buildConvenienceProvenance({ |
| 212 | generatedBy: 'priv-user', |
| 213 | source: 'companion', |
| 214 | model: 'llama-3', |
| 215 | modelVersion: '3.1', |
| 216 | lane: 'local', |
| 217 | artifactType: 'ai_summary', |
| 218 | sourceNotePath: 'private/note.md', |
| 219 | sourceEventId: 'mem_p002', |
| 220 | }); |
| 221 | const privProv = { ...prov, privacy_tier: 'privacy_max' }; |
| 222 | |
| 223 | const result = await writer.write({ summary: 'secret' }, privProv, { |
| 224 | lane: 'local', containsPrivateData: true, isDelegate: false, |
| 225 | delegatedManagedAllowed: false, |
| 226 | }); |
| 227 | |
| 228 | assert.equal(result.ok, false); |
| 229 | assert.equal(noteStore.size, 0); |
| 230 | }); |
| 231 | }); |
| 232 | |
| 233 | // ── E2E: note delete removes all derived artifacts ──────────────────────────── |
| 234 | |
| 235 | describe('E2E — note delete removes summary + vector, stale-flags insight (D6.5)', () => { |
| 236 | it('full delete flow across all stores', async () => { |
| 237 | const { noteStore, vectorStore, maintenanceLog, writeNoteFn, vs, mm } = buildE2EStores(); |
| 238 | |
| 239 | // Pre-populate via writer |
| 240 | const writer = createDerivedArtifactWriter({ |
| 241 | writeNoteFn, |
| 242 | vaultPath: '/vault', |
| 243 | vectorStore: vs, |
| 244 | mm, |
| 245 | }); |
| 246 | |
| 247 | // Write summary |
| 248 | const sumprov = buildConvenienceProvenance({ |
| 249 | generatedBy: 'u', source: 'companion', model: 'm', modelVersion: '1', |
| 250 | lane: 'local', artifactType: 'ai_summary', |
| 251 | sourceNotePath: 'notes/about-to-delete.md', sourceEventId: 'mem_s001', |
| 252 | }); |
| 253 | await writer.write({ summary: 'Will be deleted.' }, sumprov, { |
| 254 | lane: 'local', containsPrivateData: false, isDelegate: false, |
| 255 | delegatedManagedAllowed: false, |
| 256 | }); |
| 257 | |
| 258 | // Write embedding |
| 259 | const embprov = buildConvenienceProvenance({ |
| 260 | generatedBy: 'u', source: 'companion', model: 'm', modelVersion: '1', |
| 261 | lane: 'local', artifactType: 'embedding', |
| 262 | sourceNotePath: 'notes/about-to-delete.md', sourceEventId: 'mem_e001', |
| 263 | }); |
| 264 | await writer.write({ vector: [0.1, 0.2], payload: {} }, embprov, { |
| 265 | lane: 'local', containsPrivateData: false, isDelegate: false, |
| 266 | delegatedManagedAllowed: false, |
| 267 | }); |
| 268 | |
| 269 | assert.ok(noteStore.has('notes/about-to-delete.md')); |
| 270 | assert.equal(vectorStore.length, 1); |
| 271 | |
| 272 | // Delete |
| 273 | const delResult = await writer.deleteArtifacts({ notePath: 'notes/about-to-delete.md' }); |
| 274 | assert.equal(delResult.ok, true); |
| 275 | |
| 276 | // Frontmatter nulled |
| 277 | const fm = noteStore.get('notes/about-to-delete.md'); |
| 278 | assert.equal(fm.ai_summary, null); |
| 279 | assert.equal(fm.ai_summary_provenance, null); |
| 280 | |
| 281 | // Vector purged |
| 282 | assert.equal(vectorStore.filter((v) => v.path === 'notes/about-to-delete.md').length, 0); |
| 283 | |
| 284 | // Maintenance stale-flag recorded for aggregate insight re-enrichment (D6.5.2) |
| 285 | assert.ok(maintenanceLog.some((m) => m.deleted_note_path === 'notes/about-to-delete.md')); |
| 286 | }); |
| 287 | }); |
| 288 | |
| 289 | // ── E2E: runDiscoverPass end-to-end through writer ──────────────────────────── |
| 290 | |
| 291 | describe('E2E — runDiscoverPass end-to-end through writer', () => { |
| 292 | it('produces insight event in insightStore via writer', async () => { |
| 293 | const { insightStore, mm } = buildE2EStores(); |
| 294 | |
| 295 | const mockWriter = { |
| 296 | write: async (artifact, provenance, _ctx) => { |
| 297 | // Route to insightStore as the writer would |
| 298 | if (provenance.artifact_type === 'insight') { |
| 299 | insightStore.push({ ...artifact, provenance }); |
| 300 | } |
| 301 | return { ok: true, terminalState: 'host_readable' }; |
| 302 | }, |
| 303 | deleteArtifacts: async () => ({ ok: true, stores: [] }), |
| 304 | checkReEnrichmentEligibility: () => ({ eligible: false, reason: 'current' }), |
| 305 | }; |
| 306 | |
| 307 | const config = { |
| 308 | vault_path: '/vault', |
| 309 | vault_id: 'e2e-vault', |
| 310 | llm: { model: 'test-m', model_version: '1.0' }, |
| 311 | memory: {}, |
| 312 | daemon: { llm: { max_tokens: 256 } }, |
| 313 | }; |
| 314 | |
| 315 | const consolidations = [ |
| 316 | { id: 'mem_c1', data: { topic: 'quantum', facts: ['QE is real', 'QE is weird'] } }, |
| 317 | ]; |
| 318 | |
| 319 | const fakeLlm = async () => JSON.stringify({ |
| 320 | connections: ['quantum connects to cryptography'], |
| 321 | contradictions: [], |
| 322 | open_questions: ['Is QE deterministic?'], |
| 323 | }); |
| 324 | |
| 325 | const result = await runDiscoverPass(config, consolidations, { |
| 326 | llmFn: fakeLlm, |
| 327 | mm, |
| 328 | writer: mockWriter, |
| 329 | }); |
| 330 | |
| 331 | assert.equal(result.dry_run, false); |
| 332 | assert.ok(Array.isArray(result.connections)); |
| 333 | assert.equal(result.connections[0], 'quantum connects to cryptography'); |
| 334 | |
| 335 | // Insight must be in the store |
| 336 | assert.equal(insightStore.length, 1); |
| 337 | assert.ok(insightStore[0].provenance); |
| 338 | assert.equal(insightStore[0].provenance.artifact_type, 'insight'); |
| 339 | }); |
| 340 | }); |
File History
1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠
7 hours ago