bulltrackers-module 1.0.765 → 1.0.768
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.
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +298 -186
- package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
- package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
- package/functions/computation-system-v2/config/bulltrackers.config.js +26 -14
- package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +142 -4
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +119 -122
- package/functions/computation-system-v2/framework/storage/StorageManager.js +16 -18
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +155 -66
- package/functions/computation-system-v2/handlers/scheduler.js +15 -5
- package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
- package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
- package/package.json +1 -1
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
- package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
- package/functions/computation-system-v2/test/analyze-results.js +0 -238
- package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
- package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
- package/functions/computation-system-v2/test/other/test-framework.js +0 -500
- package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
- package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
- package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
- package/functions/computation-system-v2/test/other/test-results.json +0 -31
- package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
- package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
- package/functions/computation-system-v2/test/other/test-storage.js +0 -449
- package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
- package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
- package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
|
@@ -1,554 +0,0 @@
|
|
|
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 };
|