bulltrackers-module 1.0.735 → 1.0.737

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/functions/computation-system-v2/config/bulltrackers.config.js +80 -6
  2. package/functions/computation-system-v2/docs/architecture.md +59 -0
  3. package/functions/computation-system-v2/framework/data/DataFetcher.js +107 -105
  4. package/functions/computation-system-v2/framework/execution/Orchestrator.js +357 -150
  5. package/functions/computation-system-v2/framework/execution/RemoteTaskRunner.js +327 -0
  6. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +9 -4
  7. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +9 -21
  8. package/functions/computation-system-v2/framework/index.js +10 -3
  9. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +53 -57
  10. package/functions/computation-system-v2/framework/monitoring/Profiler.js +54 -52
  11. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +173 -27
  12. package/functions/computation-system-v2/framework/storage/StorageManager.js +419 -187
  13. package/functions/computation-system-v2/handlers/index.js +10 -1
  14. package/functions/computation-system-v2/handlers/scheduler.js +85 -193
  15. package/functions/computation-system-v2/handlers/worker.js +242 -0
  16. package/functions/computation-system-v2/index.js +2 -0
  17. package/functions/computation-system-v2/test/analyze-results.js +238 -0
  18. package/functions/computation-system-v2/test/{test-dispatcher.js → other/test-dispatcher.js} +6 -6
  19. package/functions/computation-system-v2/test/{test-framework.js → other/test-framework.js} +14 -14
  20. package/functions/computation-system-v2/test/{test-real-execution.js → other/test-real-execution.js} +1 -1
  21. package/functions/computation-system-v2/test/{test-real-integration.js → other/test-real-integration.js} +3 -3
  22. package/functions/computation-system-v2/test/{test-refactor-e2e.js → other/test-refactor-e2e.js} +3 -3
  23. package/functions/computation-system-v2/test/{test-risk-metrics-computation.js → other/test-risk-metrics-computation.js} +4 -4
  24. package/functions/computation-system-v2/test/{test-scheduler.js → other/test-scheduler.js} +1 -1
  25. package/functions/computation-system-v2/test/{test-storage.js → other/test-storage.js} +2 -2
  26. package/functions/computation-system-v2/test/run-pipeline-test.js +554 -0
  27. package/functions/computation-system-v2/test/test-full-pipeline.js +227 -0
  28. package/functions/computation-system-v2/test/test-worker-pool.js +266 -0
  29. package/package.json +1 -1
  30. package/functions/computation-system-v2/computations/TestComputation.js +0 -46
  31. /package/functions/computation-system-v2/test/{test-results.json → other/test-results.json} +0 -0
@@ -0,0 +1,554 @@
1
+ /**
2
+ * @fileoverview Production Pipeline Test Runner
3
+ * * Tests the ENTIRE production pipeline with:
4
+ * - Cost tracking and hard limits
5
+ * - Test tables/buckets (isolated from production)
6
+ * - Entity filtering (test subset of users)
7
+ * - Lineage tracking
8
+ * - Performance profiling
9
+ * * Run: node test/run-pipeline-test.js --date 2026-01-24 --entities user1,user2 --max-cost 5 --batch-size 1000
10
+ * * * UPDATE: Sets NODE_ENV='test' and force:true to bypass Checkpointer locks AND "Up-To-Date" checks.
11
+ */
12
+
13
+ // FORCE TEST ENVIRONMENT
14
+ process.env.NODE_ENV = 'test';
15
+
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const { Orchestrator } = require('../framework/execution/Orchestrator');
19
+ const { StorageManager } = require('../framework/storage/StorageManager');
20
+ const { CostTracker } = require('../framework/cost/CostTracker');
21
+ const { ComputationProfiler } = require('../framework/monitoring/Profiler');
22
+
23
+ // ============================================================================
24
+ // TEST CONFIGURATION BUILDER
25
+ // ============================================================================
26
+
27
+ class TestConfigBuilder {
28
+ constructor(productionConfig, testOptions) {
29
+ this.prodConfig = productionConfig;
30
+ this.testOptions = testOptions;
31
+ }
32
+
33
+ build() {
34
+ // Clone production config
35
+ const testConfig = JSON.parse(JSON.stringify(this.prodConfig));
36
+
37
+ // Restore non-serializable objects (Classes and Functions)
38
+ testConfig.computations = this.prodConfig.computations;
39
+ testConfig.rules = this.prodConfig.rules;
40
+
41
+ // Override GCS to use test bucket
42
+ const prodBucket = testConfig.gcs?.bucket;
43
+ testConfig.gcs = {
44
+ bucket: this.testOptions.testBucket || `${prodBucket}-test`,
45
+ prefix: 'test-staging'
46
+ };
47
+
48
+ // Override result storage to use test table
49
+ testConfig.resultStore = {
50
+ table: 'test_computation_results',
51
+ partitionField: 'date',
52
+ clusterFields: ['computation_name', 'category']
53
+ };
54
+
55
+ // Add test-specific metadata
56
+ testConfig.testMode = {
57
+ enabled: true,
58
+ runId: this.testOptions.runId || `test-${Date.now()}`,
59
+ targetEntities: this.testOptions.entities || null,
60
+ maxCostUSD: this.testOptions.maxCost || 10,
61
+ dateOverride: this.testOptions.date
62
+ };
63
+
64
+ // Override execution settings for testing
65
+ testConfig.execution = {
66
+ ...testConfig.execution,
67
+ entityConcurrency: this.testOptions.concurrency || 5, // Default 5 or user provided
68
+ batchSize: this.testOptions.batchSize || 100 // Default 100 or user provided
69
+ };
70
+
71
+ // FIX: Explicitly enable lock bypass in config
72
+ testConfig.bypassLocks = true;
73
+
74
+ return testConfig;
75
+ }
76
+ }
77
+
78
+ // ============================================================================
79
+ // RUNTIME INTERCEPTORS (Proxy Pattern)
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Intercepts StorageManager to redirect writes to test tables
84
+ */
85
+ class TestStorageInterceptor {
86
+ constructor(storageManager, testConfig) {
87
+ this.storage = storageManager;
88
+ this.testConfig = testConfig;
89
+ this.capturedWrites = [];
90
+ }
91
+
92
+ // Wrap commitResults to capture what would be written
93
+ async commitResults(date, entry, results, depHashes) {
94
+ this.capturedWrites.push({
95
+ date,
96
+ computation: entry.name,
97
+ entityCount: Object.keys(results).length,
98
+ timestamp: new Date().toISOString()
99
+ });
100
+
101
+ // Actually write to test storage
102
+ return this.storage.commitResults(date, entry, results, depHashes);
103
+ }
104
+
105
+ async finalizeResults(date, entry) {
106
+ return this.storage.finalizeResults(date, entry);
107
+ }
108
+
109
+ // Proxy other methods
110
+ async initCheckpoint(...args) { return this.storage.initCheckpoint(...args); }
111
+ async updateCheckpoint(...args) { return this.storage.updateCheckpoint(...args); }
112
+ async completeCheckpoint(...args) { return this.storage.completeCheckpoint(...args); }
113
+ async getLatestCheckpoint(...args) { return this.storage.getLatestCheckpoint(...args); }
114
+ async savePerformanceReport(...args) { return this.storage.savePerformanceReport(...args); }
115
+
116
+ getSummary() {
117
+ return {
118
+ totalWrites: this.capturedWrites.length,
119
+ totalEntities: this.capturedWrites.reduce((sum, w) => sum + w.entityCount, 0),
120
+ byComputation: this.capturedWrites.reduce((acc, w) => {
121
+ acc[w.computation] = (acc[w.computation] || 0) + w.entityCount;
122
+ return acc;
123
+ }, {})
124
+ };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Intercepts CostTracker to enforce hard cost limits
130
+ */
131
+ class TestCostEnforcer {
132
+ constructor(costTracker, maxCostUSD) {
133
+ this.tracker = costTracker;
134
+ this.maxCostUSD = maxCostUSD;
135
+ this.currentCostUSD = 0;
136
+ this.costByComputation = {};
137
+ }
138
+
139
+ async trackCost(computationName, date, bytesProcessed) {
140
+ // Calculate cost (same as production)
141
+ const COST_PER_TB = 5.0;
142
+ const BYTES_PER_TB = 1099511627776;
143
+ const cost = (bytesProcessed / BYTES_PER_TB) * COST_PER_TB;
144
+
145
+ this.currentCostUSD += cost;
146
+ this.costByComputation[computationName] = (this.costByComputation[computationName] || 0) + cost;
147
+
148
+ // HARD LIMIT ENFORCEMENT
149
+ if (this.currentCostUSD > this.maxCostUSD) {
150
+ throw new Error(
151
+ `🚨 COST LIMIT EXCEEDED: $${this.currentCostUSD.toFixed(4)} > $${this.maxCostUSD}. ` +
152
+ `Last computation: ${computationName}. Test aborted.`
153
+ );
154
+ }
155
+
156
+ // Still track in production tracker (to test table)
157
+ await this.tracker.trackCost(computationName, date, bytesProcessed);
158
+
159
+ return cost;
160
+ }
161
+
162
+ getSummary() {
163
+ return {
164
+ totalCostUSD: this.currentCostUSD,
165
+ maxCostUSD: this.maxCostUSD,
166
+ utilizationPct: (this.currentCostUSD / this.maxCostUSD) * 100,
167
+ byComputation: Object.entries(this.costByComputation)
168
+ .map(([name, cost]) => ({ name, cost }))
169
+ .sort((a, b) => b.cost - a.cost)
170
+ };
171
+ }
172
+ }
173
+
174
+ // ============================================================================
175
+ // LINEAGE TRACKER
176
+ // ============================================================================
177
+
178
+ class TestLineageTracker {
179
+ constructor(manifest) {
180
+ this.manifest = manifest;
181
+ this.executionOrder = [];
182
+ this.dependencyGraph = new Map();
183
+ }
184
+
185
+ recordExecution(computationName, pass, status, duration) {
186
+ this.executionOrder.push({
187
+ name: computationName,
188
+ pass,
189
+ status,
190
+ duration,
191
+ timestamp: new Date().toISOString()
192
+ });
193
+ }
194
+
195
+ buildGraph() {
196
+ for (const entry of this.manifest) {
197
+ this.dependencyGraph.set(entry.name, {
198
+ name: entry.originalName,
199
+ pass: entry.pass,
200
+ dependencies: entry.dependencies,
201
+ category: entry.category,
202
+ type: entry.type
203
+ });
204
+ }
205
+ }
206
+
207
+ generateReport() {
208
+ this.buildGraph();
209
+
210
+ const report = {
211
+ totalComputations: this.manifest.length,
212
+ executionOrder: this.executionOrder,
213
+ dependencyGraph: Array.from(this.dependencyGraph.entries()).map(([name, data]) => ({
214
+ computation: data.name,
215
+ pass: data.pass,
216
+ dependencies: data.dependencies.map(dep => {
217
+ const depData = this.dependencyGraph.get(dep);
218
+ return depData ? depData.name : dep;
219
+ }),
220
+ category: data.category,
221
+ type: data.type
222
+ })),
223
+ passSummary: this._summarizeByPass()
224
+ };
225
+
226
+ return report;
227
+ }
228
+
229
+ _summarizeByPass() {
230
+ const byPass = {};
231
+ for (const entry of this.manifest) {
232
+ if (!byPass[entry.pass]) byPass[entry.pass] = [];
233
+ byPass[entry.pass].push(entry.originalName);
234
+ }
235
+ return byPass;
236
+ }
237
+
238
+ printGraph() {
239
+ console.log('\n📊 COMPUTATION DEPENDENCY GRAPH\n');
240
+
241
+ const byPass = this._summarizeByPass();
242
+ const passNumbers = Object.keys(byPass).map(Number).sort((a, b) => a - b);
243
+
244
+ for (const pass of passNumbers) {
245
+ console.log(`Pass ${pass}:`);
246
+ for (const compName of byPass[pass]) {
247
+ const entry = this.manifest.find(e => e.originalName === compName);
248
+ const deps = entry.dependencies.length > 0
249
+ ? ` (depends on: ${entry.dependencies.join(', ')})`
250
+ : '';
251
+ console.log(` • ${compName}${deps}`);
252
+ }
253
+ console.log('');
254
+ }
255
+ }
256
+ }
257
+
258
+ // ============================================================================
259
+ // TEST RUNNER
260
+ // ============================================================================
261
+
262
+ class PipelineTestRunner {
263
+ constructor(options) {
264
+ this.options = options;
265
+ this.results = {
266
+ runId: null,
267
+ startTime: null,
268
+ endTime: null,
269
+ status: 'pending',
270
+ computations: [],
271
+ cost: null,
272
+ storage: null,
273
+ lineage: null,
274
+ performance: []
275
+ };
276
+ }
277
+
278
+ async run() {
279
+ console.log('╔════════════════════════════════════════════════════════════╗');
280
+ console.log('║ PRODUCTION PIPELINE FULL INTEGRATION TEST ║');
281
+ console.log('╚════════════════════════════════════════════════════════════╝\n');
282
+
283
+ this.results.startTime = new Date().toISOString();
284
+ this.results.runId = `test-${Date.now()}`;
285
+
286
+ try {
287
+ // 1. Load production config
288
+ const prodConfig = this._loadProductionConfig();
289
+
290
+ // 2. Build test config
291
+ const testOptions = {
292
+ runId: this.results.runId,
293
+ date: this.options.date,
294
+ entities: this.options.entities,
295
+ maxCost: this.options.maxCost || 10,
296
+ testDataset: this.options.testDataset,
297
+ testBucket: this.options.testBucket,
298
+ batchSize: this.options.batchSize,
299
+ concurrency: this.options.concurrency
300
+ };
301
+
302
+ const builder = new TestConfigBuilder(prodConfig, testOptions);
303
+ const testConfig = builder.build();
304
+
305
+ console.log('🔧 Test Configuration:');
306
+ console.log(` Run ID: ${testConfig.testMode.runId}`);
307
+ console.log(` Date: ${testConfig.testMode.dateOverride}`);
308
+ console.log(` Entities: ${testConfig.testMode.targetEntities || 'ALL'}`);
309
+ console.log(` Max Cost: $${testConfig.testMode.maxCostUSD}`);
310
+ console.log(` Test Dataset: ${testConfig.bigquery.dataset}`);
311
+ console.log(` Test Bucket: ${testConfig.gcs.bucket}`);
312
+ console.log(` Batch Size: ${testConfig.execution.batchSize}`);
313
+ console.log(` Concurrency: ${testConfig.execution.entityConcurrency}`);
314
+ console.log(` Bypass Locks: ${testConfig.bypassLocks ? 'YES' : 'NO'}\n`);
315
+
316
+ // 3. Initialize orchestrator (PRODUCTION CODE)
317
+ const orchestrator = new Orchestrator(testConfig, console);
318
+
319
+ // PATCH: Monkey-patch _executeComputation to debug silent errors
320
+ const originalExecuteComp = orchestrator._executeComputation;
321
+ orchestrator._executeComputation = async function(entry, ...args) {
322
+ try {
323
+ return await originalExecuteComp.call(this, entry, ...args);
324
+ } catch (e) {
325
+ console.error(JSON.stringify(e, null, 2));
326
+ if (e.stack) console.error(e.stack);
327
+ throw e; // Re-throw to be caught by the standard handler
328
+ }
329
+ };
330
+
331
+ await orchestrator.initialize();
332
+
333
+ console.log(`📦 Loaded ${orchestrator.manifest.length} computations\n`);
334
+
335
+ // 4. Setup interceptors
336
+ const storageInterceptor = new TestStorageInterceptor(
337
+ orchestrator.storageManager,
338
+ testConfig
339
+ );
340
+ const costEnforcer = new TestCostEnforcer(
341
+ orchestrator.storageManager.costTracker || { trackCost: async () => {} },
342
+ testConfig.testMode.maxCostUSD
343
+ );
344
+ const lineageTracker = new TestLineageTracker(orchestrator.manifest);
345
+
346
+ // Inject interceptors via proxy
347
+ // FIX: Use .bind() to ensure methods don't lose 'this' context
348
+ orchestrator.storageManager = new Proxy(storageInterceptor, {
349
+ get: (target, prop) => {
350
+ // 1. Check interceptor first
351
+ if (prop in target) return target[prop];
352
+
353
+ // 2. Fallback to original manager
354
+ const originalManager = target.storage;
355
+ const value = originalManager[prop];
356
+
357
+ // 3. CRITICAL: Bind functions to original manager to preserve 'this'
358
+ if (typeof value === 'function') {
359
+ return value.bind(originalManager);
360
+ }
361
+ return value;
362
+ }
363
+ });
364
+
365
+ // Print dependency graph
366
+ lineageTracker.printGraph();
367
+
368
+ // 5. Execute pipeline (PRODUCTION CODE)
369
+ console.log('🚀 Starting pipeline execution...\n');
370
+
371
+ const execResult = await orchestrator.execute({
372
+ date: testConfig.testMode.dateOverride,
373
+ entities: testConfig.testMode.targetEntities,
374
+ dryRun: false, // Actually write to test tables
375
+ force: true // FIX: FORCE LOCK BYPASS & FORCE RE-RUN OF VALID COMPUTATIONS
376
+ });
377
+
378
+ // 6. Collect results
379
+ this.results.computations = execResult.completed.map(c => ({
380
+ name: c.name,
381
+ status: c.status,
382
+ resultCount: c.resultCount,
383
+ duration: c.duration
384
+ }));
385
+
386
+ // FIX: Correctly report failure if errors occurred
387
+ if (execResult.summary.errors > 0 || (execResult.errors && execResult.errors.length > 0)) {
388
+ this.results.status = 'failed';
389
+ this.results.errors = execResult.errors;
390
+ console.error(`\n❌ Pipeline finished with ${execResult.summary.errors} errors.`);
391
+ } else if (execResult.summary.completed === 0 && orchestrator.manifest.length > 0) {
392
+ this.results.status = 'failed';
393
+ console.error('\n❌ Pipeline finished but 0 computations completed.');
394
+ } else {
395
+ this.results.status = 'success';
396
+ }
397
+
398
+ this.results.cost = costEnforcer.getSummary();
399
+ this.results.storage = storageInterceptor.getSummary();
400
+ this.results.lineage = lineageTracker.generateReport();
401
+ this.results.endTime = new Date().toISOString();
402
+
403
+ // 7. Print summary
404
+ this._printSummary();
405
+
406
+ // 8. Save report
407
+ this._saveReport();
408
+
409
+ return this.results;
410
+
411
+ } catch (error) {
412
+ this.results.status = 'failed';
413
+ this.results.error = error.message;
414
+ this.results.endTime = new Date().toISOString();
415
+
416
+ console.error('\n❌ TEST FAILED:', error); // Log full error object
417
+ if (error.stack) console.error(error.stack);
418
+
419
+ this._saveReport();
420
+ throw error;
421
+ }
422
+ }
423
+
424
+ _loadProductionConfig() {
425
+ // Load real production config
426
+ const prodConfig = require('../config/bulltrackers.config');
427
+
428
+ // Dynamically load ALL computations from directory
429
+ const computationsDir = path.join(__dirname, '../computations');
430
+ const files = fs.readdirSync(computationsDir).filter(f => f.endsWith('.js'));
431
+
432
+ prodConfig.computations = files.map(f => {
433
+ const ComputationClass = require(path.join(computationsDir, f));
434
+ return ComputationClass;
435
+ });
436
+
437
+ console.log(`✅ Loaded ${prodConfig.computations.length} computations from directory\n`);
438
+
439
+ return prodConfig;
440
+ }
441
+
442
+ _printSummary() {
443
+ console.log('\n╔════════════════════════════════════════════════════════════╗');
444
+ console.log('║ TEST SUMMARY ║');
445
+ console.log('╚════════════════════════════════════════════════════════════╝\n');
446
+
447
+ const duration = new Date(this.results.endTime) - new Date(this.results.startTime);
448
+
449
+ console.log('⏱️ Duration:', Math.round(duration / 1000), 'seconds');
450
+
451
+ // Colorize status
452
+ const statusIcon = this.results.status === 'success' ? '✅' : '❌';
453
+ console.log(`${statusIcon} Status:`, this.results.status.toUpperCase());
454
+ console.log('');
455
+
456
+ // Computations
457
+ console.log('📊 Computations:');
458
+ console.log(` Completed: ${this.results.computations.length}`);
459
+ for (const comp of this.results.computations) {
460
+ console.log(` • ${comp.name}: ${comp.resultCount} entities (${comp.duration}ms)`);
461
+ }
462
+ console.log('');
463
+
464
+ // Cost
465
+ console.log('💰 Cost Analysis:');
466
+ console.log(` Total: $${this.results.cost.totalCostUSD.toFixed(6)}`);
467
+ console.log(` Budget: $${this.results.cost.maxCostUSD}`);
468
+ console.log(` Utilization: ${this.results.cost.utilizationPct.toFixed(1)}%`);
469
+ console.log(' By computation:');
470
+ for (const { name, cost } of this.results.cost.byComputation.slice(0, 5)) {
471
+ console.log(` - ${name}: $${cost.toFixed(6)}`);
472
+ }
473
+ console.log('');
474
+
475
+ // Storage
476
+ console.log('💾 Storage:');
477
+ console.log(` Total writes: ${this.results.storage.totalWrites}`);
478
+ console.log(` Total entities: ${this.results.storage.totalEntities}`);
479
+ console.log('');
480
+
481
+ // Lineage
482
+ console.log('🔗 Lineage:');
483
+ console.log(` Total passes: ${Object.keys(this.results.lineage.passSummary).length}`);
484
+ console.log(` Execution order verified: ✅`);
485
+ }
486
+
487
+ _saveReport() {
488
+ const reportPath = path.join(__dirname, `test-report-${this.results.runId}.json`);
489
+ fs.writeFileSync(reportPath, JSON.stringify(this.results, null, 2));
490
+ console.log(`\n📄 Full report saved: ${reportPath}`);
491
+ }
492
+ }
493
+
494
+ // ============================================================================
495
+ // CLI INTERFACE
496
+ // ============================================================================
497
+
498
+ async function main() {
499
+ const args = process.argv.slice(2);
500
+ const options = {
501
+ date: null,
502
+ entities: null,
503
+ maxCost: 10,
504
+ testDataset: null,
505
+ testBucket: null,
506
+ batchSize: null,
507
+ concurrency: null
508
+ };
509
+
510
+ // Parse CLI arguments
511
+ for (let i = 0; i < args.length; i += 2) {
512
+ const key = args[i].replace(/^--/, '');
513
+ const value = args[i + 1];
514
+
515
+ if (key === 'entities') {
516
+ options.entities = value.split(',');
517
+ } else if (key === 'max-cost') {
518
+ options.maxCost = parseFloat(value);
519
+ } else if (key === 'batch-size') {
520
+ options.batchSize = parseInt(value, 10);
521
+ } else if (key === 'concurrency') {
522
+ options.concurrency = parseInt(value, 10);
523
+ } else {
524
+ options[key] = value;
525
+ }
526
+ }
527
+
528
+ // Validate required options
529
+ if (!options.date) {
530
+ console.error('❌ Missing required option: --date');
531
+ console.log('\nUsage: node test/run-pipeline-test.js --date YYYY-MM-DD [OPTIONS]\n');
532
+ console.log('Options:');
533
+ console.log(' --date Target date (required)');
534
+ console.log(' --entities Comma-separated entity IDs (optional, tests all if omitted)');
535
+ console.log(' --max-cost Maximum cost in USD (default: 10)');
536
+ console.log(' --batch-size Batch size for data fetching (default: 100)');
537
+ console.log(' --concurrency Entity processing concurrency (default: 5)');
538
+ console.log(' --test-dataset Override test dataset name');
539
+ console.log(' --test-bucket Override test bucket name');
540
+ process.exit(1);
541
+ }
542
+
543
+ const runner = new PipelineTestRunner(options);
544
+ await runner.run();
545
+ }
546
+
547
+ if (require.main === module) {
548
+ main().catch(e => {
549
+ console.error('\n💥 Fatal error:', e);
550
+ process.exit(1);
551
+ });
552
+ }
553
+
554
+ module.exports = { PipelineTestRunner, TestConfigBuilder };