@morningljn/mnemo 0.2.1 → 0.3.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/config.d.ts +2 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/dream-engine.d.ts +17 -0
- package/dist/dream-engine.js +144 -0
- package/dist/dream-engine.js.map +1 -0
- package/dist/llm-client.d.ts +10 -0
- package/dist/llm-client.js +55 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/resources.js +1 -1
- package/dist/store.d.ts +2 -1
- package/dist/store.js +32 -8
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/docs/superpowers/plans/2026-05-16-llm-dream.md +973 -0
- package/openspec/changes/llm-dream/.openspec.yaml +2 -0
- package/openspec/changes/llm-dream/design.md +84 -0
- package/openspec/changes/llm-dream/proposal.md +36 -0
- package/openspec/changes/llm-dream/specs/dream-cycle/spec.md +42 -0
- package/openspec/changes/llm-dream/specs/llm-client/spec.md +57 -0
- package/openspec/changes/llm-dream/specs/llm-dream-engine/spec.md +72 -0
- package/openspec/changes/llm-dream/tasks.md +32 -0
- package/package.json +1 -1
- package/src/config.ts +29 -0
- package/src/dream-engine.ts +162 -0
- package/src/llm-client.ts +59 -0
- package/src/resources.ts +1 -1
- package/src/store.ts +39 -7
- package/src/types.ts +16 -0
- package/tests/dream-engine.test.ts +163 -0
- package/tests/llm-client.test.ts +105 -0
- package/tests/store.test.ts +6 -5
package/src/store.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { mkdirSync } from 'node:fs'
|
|
|
15
15
|
import { dirname, join } from 'node:path'
|
|
16
16
|
import { SCHEMA } from './schema.js'
|
|
17
17
|
import type { Fact, FactCategory, DreamReport } from './types.js'
|
|
18
|
+
import { loadConfig } from './config.js'
|
|
19
|
+
import { LLMClient } from './llm-client.js'
|
|
20
|
+
import { DreamEngine } from './dream-engine.js'
|
|
18
21
|
|
|
19
22
|
// 信任评分常量
|
|
20
23
|
const HELPFUL_DELTA = 0.05
|
|
@@ -875,7 +878,7 @@ export class MemoryStore {
|
|
|
875
878
|
const tokensB = this.tokenizeForDedup(rows[j].content)
|
|
876
879
|
const sim = this.jaccardSimilarity(tokensA, tokensB)
|
|
877
880
|
|
|
878
|
-
if (sim > 0.
|
|
881
|
+
if (sim > 0.4) {
|
|
879
882
|
const aHighFreq = rows[i].retrieval_count > 100
|
|
880
883
|
const bHighFreq = rows[j].retrieval_count > 100
|
|
881
884
|
|
|
@@ -914,14 +917,14 @@ export class MemoryStore {
|
|
|
914
917
|
{ keywords: ['偏好', 'VS Code', '编辑器', 'IDE', '快捷键'], target: 'tool_pref' },
|
|
915
918
|
]
|
|
916
919
|
|
|
920
|
+
// 只从 general 分类移动,避免已分类 fact 被反复震荡
|
|
917
921
|
const rows = this.db.prepare(
|
|
918
|
-
|
|
922
|
+
"SELECT fact_id, content, category FROM facts WHERE category = 'general'"
|
|
919
923
|
).all() as Array<{ fact_id: number; content: string; category: string }>
|
|
920
924
|
|
|
921
925
|
let reclassified = 0
|
|
922
926
|
for (const row of rows) {
|
|
923
927
|
for (const rule of rules) {
|
|
924
|
-
if (rule.target === row.category) continue
|
|
925
928
|
if (rule.keywords.some(kw => row.content.includes(kw))) {
|
|
926
929
|
this.db.prepare("UPDATE facts SET category = ?, updated_at = datetime('now', 'localtime') WHERE fact_id = ?").run(rule.target, row.fact_id)
|
|
927
930
|
reclassified++
|
|
@@ -933,16 +936,43 @@ export class MemoryStore {
|
|
|
933
936
|
})()
|
|
934
937
|
}
|
|
935
938
|
|
|
936
|
-
/** 执行完整 dream cycle:备份 →
|
|
939
|
+
/** 执行完整 dream cycle:备份 → LLM 整理 → 降级规则引擎 → 报告 */
|
|
937
940
|
async runDream(options?: { skipBackup?: boolean }): Promise<DreamReport> {
|
|
938
941
|
if (!options?.skipBackup) {
|
|
939
942
|
await this.backupDatabase()
|
|
940
943
|
}
|
|
941
944
|
|
|
945
|
+
// 尝试 LLM 驱动的 dream
|
|
946
|
+
const config = loadConfig()
|
|
947
|
+
const llmClient = new LLMClient(config)
|
|
948
|
+
|
|
949
|
+
const available = await llmClient.isAvailable()
|
|
950
|
+
if (available) {
|
|
951
|
+
try {
|
|
952
|
+
const engine = new DreamEngine(llmClient, this)
|
|
953
|
+
const mergeResult = await engine.semanticMerge()
|
|
954
|
+
const compressed = await engine.smartCompress()
|
|
955
|
+
const reclassified = await engine.smartReclassify()
|
|
956
|
+
|
|
957
|
+
return this.buildDreamReport(mergeResult.merged, compressed, reclassified, mergeResult.details.map(d => ({ kept: d.kept, removed: d.removed, similarity: 0 })), false)
|
|
958
|
+
} catch {
|
|
959
|
+
// LLM 执行失败,降级到规则引擎
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 降级到规则引擎
|
|
942
964
|
const compressed = this.compressLongFacts()
|
|
943
965
|
const mergeResult = this.mergeOverlappingFacts()
|
|
944
966
|
const reclassified = this.reclassifyFacts()
|
|
945
967
|
|
|
968
|
+
return this.buildDreamReport(mergeResult.merged, compressed, reclassified, mergeResult.details, true, 'LLM unavailable')
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private buildDreamReport(
|
|
972
|
+
merged: number, compressed: number, reclassified: number,
|
|
973
|
+
mergeDetails: Array<{ kept: number; removed: number; similarity: number }>,
|
|
974
|
+
fallback: boolean, fallbackReason?: string,
|
|
975
|
+
): DreamReport {
|
|
946
976
|
const stats = this.db.prepare(`
|
|
947
977
|
SELECT COUNT(*) as total,
|
|
948
978
|
AVG(trust_score) as avg_trust,
|
|
@@ -958,11 +988,13 @@ export class MemoryStore {
|
|
|
958
988
|
}
|
|
959
989
|
|
|
960
990
|
return {
|
|
961
|
-
merged
|
|
991
|
+
merged,
|
|
962
992
|
compressed,
|
|
963
993
|
reclassified,
|
|
964
|
-
deleted:
|
|
965
|
-
mergeDetails
|
|
994
|
+
deleted: merged,
|
|
995
|
+
mergeDetails,
|
|
996
|
+
fallback,
|
|
997
|
+
fallbackReason,
|
|
966
998
|
health: {
|
|
967
999
|
total: stats.total,
|
|
968
1000
|
avg_trust: Math.round((stats.avg_trust ?? 0) * 100) / 100,
|
package/src/types.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface DreamReport {
|
|
|
90
90
|
reclassified: number
|
|
91
91
|
deleted: number
|
|
92
92
|
mergeDetails: Array<{ kept: number; removed: number; similarity: number }>
|
|
93
|
+
fallback?: boolean
|
|
94
|
+
fallbackReason?: string
|
|
93
95
|
health: {
|
|
94
96
|
total: number
|
|
95
97
|
avg_trust: number
|
|
@@ -106,3 +108,17 @@ export interface CompactFactResult {
|
|
|
106
108
|
trustScore: number
|
|
107
109
|
score: number
|
|
108
110
|
}
|
|
111
|
+
|
|
112
|
+
/** LLM 配置 */
|
|
113
|
+
export interface LLMConfig {
|
|
114
|
+
baseUrl: string
|
|
115
|
+
model: string
|
|
116
|
+
apiKey?: string
|
|
117
|
+
temperature: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** LLM 聊天消息 */
|
|
121
|
+
export interface LLMMessage {
|
|
122
|
+
role: 'system' | 'user' | 'assistant'
|
|
123
|
+
content: string
|
|
124
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { DreamEngine } from '../src/dream-engine.js'
|
|
3
|
+
import { LLMClient } from '../src/llm-client.js'
|
|
4
|
+
import { MemoryStore } from '../src/store.js'
|
|
5
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
|
|
9
|
+
let store: MemoryStore
|
|
10
|
+
let tmpDir: string
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'mnemo-dream-'))
|
|
14
|
+
store = new MemoryStore(join(tmpDir, 'test.db'))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
store.close()
|
|
19
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('DreamEngine - semanticMerge', () => {
|
|
23
|
+
it('merges semantically duplicate facts in same category', async () => {
|
|
24
|
+
store.addFact('用户喜欢使用 VS Code 编辑器写代码', 'tool_pref')
|
|
25
|
+
store.addFact('用户偏好 Visual Studio Code 作为开发工具', 'tool_pref')
|
|
26
|
+
|
|
27
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({
|
|
28
|
+
merges: [{ kept: 1, removed: 2, reason: '都描述偏好VS Code编辑器' }],
|
|
29
|
+
})
|
|
30
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
31
|
+
const eng = new DreamEngine(mockClient, store)
|
|
32
|
+
const result = await eng.semanticMerge()
|
|
33
|
+
|
|
34
|
+
expect(result.merged).toBe(1)
|
|
35
|
+
expect(result.details.length).toBe(1)
|
|
36
|
+
expect(result.details[0].reason).toBe('都描述偏好VS Code编辑器')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('skips batch when LLM returns invalid JSON', async () => {
|
|
40
|
+
store.addFact('一些事实内容', 'general')
|
|
41
|
+
|
|
42
|
+
const mockClient = {
|
|
43
|
+
chatJSON: vi.fn().mockRejectedValue(new Error('invalid json')),
|
|
44
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
45
|
+
} as unknown as LLMClient
|
|
46
|
+
const eng = new DreamEngine(mockClient, store)
|
|
47
|
+
const result = await eng.semanticMerge()
|
|
48
|
+
|
|
49
|
+
expect(result.merged).toBe(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('protects high-trust facts from deletion', async () => {
|
|
53
|
+
const id1 = store.addFact('高信任事实内容', 'general')
|
|
54
|
+
store.recordFeedback(id1, true)
|
|
55
|
+
store.recordFeedback(id1, true)
|
|
56
|
+
store.recordFeedback(id1, true)
|
|
57
|
+
store.recordFeedback(id1, true)
|
|
58
|
+
store.recordFeedback(id1, true)
|
|
59
|
+
store.recordFeedback(id1, true)
|
|
60
|
+
store.recordFeedback(id1, true)
|
|
61
|
+
store.recordFeedback(id1, true)
|
|
62
|
+
store.recordFeedback(id1, true)
|
|
63
|
+
store.recordFeedback(id1, true)
|
|
64
|
+
|
|
65
|
+
const id2 = store.addFact('另一个事实内容', 'general')
|
|
66
|
+
|
|
67
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({
|
|
68
|
+
merges: [{ kept: id2, removed: id1, reason: '重复' }],
|
|
69
|
+
})
|
|
70
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
71
|
+
const eng = new DreamEngine(mockClient, store)
|
|
72
|
+
const result = await eng.semanticMerge()
|
|
73
|
+
|
|
74
|
+
expect(result.merged).toBe(0)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('DreamEngine - smartCompress', () => {
|
|
79
|
+
it('generates summary for long facts without summary', async () => {
|
|
80
|
+
const longContent = '这是一段很长的记忆内容。'.repeat(20) // >200 chars
|
|
81
|
+
store.addFact(longContent, 'general')
|
|
82
|
+
|
|
83
|
+
const summary = '这是一段关于长内容的简洁摘要'
|
|
84
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({ summaries: [{ fact_id: 1, summary }] })
|
|
85
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
86
|
+
const eng = new DreamEngine(mockClient, store)
|
|
87
|
+
const result = await eng.smartCompress()
|
|
88
|
+
|
|
89
|
+
expect(result).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('skips facts that already have summary', async () => {
|
|
93
|
+
const longContent = '这是一段很长的记忆内容。'.repeat(20)
|
|
94
|
+
const id = store.addFact(longContent, 'general')
|
|
95
|
+
store.connection.prepare('UPDATE facts SET summary = ? WHERE fact_id = ?').run('已有摘要', id)
|
|
96
|
+
|
|
97
|
+
const mockChatJSON = vi.fn()
|
|
98
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
99
|
+
const eng = new DreamEngine(mockClient, store)
|
|
100
|
+
const result = await eng.smartCompress()
|
|
101
|
+
|
|
102
|
+
expect(result).toBe(0)
|
|
103
|
+
expect(mockChatJSON).not.toHaveBeenCalled()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('truncates summary longer than 150 chars', async () => {
|
|
107
|
+
const longContent = '这是一段很长的记忆内容。'.repeat(20)
|
|
108
|
+
store.addFact(longContent, 'general')
|
|
109
|
+
|
|
110
|
+
const tooLongSummary = 'a'.repeat(200)
|
|
111
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({ summaries: [{ fact_id: 1, summary: tooLongSummary }] })
|
|
112
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
113
|
+
const eng = new DreamEngine(mockClient, store)
|
|
114
|
+
await eng.smartCompress()
|
|
115
|
+
|
|
116
|
+
const fact = store.listFacts('general', 0, 10)[0]
|
|
117
|
+
expect(fact.summary!.length).toBeLessThanOrEqual(150)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('DreamEngine - smartReclassify', () => {
|
|
122
|
+
it('moves general facts to correct category via LLM', async () => {
|
|
123
|
+
store.addFact('用户编码规范要求文件不超过500行', 'general')
|
|
124
|
+
|
|
125
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({
|
|
126
|
+
reclassify: [{ fact_id: 1, to: 'coding_style' }],
|
|
127
|
+
})
|
|
128
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
129
|
+
const eng = new DreamEngine(mockClient, store)
|
|
130
|
+
const result = await eng.smartReclassify()
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(1)
|
|
133
|
+
const fact = store.listFacts('coding_style', 0, 10)[0]
|
|
134
|
+
expect(fact).toBeDefined()
|
|
135
|
+
expect(fact.content).toContain('编码规范')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('ignores invalid category from LLM', async () => {
|
|
139
|
+
store.addFact('一些内容', 'general')
|
|
140
|
+
|
|
141
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({
|
|
142
|
+
reclassify: [{ fact_id: 1, to: 'invalid_category' }],
|
|
143
|
+
})
|
|
144
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
145
|
+
const eng = new DreamEngine(mockClient, store)
|
|
146
|
+
const result = await eng.smartReclassify()
|
|
147
|
+
|
|
148
|
+
expect(result).toBe(0)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('skips when LLM says keep general', async () => {
|
|
152
|
+
store.addFact('一些杂项内容', 'general')
|
|
153
|
+
|
|
154
|
+
const mockChatJSON = vi.fn().mockResolvedValueOnce({
|
|
155
|
+
reclassify: [{ fact_id: 1, to: 'general' }],
|
|
156
|
+
})
|
|
157
|
+
const mockClient = { chatJSON: mockChatJSON, isAvailable: vi.fn().mockResolvedValue(true) } as unknown as LLMClient
|
|
158
|
+
const eng = new DreamEngine(mockClient, store)
|
|
159
|
+
const result = await eng.smartReclassify()
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(0)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { LLMClient } from '../src/llm-client.js'
|
|
3
|
+
import type { LLMConfig, LLMMessage } from '../src/types.js'
|
|
4
|
+
|
|
5
|
+
const mockConfig: LLMConfig = {
|
|
6
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
7
|
+
model: 'test-model',
|
|
8
|
+
temperature: 0.1,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mockFetchResponse(body: unknown, ok = true, status = 200) {
|
|
12
|
+
return vi.fn().mockResolvedValue({
|
|
13
|
+
ok,
|
|
14
|
+
status,
|
|
15
|
+
json: () => Promise.resolve(body),
|
|
16
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('LLMClient', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('chat', () => {
|
|
26
|
+
it('sends request and returns text content', async () => {
|
|
27
|
+
const mockResp = {
|
|
28
|
+
choices: [{ message: { content: 'Hello from LLM' } }],
|
|
29
|
+
}
|
|
30
|
+
globalThis.fetch = mockFetchResponse(mockResp)
|
|
31
|
+
|
|
32
|
+
const client = new LLMClient(mockConfig)
|
|
33
|
+
const messages: LLMMessage[] = [{ role: 'user', content: 'Hi' }]
|
|
34
|
+
const result = await client.chat(messages)
|
|
35
|
+
|
|
36
|
+
expect(result).toBe('Hello from LLM')
|
|
37
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
38
|
+
'http://localhost:11434/v1/chat/completions',
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('includes Authorization header when apiKey is set', async () => {
|
|
47
|
+
const configWithKey = { ...mockConfig, apiKey: 'sk-test-key' }
|
|
48
|
+
globalThis.fetch = mockFetchResponse({
|
|
49
|
+
choices: [{ message: { content: 'ok' } }],
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const client = new LLMClient(configWithKey)
|
|
53
|
+
await client.chat([{ role: 'user', content: 'test' }])
|
|
54
|
+
|
|
55
|
+
const callArgs = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit
|
|
56
|
+
expect(callArgs.headers).toHaveProperty('Authorization', 'Bearer sk-test-key')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('throws on connection failure', async () => {
|
|
60
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'))
|
|
61
|
+
const client = new LLMClient(mockConfig)
|
|
62
|
+
await expect(client.chat([{ role: 'user', content: 'test' }])).rejects.toThrow('ECONNREFUSED')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('throws on non-ok HTTP status', async () => {
|
|
66
|
+
globalThis.fetch = mockFetchResponse({ error: 'bad request' }, false, 400)
|
|
67
|
+
const client = new LLMClient(mockConfig)
|
|
68
|
+
await expect(client.chat([{ role: 'user', content: 'test' }])).rejects.toThrow('400')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('extracts JSON from markdown code fence', async () => {
|
|
72
|
+
const jsonBody = { result: [1, 2] }
|
|
73
|
+
const fenced = '```json\n' + JSON.stringify(jsonBody) + '\n```'
|
|
74
|
+
globalThis.fetch = mockFetchResponse({
|
|
75
|
+
choices: [{ message: { content: fenced } }],
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const client = new LLMClient(mockConfig)
|
|
79
|
+
const result = await client.chatJSON([{ role: 'user', content: 'test' }])
|
|
80
|
+
expect(result).toEqual(jsonBody)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('throws on invalid JSON response in chatJSON', async () => {
|
|
84
|
+
globalThis.fetch = mockFetchResponse({
|
|
85
|
+
choices: [{ message: { content: 'not json at all' } }],
|
|
86
|
+
})
|
|
87
|
+
const client = new LLMClient(mockConfig)
|
|
88
|
+
await expect(client.chatJSON([{ role: 'user', content: 'test' }])).rejects.toThrow()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('isAvailable', () => {
|
|
93
|
+
it('returns true when service is reachable', async () => {
|
|
94
|
+
globalThis.fetch = mockFetchResponse({ data: [] })
|
|
95
|
+
const client = new LLMClient(mockConfig)
|
|
96
|
+
expect(await client.isAvailable()).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns false when service is unreachable', async () => {
|
|
100
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('fail'))
|
|
101
|
+
const client = new LLMClient(mockConfig)
|
|
102
|
+
expect(await client.isAvailable()).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
package/tests/store.test.ts
CHANGED
|
@@ -309,16 +309,16 @@ describe('dream - merge', () => {
|
|
|
309
309
|
})
|
|
310
310
|
|
|
311
311
|
describe('dream - reclassify', () => {
|
|
312
|
-
it('moves miscategorized facts
|
|
313
|
-
const id = store.addFact('编码规范:文件不超过 500 行', '
|
|
312
|
+
it('moves miscategorized facts from general to correct category', () => {
|
|
313
|
+
const id = store.addFact('编码规范:文件不超过 500 行', 'general')
|
|
314
314
|
const result = store.reclassifyFacts()
|
|
315
315
|
expect(result).toBeGreaterThanOrEqual(1)
|
|
316
316
|
const row = store.connection.prepare('SELECT category FROM facts WHERE fact_id = ?').get(id) as any
|
|
317
317
|
expect(row.category).toBe('coding_style')
|
|
318
318
|
})
|
|
319
319
|
|
|
320
|
-
it('skips
|
|
321
|
-
store.addFact('
|
|
320
|
+
it('skips already categorized facts', () => {
|
|
321
|
+
store.addFact('编码规范:文件不超过 500 行', 'coding_style')
|
|
322
322
|
const result = store.reclassifyFacts()
|
|
323
323
|
expect(result).toBe(0)
|
|
324
324
|
})
|
|
@@ -331,7 +331,7 @@ describe('dream - runDream', () => {
|
|
|
331
331
|
// 重叠 fact(触发合并)
|
|
332
332
|
store.addFact('用户偏好使用 TypeScript 开发前端代码', 'coding_style')
|
|
333
333
|
// 分类错误 fact(触发重分类)
|
|
334
|
-
store.addFact('编码规范:文件不超过 500 行', '
|
|
334
|
+
store.addFact('编码规范:文件不超过 500 行', 'general')
|
|
335
335
|
|
|
336
336
|
const report = await store.runDream({ skipBackup: true })
|
|
337
337
|
expect(report.compressed).toBeGreaterThanOrEqual(0)
|
|
@@ -339,5 +339,6 @@ describe('dream - runDream', () => {
|
|
|
339
339
|
expect(report.reclassified).toBeGreaterThanOrEqual(0)
|
|
340
340
|
expect(report.health.total).toBeGreaterThanOrEqual(1)
|
|
341
341
|
expect(report.health.coverage).toBeTruthy()
|
|
342
|
+
expect(report.fallback).toBe(true) // 测试环境无 Ollama,应降级到规则引擎
|
|
342
343
|
})
|
|
343
344
|
})
|