@soleri/core 9.11.0 → 9.12.1
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/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +5 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +97 -10
- package/dist/brain/brain.js.map +1 -1
- package/dist/dream/cron-manager.d.ts +10 -0
- package/dist/dream/cron-manager.d.ts.map +1 -0
- package/dist/dream/cron-manager.js +122 -0
- package/dist/dream/cron-manager.js.map +1 -0
- package/dist/dream/dream-engine.d.ts +34 -0
- package/dist/dream/dream-engine.d.ts.map +1 -0
- package/dist/dream/dream-engine.js +88 -0
- package/dist/dream/dream-engine.js.map +1 -0
- package/dist/dream/dream-ops.d.ts +8 -0
- package/dist/dream/dream-ops.d.ts.map +1 -0
- package/dist/dream/dream-ops.js +49 -0
- package/dist/dream/dream-ops.js.map +1 -0
- package/dist/dream/index.d.ts +7 -0
- package/dist/dream/index.d.ts.map +1 -0
- package/dist/dream/index.js +5 -0
- package/dist/dream/index.js.map +1 -0
- package/dist/dream/schema.d.ts +3 -0
- package/dist/dream/schema.d.ts.map +1 -0
- package/dist/dream/schema.js +16 -0
- package/dist/dream/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-provider.d.ts +31 -0
- package/dist/embeddings/openai-provider.d.ts.map +1 -0
- package/dist/embeddings/openai-provider.js +120 -0
- package/dist/embeddings/openai-provider.js.map +1 -0
- package/dist/embeddings/pipeline.d.ts +36 -0
- package/dist/embeddings/pipeline.d.ts.map +1 -0
- package/dist/embeddings/pipeline.js +78 -0
- package/dist/embeddings/pipeline.js.map +1 -0
- package/dist/embeddings/types.d.ts +62 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +3 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +4 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +20 -0
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +12 -0
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/chain-types.d.ts +8 -8
- package/dist/flows/dispatch-registry.d.ts +15 -1
- package/dist/flows/dispatch-registry.d.ts.map +1 -1
- package/dist/flows/dispatch-registry.js +28 -1
- package/dist/flows/dispatch-registry.js.map +1 -1
- package/dist/flows/executor.d.ts +20 -2
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +79 -1
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +2 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/types.d.ts +43 -21
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/types.d.ts +31 -31
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +15 -0
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -2
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/embedding-ops.d.ts +12 -0
- package/dist/runtime/embedding-ops.d.ts.map +1 -0
- package/dist/runtime/embedding-ops.js +96 -0
- package/dist/runtime/embedding-ops.js.map +1 -0
- package/dist/runtime/facades/embedding-facade.d.ts +7 -0
- package/dist/runtime/facades/embedding-facade.d.ts.map +1 -0
- package/dist/runtime/facades/embedding-facade.js +8 -0
- package/dist/runtime/facades/embedding-facade.js.map +1 -0
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +12 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +120 -0
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/feature-flags.d.ts.map +1 -1
- package/dist/runtime/feature-flags.js +4 -0
- package/dist/runtime/feature-flags.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +140 -9
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +51 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/preflight.d.ts +32 -0
- package/dist/runtime/preflight.d.ts.map +1 -0
- package/dist/runtime/preflight.js +29 -0
- package/dist/runtime/preflight.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +33 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +27 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/step-tracker.d.ts +39 -0
- package/dist/skills/step-tracker.d.ts.map +1 -0
- package/dist/skills/step-tracker.js +105 -0
- package/dist/skills/step-tracker.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +3 -2
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +42 -8
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/subagent/dispatcher.d.ts +4 -3
- package/dist/subagent/dispatcher.d.ts.map +1 -1
- package/dist/subagent/dispatcher.js +57 -35
- package/dist/subagent/dispatcher.js.map +1 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/orphan-reaper.d.ts +51 -4
- package/dist/subagent/orphan-reaper.d.ts.map +1 -1
- package/dist/subagent/orphan-reaper.js +103 -3
- package/dist/subagent/orphan-reaper.js.map +1 -1
- package/dist/subagent/types.d.ts +7 -0
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.d.ts +2 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -1
- package/dist/subagent/workspace-resolver.js +3 -1
- package/dist/subagent/workspace-resolver.js.map +1 -1
- package/dist/vault/vault-entries.d.ts +18 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +73 -0
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-manager.d.ts.map +1 -1
- package/dist/vault/vault-manager.js +1 -0
- package/dist/vault/vault-manager.js.map +1 -1
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +14 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts +1 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/cron-manager.test.ts +132 -0
- package/src/__tests__/deviation-detection.test.ts +234 -0
- package/src/__tests__/embeddings.test.ts +536 -0
- package/src/__tests__/preflight.test.ts +97 -0
- package/src/__tests__/step-persistence.test.ts +324 -0
- package/src/__tests__/step-tracker.test.ts +260 -0
- package/src/__tests__/subagent/dispatcher.test.ts +122 -4
- package/src/__tests__/subagent/orphan-reaper.test.ts +148 -12
- package/src/__tests__/subagent/process-lifecycle.test.ts +422 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +6 -1
- package/src/adapters/types.ts +2 -0
- package/src/brain/brain.ts +117 -9
- package/src/dream/cron-manager.ts +137 -0
- package/src/dream/dream-engine.ts +119 -0
- package/src/dream/dream-ops.ts +56 -0
- package/src/dream/dream.test.ts +182 -0
- package/src/dream/index.ts +6 -0
- package/src/dream/schema.ts +17 -0
- package/src/embeddings/openai-provider.ts +158 -0
- package/src/embeddings/pipeline.ts +126 -0
- package/src/embeddings/types.ts +67 -0
- package/src/engine/bin/soleri-engine.ts +4 -1
- package/src/engine/module-manifest.test.ts +4 -4
- package/src/engine/module-manifest.ts +20 -0
- package/src/engine/register-engine.ts +12 -0
- package/src/flows/dispatch-registry.ts +44 -1
- package/src/flows/executor.ts +93 -2
- package/src/flows/index.ts +2 -0
- package/src/flows/types.ts +39 -1
- package/src/index.ts +11 -0
- package/src/planning/goal-ancestry.test.ts +3 -5
- package/src/planning/planner.test.ts +2 -3
- package/src/runtime/admin-ops.test.ts +2 -2
- package/src/runtime/admin-ops.ts +17 -0
- package/src/runtime/admin-setup-ops.ts +2 -2
- package/src/runtime/embedding-ops.ts +116 -0
- package/src/runtime/facades/admin-facade.test.ts +31 -0
- package/src/runtime/facades/embedding-facade.ts +11 -0
- package/src/runtime/facades/index.ts +12 -0
- package/src/runtime/facades/orchestrate-facade.test.ts +16 -0
- package/src/runtime/facades/orchestrate-facade.ts +146 -0
- package/src/runtime/feature-flags.ts +4 -0
- package/src/runtime/orchestrate-ops.test.ts +131 -0
- package/src/runtime/orchestrate-ops.ts +158 -10
- package/src/runtime/planning-extra-ops.ts +77 -0
- package/src/runtime/preflight.ts +53 -0
- package/src/runtime/runtime.ts +41 -2
- package/src/runtime/types.ts +20 -0
- package/src/skills/__tests__/sync-skills.test.ts +132 -0
- package/src/skills/step-tracker.ts +162 -0
- package/src/skills/sync-skills.ts +54 -9
- package/src/subagent/dispatcher.ts +62 -39
- package/src/subagent/index.ts +1 -0
- package/src/subagent/orphan-reaper.test.ts +135 -0
- package/src/subagent/orphan-reaper.ts +130 -7
- package/src/subagent/types.ts +10 -0
- package/src/subagent/workspace-resolver.ts +3 -1
- package/src/vault/vault-entries.ts +112 -0
- package/src/vault/vault-manager.ts +1 -0
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +15 -0
- package/src/vault/vault.ts +1 -0
- package/vitest.config.ts +2 -1
- package/dist/brain/strength-scorer.d.ts +0 -31
- package/dist/brain/strength-scorer.d.ts.map +0 -1
- package/dist/brain/strength-scorer.js +0 -264
- package/dist/brain/strength-scorer.js.map +0 -1
- package/dist/engine/index.d.ts +0 -21
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -18
- package/dist/engine/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +0 -1
- package/dist/persona/index.d.ts +0 -5
- package/dist/persona/index.d.ts.map +0 -1
- package/dist/persona/index.js +0 -4
- package/dist/persona/index.js.map +0 -1
- package/dist/vault/vault-interfaces.d.ts +0 -153
- package/dist/vault/vault-interfaces.d.ts.map +0 -1
- package/dist/vault/vault-interfaces.js +0 -2
- package/dist/vault/vault-interfaces.js.map +0 -1
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddings — Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* 1. OpenAIEmbeddingProvider (mocked fetch)
|
|
6
|
+
* 2. Vector storage CRUD (real in-memory SQLite)
|
|
7
|
+
* 3. EmbeddingPipeline (batch + incremental)
|
|
8
|
+
* 4. Brain hybrid search backward compatibility
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { SQLitePersistenceProvider } from '../persistence/sqlite-provider.js';
|
|
13
|
+
import { initializeSchema } from '../vault/vault-schema.js';
|
|
14
|
+
import {
|
|
15
|
+
storeVector,
|
|
16
|
+
getVector,
|
|
17
|
+
deleteVector,
|
|
18
|
+
getEntriesWithoutVectors,
|
|
19
|
+
cosineSearch,
|
|
20
|
+
} from '../vault/vault-entries.js';
|
|
21
|
+
import { EmbeddingPipeline } from '../embeddings/pipeline.js';
|
|
22
|
+
import { OpenAIEmbeddingProvider } from '../embeddings/openai-provider.js';
|
|
23
|
+
import type { EmbeddingProvider, EmbeddingResult } from '../embeddings/types.js';
|
|
24
|
+
import type { PersistenceProvider } from '../persistence/types.js';
|
|
25
|
+
import { Vault } from '../vault/vault.js';
|
|
26
|
+
import { Brain } from '../brain/brain.js';
|
|
27
|
+
import { LLMError } from '../llm/types.js';
|
|
28
|
+
|
|
29
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Create a normalized vector of given dimensions. */
|
|
32
|
+
function makeVector(seed: number, dims: number): number[] {
|
|
33
|
+
const v: number[] = [];
|
|
34
|
+
for (let i = 0; i < dims; i++) {
|
|
35
|
+
v.push(Math.sin(seed * (i + 1)));
|
|
36
|
+
}
|
|
37
|
+
// Normalize
|
|
38
|
+
const norm = Math.sqrt(v.reduce((s, x) => s + x * x, 0));
|
|
39
|
+
return v.map((x) => x / norm);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Insert a minimal vault entry directly via SQL. */
|
|
43
|
+
function insertEntry(
|
|
44
|
+
provider: PersistenceProvider,
|
|
45
|
+
id: string,
|
|
46
|
+
title: string,
|
|
47
|
+
description: string,
|
|
48
|
+
): void {
|
|
49
|
+
provider.run(
|
|
50
|
+
`INSERT INTO entries (id, type, domain, title, severity, description, tags, applies_to, origin)
|
|
51
|
+
VALUES (@id, 'pattern', 'test', @title, 'suggestion', @description, '[]', '[]', 'user')`,
|
|
52
|
+
{ id, title, description },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a mock OpenAI-compatible fetch response. */
|
|
57
|
+
function okResponse(vectors: number[][], tokens: number): Response {
|
|
58
|
+
const body = JSON.stringify({
|
|
59
|
+
data: vectors.map((embedding, index) => ({ embedding, index })),
|
|
60
|
+
usage: { prompt_tokens: tokens, total_tokens: tokens },
|
|
61
|
+
model: 'text-embedding-3-small',
|
|
62
|
+
});
|
|
63
|
+
return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorResponse(status: number, message: string): Response {
|
|
67
|
+
return new Response(JSON.stringify({ error: { message } }), {
|
|
68
|
+
status,
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// 1. OpenAIEmbeddingProvider
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
describe('OpenAIEmbeddingProvider', () => {
|
|
78
|
+
let fetchSpy: ReturnType<typeof vi.spyOn>;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
vi.restoreAllMocks();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('throws when no API key or key pool is provided', () => {
|
|
89
|
+
expect(
|
|
90
|
+
() => new OpenAIEmbeddingProvider({ provider: 'openai', model: 'text-embedding-3-small' }),
|
|
91
|
+
).toThrow(/API key/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('embeds multiple texts successfully', async () => {
|
|
95
|
+
const vec1 = [0.1, 0.2, 0.3];
|
|
96
|
+
const vec2 = [0.4, 0.5, 0.6];
|
|
97
|
+
fetchSpy.mockResolvedValueOnce(okResponse([vec1, vec2], 20));
|
|
98
|
+
|
|
99
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
100
|
+
provider: 'openai',
|
|
101
|
+
model: 'text-embedding-3-small',
|
|
102
|
+
apiKey: 'sk-test-key',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await provider.embed(['hello', 'world']);
|
|
106
|
+
|
|
107
|
+
expect(result.vectors).toHaveLength(2);
|
|
108
|
+
expect(result.vectors[0]).toEqual(vec1);
|
|
109
|
+
expect(result.vectors[1]).toEqual(vec2);
|
|
110
|
+
expect(result.tokensUsed).toBe(20);
|
|
111
|
+
expect(result.model).toBe('text-embedding-3-small');
|
|
112
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns empty result for empty input', async () => {
|
|
116
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
117
|
+
provider: 'openai',
|
|
118
|
+
model: 'text-embedding-3-small',
|
|
119
|
+
apiKey: 'sk-test-key',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await provider.embed([]);
|
|
123
|
+
expect(result.vectors).toHaveLength(0);
|
|
124
|
+
expect(result.tokensUsed).toBe(0);
|
|
125
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('chunks requests that exceed batchSize', async () => {
|
|
129
|
+
const batchSize = 2;
|
|
130
|
+
const vec1 = [0.1, 0.2];
|
|
131
|
+
const vec2 = [0.3, 0.4];
|
|
132
|
+
const vec3 = [0.5, 0.6];
|
|
133
|
+
|
|
134
|
+
fetchSpy
|
|
135
|
+
.mockResolvedValueOnce(okResponse([vec1, vec2], 10))
|
|
136
|
+
.mockResolvedValueOnce(okResponse([vec3], 5));
|
|
137
|
+
|
|
138
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
139
|
+
provider: 'openai',
|
|
140
|
+
model: 'text-embedding-3-small',
|
|
141
|
+
apiKey: 'sk-test-key',
|
|
142
|
+
batchSize,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await provider.embed(['a', 'b', 'c']);
|
|
146
|
+
|
|
147
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
148
|
+
expect(result.vectors).toHaveLength(3);
|
|
149
|
+
expect(result.vectors[0]).toEqual(vec1);
|
|
150
|
+
expect(result.vectors[2]).toEqual(vec3);
|
|
151
|
+
expect(result.tokensUsed).toBe(15);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('retries on 429 rate limit and succeeds', async () => {
|
|
155
|
+
const vec = [0.1, 0.2, 0.3];
|
|
156
|
+
fetchSpy
|
|
157
|
+
.mockResolvedValueOnce(errorResponse(429, 'Rate limited'))
|
|
158
|
+
.mockResolvedValueOnce(okResponse([vec], 5));
|
|
159
|
+
|
|
160
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
161
|
+
provider: 'openai',
|
|
162
|
+
model: 'text-embedding-3-small',
|
|
163
|
+
apiKey: 'sk-test-key',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await provider.embed(['hello']);
|
|
167
|
+
expect(result.vectors[0]).toEqual(vec);
|
|
168
|
+
// First call fails (429), retry succeeds
|
|
169
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('throws immediately on 401 (not retryable)', async () => {
|
|
173
|
+
fetchSpy.mockResolvedValue(errorResponse(401, 'Unauthorized'));
|
|
174
|
+
|
|
175
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
176
|
+
provider: 'openai',
|
|
177
|
+
model: 'text-embedding-3-small',
|
|
178
|
+
apiKey: 'sk-bad-key',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await expect(provider.embed(['test'])).rejects.toThrow(LLMError);
|
|
182
|
+
// Only one attempt — 401 is not retryable
|
|
183
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('retries on 500 server error', async () => {
|
|
187
|
+
const vec = [0.9, 0.8];
|
|
188
|
+
fetchSpy
|
|
189
|
+
.mockResolvedValueOnce(errorResponse(500, 'Internal Server Error'))
|
|
190
|
+
.mockResolvedValueOnce(okResponse([vec], 3));
|
|
191
|
+
|
|
192
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
193
|
+
provider: 'openai',
|
|
194
|
+
model: 'text-embedding-3-small',
|
|
195
|
+
apiKey: 'sk-test-key',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await provider.embed(['retry me']);
|
|
199
|
+
expect(result.vectors[0]).toEqual(vec);
|
|
200
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('exhausts retries on persistent 5xx', async () => {
|
|
204
|
+
// Each retry reads the body, so we need a fresh Response each time
|
|
205
|
+
fetchSpy.mockImplementation(async () => errorResponse(503, 'Service Unavailable'));
|
|
206
|
+
|
|
207
|
+
const provider = new OpenAIEmbeddingProvider({
|
|
208
|
+
provider: 'openai',
|
|
209
|
+
model: 'text-embedding-3-small',
|
|
210
|
+
apiKey: 'sk-test-key',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await expect(provider.embed(['fail'])).rejects.toThrow(/503/);
|
|
214
|
+
// 3 attempts total (default maxAttempts)
|
|
215
|
+
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// 2. Vector Storage (vault-entries)
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
describe('Vector Storage', () => {
|
|
224
|
+
let provider: SQLitePersistenceProvider;
|
|
225
|
+
|
|
226
|
+
beforeEach(() => {
|
|
227
|
+
provider = new SQLitePersistenceProvider(':memory:');
|
|
228
|
+
initializeSchema(provider);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
provider.close();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('storeVector + getVector roundtrip', () => {
|
|
236
|
+
insertEntry(provider, 'e1', 'Test Entry', 'A test');
|
|
237
|
+
const vec = [0.1, 0.2, 0.3, 0.4];
|
|
238
|
+
|
|
239
|
+
storeVector(provider, 'e1', vec, 'test-model', 4);
|
|
240
|
+
|
|
241
|
+
const stored = getVector(provider, 'e1');
|
|
242
|
+
expect(stored).not.toBeNull();
|
|
243
|
+
expect(stored!.entryId).toBe('e1');
|
|
244
|
+
expect(stored!.model).toBe('test-model');
|
|
245
|
+
expect(stored!.dimensions).toBe(4);
|
|
246
|
+
// Float32 precision — compare with tolerance
|
|
247
|
+
for (let i = 0; i < vec.length; i++) {
|
|
248
|
+
expect(stored!.vector[i]).toBeCloseTo(vec[i], 5);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('storeVector upserts — second store overwrites', () => {
|
|
253
|
+
insertEntry(provider, 'e1', 'Test', 'Desc');
|
|
254
|
+
storeVector(provider, 'e1', [0.1, 0.2], 'model-v1', 2);
|
|
255
|
+
storeVector(provider, 'e1', [0.9, 0.8], 'model-v2', 2);
|
|
256
|
+
|
|
257
|
+
const stored = getVector(provider, 'e1');
|
|
258
|
+
expect(stored!.model).toBe('model-v2');
|
|
259
|
+
expect(stored!.vector[0]).toBeCloseTo(0.9, 5);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('getVector returns null for missing entry', () => {
|
|
263
|
+
expect(getVector(provider, 'nonexistent')).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('deleteVector removes stored vector', () => {
|
|
267
|
+
insertEntry(provider, 'e1', 'Test', 'Desc');
|
|
268
|
+
storeVector(provider, 'e1', [0.5, 0.5], 'model', 2);
|
|
269
|
+
expect(getVector(provider, 'e1')).toBeTruthy();
|
|
270
|
+
|
|
271
|
+
deleteVector(provider, 'e1');
|
|
272
|
+
const result = getVector(provider, 'e1');
|
|
273
|
+
expect(result).toBeFalsy();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('getEntriesWithoutVectors returns only entries missing vectors', () => {
|
|
277
|
+
insertEntry(provider, 'e1', 'Has vector', 'Desc');
|
|
278
|
+
insertEntry(provider, 'e2', 'No vector', 'Desc');
|
|
279
|
+
insertEntry(provider, 'e3', 'Also no vector', 'Desc');
|
|
280
|
+
|
|
281
|
+
storeVector(provider, 'e1', [0.1, 0.2], 'model-a', 2);
|
|
282
|
+
|
|
283
|
+
const missing = getEntriesWithoutVectors(provider, 'model-a');
|
|
284
|
+
expect(missing).toContain('e2');
|
|
285
|
+
expect(missing).toContain('e3');
|
|
286
|
+
expect(missing).not.toContain('e1');
|
|
287
|
+
expect(missing).toHaveLength(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('getEntriesWithoutVectors considers model — different model means missing', () => {
|
|
291
|
+
insertEntry(provider, 'e1', 'Test', 'Desc');
|
|
292
|
+
storeVector(provider, 'e1', [0.1, 0.2], 'model-a', 2);
|
|
293
|
+
|
|
294
|
+
// Entry has vector for model-a, not model-b
|
|
295
|
+
const missingForB = getEntriesWithoutVectors(provider, 'model-b');
|
|
296
|
+
expect(missingForB).toContain('e1');
|
|
297
|
+
|
|
298
|
+
const missingForA = getEntriesWithoutVectors(provider, 'model-a');
|
|
299
|
+
expect(missingForA).not.toContain('e1');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('cosineSearch returns results ordered by similarity', () => {
|
|
303
|
+
insertEntry(provider, 'e1', 'First', 'Desc');
|
|
304
|
+
insertEntry(provider, 'e2', 'Second', 'Desc');
|
|
305
|
+
insertEntry(provider, 'e3', 'Third', 'Desc');
|
|
306
|
+
|
|
307
|
+
// Store vectors: e1 is close to query, e3 is far
|
|
308
|
+
const dims = 8;
|
|
309
|
+
const query = makeVector(1, dims);
|
|
310
|
+
const closeVec = makeVector(1.05, dims); // very similar to query
|
|
311
|
+
const midVec = makeVector(3, dims); // somewhat different
|
|
312
|
+
const farVec = makeVector(50, dims); // very different
|
|
313
|
+
|
|
314
|
+
storeVector(provider, 'e1', closeVec, 'model', dims);
|
|
315
|
+
storeVector(provider, 'e2', midVec, 'model', dims);
|
|
316
|
+
storeVector(provider, 'e3', farVec, 'model', dims);
|
|
317
|
+
|
|
318
|
+
const results = cosineSearch(provider, query, 3);
|
|
319
|
+
|
|
320
|
+
expect(results).toHaveLength(3);
|
|
321
|
+
// e1 (closeVec) should be most similar
|
|
322
|
+
expect(results[0].entryId).toBe('e1');
|
|
323
|
+
expect(results[0].similarity).toBeGreaterThan(results[1].similarity);
|
|
324
|
+
// Similarities should be in descending order
|
|
325
|
+
expect(results[0].similarity).toBeGreaterThanOrEqual(results[1].similarity);
|
|
326
|
+
expect(results[1].similarity).toBeGreaterThanOrEqual(results[2].similarity);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('cosineSearch respects topK limit', () => {
|
|
330
|
+
const dims = 4;
|
|
331
|
+
for (let i = 1; i <= 5; i++) {
|
|
332
|
+
insertEntry(provider, `e${i}`, `Entry ${i}`, 'Desc');
|
|
333
|
+
storeVector(provider, `e${i}`, makeVector(i, dims), 'model', dims);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const results = cosineSearch(provider, makeVector(1, dims), 2);
|
|
337
|
+
expect(results).toHaveLength(2);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('cosineSearch returns empty for zero-norm query', () => {
|
|
341
|
+
insertEntry(provider, 'e1', 'Test', 'Desc');
|
|
342
|
+
storeVector(provider, 'e1', [0.1, 0.2], 'model', 2);
|
|
343
|
+
|
|
344
|
+
const results = cosineSearch(provider, [0, 0], 5);
|
|
345
|
+
expect(results).toHaveLength(0);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// =============================================================================
|
|
350
|
+
// 3. EmbeddingPipeline
|
|
351
|
+
// =============================================================================
|
|
352
|
+
|
|
353
|
+
describe('EmbeddingPipeline', () => {
|
|
354
|
+
let persistence: SQLitePersistenceProvider;
|
|
355
|
+
let mockProvider: EmbeddingProvider;
|
|
356
|
+
let embedCalls: string[][];
|
|
357
|
+
|
|
358
|
+
beforeEach(() => {
|
|
359
|
+
persistence = new SQLitePersistenceProvider(':memory:');
|
|
360
|
+
initializeSchema(persistence);
|
|
361
|
+
embedCalls = [];
|
|
362
|
+
|
|
363
|
+
mockProvider = {
|
|
364
|
+
providerName: 'mock',
|
|
365
|
+
model: 'mock-model',
|
|
366
|
+
dimensions: 4,
|
|
367
|
+
embed: vi.fn(async (texts: string[]): Promise<EmbeddingResult> => {
|
|
368
|
+
embedCalls.push(texts);
|
|
369
|
+
return {
|
|
370
|
+
vectors: texts.map((_, i) => makeVector(i + 1, 4)),
|
|
371
|
+
tokensUsed: texts.length * 5,
|
|
372
|
+
model: 'mock-model',
|
|
373
|
+
};
|
|
374
|
+
}),
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
afterEach(() => {
|
|
379
|
+
persistence.close();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('batchEmbed embeds all entries missing vectors', async () => {
|
|
383
|
+
insertEntry(persistence, 'e1', 'First', 'Description one');
|
|
384
|
+
insertEntry(persistence, 'e2', 'Second', 'Description two');
|
|
385
|
+
|
|
386
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
387
|
+
const result = await pipeline.batchEmbed();
|
|
388
|
+
|
|
389
|
+
expect(result.embedded).toBe(2);
|
|
390
|
+
expect(result.failed).toBe(0);
|
|
391
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
392
|
+
|
|
393
|
+
// Both entries should now have vectors
|
|
394
|
+
expect(getVector(persistence, 'e1')).toBeTruthy();
|
|
395
|
+
expect(getVector(persistence, 'e2')).toBeTruthy();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('batchEmbed fires onProgress callback', async () => {
|
|
399
|
+
insertEntry(persistence, 'e1', 'First', 'Desc');
|
|
400
|
+
insertEntry(persistence, 'e2', 'Second', 'Desc');
|
|
401
|
+
|
|
402
|
+
const progress: Array<[number, number]> = [];
|
|
403
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
404
|
+
await pipeline.batchEmbed({
|
|
405
|
+
onProgress: (completed, total) => progress.push([completed, total]),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(progress.length).toBeGreaterThan(0);
|
|
409
|
+
// Last progress call should have completed == total
|
|
410
|
+
const last = progress[progress.length - 1];
|
|
411
|
+
expect(last[0]).toBe(last[1]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('batchEmbed skips entries that already have vectors', async () => {
|
|
415
|
+
insertEntry(persistence, 'e1', 'Has Vector', 'Desc');
|
|
416
|
+
insertEntry(persistence, 'e2', 'No Vector', 'Desc');
|
|
417
|
+
|
|
418
|
+
// Pre-store vector for e1
|
|
419
|
+
storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
|
|
420
|
+
|
|
421
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
422
|
+
const result = await pipeline.batchEmbed();
|
|
423
|
+
|
|
424
|
+
expect(result.embedded).toBe(1); // Only e2
|
|
425
|
+
// The mock embed should only have been called with e2's text
|
|
426
|
+
expect(embedCalls).toHaveLength(1);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('batchEmbed returns zeros when all entries already embedded', async () => {
|
|
430
|
+
insertEntry(persistence, 'e1', 'Already done', 'Desc');
|
|
431
|
+
storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
|
|
432
|
+
|
|
433
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
434
|
+
const result = await pipeline.batchEmbed();
|
|
435
|
+
|
|
436
|
+
expect(result.embedded).toBe(0);
|
|
437
|
+
expect(result.skipped).toBe(0);
|
|
438
|
+
expect(result.failed).toBe(0);
|
|
439
|
+
expect(mockProvider.embed).not.toHaveBeenCalled();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('embedEntry embeds a single entry', async () => {
|
|
443
|
+
insertEntry(persistence, 'e1', 'Test', 'Desc');
|
|
444
|
+
|
|
445
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
446
|
+
const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
|
|
447
|
+
|
|
448
|
+
expect(embedded).toBe(true);
|
|
449
|
+
expect(getVector(persistence, 'e1')).toBeTruthy();
|
|
450
|
+
expect(mockProvider.embed).toHaveBeenCalledTimes(1);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('embedEntry returns false when vector already exists for same model', async () => {
|
|
454
|
+
insertEntry(persistence, 'e1', 'Test', 'Desc');
|
|
455
|
+
storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'mock-model', 4);
|
|
456
|
+
|
|
457
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
458
|
+
const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
|
|
459
|
+
|
|
460
|
+
expect(embedded).toBe(false);
|
|
461
|
+
expect(mockProvider.embed).not.toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('embedEntry re-embeds when stored vector is for a different model', async () => {
|
|
465
|
+
insertEntry(persistence, 'e1', 'Test', 'Desc');
|
|
466
|
+
storeVector(persistence, 'e1', [0.1, 0.2, 0.3, 0.4], 'old-model', 4);
|
|
467
|
+
|
|
468
|
+
const pipeline = new EmbeddingPipeline(mockProvider, persistence);
|
|
469
|
+
const embedded = await pipeline.embedEntry('e1', 'Test\nDesc');
|
|
470
|
+
|
|
471
|
+
expect(embedded).toBe(true);
|
|
472
|
+
const stored = getVector(persistence, 'e1');
|
|
473
|
+
expect(stored!.model).toBe('mock-model');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// =============================================================================
|
|
478
|
+
// 4. Brain Hybrid Search (backward compatibility)
|
|
479
|
+
// =============================================================================
|
|
480
|
+
|
|
481
|
+
describe('Brain hybrid search compatibility', () => {
|
|
482
|
+
it('Brain constructor accepts optional embeddingProvider', () => {
|
|
483
|
+
const vault = new Vault(':memory:');
|
|
484
|
+
const mockEmb: EmbeddingProvider = {
|
|
485
|
+
providerName: 'mock',
|
|
486
|
+
model: 'mock-model',
|
|
487
|
+
dimensions: 4,
|
|
488
|
+
embed: vi.fn(async () => ({ vectors: [], tokensUsed: 0, model: 'mock-model' })),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
expect(() => new Brain(vault, undefined, mockEmb)).not.toThrow();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('Brain works without embeddingProvider (backward compat)', async () => {
|
|
495
|
+
const vault = new Vault(':memory:');
|
|
496
|
+
|
|
497
|
+
// Seed an entry so search can find something
|
|
498
|
+
vault.seed([
|
|
499
|
+
{
|
|
500
|
+
id: 'compat-1',
|
|
501
|
+
type: 'pattern',
|
|
502
|
+
domain: 'test',
|
|
503
|
+
title: 'Backward compatibility pattern',
|
|
504
|
+
severity: 'suggestion',
|
|
505
|
+
description: 'This tests that Brain search works without embedding provider',
|
|
506
|
+
tags: ['compat'],
|
|
507
|
+
appliesTo: [],
|
|
508
|
+
},
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
const brain = new Brain(vault);
|
|
512
|
+
const results = await brain.intelligentSearch('backward compatibility');
|
|
513
|
+
|
|
514
|
+
// Should not throw — returns results from FTS only
|
|
515
|
+
expect(Array.isArray(results)).toBe(true);
|
|
516
|
+
expect(results.length).toBeGreaterThan(0);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('Brain.setEmbeddingProvider can set and clear provider', () => {
|
|
520
|
+
const vault = new Vault(':memory:');
|
|
521
|
+
const brain = new Brain(vault);
|
|
522
|
+
|
|
523
|
+
const mockEmb: EmbeddingProvider = {
|
|
524
|
+
providerName: 'mock',
|
|
525
|
+
model: 'mock-model',
|
|
526
|
+
dimensions: 4,
|
|
527
|
+
embed: vi.fn(async () => ({ vectors: [], tokensUsed: 0, model: 'mock-model' })),
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Set provider
|
|
531
|
+
expect(() => brain.setEmbeddingProvider(mockEmb)).not.toThrow();
|
|
532
|
+
|
|
533
|
+
// Clear provider
|
|
534
|
+
expect(() => brain.setEmbeddingProvider(undefined)).not.toThrow();
|
|
535
|
+
});
|
|
536
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildPreflightManifest, type PreflightInput } from '../runtime/preflight.js';
|
|
3
|
+
|
|
4
|
+
function makeInput(overrides: Partial<PreflightInput> = {}): PreflightInput {
|
|
5
|
+
return {
|
|
6
|
+
facades: [
|
|
7
|
+
{
|
|
8
|
+
name: 'agent_vault',
|
|
9
|
+
ops: [
|
|
10
|
+
{ name: 'search_intelligent', description: 'Search knowledge' },
|
|
11
|
+
{ name: 'capture_knowledge', description: 'Capture knowledge' },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'agent_plan',
|
|
16
|
+
ops: [{ name: 'create_plan', description: 'Create a plan' }],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
skills: ['vault-capture', 'debugging'],
|
|
20
|
+
executingPlans: [{ id: 'plan-1', objective: 'Add preflight manifest', status: 'executing' }],
|
|
21
|
+
vaultStats: {
|
|
22
|
+
totalEntries: 42,
|
|
23
|
+
byDomain: { architecture: 10, testing: 15, patterns: 17 },
|
|
24
|
+
},
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('buildPreflightManifest', () => {
|
|
30
|
+
it('flattens facade ops into tools array', () => {
|
|
31
|
+
const manifest = buildPreflightManifest(makeInput());
|
|
32
|
+
expect(manifest.tools).toHaveLength(3);
|
|
33
|
+
expect(manifest.tools[0]).toEqual({
|
|
34
|
+
facade: 'agent_vault',
|
|
35
|
+
op: 'search_intelligent',
|
|
36
|
+
description: 'Search knowledge',
|
|
37
|
+
});
|
|
38
|
+
expect(manifest.tools[2]).toEqual({
|
|
39
|
+
facade: 'agent_plan',
|
|
40
|
+
op: 'create_plan',
|
|
41
|
+
description: 'Create a plan',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes through skills array', () => {
|
|
46
|
+
const manifest = buildPreflightManifest(makeInput());
|
|
47
|
+
expect(manifest.skills).toEqual(['vault-capture', 'debugging']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('maps executing plans to activePlans', () => {
|
|
51
|
+
const manifest = buildPreflightManifest(makeInput());
|
|
52
|
+
expect(manifest.activePlans).toEqual([
|
|
53
|
+
{ planId: 'plan-1', title: 'Add preflight manifest', status: 'executing' },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('builds vault summary from stats', () => {
|
|
58
|
+
const manifest = buildPreflightManifest(makeInput());
|
|
59
|
+
expect(manifest.vaultSummary).toEqual({
|
|
60
|
+
entryCount: 42,
|
|
61
|
+
connected: true,
|
|
62
|
+
domains: ['architecture', 'testing', 'patterns'],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles empty inputs gracefully', () => {
|
|
67
|
+
const manifest = buildPreflightManifest(
|
|
68
|
+
makeInput({
|
|
69
|
+
facades: [],
|
|
70
|
+
skills: [],
|
|
71
|
+
executingPlans: [],
|
|
72
|
+
vaultStats: { totalEntries: 0, byDomain: {} },
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
expect(manifest.tools).toEqual([]);
|
|
76
|
+
expect(manifest.skills).toEqual([]);
|
|
77
|
+
expect(manifest.activePlans).toEqual([]);
|
|
78
|
+
expect(manifest.vaultSummary).toEqual({
|
|
79
|
+
entryCount: 0,
|
|
80
|
+
connected: true,
|
|
81
|
+
domains: [],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles multiple executing plans', () => {
|
|
86
|
+
const manifest = buildPreflightManifest(
|
|
87
|
+
makeInput({
|
|
88
|
+
executingPlans: [
|
|
89
|
+
{ id: 'plan-1', objective: 'First plan', status: 'executing' },
|
|
90
|
+
{ id: 'plan-2', objective: 'Second plan', status: 'executing' },
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
expect(manifest.activePlans).toHaveLength(2);
|
|
95
|
+
expect(manifest.activePlans[1].planId).toBe('plan-2');
|
|
96
|
+
});
|
|
97
|
+
});
|