@soleri/core 2.4.0 → 2.6.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/brain/brain.d.ts +7 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +56 -9
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +1 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +164 -148
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +2 -2
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cognee/client.d.ts +3 -0
- package/dist/cognee/client.d.ts.map +1 -1
- package/dist/cognee/client.js +17 -0
- package/dist/cognee/client.js.map +1 -1
- package/dist/cognee/sync-manager.d.ts +94 -0
- package/dist/cognee/sync-manager.d.ts.map +1 -0
- package/dist/cognee/sync-manager.js +293 -0
- package/dist/cognee/sync-manager.js.map +1 -0
- package/dist/control/identity-manager.d.ts +3 -1
- package/dist/control/identity-manager.d.ts.map +1 -1
- package/dist/control/identity-manager.js +49 -51
- package/dist/control/identity-manager.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +32 -32
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +9 -1
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +104 -92
- package/dist/curator/curator.js.map +1 -1
- package/dist/errors/classify.d.ts +13 -0
- package/dist/errors/classify.d.ts.map +1 -0
- package/dist/errors/classify.js +97 -0
- package/dist/errors/classify.js.map +1 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/retry.d.ts +40 -0
- package/dist/errors/retry.d.ts.map +1 -0
- package/dist/errors/retry.js +97 -0
- package/dist/errors/retry.js.map +1 -0
- package/dist/errors/types.d.ts +48 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/errors/types.js +59 -0
- package/dist/errors/types.js.map +1 -0
- package/dist/governance/governance.d.ts +1 -0
- package/dist/governance/governance.d.ts.map +1 -1
- package/dist/governance/governance.js +51 -68
- package/dist/governance/governance.js.map +1 -1
- package/dist/index.d.ts +26 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +14 -0
- package/dist/intake/content-classifier.d.ts.map +1 -0
- package/dist/intake/content-classifier.js +125 -0
- package/dist/intake/content-classifier.js.map +1 -0
- package/dist/intake/dedup-gate.d.ts +17 -0
- package/dist/intake/dedup-gate.d.ts.map +1 -0
- package/dist/intake/dedup-gate.js +66 -0
- package/dist/intake/dedup-gate.js.map +1 -0
- package/dist/intake/intake-pipeline.d.ts +63 -0
- package/dist/intake/intake-pipeline.d.ts.map +1 -0
- package/dist/intake/intake-pipeline.js +373 -0
- package/dist/intake/intake-pipeline.js.map +1 -0
- package/dist/intake/types.d.ts +65 -0
- package/dist/intake/types.d.ts.map +1 -0
- package/dist/intake/types.js +3 -0
- package/dist/intake/types.js.map +1 -0
- package/dist/intelligence/loader.js +1 -1
- package/dist/intelligence/loader.js.map +1 -1
- package/dist/intelligence/types.d.ts +3 -1
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/loop/loop-manager.d.ts +58 -7
- package/dist/loop/loop-manager.d.ts.map +1 -1
- package/dist/loop/loop-manager.js +280 -6
- package/dist/loop/loop-manager.js.map +1 -1
- package/dist/loop/types.d.ts +69 -1
- package/dist/loop/types.d.ts.map +1 -1
- package/dist/loop/types.js +4 -1
- package/dist/loop/types.js.map +1 -1
- package/dist/persistence/index.d.ts +4 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/postgres-provider.d.ts +46 -0
- package/dist/persistence/postgres-provider.d.ts.map +1 -0
- package/dist/persistence/postgres-provider.js +115 -0
- package/dist/persistence/postgres-provider.js.map +1 -0
- package/dist/persistence/sqlite-provider.d.ts +28 -0
- package/dist/persistence/sqlite-provider.d.ts.map +1 -0
- package/dist/persistence/sqlite-provider.js +97 -0
- package/dist/persistence/sqlite-provider.js.map +1 -0
- package/dist/persistence/types.d.ts +58 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +8 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +47 -4
- package/dist/planning/gap-analysis.d.ts.map +1 -1
- package/dist/planning/gap-analysis.js +190 -13
- package/dist/planning/gap-analysis.js.map +1 -1
- package/dist/planning/gap-types.d.ts +1 -1
- package/dist/planning/gap-types.d.ts.map +1 -1
- package/dist/planning/gap-types.js.map +1 -1
- package/dist/planning/planner.d.ts +277 -9
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +611 -46
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/generic/brainstorming.d.ts +9 -0
- package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
- package/dist/playbooks/generic/brainstorming.js +105 -0
- package/dist/playbooks/generic/brainstorming.js.map +1 -0
- package/dist/playbooks/generic/code-review.d.ts +11 -0
- package/dist/playbooks/generic/code-review.d.ts.map +1 -0
- package/dist/playbooks/generic/code-review.js +176 -0
- package/dist/playbooks/generic/code-review.js.map +1 -0
- package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
- package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
- package/dist/playbooks/generic/subagent-execution.js +68 -0
- package/dist/playbooks/generic/subagent-execution.js.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.js +87 -0
- package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
- package/dist/playbooks/generic/tdd.d.ts +9 -0
- package/dist/playbooks/generic/tdd.d.ts.map +1 -0
- package/dist/playbooks/generic/tdd.js +70 -0
- package/dist/playbooks/generic/tdd.js.map +1 -0
- package/dist/playbooks/generic/verification.d.ts +9 -0
- package/dist/playbooks/generic/verification.d.ts.map +1 -0
- package/dist/playbooks/generic/verification.js +74 -0
- package/dist/playbooks/generic/verification.js.map +1 -0
- package/dist/playbooks/index.d.ts +4 -0
- package/dist/playbooks/index.d.ts.map +1 -0
- package/dist/playbooks/index.js +5 -0
- package/dist/playbooks/index.js.map +1 -0
- package/dist/playbooks/playbook-registry.d.ts +42 -0
- package/dist/playbooks/playbook-registry.d.ts.map +1 -0
- package/dist/playbooks/playbook-registry.js +227 -0
- package/dist/playbooks/playbook-registry.js.map +1 -0
- package/dist/playbooks/playbook-seeder.d.ts +47 -0
- package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
- package/dist/playbooks/playbook-seeder.js +104 -0
- package/dist/playbooks/playbook-seeder.js.map +1 -0
- package/dist/playbooks/playbook-types.d.ts +132 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -0
- package/dist/playbooks/playbook-types.js +12 -0
- package/dist/playbooks/playbook-types.js.map +1 -0
- package/dist/project/project-registry.d.ts +4 -4
- package/dist/project/project-registry.d.ts.map +1 -1
- package/dist/project/project-registry.js +30 -57
- package/dist/project/project-registry.js.map +1 -1
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/parser.d.ts +17 -0
- package/dist/prompts/parser.d.ts.map +1 -0
- package/dist/prompts/parser.js +47 -0
- package/dist/prompts/parser.js.map +1 -0
- package/dist/prompts/template-manager.d.ts +25 -0
- package/dist/prompts/template-manager.d.ts.map +1 -0
- package/dist/prompts/template-manager.js +71 -0
- package/dist/prompts/template-manager.js.map +1 -0
- package/dist/prompts/types.d.ts +26 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +5 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +5 -3
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +348 -11
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +10 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +20 -2
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/cognee-sync-ops.d.ts +12 -0
- package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
- package/dist/runtime/cognee-sync-ops.js +55 -0
- package/dist/runtime/cognee-sync-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +8 -6
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +226 -9
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +2 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +15 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/domain-ops.js +2 -2
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts.map +1 -1
- package/dist/runtime/grading-ops.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts +14 -0
- package/dist/runtime/intake-ops.d.ts.map +1 -0
- package/dist/runtime/intake-ops.js +110 -0
- package/dist/runtime/intake-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +5 -4
- package/dist/runtime/loop-ops.d.ts.map +1 -1
- package/dist/runtime/loop-ops.js +84 -12
- package/dist/runtime/loop-ops.js.map +1 -1
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
- package/dist/runtime/memory-cross-project-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.js +5 -5
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +8 -2
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts +13 -5
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +381 -18
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.d.ts +14 -0
- package/dist/runtime/playbook-ops.d.ts.map +1 -0
- package/dist/runtime/playbook-ops.js +141 -0
- package/dist/runtime/playbook-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts.map +1 -1
- package/dist/runtime/project-ops.js +7 -2
- package/dist/runtime/project-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +28 -9
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +8 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +4 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +383 -4
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- package/dist/vault/playbook.d.ts +34 -0
- package/dist/vault/playbook.d.ts.map +1 -0
- package/dist/vault/playbook.js +60 -0
- package/dist/vault/playbook.js.map +1 -0
- package/dist/vault/vault.d.ts +52 -32
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +300 -181
- package/dist/vault/vault.js.map +1 -1
- package/package.json +9 -3
- package/src/__tests__/admin-extra-ops.test.ts +62 -15
- package/src/__tests__/admin-ops.test.ts +2 -2
- package/src/__tests__/brain.test.ts +3 -3
- package/src/__tests__/cognee-integration.test.ts +80 -0
- package/src/__tests__/cognee-sync-manager.test.ts +103 -0
- package/src/__tests__/core-ops.test.ts +36 -4
- package/src/__tests__/curator-extra-ops.test.ts +24 -2
- package/src/__tests__/errors.test.ts +388 -0
- package/src/__tests__/grading-ops.test.ts +28 -7
- package/src/__tests__/intake-pipeline.test.ts +162 -0
- package/src/__tests__/loop-ops.test.ts +74 -3
- package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
- package/src/__tests__/orchestrate-ops.test.ts +8 -3
- package/src/__tests__/persistence.test.ts +291 -0
- package/src/__tests__/planner.test.ts +99 -21
- package/src/__tests__/planning-extra-ops.test.ts +168 -10
- package/src/__tests__/playbook-registry.test.ts +326 -0
- package/src/__tests__/playbook-seeder.test.ts +163 -0
- package/src/__tests__/playbook.test.ts +389 -0
- package/src/__tests__/postgres-provider.test.ts +58 -0
- package/src/__tests__/project-ops.test.ts +18 -4
- package/src/__tests__/template-manager.test.ts +222 -0
- package/src/__tests__/vault-extra-ops.test.ts +82 -7
- package/src/__tests__/vault.test.ts +184 -0
- package/src/brain/brain.ts +71 -9
- package/src/brain/intelligence.ts +258 -307
- package/src/brain/types.ts +2 -2
- package/src/cognee/client.ts +18 -0
- package/src/cognee/sync-manager.ts +389 -0
- package/src/control/identity-manager.ts +77 -75
- package/src/control/intent-router.ts +55 -57
- package/src/curator/curator.ts +199 -139
- package/src/errors/classify.ts +102 -0
- package/src/errors/index.ts +5 -0
- package/src/errors/retry.ts +132 -0
- package/src/errors/types.ts +81 -0
- package/src/governance/governance.ts +90 -107
- package/src/index.ts +116 -3
- package/src/intake/content-classifier.ts +146 -0
- package/src/intake/dedup-gate.ts +92 -0
- package/src/intake/intake-pipeline.ts +503 -0
- package/src/intake/types.ts +69 -0
- package/src/intelligence/loader.ts +1 -1
- package/src/intelligence/types.ts +3 -1
- package/src/loop/loop-manager.ts +325 -7
- package/src/loop/types.ts +72 -1
- package/src/persistence/index.ts +9 -0
- package/src/persistence/postgres-provider.ts +157 -0
- package/src/persistence/sqlite-provider.ts +115 -0
- package/src/persistence/types.ts +74 -0
- package/src/planning/gap-analysis.ts +286 -17
- package/src/planning/gap-types.ts +4 -1
- package/src/planning/planner.ts +828 -55
- package/src/playbooks/generic/brainstorming.ts +110 -0
- package/src/playbooks/generic/code-review.ts +181 -0
- package/src/playbooks/generic/subagent-execution.ts +74 -0
- package/src/playbooks/generic/systematic-debugging.ts +92 -0
- package/src/playbooks/generic/tdd.ts +75 -0
- package/src/playbooks/generic/verification.ts +79 -0
- package/src/playbooks/index.ts +27 -0
- package/src/playbooks/playbook-registry.ts +284 -0
- package/src/playbooks/playbook-seeder.ts +119 -0
- package/src/playbooks/playbook-types.ts +162 -0
- package/src/project/project-registry.ts +81 -74
- package/src/prompts/index.ts +3 -0
- package/src/prompts/parser.ts +59 -0
- package/src/prompts/template-manager.ts +77 -0
- package/src/prompts/types.ts +28 -0
- package/src/runtime/admin-extra-ops.ts +391 -13
- package/src/runtime/admin-ops.ts +17 -6
- package/src/runtime/capture-ops.ts +25 -6
- package/src/runtime/cognee-sync-ops.ts +63 -0
- package/src/runtime/core-ops.ts +258 -8
- package/src/runtime/curator-extra-ops.ts +17 -3
- package/src/runtime/domain-ops.ts +2 -2
- package/src/runtime/grading-ops.ts +11 -2
- package/src/runtime/intake-ops.ts +126 -0
- package/src/runtime/loop-ops.ts +96 -13
- package/src/runtime/memory-cross-project-ops.ts +1 -2
- package/src/runtime/memory-extra-ops.ts +5 -5
- package/src/runtime/orchestrate-ops.ts +8 -2
- package/src/runtime/planning-extra-ops.ts +414 -23
- package/src/runtime/playbook-ops.ts +169 -0
- package/src/runtime/project-ops.ts +9 -3
- package/src/runtime/runtime.ts +36 -10
- package/src/runtime/types.ts +8 -0
- package/src/runtime/vault-extra-ops.ts +425 -4
- package/src/vault/playbook.ts +87 -0
- package/src/vault/vault.ts +419 -235
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
SoleriErrorCode,
|
|
4
|
+
SoleriError,
|
|
5
|
+
ok,
|
|
6
|
+
err,
|
|
7
|
+
isOk,
|
|
8
|
+
isErr,
|
|
9
|
+
classifyError,
|
|
10
|
+
shouldRetry,
|
|
11
|
+
getRetryDelay,
|
|
12
|
+
retryWithPreset,
|
|
13
|
+
RETRY_PRESETS,
|
|
14
|
+
} from '../errors/index.js';
|
|
15
|
+
|
|
16
|
+
// ─── SoleriError ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('SoleriError', () => {
|
|
19
|
+
it('classifies RATE_LIMIT as retryable', () => {
|
|
20
|
+
const e = new SoleriError('rate limited', SoleriErrorCode.RATE_LIMIT);
|
|
21
|
+
expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
|
|
22
|
+
expect(e.classification).toBe('retryable');
|
|
23
|
+
expect(e.retryable).toBe(true);
|
|
24
|
+
expect(e.name).toBe('SoleriError');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('classifies AUTH as permanent', () => {
|
|
28
|
+
const e = new SoleriError('unauthorized', SoleriErrorCode.AUTH);
|
|
29
|
+
expect(e.classification).toBe('permanent');
|
|
30
|
+
expect(e.retryable).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('classifies VALIDATION as fixable', () => {
|
|
34
|
+
const e = new SoleriError('bad input', SoleriErrorCode.VALIDATION);
|
|
35
|
+
expect(e.classification).toBe('fixable');
|
|
36
|
+
expect(e.retryable).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('classifies NETWORK as retryable', () => {
|
|
40
|
+
const e = new SoleriError('connection refused', SoleriErrorCode.NETWORK);
|
|
41
|
+
expect(e.classification).toBe('retryable');
|
|
42
|
+
expect(e.retryable).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('classifies RESOURCE_NOT_FOUND as permanent', () => {
|
|
46
|
+
const e = new SoleriError('not found', SoleriErrorCode.RESOURCE_NOT_FOUND);
|
|
47
|
+
expect(e.classification).toBe('permanent');
|
|
48
|
+
expect(e.retryable).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('classifies CONFIG_ERROR as permanent', () => {
|
|
52
|
+
const e = new SoleriError('missing key', SoleriErrorCode.CONFIG_ERROR);
|
|
53
|
+
expect(e.classification).toBe('permanent');
|
|
54
|
+
expect(e.retryable).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('preserves cause and context', () => {
|
|
58
|
+
const cause = new Error('original');
|
|
59
|
+
const e = new SoleriError('wrapped', SoleriErrorCode.INTERNAL, {
|
|
60
|
+
cause,
|
|
61
|
+
context: { attempt: 3, url: 'http://example.com' },
|
|
62
|
+
});
|
|
63
|
+
expect(e.cause).toBe(cause);
|
|
64
|
+
expect(e.context).toEqual({ attempt: 3, url: 'http://example.com' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('extends Error', () => {
|
|
68
|
+
const e = new SoleriError('test', SoleriErrorCode.INTERNAL);
|
|
69
|
+
expect(e).toBeInstanceOf(Error);
|
|
70
|
+
expect(e).toBeInstanceOf(SoleriError);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ─── classifyError ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('classifyError', () => {
|
|
77
|
+
it('passes through SoleriError unchanged', () => {
|
|
78
|
+
const original = new SoleriError('test', SoleriErrorCode.AUTH);
|
|
79
|
+
expect(classifyError(original)).toBe(original);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('classifies HTTP 429 as RATE_LIMIT', () => {
|
|
83
|
+
const e = classifyError({ status: 429, message: 'Too many requests' });
|
|
84
|
+
expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
|
|
85
|
+
expect(e.retryable).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('classifies HTTP 401 as AUTH', () => {
|
|
89
|
+
const e = classifyError({ status: 401, message: 'Unauthorized' });
|
|
90
|
+
expect(e.code).toBe(SoleriErrorCode.AUTH);
|
|
91
|
+
expect(e.retryable).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('classifies HTTP 403 as AUTH', () => {
|
|
95
|
+
const e = classifyError({ status: 403, message: 'Forbidden' });
|
|
96
|
+
expect(e.code).toBe(SoleriErrorCode.AUTH);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('classifies HTTP 404 as RESOURCE_NOT_FOUND', () => {
|
|
100
|
+
const e = classifyError({ status: 404, message: 'Not Found' });
|
|
101
|
+
expect(e.code).toBe(SoleriErrorCode.RESOURCE_NOT_FOUND);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('classifies HTTP 503 as INTERNAL (retryable)', () => {
|
|
105
|
+
const e = classifyError({ status: 503, message: 'Service Unavailable' });
|
|
106
|
+
expect(e.code).toBe(SoleriErrorCode.INTERNAL);
|
|
107
|
+
expect(e.retryable).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('classifies HTTP 408 as TIMEOUT', () => {
|
|
111
|
+
const e = classifyError({ status: 408, message: 'Request Timeout' });
|
|
112
|
+
expect(e.code).toBe(SoleriErrorCode.TIMEOUT);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('classifies HTTP 422 as VALIDATION', () => {
|
|
116
|
+
const e = classifyError({ status: 422, message: 'Unprocessable Entity' });
|
|
117
|
+
expect(e.code).toBe(SoleriErrorCode.VALIDATION);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('classifies ECONNREFUSED as NETWORK', () => {
|
|
121
|
+
const error = new Error('connect ECONNREFUSED');
|
|
122
|
+
(error as unknown as Record<string, string>).code = 'ECONNREFUSED';
|
|
123
|
+
const e = classifyError(error);
|
|
124
|
+
expect(e.code).toBe(SoleriErrorCode.NETWORK);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('classifies ETIMEDOUT as TIMEOUT', () => {
|
|
128
|
+
const error = new Error('connect ETIMEDOUT');
|
|
129
|
+
(error as unknown as Record<string, string>).code = 'ETIMEDOUT';
|
|
130
|
+
const e = classifyError(error);
|
|
131
|
+
expect(e.code).toBe(SoleriErrorCode.TIMEOUT);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('classifies "model overloaded" message as LLM_OVERLOAD', () => {
|
|
135
|
+
const e = classifyError(new Error('model overloaded, please retry'));
|
|
136
|
+
expect(e.code).toBe(SoleriErrorCode.LLM_OVERLOAD);
|
|
137
|
+
expect(e.retryable).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('classifies "capacity" message as LLM_OVERLOAD', () => {
|
|
141
|
+
const e = classifyError(new Error('server at capacity'));
|
|
142
|
+
expect(e.code).toBe(SoleriErrorCode.LLM_OVERLOAD);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('classifies "vault" message as VAULT_UNREACHABLE', () => {
|
|
146
|
+
const e = classifyError(new Error('vault connection lost'));
|
|
147
|
+
expect(e.code).toBe(SoleriErrorCode.VAULT_UNREACHABLE);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('classifies "invalid" message as VALIDATION', () => {
|
|
151
|
+
const e = classifyError(new Error('invalid input format'));
|
|
152
|
+
expect(e.code).toBe(SoleriErrorCode.VALIDATION);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('classifies "configuration" message as CONFIG_ERROR', () => {
|
|
156
|
+
const e = classifyError(new Error('missing configuration'));
|
|
157
|
+
expect(e.code).toBe(SoleriErrorCode.CONFIG_ERROR);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('defaults unknown error to INTERNAL (permanent)', () => {
|
|
161
|
+
const e = classifyError(new Error('something weird happened'));
|
|
162
|
+
expect(e.code).toBe(SoleriErrorCode.INTERNAL);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('handles string input', () => {
|
|
166
|
+
const e = classifyError('plain string error');
|
|
167
|
+
expect(e).toBeInstanceOf(SoleriError);
|
|
168
|
+
expect(e.message).toBe('plain string error');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('handles null input', () => {
|
|
172
|
+
const e = classifyError(null);
|
|
173
|
+
expect(e).toBeInstanceOf(SoleriError);
|
|
174
|
+
expect(e.message).toBe('null');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles undefined input', () => {
|
|
178
|
+
const e = classifyError(undefined);
|
|
179
|
+
expect(e).toBeInstanceOf(SoleriError);
|
|
180
|
+
expect(e.message).toBe('undefined');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('prefers HTTP status over message pattern', () => {
|
|
184
|
+
// status=401 should win over "invalid" in message
|
|
185
|
+
const e = classifyError({ status: 401, message: 'invalid credentials' });
|
|
186
|
+
expect(e.code).toBe(SoleriErrorCode.AUTH);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('preserves original error as cause', () => {
|
|
190
|
+
const original = new Error('original');
|
|
191
|
+
const e = classifyError(original);
|
|
192
|
+
expect(e.cause).toBe(original);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('uses statusCode if status is absent', () => {
|
|
196
|
+
const e = classifyError({ statusCode: 429, message: 'rate limited' });
|
|
197
|
+
expect(e.code).toBe(SoleriErrorCode.RATE_LIMIT);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── shouldRetry ──────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe('shouldRetry', () => {
|
|
204
|
+
it('returns true for retryable error below max attempts', () => {
|
|
205
|
+
const e = new SoleriError('net', SoleriErrorCode.NETWORK);
|
|
206
|
+
expect(shouldRetry(e, 1, 'fast')).toBe(true); // max=3, attempt=1
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns false for retryable error at max attempts', () => {
|
|
210
|
+
const e = new SoleriError('net', SoleriErrorCode.NETWORK);
|
|
211
|
+
expect(shouldRetry(e, 3, 'fast')).toBe(false); // max=3, attempt=3
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns false for permanent error regardless', () => {
|
|
215
|
+
const e = new SoleriError('auth', SoleriErrorCode.AUTH);
|
|
216
|
+
expect(shouldRetry(e, 0, 'patient')).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns false for fixable error regardless', () => {
|
|
220
|
+
const e = new SoleriError('val', SoleriErrorCode.VALIDATION);
|
|
221
|
+
expect(shouldRetry(e, 0, 'normal')).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ─── getRetryDelay ────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
describe('getRetryDelay', () => {
|
|
228
|
+
it('increases with attempt number', () => {
|
|
229
|
+
const delays = Array.from({ length: 5 }, (_, i) => getRetryDelay(i, 'normal'));
|
|
230
|
+
// Check trend is generally increasing (jitter may cause minor inversions)
|
|
231
|
+
const avg0 = getAvgDelay(0, 'normal');
|
|
232
|
+
const avg2 = getAvgDelay(2, 'normal');
|
|
233
|
+
expect(avg2).toBeGreaterThan(avg0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('caps at maxInterval', () => {
|
|
237
|
+
const delay = getRetryDelay(100, 'fast');
|
|
238
|
+
// maxInterval for fast is 10_000, with 25% jitter max = 12_500
|
|
239
|
+
expect(delay).toBeLessThanOrEqual(12_500);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns non-negative values', () => {
|
|
243
|
+
for (let i = 0; i < 50; i++) {
|
|
244
|
+
expect(getRetryDelay(i, 'patient')).toBeGreaterThanOrEqual(0);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Average over multiple calls to smooth jitter
|
|
250
|
+
function getAvgDelay(attempt: number, preset: 'fast' | 'normal' | 'patient'): number {
|
|
251
|
+
let sum = 0;
|
|
252
|
+
const runs = 100;
|
|
253
|
+
for (let i = 0; i < runs; i++) sum += getRetryDelay(attempt, preset);
|
|
254
|
+
return sum / runs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── RETRY_PRESETS ────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
describe('RETRY_PRESETS', () => {
|
|
260
|
+
it('has fast preset (1s/10s/3)', () => {
|
|
261
|
+
expect(RETRY_PRESETS.fast).toEqual({
|
|
262
|
+
initialIntervalMs: 1_000,
|
|
263
|
+
maxIntervalMs: 10_000,
|
|
264
|
+
maxAttempts: 3,
|
|
265
|
+
backoffMultiplier: 2,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('has normal preset (10s/2min/10)', () => {
|
|
270
|
+
expect(RETRY_PRESETS.normal).toEqual({
|
|
271
|
+
initialIntervalMs: 10_000,
|
|
272
|
+
maxIntervalMs: 120_000,
|
|
273
|
+
maxAttempts: 10,
|
|
274
|
+
backoffMultiplier: 2,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('has patient preset (1min/15min/25)', () => {
|
|
279
|
+
expect(RETRY_PRESETS.patient).toEqual({
|
|
280
|
+
initialIntervalMs: 60_000,
|
|
281
|
+
maxIntervalMs: 900_000,
|
|
282
|
+
maxAttempts: 25,
|
|
283
|
+
backoffMultiplier: 1.5,
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ─── retryWithPreset ──────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe('retryWithPreset', () => {
|
|
291
|
+
it('succeeds on first try', async () => {
|
|
292
|
+
const result = await retryWithPreset(() => Promise.resolve(42), 'fast');
|
|
293
|
+
expect(result).toEqual({ ok: true, value: 42 });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('retries and eventually succeeds', async () => {
|
|
297
|
+
let calls = 0;
|
|
298
|
+
const fn = async () => {
|
|
299
|
+
calls++;
|
|
300
|
+
if (calls < 3) throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' });
|
|
301
|
+
return 'done';
|
|
302
|
+
};
|
|
303
|
+
const result = await retryWithPreset(fn, 'fast', {
|
|
304
|
+
onRetry: vi.fn(),
|
|
305
|
+
});
|
|
306
|
+
expect(result).toEqual({ ok: true, value: 'done' });
|
|
307
|
+
expect(calls).toBe(3);
|
|
308
|
+
}, 30_000);
|
|
309
|
+
|
|
310
|
+
it('returns err immediately for permanent error', async () => {
|
|
311
|
+
const fn = async () => {
|
|
312
|
+
throw Object.assign(new Error('forbidden'), { status: 403 });
|
|
313
|
+
};
|
|
314
|
+
const result = await retryWithPreset(fn, 'fast');
|
|
315
|
+
expect(result.ok).toBe(false);
|
|
316
|
+
if (!result.ok) {
|
|
317
|
+
expect(result.error.code).toBe(SoleriErrorCode.AUTH);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('returns err after exhausting max attempts', async () => {
|
|
322
|
+
let calls = 0;
|
|
323
|
+
const fn = async () => {
|
|
324
|
+
calls++;
|
|
325
|
+
throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' });
|
|
326
|
+
};
|
|
327
|
+
const result = await retryWithPreset(fn, 'fast');
|
|
328
|
+
expect(result.ok).toBe(false);
|
|
329
|
+
expect(calls).toBe(3); // fast preset has maxAttempts=3
|
|
330
|
+
}, 30_000);
|
|
331
|
+
|
|
332
|
+
it('calls onRetry callback', async () => {
|
|
333
|
+
const onRetry = vi.fn();
|
|
334
|
+
let calls = 0;
|
|
335
|
+
const fn = async () => {
|
|
336
|
+
calls++;
|
|
337
|
+
if (calls < 2) throw Object.assign(new Error('net'), { code: 'ECONNRESET' });
|
|
338
|
+
return 'ok';
|
|
339
|
+
};
|
|
340
|
+
await retryWithPreset(fn, 'fast', { onRetry });
|
|
341
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(onRetry).toHaveBeenCalledWith(expect.any(SoleriError), 1, expect.any(Number));
|
|
343
|
+
}, 30_000);
|
|
344
|
+
|
|
345
|
+
it('respects abort signal', async () => {
|
|
346
|
+
const controller = new AbortController();
|
|
347
|
+
let calls = 0;
|
|
348
|
+
const fn = async () => {
|
|
349
|
+
calls++;
|
|
350
|
+
if (calls === 1) {
|
|
351
|
+
controller.abort();
|
|
352
|
+
throw Object.assign(new Error('net'), { code: 'ECONNREFUSED' });
|
|
353
|
+
}
|
|
354
|
+
return 'ok';
|
|
355
|
+
};
|
|
356
|
+
const result = await retryWithPreset(fn, 'fast', { signal: controller.signal });
|
|
357
|
+
expect(result.ok).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ─── Result helpers ───────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
describe('Result helpers', () => {
|
|
364
|
+
it('ok() creates success result', () => {
|
|
365
|
+
const r = ok(42);
|
|
366
|
+
expect(r.ok).toBe(true);
|
|
367
|
+
if (r.ok) expect(r.value).toBe(42);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('err() creates failure result', () => {
|
|
371
|
+
const error = new SoleriError('fail', SoleriErrorCode.INTERNAL);
|
|
372
|
+
const r = err(error);
|
|
373
|
+
expect(r.ok).toBe(false);
|
|
374
|
+
if (!r.ok) expect(r.error).toBe(error);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('isOk() type guard works', () => {
|
|
378
|
+
const r = ok('hello');
|
|
379
|
+
expect(isOk(r)).toBe(true);
|
|
380
|
+
expect(isErr(r)).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('isErr() type guard works', () => {
|
|
384
|
+
const r = err(new SoleriError('x', SoleriErrorCode.AUTH));
|
|
385
|
+
expect(isErr(r)).toBe(true);
|
|
386
|
+
expect(isOk(r)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -56,7 +56,12 @@ describe('Grading Ops', () => {
|
|
|
56
56
|
const check = (await findOp('plan_grade').handler({ planId })) as {
|
|
57
57
|
score: number;
|
|
58
58
|
grade: string;
|
|
59
|
-
gaps: Array<{
|
|
59
|
+
gaps: Array<{
|
|
60
|
+
severity: string;
|
|
61
|
+
category: string;
|
|
62
|
+
description: string;
|
|
63
|
+
recommendation: string;
|
|
64
|
+
}>;
|
|
60
65
|
iteration: number;
|
|
61
66
|
};
|
|
62
67
|
// 3 critical gaps: no objective, no scope, no tasks = -90
|
|
@@ -77,9 +82,15 @@ describe('Grading Ops', () => {
|
|
|
77
82
|
'Set TTL to 5 minutes since average data freshness requirement is 10 minutes',
|
|
78
83
|
],
|
|
79
84
|
tasks: [
|
|
80
|
-
{
|
|
85
|
+
{
|
|
86
|
+
title: 'Setup Redis client',
|
|
87
|
+
description: 'Install and configure Redis connection pool',
|
|
88
|
+
},
|
|
81
89
|
{ title: 'Add middleware', description: 'Express transparent caching middleware layer' },
|
|
82
|
-
{
|
|
90
|
+
{
|
|
91
|
+
title: 'Add invalidation',
|
|
92
|
+
description: 'Cache invalidation on writes for consistency',
|
|
93
|
+
},
|
|
83
94
|
{ title: 'Add tests', description: 'Integration tests for cache hit/miss scenarios' },
|
|
84
95
|
{ title: 'Add metrics', description: 'Track and verify cache hit rate monitoring' },
|
|
85
96
|
],
|
|
@@ -97,7 +108,9 @@ describe('Grading Ops', () => {
|
|
|
97
108
|
const planId = await createPlan({
|
|
98
109
|
objective: 'Test duplicate title detection in the grading engine',
|
|
99
110
|
scope: 'Testing only, does not include production changes',
|
|
100
|
-
decisions: [
|
|
111
|
+
decisions: [
|
|
112
|
+
'Use assertions because they provide clear error messages due to descriptive output',
|
|
113
|
+
],
|
|
101
114
|
tasks: [
|
|
102
115
|
{ title: 'Same name', description: 'First task implementation' },
|
|
103
116
|
{ title: 'Same name', description: 'Second task implementation' },
|
|
@@ -228,7 +241,9 @@ describe('Grading Ops', () => {
|
|
|
228
241
|
const planId = await createPlan({
|
|
229
242
|
objective: 'Build a comprehensive feature for the testing module',
|
|
230
243
|
scope: 'Testing module only, does not include deployment or infrastructure',
|
|
231
|
-
decisions: [
|
|
244
|
+
decisions: [
|
|
245
|
+
'Use vitest because it integrates natively with TypeScript due to built-in support',
|
|
246
|
+
],
|
|
232
247
|
tasks: [
|
|
233
248
|
{ title: 'Write unit tests', description: 'Cover all edge cases in the module' },
|
|
234
249
|
{ title: 'Write integration tests', description: 'End-to-end API tests for the flow' },
|
|
@@ -262,7 +277,10 @@ describe('Grading Ops', () => {
|
|
|
262
277
|
score: number;
|
|
263
278
|
iteration: number;
|
|
264
279
|
totalGaps: number;
|
|
265
|
-
gapsBySeverity: Record<
|
|
280
|
+
gapsBySeverity: Record<
|
|
281
|
+
string,
|
|
282
|
+
Array<{ category: string; description: string; recommendation: string }>
|
|
283
|
+
>;
|
|
266
284
|
nextAction: string;
|
|
267
285
|
};
|
|
268
286
|
expect(result.score).toBeLessThan(100);
|
|
@@ -285,7 +303,10 @@ describe('Grading Ops', () => {
|
|
|
285
303
|
tasks: [
|
|
286
304
|
{ title: 'Setup Redis', description: 'Install and configure Redis connection pool' },
|
|
287
305
|
{ title: 'Add middleware', description: 'Express transparent caching middleware layer' },
|
|
288
|
-
{
|
|
306
|
+
{
|
|
307
|
+
title: 'Add invalidation',
|
|
308
|
+
description: 'Cache invalidation on writes for consistency',
|
|
309
|
+
},
|
|
289
310
|
{ title: 'Add tests', description: 'Integration tests for cache hit/miss scenarios' },
|
|
290
311
|
{ title: 'Add metrics', description: 'Track and verify cache hit rate monitoring' },
|
|
291
312
|
],
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { dedupItems, DEDUP_THRESHOLD } from '../intake/dedup-gate.js';
|
|
3
|
+
import { Vault } from '../vault/vault.js';
|
|
4
|
+
import type { ClassifiedItem } from '../intake/types.js';
|
|
5
|
+
|
|
6
|
+
function makeItem(overrides: Partial<ClassifiedItem> = {}): ClassifiedItem {
|
|
7
|
+
return {
|
|
8
|
+
type: overrides.type ?? 'pattern',
|
|
9
|
+
title: overrides.title ?? 'Test Pattern',
|
|
10
|
+
description: overrides.description ?? 'A generic test description.',
|
|
11
|
+
tags: overrides.tags ?? ['test'],
|
|
12
|
+
severity: overrides.severity ?? 'suggestion',
|
|
13
|
+
citation: overrides.citation ?? 'p.1',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Dedup Gate', () => {
|
|
18
|
+
let vault: Vault;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vault = new Vault(':memory:');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vault.close();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should mark items as non-duplicate when vault is empty', () => {
|
|
29
|
+
const items: ClassifiedItem[] = [
|
|
30
|
+
makeItem({ title: 'Brand New Pattern', description: 'Something entirely new.' }),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const results = dedupItems(items, vault);
|
|
34
|
+
|
|
35
|
+
expect(results).toHaveLength(1);
|
|
36
|
+
expect(results[0].isDuplicate).toBe(false);
|
|
37
|
+
expect(results[0].similarity).toBe(0);
|
|
38
|
+
expect(results[0].bestMatchId).toBeUndefined();
|
|
39
|
+
expect(results[0].item).toBe(items[0]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should detect near-duplicates with high similarity', () => {
|
|
43
|
+
vault.seed([
|
|
44
|
+
{
|
|
45
|
+
id: 'existing-1',
|
|
46
|
+
type: 'pattern',
|
|
47
|
+
domain: 'design-patterns',
|
|
48
|
+
title: 'Singleton Pattern Implementation',
|
|
49
|
+
severity: 'suggestion',
|
|
50
|
+
description:
|
|
51
|
+
'The singleton pattern ensures a class has only one instance and provides a global point of access to it.',
|
|
52
|
+
tags: ['singleton', 'design-pattern', 'creational'],
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const items: ClassifiedItem[] = [
|
|
57
|
+
makeItem({
|
|
58
|
+
title: 'Singleton Pattern Implementation',
|
|
59
|
+
description:
|
|
60
|
+
'The singleton pattern ensures a class has only one instance and provides a global point of access to it.',
|
|
61
|
+
tags: ['singleton', 'design-pattern', 'creational'],
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const results = dedupItems(items, vault);
|
|
66
|
+
|
|
67
|
+
expect(results).toHaveLength(1);
|
|
68
|
+
expect(results[0].similarity).toBeGreaterThan(0.5);
|
|
69
|
+
expect(results[0].bestMatchId).toBe('existing-1');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not flag dissimilar entries as duplicates', () => {
|
|
73
|
+
vault.seed([
|
|
74
|
+
{
|
|
75
|
+
id: 'existing-2',
|
|
76
|
+
type: 'pattern',
|
|
77
|
+
domain: 'design-patterns',
|
|
78
|
+
title: 'Observer Pattern',
|
|
79
|
+
severity: 'suggestion',
|
|
80
|
+
description:
|
|
81
|
+
'The observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.',
|
|
82
|
+
tags: ['observer', 'design-pattern', 'behavioral'],
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const items: ClassifiedItem[] = [
|
|
87
|
+
makeItem({
|
|
88
|
+
title: 'God Object Anti-Pattern',
|
|
89
|
+
description:
|
|
90
|
+
'A god object is an anti-pattern where a single class knows too much or does too much, violating the single responsibility principle.',
|
|
91
|
+
tags: ['anti-pattern', 'code-smell'],
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const results = dedupItems(items, vault);
|
|
96
|
+
|
|
97
|
+
expect(results).toHaveLength(1);
|
|
98
|
+
expect(results[0].isDuplicate).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle multiple items at once', () => {
|
|
102
|
+
vault.seed([
|
|
103
|
+
{
|
|
104
|
+
id: 'existing-3',
|
|
105
|
+
type: 'pattern',
|
|
106
|
+
domain: 'design-patterns',
|
|
107
|
+
title: 'Factory Method Pattern',
|
|
108
|
+
severity: 'suggestion',
|
|
109
|
+
description:
|
|
110
|
+
'The factory method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.',
|
|
111
|
+
tags: ['factory', 'design-pattern', 'creational'],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const items: ClassifiedItem[] = [
|
|
116
|
+
// Near-duplicate of vault entry
|
|
117
|
+
makeItem({
|
|
118
|
+
title: 'Factory Method Pattern',
|
|
119
|
+
description:
|
|
120
|
+
'The factory method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.',
|
|
121
|
+
tags: ['factory', 'design-pattern', 'creational'],
|
|
122
|
+
}),
|
|
123
|
+
// Completely different
|
|
124
|
+
makeItem({
|
|
125
|
+
title: 'Dependency Injection',
|
|
126
|
+
description:
|
|
127
|
+
'Dependency injection is a technique where an object receives other objects that it depends on, called dependencies, rather than creating them internally.',
|
|
128
|
+
tags: ['dependency-injection', 'inversion-of-control'],
|
|
129
|
+
}),
|
|
130
|
+
// Also different
|
|
131
|
+
makeItem({
|
|
132
|
+
title: 'Circuit Breaker Pattern',
|
|
133
|
+
description:
|
|
134
|
+
'The circuit breaker pattern prevents an application from repeatedly trying to execute an operation that is likely to fail, allowing it to recover gracefully.',
|
|
135
|
+
tags: ['resilience', 'distributed-systems'],
|
|
136
|
+
}),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const results = dedupItems(items, vault);
|
|
140
|
+
|
|
141
|
+
expect(results).toHaveLength(3);
|
|
142
|
+
|
|
143
|
+
// First item is a near-duplicate — should have high similarity
|
|
144
|
+
expect(results[0].similarity).toBeGreaterThan(0.5);
|
|
145
|
+
expect(results[0].bestMatchId).toBe('existing-3');
|
|
146
|
+
|
|
147
|
+
// Second and third items are different — should not be duplicates
|
|
148
|
+
expect(results[1].isDuplicate).toBe(false);
|
|
149
|
+
expect(results[2].isDuplicate).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('DEDUP_THRESHOLD should be 0.85', () => {
|
|
153
|
+
expect(DEDUP_THRESHOLD).toBe(0.85);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('Intake Types', () => {
|
|
158
|
+
it('should import all type definitions', async () => {
|
|
159
|
+
const types = await import('../intake/types.js');
|
|
160
|
+
expect(types).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
});
|