@morningljn/mnemo 0.1.3 → 0.2.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/README.md +43 -14
- package/dist/init.js +16 -8
- package/dist/init.js.map +1 -1
- package/dist/refine.d.ts +14 -0
- package/dist/refine.js +115 -0
- package/dist/refine.js.map +1 -0
- package/dist/resources.d.ts +27 -0
- package/dist/resources.js +56 -0
- package/dist/resources.js.map +1 -0
- package/dist/retriever.d.ts +3 -1
- package/dist/retriever.js +42 -42
- package/dist/retriever.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +21 -10
- package/dist/schema.js.map +1 -1
- package/dist/server.js +41 -1
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +37 -0
- package/dist/store.js +166 -9
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/docs/superpowers/plans/2026-05-15-mnemo-mcp.md +1154 -0
- package/docs/superpowers/plans/2026-05-16-memory-self-learning.md +932 -0
- package/docs/superpowers/plans/2026-05-16-mnemo-query-cache.md +613 -0
- package/docs/superpowers/plans/2026-05-16-retrieval-and-injection-optimization.md +770 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/design.md +83 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/proposal.md +32 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-retrieval/spec.md +75 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-store/spec.md +83 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/mcp-server/spec.md +34 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/security/spec.md +37 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/tasks.md +44 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/design.md +96 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/proposal.md +29 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/batch-operations/spec.md +42 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/perf-metrics/spec.md +55 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/query-cache/spec.md +65 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/tasks.md +45 -0
- package/openspec/changes/memory-self-learning/.openspec.yaml +2 -0
- package/openspec/changes/memory-self-learning/design.md +174 -0
- package/openspec/changes/memory-self-learning/proposal.md +35 -0
- package/openspec/changes/memory-self-learning/specs/fact-retrieval/spec.md +35 -0
- package/openspec/changes/memory-self-learning/specs/fact-summary/spec.md +45 -0
- package/openspec/changes/memory-self-learning/specs/length-penalty/spec.md +27 -0
- package/openspec/changes/memory-self-learning/specs/retrieval-log/spec.md +41 -0
- package/openspec/changes/memory-self-learning/specs/self-learning/spec.md +68 -0
- package/openspec/changes/memory-self-learning/tasks.md +56 -0
- package/openspec/changes/retrieval-and-injection-optimization/.openspec.yaml +2 -0
- package/openspec/changes/retrieval-and-injection-optimization/design.md +117 -0
- package/openspec/changes/retrieval-and-injection-optimization/proposal.md +30 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/adaptive-scoring/spec.md +43 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/injection-protocol/spec.md +48 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/mcp-resources/spec.md +39 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/query-refinement/spec.md +39 -0
- package/openspec/changes/retrieval-and-injection-optimization/tasks.md +33 -0
- package/openspec/config.yaml +20 -0
- package/package.json +1 -1
- package/src/init.ts +17 -9
- package/src/refine.ts +127 -0
- package/src/resources.ts +78 -0
- package/src/retriever.ts +46 -44
- package/src/schema.ts +21 -10
- package/src/server.ts +44 -1
- package/src/store.ts +215 -9
- package/src/types.ts +4 -1
- package/tests/refine.test.ts +52 -0
- package/tests/resource.test.ts +62 -0
- package/tests/retriever.test.ts +53 -0
- package/tests/store.test.ts +112 -0
package/src/types.ts
CHANGED
|
@@ -8,9 +8,11 @@ export interface Fact {
|
|
|
8
8
|
category: FactCategory
|
|
9
9
|
tags: string
|
|
10
10
|
keywords: string
|
|
11
|
+
summary: string | null
|
|
11
12
|
trustScore: number
|
|
12
13
|
retrievalCount: number
|
|
13
14
|
helpfulCount: number
|
|
15
|
+
lastRetrievedAt: string | null
|
|
14
16
|
createdAt: string
|
|
15
17
|
updatedAt: string
|
|
16
18
|
}
|
|
@@ -53,7 +55,7 @@ export interface RetrieverOptions {
|
|
|
53
55
|
|
|
54
56
|
/** fact_store 工具调用参数 */
|
|
55
57
|
export interface FactStoreArgs {
|
|
56
|
-
action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list'
|
|
58
|
+
action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list' | 'learn' | 'audit'
|
|
57
59
|
content?: string | string[]
|
|
58
60
|
query?: string
|
|
59
61
|
entity?: string
|
|
@@ -61,6 +63,7 @@ export interface FactStoreArgs {
|
|
|
61
63
|
fact_id?: number | number[]
|
|
62
64
|
category?: string
|
|
63
65
|
tags?: string
|
|
66
|
+
summary?: string
|
|
64
67
|
trust_delta?: number
|
|
65
68
|
min_trust?: number
|
|
66
69
|
limit?: number
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { refineQuery } from '../src/refine.js'
|
|
3
|
+
|
|
4
|
+
describe('refineQuery', () => {
|
|
5
|
+
it('filters action words from Chinese query', () => {
|
|
6
|
+
const result = refineQuery('帮我用 TypeScript 重构 auth 模块')
|
|
7
|
+
expect(result).not.toBeNull()
|
|
8
|
+
expect(result!.query).toContain('TypeScript')
|
|
9
|
+
expect(result!.query).toContain('auth')
|
|
10
|
+
expect(result!.query).not.toContain('帮我')
|
|
11
|
+
expect(result!.query).not.toContain('重构')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns null for pure operation commands', () => {
|
|
15
|
+
expect(refineQuery('运行测试')).toBeNull()
|
|
16
|
+
expect(refineQuery('git status')).toBeNull()
|
|
17
|
+
expect(refineQuery('创建文件')).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('extracts quoted Chinese entities', () => {
|
|
21
|
+
const result = refineQuery('我喜欢「深色主题」')
|
|
22
|
+
expect(result).not.toBeNull()
|
|
23
|
+
expect(result!.entityTokens).toContain('深色主题')
|
|
24
|
+
expect(result!.query).toContain('深色主题')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('extracts book title entities', () => {
|
|
28
|
+
const result = refineQuery('读了《设计模式》这本书')
|
|
29
|
+
expect(result).not.toBeNull()
|
|
30
|
+
expect(result!.entityTokens).toContain('设计模式')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('extracts capitalized English phrases', () => {
|
|
34
|
+
const result = refineQuery('使用 Visual Studio Code 编辑器')
|
|
35
|
+
expect(result).not.toBeNull()
|
|
36
|
+
expect(result!.entityTokens).toContain('Visual Studio Code')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns null for empty string', () => {
|
|
40
|
+
expect(refineQuery('')).toBeNull()
|
|
41
|
+
expect(refineQuery(' ')).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('preserves meaningful Chinese tokens', () => {
|
|
45
|
+
const result = refineQuery('用户偏好深色主题')
|
|
46
|
+
expect(result).not.toBeNull()
|
|
47
|
+
expect(result!.query).toContain('用户')
|
|
48
|
+
expect(result!.query).toContain('偏好')
|
|
49
|
+
expect(result!.query).toContain('深色')
|
|
50
|
+
expect(result!.query).toContain('主题')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { MemoryStore } from '../src/store.js'
|
|
3
|
+
import { ResourceManager } from '../src/resources.js'
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
|
|
8
|
+
let store: MemoryStore
|
|
9
|
+
let manager: ResourceManager
|
|
10
|
+
let tmpDir: string
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'mnemo-test-'))
|
|
14
|
+
store = new MemoryStore(join(tmpDir, 'test.db'))
|
|
15
|
+
manager = new ResourceManager(store)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
store.close()
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('ResourceManager', () => {
|
|
24
|
+
it('returns empty array for empty category', () => {
|
|
25
|
+
const facts = manager.getFacts('identity')
|
|
26
|
+
expect(facts).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns facts ordered by trust score', () => {
|
|
30
|
+
store.addFact('用户偏好深色主题', 'tool_pref')
|
|
31
|
+
store.addFact('用户喜欢 VS Code', 'tool_pref')
|
|
32
|
+
const facts = manager.getFacts('tool_pref')
|
|
33
|
+
expect(facts.length).toBe(2)
|
|
34
|
+
// listFacts returns trust_score DESC, first fact added gets default trust
|
|
35
|
+
expect(facts.length).toBeGreaterThan(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('caches results', () => {
|
|
39
|
+
store.addFact('测试事实', 'general')
|
|
40
|
+
manager.getFacts('general')
|
|
41
|
+
expect(manager.cacheSize()).toBe(1)
|
|
42
|
+
// Second call should hit cache
|
|
43
|
+
const facts2 = manager.getFacts('general')
|
|
44
|
+
expect(facts2.length).toBe(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('invalidates cache on write', () => {
|
|
48
|
+
store.addFact('测试事实', 'general')
|
|
49
|
+
manager.getFacts('general')
|
|
50
|
+
expect(manager.cacheSize()).toBe(1)
|
|
51
|
+
manager.invalidate()
|
|
52
|
+
expect(manager.cacheSize()).toBe(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('limits to top 10 facts', () => {
|
|
56
|
+
for (let i = 0; i < 15; i++) {
|
|
57
|
+
store.addFact(`事实 ${i}`, 'general')
|
|
58
|
+
}
|
|
59
|
+
const facts = manager.getFacts('general')
|
|
60
|
+
expect(facts.length).toBe(10)
|
|
61
|
+
})
|
|
62
|
+
})
|
package/tests/retriever.test.ts
CHANGED
|
@@ -53,3 +53,56 @@ describe('FactRetriever', () => {
|
|
|
53
53
|
expect(results.length).toBeGreaterThan(0)
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
describe('static weights (no dynamic)', () => {
|
|
58
|
+
it('uses same weights for short and long queries', () => {
|
|
59
|
+
store.addFact('用户偏好 VS Code 编辑器', 'tool_pref')
|
|
60
|
+
const shortResults = retriever.search('VS Code')
|
|
61
|
+
const longResults = retriever.search('为什么 VS Code 编辑器总是报错说找不到模块')
|
|
62
|
+
// Both should return the same fact — static weights don't change by query length
|
|
63
|
+
expect(shortResults.some(r => r.content.includes('VS Code'))).toBe(true)
|
|
64
|
+
expect(longResults.some(r => r.content.includes('VS Code'))).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('length penalty', () => {
|
|
69
|
+
it('penalizes long facts without summary', () => {
|
|
70
|
+
const longContent = '用户偏好 ' + '详细说明'.repeat(200) // ~800 chars
|
|
71
|
+
store.addFact(longContent, 'tool_pref')
|
|
72
|
+
store.addFact('用户偏好 VS Code', 'tool_pref')
|
|
73
|
+
const results = retriever.search('用户偏好')
|
|
74
|
+
// Short fact should rank higher
|
|
75
|
+
if (results.length >= 2) {
|
|
76
|
+
const shortFact = results.find(r => r.content === '用户偏好 VS Code')
|
|
77
|
+
const longFact = results.find(r => r.content.length > 500)
|
|
78
|
+
if (shortFact && longFact) {
|
|
79
|
+
expect(shortFact.score).toBeGreaterThan(longFact.score)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does not penalize long facts with short summary', () => {
|
|
85
|
+
// 长内容包含搜索词(确保 FTS5 能匹配),同时设置短 summary
|
|
86
|
+
const longContent = '用户偏好的详细内容' + '补充说明'.repeat(200)
|
|
87
|
+
const id = store.addFact(longContent, 'general')
|
|
88
|
+
store.connection.prepare('UPDATE facts SET summary = ? WHERE fact_id = ?').run('用户偏好', id)
|
|
89
|
+
// 短事实也包含搜索词,作为对比
|
|
90
|
+
store.addFact('用户偏好 VS Code', 'tool_pref')
|
|
91
|
+
const results = retriever.search('用户偏好')
|
|
92
|
+
const summaryFact = results.find(r => r.factId === id)
|
|
93
|
+
// 有 summary 的事实应该被找到(不会被 length penalty 过度惩罚)
|
|
94
|
+
expect(summaryFact).toBeTruthy()
|
|
95
|
+
// summary 事实的 score 应该合理(不会被 content 长度拖累)
|
|
96
|
+
if (summaryFact) {
|
|
97
|
+
expect(summaryFact.score).toBeGreaterThan(0)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('no relevance gate', () => {
|
|
103
|
+
it('returns results even with low scores', () => {
|
|
104
|
+
store.addFact('完全不相关关于天气', 'general')
|
|
105
|
+
const results = retriever.search('天气')
|
|
106
|
+
expect(results.length).toBeGreaterThanOrEqual(1)
|
|
107
|
+
})
|
|
108
|
+
})
|
package/tests/store.test.ts
CHANGED
|
@@ -102,3 +102,115 @@ describe('MemoryStore', () => {
|
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
describe('schema migration', () => {
|
|
107
|
+
it('has summary column on facts table', () => {
|
|
108
|
+
const cols = store.connection.pragma('table_info(facts)') as Array<{ name: string }>
|
|
109
|
+
const colNames = cols.map(c => c.name)
|
|
110
|
+
expect(colNames).toContain('summary')
|
|
111
|
+
expect(colNames).toContain('last_retrieved_at')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('has retrieval_log table', () => {
|
|
115
|
+
const tables = store.connection.prepare(
|
|
116
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='retrieval_log'"
|
|
117
|
+
).get()
|
|
118
|
+
expect(tables).toBeTruthy()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('summary defaults to null', () => {
|
|
122
|
+
const id = store.addFact('test summary fact', 'general')
|
|
123
|
+
const row = store.connection.prepare('SELECT summary FROM facts WHERE fact_id = ?').get(id) as { summary: string | null }
|
|
124
|
+
expect(row.summary).toBeNull()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('logRetrieval', () => {
|
|
129
|
+
it('writes retrieval log with results', () => {
|
|
130
|
+
const id = store.addFact('test fact for logging', 'general')
|
|
131
|
+
store.logRetrieval('test query', [{ id, score: 0.8 }])
|
|
132
|
+
const rows = store.connection.prepare('SELECT * FROM retrieval_log').all() as Array<any>
|
|
133
|
+
expect(rows.length).toBe(1)
|
|
134
|
+
expect(rows[0].query).toBe('test query')
|
|
135
|
+
expect(JSON.parse(rows[0].results)).toEqual([{ id, score: 0.8 }])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('updates last_retrieved_at for returned facts', () => {
|
|
139
|
+
const id = store.addFact('test fact for timestamp', 'general')
|
|
140
|
+
store.logRetrieval('query', [{ id, score: 0.5 }])
|
|
141
|
+
const row = store.connection.prepare('SELECT last_retrieved_at FROM facts WHERE fact_id = ?').get(id) as any
|
|
142
|
+
expect(row.last_retrieved_at).not.toBeNull()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('prunes log to max entries', () => {
|
|
146
|
+
for (let i = 0; i < 12; i++) {
|
|
147
|
+
store.logRetrieval(`query ${i}`, [])
|
|
148
|
+
}
|
|
149
|
+
store.pruneRetrievalLog(10)
|
|
150
|
+
const count = (store.connection.prepare('SELECT COUNT(*) as c FROM retrieval_log').get() as any).c
|
|
151
|
+
expect(count).toBe(10)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('runLearning', () => {
|
|
156
|
+
it('demotes high retrieval low helpful facts', () => {
|
|
157
|
+
const id = store.addFact('demote me', 'general')
|
|
158
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 100, helpful_count = 2, trust_score = 1.0 WHERE fact_id = ?').run(id)
|
|
159
|
+
const result = store.runLearning()
|
|
160
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
161
|
+
expect(row.trust_score).toBeLessThan(1.0)
|
|
162
|
+
expect(result.demoted).toBeGreaterThanOrEqual(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('promotes high helpful rate facts', () => {
|
|
166
|
+
const id = store.addFact('promote me', 'general')
|
|
167
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 50, helpful_count = 20, trust_score = 0.5 WHERE fact_id = ?').run(id)
|
|
168
|
+
store.runLearning()
|
|
169
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
170
|
+
expect(row.trust_score).toBeGreaterThan(0.5)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('does not adjust facts with low retrieval count', () => {
|
|
174
|
+
const id = store.addFact('new fact', 'general')
|
|
175
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 5, helpful_count = 0, trust_score = 0.8 WHERE fact_id = ?').run(id)
|
|
176
|
+
store.runLearning()
|
|
177
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
178
|
+
expect(row.trust_score).toBe(0.8)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('ages facts not retrieved for 60 days', () => {
|
|
182
|
+
const id = store.addFact('old fact', 'general')
|
|
183
|
+
store.connection.prepare("UPDATE facts SET last_retrieved_at = datetime('now', '-61 days'), trust_score = 0.8 WHERE fact_id = ?").run(id)
|
|
184
|
+
store.runLearning()
|
|
185
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
186
|
+
expect(row.trust_score).toBeLessThan(0.8)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('protects new facts with null last_retrieved_at from aging', () => {
|
|
190
|
+
const id = store.addFact('brand new fact', 'general')
|
|
191
|
+
store.connection.prepare('UPDATE facts SET trust_score = 0.5, last_retrieved_at = NULL WHERE fact_id = ?').run(id)
|
|
192
|
+
store.runLearning()
|
|
193
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
194
|
+
expect(row.trust_score).toBe(0.5)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('returns long_facts report', () => {
|
|
198
|
+
const id = store.addFact('x'.repeat(600), 'general')
|
|
199
|
+
const result = store.runLearning()
|
|
200
|
+
expect(result.long_facts.length).toBeGreaterThanOrEqual(1)
|
|
201
|
+
expect(result.long_facts.some((f: any) => f.id === id)).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('runAudit', () => {
|
|
206
|
+
it('returns quality report without modifying data', () => {
|
|
207
|
+
const id = store.addFact('a'.repeat(600), 'general')
|
|
208
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 100, helpful_count = 1 WHERE fact_id = ?').run(id)
|
|
209
|
+
const before = (store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any).trust_score
|
|
210
|
+
const report = store.runAudit()
|
|
211
|
+
const after = (store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any).trust_score
|
|
212
|
+
expect(before).toBe(after)
|
|
213
|
+
expect(report.total_facts).toBeGreaterThanOrEqual(1)
|
|
214
|
+
expect(report.long_without_summary.length).toBeGreaterThanOrEqual(1)
|
|
215
|
+
})
|
|
216
|
+
})
|