@soleri/core 7.0.0 → 8.1.0
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/dist/agency/agency-manager.d.ts +27 -1
- package/dist/agency/agency-manager.d.ts.map +1 -1
- package/dist/agency/agency-manager.js +180 -9
- package/dist/agency/agency-manager.js.map +1 -1
- package/dist/agency/default-rules.d.ts +7 -0
- package/dist/agency/default-rules.d.ts.map +1 -0
- package/dist/agency/default-rules.js +79 -0
- package/dist/agency/default-rules.js.map +1 -0
- package/dist/agency/types.d.ts +48 -0
- package/dist/agency/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +17 -2
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +118 -8
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/knowledge-synthesizer.d.ts +37 -0
- package/dist/brain/knowledge-synthesizer.d.ts.map +1 -0
- package/dist/brain/knowledge-synthesizer.js +159 -0
- package/dist/brain/knowledge-synthesizer.js.map +1 -0
- package/dist/brain/learning-radar.d.ts +96 -0
- package/dist/brain/learning-radar.d.ts.map +1 -0
- package/dist/brain/learning-radar.js +202 -0
- package/dist/brain/learning-radar.js.map +1 -0
- package/dist/brain/types.d.ts +15 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/context/context-engine.d.ts.map +1 -1
- package/dist/context/context-engine.js +82 -17
- package/dist/context/context-engine.js.map +1 -1
- package/dist/context/types.d.ts +5 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/control/intent-router.d.ts +12 -1
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +68 -0
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +17 -0
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/classifier.d.ts +18 -0
- package/dist/curator/classifier.d.ts.map +1 -0
- package/dist/curator/classifier.js +59 -0
- package/dist/curator/classifier.js.map +1 -0
- package/dist/curator/quality-gate.d.ts +29 -0
- package/dist/curator/quality-gate.d.ts.map +1 -0
- package/dist/curator/quality-gate.js +86 -0
- package/dist/curator/quality-gate.js.map +1 -0
- package/dist/domain-packs/index.d.ts +0 -3
- package/dist/domain-packs/index.d.ts.map +1 -1
- package/dist/domain-packs/index.js +0 -3
- package/dist/domain-packs/index.js.map +1 -1
- package/dist/domain-packs/loader.d.ts.map +1 -1
- package/dist/domain-packs/loader.js +20 -4
- package/dist/domain-packs/loader.js.map +1 -1
- package/dist/domain-packs/pack-runtime.d.ts +5 -5
- package/dist/domain-packs/pack-runtime.d.ts.map +1 -1
- package/dist/domain-packs/pack-runtime.js +2 -2
- package/dist/domain-packs/pack-runtime.js.map +1 -1
- package/dist/domain-packs/types.d.ts +8 -2
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/bin/soleri-engine.js +13 -2
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/module-manifest.d.ts +28 -0
- package/dist/engine/module-manifest.d.ts.map +1 -0
- package/dist/engine/module-manifest.js +85 -0
- package/dist/engine/module-manifest.js.map +1 -0
- package/dist/engine/register-engine.d.ts +19 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +15 -2
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/events/event-bus.d.ts +30 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +51 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/flows/chain-runner.d.ts +46 -0
- package/dist/flows/chain-runner.d.ts.map +1 -0
- package/dist/flows/chain-runner.js +271 -0
- package/dist/flows/chain-runner.js.map +1 -0
- package/dist/flows/chain-types.d.ts +103 -0
- package/dist/flows/chain-types.d.ts.map +1 -0
- package/dist/flows/chain-types.js +23 -0
- package/dist/flows/chain-types.js.map +1 -0
- package/dist/health/doctor-checks.d.ts +15 -0
- package/dist/health/doctor-checks.d.ts.map +1 -0
- package/dist/health/doctor-checks.js +98 -0
- package/dist/health/doctor-checks.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +0 -2
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +52 -0
- package/dist/intake/text-ingester.d.ts.map +1 -0
- package/dist/intake/text-ingester.js +181 -0
- package/dist/intake/text-ingester.js.map +1 -0
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +45 -5
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/llm/oauth-discovery.d.ts +18 -0
- package/dist/llm/oauth-discovery.d.ts.map +1 -0
- package/dist/llm/oauth-discovery.js +130 -0
- package/dist/llm/oauth-discovery.js.map +1 -0
- package/dist/llm/types.d.ts +4 -2
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/packs/pack-installer.d.ts +2 -1
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +10 -1
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/persistence/index.d.ts +0 -1
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +0 -1
- package/dist/persistence/index.js.map +1 -1
- package/dist/persistence/types.d.ts +2 -6
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/planning/evidence-collector.d.ts +41 -0
- package/dist/planning/evidence-collector.d.ts.map +1 -0
- package/dist/planning/evidence-collector.js +194 -0
- package/dist/planning/evidence-collector.js.map +1 -0
- package/dist/planning/planner.d.ts +4 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +11 -0
- package/dist/planning/planner.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/plugin-registry.d.ts +4 -0
- package/dist/plugins/plugin-registry.d.ts.map +1 -1
- package/dist/plugins/plugin-registry.js +4 -0
- package/dist/plugins/plugin-registry.js.map +1 -1
- package/dist/plugins/types.d.ts +32 -27
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js +6 -3
- package/dist/plugins/types.js.map +1 -1
- package/dist/queue/job-queue.d.ts +92 -0
- package/dist/queue/job-queue.d.ts.map +1 -0
- package/dist/queue/job-queue.js +180 -0
- package/dist/queue/job-queue.js.map +1 -0
- package/dist/queue/pipeline-runner.d.ts +62 -0
- package/dist/queue/pipeline-runner.d.ts.map +1 -0
- package/dist/queue/pipeline-runner.js +126 -0
- package/dist/queue/pipeline-runner.js.map +1 -0
- package/dist/runtime/admin-setup-ops.d.ts +20 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -0
- package/dist/runtime/admin-setup-ops.js +583 -0
- package/dist/runtime/admin-setup-ops.js.map +1 -0
- package/dist/runtime/chain-ops.d.ts +9 -0
- package/dist/runtime/chain-ops.d.ts.map +1 -0
- package/dist/runtime/chain-ops.js +107 -0
- package/dist/runtime/chain-ops.js.map +1 -0
- package/dist/runtime/claude-md-helpers.d.ts +56 -0
- package/dist/runtime/claude-md-helpers.d.ts.map +1 -0
- package/dist/runtime/claude-md-helpers.js +160 -0
- package/dist/runtime/claude-md-helpers.js.map +1 -0
- package/dist/runtime/curator-extra-ops.d.ts +3 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +81 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
- package/dist/runtime/facades/admin-facade.js +5 -2
- package/dist/runtime/facades/admin-facade.js.map +1 -1
- package/dist/runtime/facades/agency-facade.d.ts.map +1 -1
- package/dist/runtime/facades/agency-facade.js +64 -0
- package/dist/runtime/facades/agency-facade.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +122 -1
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +42 -0
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -2
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +2 -0
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +25 -5
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts +7 -5
- package/dist/runtime/intake-ops.d.ts.map +1 -1
- package/dist/runtime/intake-ops.js +98 -5
- package/dist/runtime/intake-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.d.ts +6 -3
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -1
- package/dist/runtime/memory-extra-ops.js +292 -4
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/pack-ops.d.ts +3 -0
- package/dist/runtime/pack-ops.d.ts.map +1 -1
- package/dist/runtime/pack-ops.js +18 -1
- package/dist/runtime/pack-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +85 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.js +1 -1
- package/dist/runtime/playbook-ops.js.map +1 -1
- package/dist/runtime/plugin-ops.d.ts.map +1 -1
- package/dist/runtime/plugin-ops.js +3 -0
- package/dist/runtime/plugin-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +143 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts +23 -0
- package/dist/runtime/session-briefing.d.ts.map +1 -0
- package/dist/runtime/session-briefing.js +154 -0
- package/dist/runtime/session-briefing.js.map +1 -0
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +3 -7
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/vault/vault.d.ts +34 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +89 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +6 -4
- package/src/__tests__/admin-setup-ops.test.ts +355 -0
- package/src/__tests__/async-infrastructure.test.ts +307 -0
- package/src/__tests__/cognee-client-gaps.test.ts +6 -2
- package/src/__tests__/cognee-hybrid-search.test.ts +49 -35
- package/src/__tests__/cognee-sync-manager-deep.test.ts +89 -65
- package/src/__tests__/curator-extra-ops.test.ts +6 -2
- package/src/__tests__/curator-pipeline-e2e.test.ts +545 -0
- package/src/__tests__/memory-extra-ops.test.ts +2 -2
- package/src/__tests__/module-manifest-drift.test.ts +59 -0
- package/src/__tests__/planning-extra-ops.test.ts +2 -2
- package/src/__tests__/second-brain-features.test.ts +583 -0
- package/src/agency/agency-manager.ts +217 -9
- package/src/agency/default-rules.ts +83 -0
- package/src/agency/types.ts +61 -0
- package/src/brain/brain.ts +110 -8
- package/src/brain/knowledge-synthesizer.ts +216 -0
- package/src/brain/learning-radar.ts +340 -0
- package/src/brain/types.ts +16 -0
- package/src/context/context-engine.ts +114 -15
- package/src/context/types.ts +5 -0
- package/src/control/intent-router.ts +107 -0
- package/src/control/types.ts +10 -0
- package/src/curator/classifier.ts +86 -0
- package/src/curator/quality-gate.ts +127 -0
- package/src/domain-packs/index.ts +0 -6
- package/src/domain-packs/loader.ts +25 -5
- package/src/domain-packs/pack-runtime.ts +6 -6
- package/src/domain-packs/types.ts +8 -2
- package/src/engine/bin/soleri-engine.ts +18 -2
- package/src/engine/index.ts +2 -0
- package/src/engine/module-manifest.ts +99 -0
- package/src/engine/register-engine.ts +21 -2
- package/src/events/event-bus.ts +58 -0
- package/src/flows/chain-runner.ts +369 -0
- package/src/flows/chain-types.ts +57 -0
- package/src/index.ts +0 -1
- package/src/intake/content-classifier.ts +0 -2
- package/src/intake/text-ingester.ts +234 -0
- package/src/llm/llm-client.ts +50 -7
- package/src/llm/oauth-discovery.ts +151 -0
- package/src/llm/types.ts +4 -2
- package/src/packs/pack-installer.ts +16 -1
- package/src/persistence/index.ts +0 -1
- package/src/persistence/types.ts +2 -6
- package/src/planning/evidence-collector.ts +247 -0
- package/src/planning/planner.ts +11 -0
- package/src/plugins/index.ts +4 -0
- package/src/plugins/plugin-registry.ts +6 -1
- package/src/plugins/types.ts +10 -5
- package/src/queue/job-queue.ts +281 -0
- package/src/queue/pipeline-runner.ts +149 -0
- package/src/runtime/admin-setup-ops.ts +664 -0
- package/src/runtime/chain-ops.ts +121 -0
- package/src/runtime/claude-md-helpers.ts +218 -0
- package/src/runtime/curator-extra-ops.ts +86 -3
- package/src/runtime/facades/admin-facade.ts +5 -2
- package/src/runtime/facades/agency-facade.ts +68 -0
- package/src/runtime/facades/brain-facade.ts +142 -1
- package/src/runtime/facades/control-facade.ts +45 -0
- package/src/runtime/facades/memory-facade.ts +20 -2
- package/src/runtime/facades/plan-facade.ts +2 -0
- package/src/runtime/facades/vault-facade.ts +28 -5
- package/src/runtime/intake-ops.ts +107 -5
- package/src/runtime/memory-extra-ops.ts +312 -4
- package/src/runtime/pack-ops.ts +26 -1
- package/src/runtime/planning-extra-ops.ts +94 -0
- package/src/runtime/playbook-ops.ts +1 -1
- package/src/runtime/plugin-ops.ts +3 -0
- package/src/runtime/runtime.ts +138 -2
- package/src/runtime/session-briefing.ts +175 -0
- package/src/runtime/types.ts +23 -0
- package/src/runtime/vault-linking-ops.ts +3 -7
- package/src/vault/vault.ts +105 -4
- package/src/__tests__/postgres-provider.test.ts +0 -116
- package/src/persistence/postgres-provider.ts +0 -310
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for curator async infrastructure (#210).
|
|
3
|
+
*
|
|
4
|
+
* Tests the full pipeline: enqueue → DAG resolution → handler dispatch → completion.
|
|
5
|
+
* Also tests quality gate and classifier graceful degradation (no LLM).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
9
|
+
import { Vault } from '../vault/vault.js';
|
|
10
|
+
import { Brain } from '../brain/brain.js';
|
|
11
|
+
import { Curator } from '../curator/curator.js';
|
|
12
|
+
import { JobQueue } from '../queue/job-queue.js';
|
|
13
|
+
import { PipelineRunner } from '../queue/pipeline-runner.js';
|
|
14
|
+
import { TypedEventBus } from '../events/event-bus.js';
|
|
15
|
+
import { LinkManager } from '../vault/linking.js';
|
|
16
|
+
import { evaluateQuality } from '../curator/quality-gate.js';
|
|
17
|
+
import { classifyEntry } from '../curator/classifier.js';
|
|
18
|
+
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
19
|
+
|
|
20
|
+
// ─── Shared Setup ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
let vault: Vault;
|
|
23
|
+
let brain: Brain;
|
|
24
|
+
let curator: Curator;
|
|
25
|
+
let queue: JobQueue;
|
|
26
|
+
let runner: PipelineRunner;
|
|
27
|
+
let linkManager: LinkManager;
|
|
28
|
+
|
|
29
|
+
const SEED: IntelligenceEntry[] = [
|
|
30
|
+
{
|
|
31
|
+
id: 'pattern-circuit-breaker',
|
|
32
|
+
type: 'pattern',
|
|
33
|
+
domain: 'architecture',
|
|
34
|
+
title: 'Circuit Breaker for External Services',
|
|
35
|
+
severity: 'critical',
|
|
36
|
+
description: 'Wrap external service calls in a circuit breaker to prevent cascade failures.',
|
|
37
|
+
tags: ['networking', 'resilience', 'microservices'],
|
|
38
|
+
why: 'Prevents one failing service from taking down the entire system.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'anti-pattern-no-timeout',
|
|
42
|
+
type: 'anti-pattern',
|
|
43
|
+
domain: 'architecture',
|
|
44
|
+
title: 'HTTP Calls Without Timeout',
|
|
45
|
+
severity: 'critical',
|
|
46
|
+
description: 'Never make HTTP calls without a timeout. Can hang indefinitely.',
|
|
47
|
+
tags: ['networking', 'timeout', 'http'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'pattern-semantic-tokens',
|
|
51
|
+
type: 'pattern',
|
|
52
|
+
domain: 'design',
|
|
53
|
+
title: 'Use Semantic Tokens',
|
|
54
|
+
severity: 'warning',
|
|
55
|
+
description: 'Prefer semantic tokens over primitive color values for maintainability.',
|
|
56
|
+
tags: ['tokens', 'design-system'],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
beforeAll(() => {
|
|
61
|
+
vault = new Vault(':memory:');
|
|
62
|
+
vault.seed(SEED);
|
|
63
|
+
brain = new Brain(vault);
|
|
64
|
+
curator = new Curator(vault);
|
|
65
|
+
queue = new JobQueue(vault.getProvider());
|
|
66
|
+
linkManager = new LinkManager(vault.getProvider());
|
|
67
|
+
runner = new PipelineRunner(queue);
|
|
68
|
+
|
|
69
|
+
// Register handlers matching the runtime setup
|
|
70
|
+
runner.registerHandler('tag-normalize', async (job) => {
|
|
71
|
+
const entry = vault.get(job.entryId ?? '');
|
|
72
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
73
|
+
return curator.normalizeTag(entry.tags[0] ?? '');
|
|
74
|
+
});
|
|
75
|
+
runner.registerHandler('dedup-check', async (job) => {
|
|
76
|
+
const entry = vault.get(job.entryId ?? '');
|
|
77
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
78
|
+
return curator.detectDuplicates(entry.id);
|
|
79
|
+
});
|
|
80
|
+
runner.registerHandler('auto-link', async (job) => {
|
|
81
|
+
const suggestions = linkManager.suggestLinks(job.entryId ?? '', 3);
|
|
82
|
+
for (const s of suggestions) {
|
|
83
|
+
linkManager.addLink(job.entryId ?? '', s.entryId, s.suggestedType, `pipeline: ${s.reason}`);
|
|
84
|
+
}
|
|
85
|
+
return { linked: suggestions.length };
|
|
86
|
+
});
|
|
87
|
+
runner.registerHandler('quality-gate', async (job) => {
|
|
88
|
+
const entry = vault.get(job.entryId ?? '');
|
|
89
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
90
|
+
return evaluateQuality(entry, null); // No LLM in tests
|
|
91
|
+
});
|
|
92
|
+
runner.registerHandler('classify', async (job) => {
|
|
93
|
+
const entry = vault.get(job.entryId ?? '');
|
|
94
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
95
|
+
return classifyEntry(entry, null); // No LLM in tests
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── 9 additional handlers for full Salvador parity (#216) ────
|
|
99
|
+
runner.registerHandler('enrich-frontmatter', async (job) => {
|
|
100
|
+
const entry = vault.get(job.entryId ?? '');
|
|
101
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
102
|
+
return curator.enrichMetadata(entry.id);
|
|
103
|
+
});
|
|
104
|
+
runner.registerHandler('detect-staleness', async (job) => {
|
|
105
|
+
const entry = vault.get(job.entryId ?? '');
|
|
106
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
107
|
+
const entryTimestamp = (entry.validFrom ?? 0) * 1000 || Date.now();
|
|
108
|
+
const ageMs = Date.now() - entryTimestamp;
|
|
109
|
+
const isStale = ageMs > 90 * 86400000;
|
|
110
|
+
return { stale: isStale, ageDays: Math.floor(ageMs / 86400000), entryId: entry.id };
|
|
111
|
+
});
|
|
112
|
+
runner.registerHandler('detect-duplicate', async (job) => {
|
|
113
|
+
const entry = vault.get(job.entryId ?? '');
|
|
114
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
115
|
+
return curator.detectDuplicates(entry.id);
|
|
116
|
+
});
|
|
117
|
+
runner.registerHandler('detect-contradiction', async (job) => {
|
|
118
|
+
const entry = vault.get(job.entryId ?? '');
|
|
119
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
120
|
+
const contradictions = curator.detectContradictions(0.4);
|
|
121
|
+
const relevant = contradictions.filter(
|
|
122
|
+
(c) => c.patternId === job.entryId || c.antipatternId === job.entryId,
|
|
123
|
+
);
|
|
124
|
+
return { found: relevant.length, contradictions: relevant };
|
|
125
|
+
});
|
|
126
|
+
runner.registerHandler('consolidate-duplicates', async () => {
|
|
127
|
+
return curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
128
|
+
});
|
|
129
|
+
runner.registerHandler('archive-stale', async () => {
|
|
130
|
+
const result = curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
131
|
+
return { archived: result.staleEntries.length, result };
|
|
132
|
+
});
|
|
133
|
+
runner.registerHandler('cognee-ingest', async (job) => {
|
|
134
|
+
// No Cognee in tests — graceful degradation
|
|
135
|
+
return { skipped: true, reason: 'cognee not available' };
|
|
136
|
+
});
|
|
137
|
+
runner.registerHandler('cognee-cognify', async () => {
|
|
138
|
+
return { skipped: true, reason: 'cognee not available' };
|
|
139
|
+
});
|
|
140
|
+
runner.registerHandler('verify-searchable', async (job) => {
|
|
141
|
+
const entry = vault.get(job.entryId ?? '');
|
|
142
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
143
|
+
const searchResults = vault.search(entry.title, { limit: 1 });
|
|
144
|
+
const found = searchResults.some((r) => r.entry.id === entry.id);
|
|
145
|
+
return { searchable: found, entryId: entry.id };
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterAll(() => {
|
|
150
|
+
runner.stop();
|
|
151
|
+
vault.close();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── Job Queue Integration ──────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe('Job Queue — curator pipeline', () => {
|
|
157
|
+
it('enqueues a 3-step DAG pipeline for an entry', () => {
|
|
158
|
+
const pipelineId = 'pipe-test-1';
|
|
159
|
+
const entryId = 'pattern-circuit-breaker';
|
|
160
|
+
|
|
161
|
+
const step1 = queue.enqueue('tag-normalize', { entryId, pipelineId });
|
|
162
|
+
const step2 = queue.enqueue('dedup-check', { entryId, pipelineId, dependsOn: [step1] });
|
|
163
|
+
const step3 = queue.enqueue('auto-link', { entryId, pipelineId, dependsOn: [step2] });
|
|
164
|
+
|
|
165
|
+
const jobs = queue.getByPipeline(pipelineId);
|
|
166
|
+
expect(jobs.length).toBe(3);
|
|
167
|
+
expect(jobs[0].type).toBe('tag-normalize');
|
|
168
|
+
expect(jobs[1].dependsOn).toContain(step1);
|
|
169
|
+
expect(jobs[2].dependsOn).toContain(step2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('dequeueReady only returns jobs with completed deps', () => {
|
|
173
|
+
const pipelineId = 'pipe-test-dag';
|
|
174
|
+
const step1 = queue.enqueue('tag-normalize', {
|
|
175
|
+
entryId: 'pattern-circuit-breaker',
|
|
176
|
+
pipelineId,
|
|
177
|
+
});
|
|
178
|
+
const step2 = queue.enqueue('dedup-check', {
|
|
179
|
+
entryId: 'pattern-circuit-breaker',
|
|
180
|
+
pipelineId,
|
|
181
|
+
dependsOn: [step1],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Only step1 should be ready
|
|
185
|
+
const ready = queue.dequeueReady(10);
|
|
186
|
+
const readyIds = ready.map((j) => j.id);
|
|
187
|
+
expect(readyIds).toContain(step1);
|
|
188
|
+
expect(readyIds).not.toContain(step2);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─── Pipeline Runner Integration ────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe('Pipeline Runner — full pipeline execution', () => {
|
|
195
|
+
it('executes a 3-step pipeline in DAG order', async () => {
|
|
196
|
+
const pipelineId = 'pipe-runner-test';
|
|
197
|
+
const entryId = 'pattern-circuit-breaker';
|
|
198
|
+
|
|
199
|
+
const step1 = queue.enqueue('tag-normalize', { entryId, pipelineId });
|
|
200
|
+
const step2 = queue.enqueue('dedup-check', { entryId, pipelineId, dependsOn: [step1] });
|
|
201
|
+
const step3 = queue.enqueue('auto-link', { entryId, pipelineId, dependsOn: [step2] });
|
|
202
|
+
|
|
203
|
+
// Process step 1
|
|
204
|
+
const batch1 = await runner.processOnce(10);
|
|
205
|
+
expect(batch1).toBeGreaterThanOrEqual(1);
|
|
206
|
+
expect(queue.get(step1)!.status).toBe('completed');
|
|
207
|
+
|
|
208
|
+
// Process step 2
|
|
209
|
+
const batch2 = await runner.processOnce(10);
|
|
210
|
+
expect(batch2).toBeGreaterThanOrEqual(1);
|
|
211
|
+
expect(queue.get(step2)!.status).toBe('completed');
|
|
212
|
+
|
|
213
|
+
// Process step 3
|
|
214
|
+
const batch3 = await runner.processOnce(10);
|
|
215
|
+
expect(batch3).toBeGreaterThanOrEqual(1);
|
|
216
|
+
expect(queue.get(step3)!.status).toBe('completed');
|
|
217
|
+
|
|
218
|
+
// All jobs in pipeline should be completed
|
|
219
|
+
const jobs = queue.getByPipeline(pipelineId);
|
|
220
|
+
expect(jobs.every((j) => j.status === 'completed')).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('tag-normalize handler returns normalization result', async () => {
|
|
224
|
+
const id = queue.enqueue('tag-normalize', { entryId: 'pattern-circuit-breaker' });
|
|
225
|
+
await runner.processOnce();
|
|
226
|
+
|
|
227
|
+
const job = queue.get(id);
|
|
228
|
+
expect(job!.status).toBe('completed');
|
|
229
|
+
expect(job!.result).toBeDefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('dedup-check handler detects duplicates', async () => {
|
|
233
|
+
const id = queue.enqueue('dedup-check', { entryId: 'pattern-circuit-breaker' });
|
|
234
|
+
await runner.processOnce();
|
|
235
|
+
|
|
236
|
+
const job = queue.get(id);
|
|
237
|
+
expect(job!.status).toBe('completed');
|
|
238
|
+
expect(job!.result).toBeDefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('auto-link handler creates Zettelkasten links', async () => {
|
|
242
|
+
const id = queue.enqueue('auto-link', { entryId: 'pattern-circuit-breaker' });
|
|
243
|
+
await runner.processOnce();
|
|
244
|
+
|
|
245
|
+
const job = queue.get(id);
|
|
246
|
+
expect(job!.status).toBe('completed');
|
|
247
|
+
const result = job!.result as { linked: number };
|
|
248
|
+
expect(typeof result.linked).toBe('number');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('handles missing entry gracefully', async () => {
|
|
252
|
+
const id = queue.enqueue('tag-normalize', { entryId: 'nonexistent-entry' });
|
|
253
|
+
await runner.processOnce();
|
|
254
|
+
|
|
255
|
+
const job = queue.get(id);
|
|
256
|
+
expect(job!.status).toBe('completed');
|
|
257
|
+
expect((job!.result as Record<string, unknown>).skipped).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('runner status tracks processed/failed counts', async () => {
|
|
261
|
+
const status = runner.getStatus();
|
|
262
|
+
expect(status.jobsProcessed).toBeGreaterThan(0);
|
|
263
|
+
expect(typeof status.jobsFailed).toBe('number');
|
|
264
|
+
expect(typeof status.jobsRetried).toBe('number');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ─── Quality Gate (no LLM) ──────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe('Quality Gate — graceful degradation', () => {
|
|
271
|
+
it('returns ACCEPT with default scores when no LLM', async () => {
|
|
272
|
+
const entry = vault.get('pattern-circuit-breaker')!;
|
|
273
|
+
const result = await evaluateQuality(entry, null);
|
|
274
|
+
|
|
275
|
+
expect(result.evaluated).toBe(false);
|
|
276
|
+
expect(result.verdict).toBe('ACCEPT');
|
|
277
|
+
expect(result.overallScore).toBe(50);
|
|
278
|
+
expect(result.scores.novelty).toBe(50);
|
|
279
|
+
expect(result.scores.actionability).toBe(50);
|
|
280
|
+
expect(result.reasoning).toContain('unavailable');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('quality-gate job handler works via pipeline', async () => {
|
|
284
|
+
const id = queue.enqueue('quality-gate', { entryId: 'pattern-circuit-breaker' });
|
|
285
|
+
await runner.processOnce();
|
|
286
|
+
|
|
287
|
+
const job = queue.get(id);
|
|
288
|
+
expect(job!.status).toBe('completed');
|
|
289
|
+
const result = job!.result as { evaluated: boolean; verdict: string };
|
|
290
|
+
expect(result.verdict).toBe('ACCEPT');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ─── Classifier (no LLM) ────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
describe('Classifier — graceful degradation', () => {
|
|
297
|
+
it('returns empty classification when no LLM', async () => {
|
|
298
|
+
const entry = vault.get('pattern-circuit-breaker')!;
|
|
299
|
+
const result = await classifyEntry(entry, null);
|
|
300
|
+
|
|
301
|
+
expect(result.classified).toBe(false);
|
|
302
|
+
expect(result.suggestedDomain).toBeNull();
|
|
303
|
+
expect(result.suggestedSeverity).toBeNull();
|
|
304
|
+
expect(result.suggestedTags).toEqual([]);
|
|
305
|
+
expect(result.confidence).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('classify job handler works via pipeline', async () => {
|
|
309
|
+
const id = queue.enqueue('classify', { entryId: 'anti-pattern-no-timeout' });
|
|
310
|
+
await runner.processOnce();
|
|
311
|
+
|
|
312
|
+
const job = queue.get(id);
|
|
313
|
+
expect(job!.status).toBe('completed');
|
|
314
|
+
const result = job!.result as { classified: boolean };
|
|
315
|
+
expect(result.classified).toBe(false); // No LLM
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── Salvador Parity Handlers (#216) ─────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('Pipeline Runner — Salvador parity handlers (#216)', () => {
|
|
322
|
+
it('enrich-frontmatter enriches entry metadata', async () => {
|
|
323
|
+
const id = queue.enqueue('enrich-frontmatter', { entryId: 'pattern-circuit-breaker' });
|
|
324
|
+
await runner.processOnce();
|
|
325
|
+
const job = queue.get(id);
|
|
326
|
+
expect(job!.status).toBe('completed');
|
|
327
|
+
expect(job!.result).toBeDefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('detect-staleness checks entry age', async () => {
|
|
331
|
+
const id = queue.enqueue('detect-staleness', { entryId: 'pattern-circuit-breaker' });
|
|
332
|
+
await runner.processOnce();
|
|
333
|
+
const job = queue.get(id);
|
|
334
|
+
expect(job!.status).toBe('completed');
|
|
335
|
+
const result = job!.result as { stale: boolean; ageDays: number };
|
|
336
|
+
expect(typeof result.stale).toBe('boolean');
|
|
337
|
+
expect(typeof result.ageDays).toBe('number');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('detect-duplicate runs dedup on specific entry', async () => {
|
|
341
|
+
const id = queue.enqueue('detect-duplicate', { entryId: 'pattern-circuit-breaker' });
|
|
342
|
+
await runner.processOnce();
|
|
343
|
+
const job = queue.get(id);
|
|
344
|
+
expect(job!.status).toBe('completed');
|
|
345
|
+
expect(job!.result).toBeDefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('detect-contradiction finds pattern/anti-pattern conflicts', async () => {
|
|
349
|
+
const id = queue.enqueue('detect-contradiction', { entryId: 'pattern-circuit-breaker' });
|
|
350
|
+
await runner.processOnce();
|
|
351
|
+
const job = queue.get(id);
|
|
352
|
+
expect(job!.status).toBe('completed');
|
|
353
|
+
const result = job!.result as { found: number };
|
|
354
|
+
expect(typeof result.found).toBe('number');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('consolidate-duplicates runs consolidation', async () => {
|
|
358
|
+
const id = queue.enqueue('consolidate-duplicates', {});
|
|
359
|
+
await runner.processOnce();
|
|
360
|
+
const job = queue.get(id);
|
|
361
|
+
expect(job!.status).toBe('completed');
|
|
362
|
+
expect(job!.result).toBeDefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('archive-stale archives old entries', async () => {
|
|
366
|
+
const id = queue.enqueue('archive-stale', {});
|
|
367
|
+
await runner.processOnce();
|
|
368
|
+
const job = queue.get(id);
|
|
369
|
+
expect(job!.status).toBe('completed');
|
|
370
|
+
const result = job!.result as { archived: number };
|
|
371
|
+
expect(typeof result.archived).toBe('number');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('cognee-ingest degrades gracefully without Cognee', async () => {
|
|
375
|
+
const id = queue.enqueue('cognee-ingest', { entryId: 'pattern-circuit-breaker' });
|
|
376
|
+
await runner.processOnce();
|
|
377
|
+
const job = queue.get(id);
|
|
378
|
+
expect(job!.status).toBe('completed');
|
|
379
|
+
expect((job!.result as Record<string, unknown>).skipped).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('cognee-cognify degrades gracefully without Cognee', async () => {
|
|
383
|
+
const id = queue.enqueue('cognee-cognify', {});
|
|
384
|
+
await runner.processOnce();
|
|
385
|
+
const job = queue.get(id);
|
|
386
|
+
expect(job!.status).toBe('completed');
|
|
387
|
+
expect((job!.result as Record<string, unknown>).skipped).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('verify-searchable confirms entry is FTS-indexed', async () => {
|
|
391
|
+
const id = queue.enqueue('verify-searchable', { entryId: 'pattern-circuit-breaker' });
|
|
392
|
+
await runner.processOnce();
|
|
393
|
+
const job = queue.get(id);
|
|
394
|
+
expect(job!.status).toBe('completed');
|
|
395
|
+
const result = job!.result as { searchable: boolean };
|
|
396
|
+
expect(result.searchable).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('all handlers handle missing entries gracefully', async () => {
|
|
400
|
+
const types = [
|
|
401
|
+
'enrich-frontmatter',
|
|
402
|
+
'detect-staleness',
|
|
403
|
+
'detect-duplicate',
|
|
404
|
+
'detect-contradiction',
|
|
405
|
+
'verify-searchable',
|
|
406
|
+
];
|
|
407
|
+
for (const type of types) {
|
|
408
|
+
const id = queue.enqueue(type, { entryId: 'nonexistent' });
|
|
409
|
+
await runner.processOnce();
|
|
410
|
+
const job = queue.get(id);
|
|
411
|
+
expect(job!.status).toBe('completed');
|
|
412
|
+
expect((job!.result as Record<string, unknown>).skipped).toBe(true);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('Full Salvador DAG — 8-step curator pipeline', () => {
|
|
418
|
+
it('runs the complete quality pipeline in DAG order', async () => {
|
|
419
|
+
const entryId = 'pattern-semantic-tokens';
|
|
420
|
+
const pipelineId = 'full-salvador-dag';
|
|
421
|
+
|
|
422
|
+
const step1 = queue.enqueue('quality-gate', { entryId, pipelineId });
|
|
423
|
+
const step2 = queue.enqueue('enrich-frontmatter', { entryId, pipelineId, dependsOn: [step1] });
|
|
424
|
+
const step3 = queue.enqueue('tag-normalize', { entryId, pipelineId, dependsOn: [step2] });
|
|
425
|
+
const step4 = queue.enqueue('dedup-check', { entryId, pipelineId, dependsOn: [step3] });
|
|
426
|
+
const step5 = queue.enqueue('detect-contradiction', {
|
|
427
|
+
entryId,
|
|
428
|
+
pipelineId,
|
|
429
|
+
dependsOn: [step4],
|
|
430
|
+
});
|
|
431
|
+
const step6 = queue.enqueue('cognee-ingest', { entryId, pipelineId, dependsOn: [step5] });
|
|
432
|
+
const step7 = queue.enqueue('auto-link', { entryId, pipelineId, dependsOn: [step6] });
|
|
433
|
+
const step8 = queue.enqueue('verify-searchable', { entryId, pipelineId, dependsOn: [step7] });
|
|
434
|
+
|
|
435
|
+
// Drain the DAG
|
|
436
|
+
for (let i = 0; i < 15; i++) {
|
|
437
|
+
await runner.processOnce(20);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const jobs = queue.getByPipeline(pipelineId);
|
|
441
|
+
const statuses = jobs.map((j) => `${j.type}:${j.status}`);
|
|
442
|
+
|
|
443
|
+
expect(statuses).toEqual([
|
|
444
|
+
'quality-gate:completed',
|
|
445
|
+
'enrich-frontmatter:completed',
|
|
446
|
+
'tag-normalize:completed',
|
|
447
|
+
'dedup-check:completed',
|
|
448
|
+
'detect-contradiction:completed',
|
|
449
|
+
'cognee-ingest:completed',
|
|
450
|
+
'auto-link:completed',
|
|
451
|
+
'verify-searchable:completed',
|
|
452
|
+
]);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ─── Event Bus Integration ──────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
describe('Event Bus — curator events', () => {
|
|
459
|
+
type CuratorEvents = {
|
|
460
|
+
'entry:captured': { id: string; title: string };
|
|
461
|
+
'pipeline:started': { pipelineId: string; entryId: string };
|
|
462
|
+
'pipeline:completed': { pipelineId: string; jobsCompleted: number };
|
|
463
|
+
'quality:rejected': { entryId: string; score: number; reasons: string[] };
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
it('fires entry:captured event and listeners receive it', () => {
|
|
467
|
+
const bus = new TypedEventBus<CuratorEvents>();
|
|
468
|
+
let received: CuratorEvents['entry:captured'] | null = null;
|
|
469
|
+
|
|
470
|
+
bus.on('entry:captured', (payload) => {
|
|
471
|
+
received = payload;
|
|
472
|
+
});
|
|
473
|
+
bus.emit('entry:captured', { id: 'test-1', title: 'Test Entry' });
|
|
474
|
+
|
|
475
|
+
expect(received).toEqual({ id: 'test-1', title: 'Test Entry' });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('fires pipeline events in sequence', () => {
|
|
479
|
+
const bus = new TypedEventBus<CuratorEvents>();
|
|
480
|
+
const events: string[] = [];
|
|
481
|
+
|
|
482
|
+
bus.on('pipeline:started', () => events.push('started'));
|
|
483
|
+
bus.on('pipeline:completed', () => events.push('completed'));
|
|
484
|
+
|
|
485
|
+
bus.emit('pipeline:started', { pipelineId: 'p1', entryId: 'e1' });
|
|
486
|
+
bus.emit('pipeline:completed', { pipelineId: 'p1', jobsCompleted: 3 });
|
|
487
|
+
|
|
488
|
+
expect(events).toEqual(['started', 'completed']);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('fires quality:rejected for low-scoring entries', () => {
|
|
492
|
+
const bus = new TypedEventBus<CuratorEvents>();
|
|
493
|
+
let rejected: CuratorEvents['quality:rejected'] | null = null;
|
|
494
|
+
|
|
495
|
+
bus.on('quality:rejected', (payload) => {
|
|
496
|
+
rejected = payload;
|
|
497
|
+
});
|
|
498
|
+
bus.emit('quality:rejected', {
|
|
499
|
+
entryId: 'junk-1',
|
|
500
|
+
score: 25,
|
|
501
|
+
reasons: ['too vague', 'no actionability'],
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
expect(rejected!.score).toBe(25);
|
|
505
|
+
expect(rejected!.reasons).toContain('too vague');
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// ─── Full Pipeline E2E ──────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
describe('Full pipeline E2E — capture to completion', () => {
|
|
512
|
+
it('processes a 5-step pipeline: quality-gate → tag-normalize → dedup → classify → auto-link', async () => {
|
|
513
|
+
const entryId = 'anti-pattern-no-timeout';
|
|
514
|
+
const pipelineId = 'full-e2e';
|
|
515
|
+
|
|
516
|
+
const step1 = queue.enqueue('quality-gate', { entryId, pipelineId });
|
|
517
|
+
const step2 = queue.enqueue('tag-normalize', { entryId, pipelineId, dependsOn: [step1] });
|
|
518
|
+
const step3 = queue.enqueue('dedup-check', { entryId, pipelineId, dependsOn: [step2] });
|
|
519
|
+
const step4 = queue.enqueue('classify', { entryId, pipelineId, dependsOn: [step3] });
|
|
520
|
+
const step5 = queue.enqueue('auto-link', { entryId, pipelineId, dependsOn: [step4] });
|
|
521
|
+
|
|
522
|
+
// Process all steps — run enough batches to drain DAG
|
|
523
|
+
// (earlier tests may have left pending jobs, so process generously)
|
|
524
|
+
for (let i = 0; i < 10; i++) {
|
|
525
|
+
await runner.processOnce(20);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const jobs = queue.getByPipeline(pipelineId);
|
|
529
|
+
const statuses = jobs.map((j) => `${j.type}:${j.status}`);
|
|
530
|
+
|
|
531
|
+
expect(statuses).toEqual([
|
|
532
|
+
'quality-gate:completed',
|
|
533
|
+
'tag-normalize:completed',
|
|
534
|
+
'dedup-check:completed',
|
|
535
|
+
'classify:completed',
|
|
536
|
+
'auto-link:completed',
|
|
537
|
+
]);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('queue stats reflect pipeline completion', () => {
|
|
541
|
+
const stats = queue.getStats();
|
|
542
|
+
expect(stats.completed).toBeGreaterThan(5);
|
|
543
|
+
expect(stats.total).toBeGreaterThan(10);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Drift Detection — Issue #227
|
|
3
|
+
*
|
|
4
|
+
* Ensures ENGINE_MODULE_MANIFEST (used by forge for template generation)
|
|
5
|
+
* stays in sync with ENGINE_MODULES (used by register-engine at runtime).
|
|
6
|
+
*
|
|
7
|
+
* If this test fails, someone added/removed/renamed a module in one place
|
|
8
|
+
* but not the other. Fix by updating both files to match.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { ENGINE_MODULE_MANIFEST } from '../engine/module-manifest.js';
|
|
13
|
+
import { ENGINE_MODULES } from '../engine/register-engine.js';
|
|
14
|
+
|
|
15
|
+
describe('Module manifest drift detection', () => {
|
|
16
|
+
const manifestSuffixes = ENGINE_MODULE_MANIFEST.map((m) => m.suffix);
|
|
17
|
+
const runtimeSuffixes = ENGINE_MODULES.map((m) => m.suffix);
|
|
18
|
+
|
|
19
|
+
it('manifest and runtime have the same number of modules', () => {
|
|
20
|
+
expect(manifestSuffixes).toHaveLength(runtimeSuffixes.length);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('manifest and runtime have identical suffixes in the same order', () => {
|
|
24
|
+
expect(manifestSuffixes).toEqual(runtimeSuffixes);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('conditional flags match between manifest and runtime', () => {
|
|
28
|
+
for (const manifest of ENGINE_MODULE_MANIFEST) {
|
|
29
|
+
const runtime = ENGINE_MODULES.find((m) => m.suffix === manifest.suffix);
|
|
30
|
+
expect(runtime, `module "${manifest.suffix}" missing from ENGINE_MODULES`).toBeDefined();
|
|
31
|
+
|
|
32
|
+
const runtimeConditional = runtime!.condition !== undefined;
|
|
33
|
+
const manifestConditional = manifest.conditional === true;
|
|
34
|
+
|
|
35
|
+
expect(runtimeConditional, `conditional flag mismatch for "${manifest.suffix}"`).toBe(
|
|
36
|
+
manifestConditional,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('no runtime module is missing from manifest', () => {
|
|
42
|
+
for (const runtime of ENGINE_MODULES) {
|
|
43
|
+
const manifest = ENGINE_MODULE_MANIFEST.find((m) => m.suffix === runtime.suffix);
|
|
44
|
+
expect(
|
|
45
|
+
manifest,
|
|
46
|
+
`module "${runtime.suffix}" exists in ENGINE_MODULES but missing from ENGINE_MODULE_MANIFEST`,
|
|
47
|
+
).toBeDefined();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('every manifest entry has at least one keyOp', () => {
|
|
52
|
+
for (const entry of ENGINE_MODULE_MANIFEST) {
|
|
53
|
+
expect(
|
|
54
|
+
entry.keyOps.length,
|
|
55
|
+
`module "${entry.suffix}" has empty keyOps — placeholder tables need at least one op`,
|
|
56
|
+
).toBeGreaterThan(0);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -34,8 +34,8 @@ describe('createPlanningExtraOps', () => {
|
|
|
34
34
|
return op;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
it('should return
|
|
38
|
-
expect(ops.length).toBe(
|
|
37
|
+
it('should return 24 ops', () => {
|
|
38
|
+
expect(ops.length).toBe(24);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('should have all expected op names', () => {
|