livrare lot 2
This commit is contained in:
commit
8ecc78e729
763 changed files with 164593 additions and 0 deletions
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* TECHNIQUES V3 PILOT - Test Script
|
||||
*
|
||||
* Usage: npx ts-node scripts/test-techniques-v3.ts
|
||||
*/
|
||||
|
||||
import { createRedisConnection } from '../src/shared/redis/connection';
|
||||
import { TechniquesV3Executor, LLMClient, ModelConfig } from '../src/components/techniques/executor';
|
||||
|
||||
// ============================================================================
|
||||
// MOCK LLM CLIENT (for testing without real API calls)
|
||||
// ============================================================================
|
||||
|
||||
class MockLLMClient implements LLMClient {
|
||||
async call(prompt: string, systemPrompt: string, config: ModelConfig): Promise<string> {
|
||||
console.log(`\n📡 LLM Call to: ${config.model_key}`);
|
||||
console.log(` Prompt length: ${prompt.length} chars`);
|
||||
|
||||
// Simulate screening response
|
||||
if (prompt.includes('AVAILABLE DIMENSIONS')) {
|
||||
return JSON.stringify({
|
||||
detected_dimensions: ['D1', 'D4'],
|
||||
confidence_per_dimension: {
|
||||
D1: 85,
|
||||
D4: 72
|
||||
},
|
||||
quick_reasoning: 'Text contains fear appeal language and out-of-context claims'
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate deep analysis response
|
||||
if (prompt.includes('TECHNIQUES TO DETECT')) {
|
||||
const dimension = prompt.match(/\(D\d\)/)?.[0]?.replace(/[()]/g, '') || 'D1';
|
||||
|
||||
if (dimension === 'D1') {
|
||||
return JSON.stringify({
|
||||
dimension: 'D1',
|
||||
detected_techniques: [
|
||||
{
|
||||
technique_id: 5,
|
||||
confidence: 92,
|
||||
intensity: 3,
|
||||
evidence: 'This is a THREAT to our survival!'
|
||||
},
|
||||
{
|
||||
technique_id: 6,
|
||||
confidence: 78,
|
||||
intensity: 2,
|
||||
evidence: 'We must ACT NOW before it is too late!'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (dimension === 'D4') {
|
||||
return JSON.stringify({
|
||||
dimension: 'D4',
|
||||
detected_techniques: [
|
||||
{
|
||||
technique_id: 77,
|
||||
confidence: 65,
|
||||
intensity: 2,
|
||||
evidence: 'The image shows clear manipulation artifacts'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST DATA
|
||||
// ============================================================================
|
||||
|
||||
const TEST_TEXT = `
|
||||
URGENT: This is a THREAT to our survival! The government is hiding the truth from you.
|
||||
|
||||
We must ACT NOW before it is too late! They don't want you to know about this conspiracy.
|
||||
|
||||
The image shows clear manipulation artifacts around the edges. Expert sources (who wish to remain anonymous)
|
||||
confirm that this vaccine is dangerous.
|
||||
|
||||
Share this with everyone you know before they delete it! This is being censored on all platforms.
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// MAIN TEST
|
||||
// ============================================================================
|
||||
|
||||
async function runTest() {
|
||||
console.log('='.repeat(70));
|
||||
console.log('TECHNIQUES V3 PILOT - TEST');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const redis = createRedisConnection({ label: 'test-techniques-v3' });
|
||||
|
||||
try {
|
||||
// Check if pilot data is loaded
|
||||
const manifest = await redis.get('didi:config:techniques:v3:stage_assignments');
|
||||
if (!manifest) {
|
||||
console.log('\n⚠️ Config data not found in Redis!');
|
||||
console.log(' Run: curl -X POST http://localhost:3005/api/sync-redis');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n✅ Config data found in Redis');
|
||||
console.log(` Manifest: ${manifest}`);
|
||||
|
||||
// Check for framework techniques (needed for deep analysis)
|
||||
const frameworkTechniques = await redis.get('didi:framework:techniques');
|
||||
if (!frameworkTechniques) {
|
||||
console.log('\n⚠️ Framework techniques not found!');
|
||||
console.log(' Using mock technique hierarchy for test...');
|
||||
|
||||
// Create mock hierarchy
|
||||
const mockHierarchy = {
|
||||
dimensions: [
|
||||
{
|
||||
dimension_id: 1,
|
||||
dimension_code: 'D1',
|
||||
dimension_name: 'Emotional Manipulation',
|
||||
subdimensions: [
|
||||
{
|
||||
subdimension_id: 1,
|
||||
subdimension_name: 'Fear Appeals',
|
||||
techniques: [
|
||||
{ technique_id: 5, technique_name: 'Fear Appeal', severity: 8, confidence: 5, indicators: [] },
|
||||
{ technique_id: 6, technique_name: 'Urgency Appeal', severity: 7, confidence: 5, indicators: [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
dimension_id: 4,
|
||||
dimension_code: 'D4',
|
||||
dimension_name: 'Content Manipulation',
|
||||
subdimensions: [
|
||||
{
|
||||
subdimension_id: 10,
|
||||
subdimension_name: 'Media Manipulation',
|
||||
techniques: [
|
||||
{ technique_id: 77, technique_name: 'Deepfake', severity: 9, confidence: 5, indicators: [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await redis.set('didi:framework:techniques', JSON.stringify(mockHierarchy));
|
||||
console.log(' Mock hierarchy created');
|
||||
}
|
||||
|
||||
// Create executor with mock LLM
|
||||
const mockLLM = new MockLLMClient();
|
||||
const executor = new TechniquesV3Executor(redis, mockLLM);
|
||||
|
||||
// Run test
|
||||
console.log('\n' + '-'.repeat(70));
|
||||
console.log('RUNNING ANALYSIS...');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(`\nInput text (${TEST_TEXT.length} chars):`);
|
||||
console.log(TEST_TEXT.substring(0, 200) + '...');
|
||||
|
||||
const sessionId = `test-${Date.now()}`;
|
||||
const result = await executor.execute(TEST_TEXT, sessionId);
|
||||
|
||||
// Display results
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('RESULTS');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
console.log('\n📊 MANIPULATION SCORE:', (result.manipulation_score * 100).toFixed(1) + '%');
|
||||
console.log('📈 DIMENSIONS AFFECTED:', result.dimensions_affected.join(', '));
|
||||
console.log('📝 TECHNIQUES DETECTED:', result.techniques.length);
|
||||
|
||||
console.log('\n🔍 DETECTED TECHNIQUES:');
|
||||
for (const t of result.techniques) {
|
||||
console.log(` [${t.id}] ${t.name} (${t.dimension})`);
|
||||
console.log(` Confidence: ${t.confidence}%, Intensity: ${t.intensity}, Severity: ${t.severity}`);
|
||||
console.log(` Evidence: "${t.evidence.substring(0, 50)}..."`);
|
||||
}
|
||||
|
||||
console.log('\n🔗 COUPLING CONTEXT:');
|
||||
console.log(' For Claims:');
|
||||
console.log(` - Emotional manipulation: ${result.coupling_context.for_claims.has_emotional_manipulation}`);
|
||||
console.log(` - Logical fallacies: ${result.coupling_context.for_claims.has_logical_fallacies}`);
|
||||
console.log(` - Manipulation level: ${result.coupling_context.for_claims.manipulation_level}`);
|
||||
console.log(` - Warning flags: ${result.coupling_context.for_claims.warning_flags.join(', ') || 'none'}`);
|
||||
|
||||
console.log(' For Verdict:');
|
||||
console.log(` - Risk score: ${result.coupling_context.for_verdict.risk_score.toFixed(3)}`);
|
||||
console.log(` - Dimension count: ${result.coupling_context.for_verdict.dimension_count}`);
|
||||
console.log(` - Severe techniques: ${result.coupling_context.for_verdict.severe_technique_count}`);
|
||||
console.log(` - Needs override check: ${result.coupling_context.for_verdict.needs_override_check}`);
|
||||
|
||||
console.log('\n⏱️ TIMING:');
|
||||
console.log(` Screening: ${result.metadata.screening_duration_ms}ms (${result.metadata.llm_screening})`);
|
||||
console.log(` Deep analysis: ${result.metadata.deep_analysis_duration_ms}ms (${result.metadata.llm_deep})`);
|
||||
console.log(` Total: ${result.metadata.total_duration_ms}ms`);
|
||||
|
||||
// Verify Redis storage
|
||||
console.log('\n📦 REDIS STORAGE:');
|
||||
const keys = await redis.keys(`agent:result:${sessionId}:*`);
|
||||
for (const key of keys.sort()) {
|
||||
console.log(` - ${key}`);
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('TEST COMPLETE ✅');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error);
|
||||
} finally {
|
||||
await redis.quit();
|
||||
}
|
||||
}
|
||||
|
||||
// Run test
|
||||
runTest().catch(console.error);
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* RabbitMQ cluster readiness verification.
|
||||
*
|
||||
* Runs checks against the RabbitMQ instance configured via env vars
|
||||
* (RABBITMQ_HOST/PORT/USER/PASS/VHOST). Intended to be run:
|
||||
* - Pre-cutover: confirm cluster accessible + vhost + privileges
|
||||
* - Post-cutover: confirm expected topology created
|
||||
* - CI / healthcheck: block container start until broker is reachable
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all checks pass
|
||||
* 1 — fatal issue (connectivity, auth, missing vhost)
|
||||
* 2 — warning (topology partially initialized — normal pre-workers)
|
||||
*
|
||||
* Usage:
|
||||
* RABBITMQ_HOST=10.11.50.100 RABBITMQ_PORT=16672 \
|
||||
* RABBITMQ_USER=didi RABBITMQ_PASS=... RABBITMQ_VHOST=/didi \
|
||||
* npx ts-node --transpile-only scripts/verify-rabbitmq-cluster.ts
|
||||
*/
|
||||
|
||||
import amqp from 'amqplib';
|
||||
import {
|
||||
getRabbitMQUrl,
|
||||
getRabbitMQConfig,
|
||||
EXCHANGE_NAME,
|
||||
QUEUE,
|
||||
MEDIA_QUEUE,
|
||||
ANALYSIS_COMPONENTS,
|
||||
type PlanType,
|
||||
} from '../src/shared/queue/constants';
|
||||
|
||||
const PLAN_TYPES: PlanType[] = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
type Severity = 'OK' | 'WARN' | 'FAIL';
|
||||
interface Check { name: string; severity: Severity; detail: string; }
|
||||
|
||||
const results: Check[] = [];
|
||||
let fatal = 0, warn = 0;
|
||||
|
||||
function record(name: string, severity: Severity, detail: string) {
|
||||
results.push({ name, severity, detail });
|
||||
if (severity === 'FAIL') fatal++;
|
||||
else if (severity === 'WARN') warn++;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cfg = getRabbitMQConfig();
|
||||
const target = `${cfg.host}:${cfg.port}${cfg.vhost}`;
|
||||
|
||||
console.log(`\n${'='.repeat(70)}`);
|
||||
console.log(`RabbitMQ cluster readiness — ${target} (user: ${cfg.user})`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Connect + auth
|
||||
// -------------------------------------------------------------------------
|
||||
let conn: any = null;
|
||||
try {
|
||||
conn = await amqp.connect(getRabbitMQUrl());
|
||||
record('connectivity', 'OK', `amqp.connect → ${target}`);
|
||||
} catch (err: any) {
|
||||
// Map common errors to actionable messages
|
||||
let hint = err.message;
|
||||
if (err.code === 'ENOTFOUND') hint = `DNS resolution failed for ${cfg.host} — check network`;
|
||||
else if (err.code === 'ECONNREFUSED') hint = `port ${cfg.port} refused — broker down?`;
|
||||
else if (/ACCESS_REFUSED/i.test(err.message)) hint = `auth failed — check user/pass`;
|
||||
else if (/NOT_ALLOWED.*vhost/i.test(err.message)) hint = `vhost '${cfg.vhost}' missing or no access`;
|
||||
|
||||
record('connectivity', 'FAIL', hint);
|
||||
await finalize(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Channel creation
|
||||
// -------------------------------------------------------------------------
|
||||
let ch: any = null;
|
||||
try {
|
||||
ch = await conn.createChannel();
|
||||
record('channel', 'OK', 'createChannel → ready');
|
||||
} catch (err: any) {
|
||||
record('channel', 'FAIL', `cannot create channel: ${err.message}`);
|
||||
await finalize(conn);
|
||||
return;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Exchange declare (idempotent — won't break existing)
|
||||
// -------------------------------------------------------------------------
|
||||
try {
|
||||
await ch.assertExchange(EXCHANGE_NAME, 'topic', { durable: true });
|
||||
record('exchange:analysis', 'OK', `exchange '${EXCHANGE_NAME}' (topic, durable) asserted`);
|
||||
} catch (err: any) {
|
||||
record('exchange:analysis', 'FAIL', `cannot assert exchange: ${err.message}`);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Test queue write privilege (create a temp queue, then delete)
|
||||
// -------------------------------------------------------------------------
|
||||
try {
|
||||
const tempQueue = `didi.verify.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
||||
await ch.assertQueue(tempQueue, { durable: false, autoDelete: true, exclusive: true });
|
||||
await ch.deleteQueue(tempQueue);
|
||||
record('privileges:write', 'OK', 'can create + delete queues on vhost');
|
||||
} catch (err: any) {
|
||||
record('privileges:write', 'FAIL', `no write privilege on vhost '${cfg.vhost}': ${err.message}`);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Expected topology — check which queues already exist
|
||||
// (All 31 queues are OK if workers have started; 0 is OK pre-cutover)
|
||||
// -------------------------------------------------------------------------
|
||||
const expectedQueues: string[] = [];
|
||||
for (const comp of ANALYSIS_COMPONENTS) {
|
||||
for (const plan of PLAN_TYPES) {
|
||||
expectedQueues.push(QUEUE.queueName(comp, plan));
|
||||
}
|
||||
}
|
||||
for (const plan of PLAN_TYPES) {
|
||||
expectedQueues.push(MEDIA_QUEUE.queueName(plan));
|
||||
}
|
||||
expectedQueues.push(QUEUE.RESULTS_QUEUE);
|
||||
|
||||
let foundQueues = 0;
|
||||
for (const q of expectedQueues) {
|
||||
try {
|
||||
// checkQueue throws if queue doesn't exist (and poisons the channel!)
|
||||
// Use a new channel per check to avoid cascade failures
|
||||
const probeCh = await conn.createChannel();
|
||||
probeCh.on('error', () => { /* swallow */ });
|
||||
try {
|
||||
await probeCh.checkQueue(q);
|
||||
foundQueues++;
|
||||
} catch {
|
||||
// Queue doesn't exist yet — normal pre-cutover
|
||||
}
|
||||
try { await probeCh.close(); } catch { /* ignore */ }
|
||||
} catch {
|
||||
// Channel creation failed — broker issue
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundQueues === expectedQueues.length) {
|
||||
record('topology', 'OK', `all ${expectedQueues.length} expected queues present`);
|
||||
} else if (foundQueues === 0) {
|
||||
record('topology', 'WARN', `0/${expectedQueues.length} queues — normal if workers not started yet (will auto-create)`);
|
||||
} else {
|
||||
record('topology', 'WARN', `${foundQueues}/${expectedQueues.length} queues present (partial — may be mid-cutover)`);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Cleanup
|
||||
// -------------------------------------------------------------------------
|
||||
try { await ch.close(); } catch { /* ignore */ }
|
||||
await finalize(conn);
|
||||
}
|
||||
|
||||
async function finalize(conn: any) {
|
||||
if (conn) {
|
||||
try { await conn.close(); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
console.log('');
|
||||
for (const r of results) {
|
||||
const badge =
|
||||
r.severity === 'OK' ? ' ✓ ' :
|
||||
r.severity === 'WARN' ? ' ⚠ ' : ' ✘ ';
|
||||
console.log(`${badge} [${r.severity.padEnd(4)}] ${r.name.padEnd(28)} ${r.detail}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('='.repeat(70));
|
||||
const passed = results.filter(r => r.severity === 'OK').length;
|
||||
console.log(`Results: ${passed} OK · ${warn} WARN · ${fatal} FAIL`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
if (fatal > 0) {
|
||||
console.log(`\n✘ NOT READY — ${fatal} fatal issue(s).\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (warn > 0) {
|
||||
console.log(`\n⚠ READY with ${warn} warning(s) — topology will auto-create when workers start.\n`);
|
||||
process.exit(0); // warnings are expected pre-cutover
|
||||
}
|
||||
console.log(`\n✓ CLUSTER READY — safe to cutover.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Unhandled error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* Redis cluster readiness verification.
|
||||
*
|
||||
* Runs a battery of checks against the Redis instance configured via env vars
|
||||
* (REDIS_HOST/PORT/USERNAME/PASSWORD/DB). Intended to be run:
|
||||
* - Pre-cutover: confirm the new cluster has all required keys before switching env
|
||||
* - Post-cutover smoke test: confirm nothing got lost
|
||||
* - CI / healthcheck: block container start until cluster is ready
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all checks pass
|
||||
* 1 — at least one FATAL check fails (connectivity, missing framework keys)
|
||||
* 2 — only WARNING-level issues (missing optional keys, empty scan results)
|
||||
*
|
||||
* Usage:
|
||||
* REDIS_HOST=10.11.50.100 REDIS_PORT=16379 REDIS_USERNAME=didi \
|
||||
* REDIS_PASSWORD=... npx tsx scripts/verify-redis-cluster.ts
|
||||
*/
|
||||
|
||||
import { createRedisConnection } from '../src/shared/redis/connection';
|
||||
import { FrameworkKeys, ConfigKeys } from '../src/shared/redis/keys';
|
||||
|
||||
type Severity = 'OK' | 'WARN' | 'FAIL';
|
||||
interface CheckResult {
|
||||
name: string;
|
||||
severity: Severity;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
const results: CheckResult[] = [];
|
||||
let fatalCount = 0;
|
||||
let warnCount = 0;
|
||||
|
||||
function record(name: string, severity: Severity, detail: string) {
|
||||
results.push({ name, severity, detail });
|
||||
if (severity === 'FAIL') fatalCount++;
|
||||
else if (severity === 'WARN') warnCount++;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const host = process.env.REDIS_HOST || 'didi-cache';
|
||||
const port = process.env.REDIS_PORT || '6379';
|
||||
const user = process.env.REDIS_USERNAME || '(legacy-auth)';
|
||||
|
||||
console.log(`\n${'='.repeat(70)}`);
|
||||
console.log(`Redis cluster readiness check — ${host}:${port} (user: ${user})`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const redis = createRedisConnection({
|
||||
label: 'verify-cluster',
|
||||
overrides: { maxRetriesPerRequest: 3 },
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Connectivity + auth
|
||||
// -------------------------------------------------------------------------
|
||||
try {
|
||||
const pong = await redis.ping();
|
||||
if (pong !== 'PONG') throw new Error(`unexpected PING reply: ${pong}`);
|
||||
record('connectivity', 'OK', 'PING → PONG');
|
||||
} catch (err: any) {
|
||||
record('connectivity', 'FAIL', `cannot reach cluster: ${err.message}`);
|
||||
await finalize(redis);
|
||||
return;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Server info (master/replica, memory)
|
||||
// -------------------------------------------------------------------------
|
||||
try {
|
||||
const info = await redis.info('replication');
|
||||
const role = /role:(\w+)/.exec(info)?.[1] || 'unknown';
|
||||
const slaves = /connected_slaves:(\d+)/.exec(info)?.[1] || '0';
|
||||
record('replication', role === 'master' ? 'OK' : 'WARN',
|
||||
`role=${role}, connected_slaves=${slaves}`);
|
||||
} catch (err: any) {
|
||||
record('replication', 'WARN', `INFO replication failed: ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const mem = await redis.info('memory');
|
||||
const used = /used_memory_human:(\S+)/.exec(mem)?.[1] || '?';
|
||||
const max = /maxmemory_human:(\S+)/.exec(mem)?.[1] || '?';
|
||||
const policy = /maxmemory_policy:(\S+)/.exec(mem)?.[1] || '?';
|
||||
record('memory', 'OK', `used=${used}, max=${max}, policy=${policy}`);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Framework keys (REQUIRED — agent-v3 fails without these)
|
||||
// -------------------------------------------------------------------------
|
||||
const requiredFrameworkKeys = [
|
||||
FrameworkKeys.manifest,
|
||||
FrameworkKeys.techniques,
|
||||
FrameworkKeys.sources,
|
||||
FrameworkKeys.claims,
|
||||
FrameworkKeys.verdicts,
|
||||
FrameworkKeys.weights,
|
||||
FrameworkKeys.dimensionsCompact,
|
||||
];
|
||||
|
||||
for (const key of requiredFrameworkKeys) {
|
||||
const exists = await redis.exists(key);
|
||||
if (exists) {
|
||||
const val = await redis.get(key);
|
||||
const size = val ? val.length : 0;
|
||||
record(`framework:${key.split(':').pop()}`, 'OK', `present (${size} bytes)`);
|
||||
} else {
|
||||
record(`framework:${key.split(':').pop()}`, 'FAIL', `MISSING: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional
|
||||
const providers = await redis.exists(FrameworkKeys.providers);
|
||||
record('framework:providers', providers ? 'OK' : 'WARN',
|
||||
providers ? 'present (optional)' : 'absent (optional, OK)');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Component config keys (REQUIRED for tier routing)
|
||||
// -------------------------------------------------------------------------
|
||||
const stageAssignmentKeys = [
|
||||
{ label: 'techniques', key: ConfigKeys.techniquesStageAssignments },
|
||||
{ label: 'ai-tampered', key: ConfigKeys.aiTamperedStageAssignments },
|
||||
{ label: 'claims', key: ConfigKeys.claimsStageAssignments },
|
||||
{ label: 'source-assessment', key: ConfigKeys.sourceAssessmentStageAssignments },
|
||||
{ label: 'vision', key: 'didi:config:vision:v1:stage_assignments' },
|
||||
{ label: 'verdict', key: 'didi:config:verdict:v1:stage_assignments' },
|
||||
];
|
||||
|
||||
for (const { label, key } of stageAssignmentKeys) {
|
||||
const val = await redis.get(key);
|
||||
if (!val) {
|
||||
record(`stage_assignments:${label}`, 'FAIL', `MISSING: ${key}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
const stages = Object.keys(parsed);
|
||||
if (stages.length === 0) {
|
||||
record(`stage_assignments:${label}`, 'FAIL', `empty object in ${key}`);
|
||||
continue;
|
||||
}
|
||||
// Verify tier-nested structure on first stage
|
||||
const firstStage = parsed[stages[0]];
|
||||
const hasFree = firstStage?.free?.models && Array.isArray(firstStage.free.models);
|
||||
const hasPremium = firstStage?.premium?.models && Array.isArray(firstStage.premium.models);
|
||||
if (hasFree && hasPremium) {
|
||||
record(`stage_assignments:${label}`, 'OK',
|
||||
`${stages.length} stage(s), tier-nested (free+premium) ✓`);
|
||||
} else if (hasFree) {
|
||||
record(`stage_assignments:${label}`, 'WARN',
|
||||
`${stages.length} stage(s), free only (premium missing — fallback works)`);
|
||||
} else {
|
||||
record(`stage_assignments:${label}`, 'FAIL',
|
||||
`${stages.length} stage(s), not tier-nested — old format? Re-run sync-redis.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
record(`stage_assignments:${label}`, 'FAIL', `invalid JSON: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Available models (union, used for UI)
|
||||
const availableModelKeys = [
|
||||
ConfigKeys.techniquesAvailableModels,
|
||||
ConfigKeys.aiTamperedAvailableModels,
|
||||
ConfigKeys.claimsAvailableModels,
|
||||
ConfigKeys.sourceAssessmentAvailableModels,
|
||||
];
|
||||
let missingModels = 0;
|
||||
for (const key of availableModelKeys) {
|
||||
if (!(await redis.exists(key))) missingModels++;
|
||||
}
|
||||
record('available_models',
|
||||
missingModels === 0 ? 'OK' : missingModels < availableModelKeys.length ? 'WARN' : 'FAIL',
|
||||
`${availableModelKeys.length - missingModels}/${availableModelKeys.length} keys present`);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Pipeline config + input profiles (REQUIRED by verdict calculator)
|
||||
// -------------------------------------------------------------------------
|
||||
const pipelineKeys = [
|
||||
ConfigKeys.pipelineComponentConfig,
|
||||
ConfigKeys.pipelineSessionConfig,
|
||||
ConfigKeys.pipelineInputProfiles,
|
||||
];
|
||||
for (const key of pipelineKeys) {
|
||||
const exists = await redis.exists(key);
|
||||
const label = key.split(':').slice(-1)[0];
|
||||
record(`pipeline:${label}`, exists ? 'OK' : 'FAIL',
|
||||
exists ? 'present' : `MISSING: ${key}`);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Aggregate stats
|
||||
// -------------------------------------------------------------------------
|
||||
try {
|
||||
const dbsize = await redis.dbsize();
|
||||
record('dbsize', 'OK', `${dbsize} keys total`);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
await finalize(redis);
|
||||
}
|
||||
|
||||
async function finalize(redis: any) {
|
||||
redis.disconnect();
|
||||
|
||||
// Pretty print
|
||||
console.log('');
|
||||
for (const r of results) {
|
||||
const badge =
|
||||
r.severity === 'OK' ? ' ✓ ' :
|
||||
r.severity === 'WARN' ? ' ⚠ ' : ' ✘ ';
|
||||
console.log(`${badge} [${r.severity.padEnd(4)}] ${r.name.padEnd(35)} ${r.detail}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('='.repeat(70));
|
||||
const passed = results.filter(r => r.severity === 'OK').length;
|
||||
console.log(`Results: ${passed} OK · ${warnCount} WARN · ${fatalCount} FAIL`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
if (fatalCount > 0) {
|
||||
console.log(`\n✘ NOT READY — ${fatalCount} fatal issue(s). Run sync-redis against this cluster before cutover.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (warnCount > 0) {
|
||||
console.log(`\n⚠ READY with warnings — ${warnCount} non-blocking issue(s).\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
console.log(`\n✓ CLUSTER READY — safe to cutover.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Unhandled error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue