@exaudeus/memory-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/__tests__/clock-and-validators.test.d.ts +1 -0
- package/dist/__tests__/clock-and-validators.test.js +237 -0
- package/dist/__tests__/config-manager.test.d.ts +1 -0
- package/dist/__tests__/config-manager.test.js +142 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +236 -0
- package/dist/__tests__/crash-journal.test.d.ts +1 -0
- package/dist/__tests__/crash-journal.test.js +203 -0
- package/dist/__tests__/e2e.test.d.ts +1 -0
- package/dist/__tests__/e2e.test.js +788 -0
- package/dist/__tests__/ephemeral-benchmark.test.d.ts +1 -0
- package/dist/__tests__/ephemeral-benchmark.test.js +651 -0
- package/dist/__tests__/ephemeral.test.d.ts +1 -0
- package/dist/__tests__/ephemeral.test.js +435 -0
- package/dist/__tests__/git-service.test.d.ts +1 -0
- package/dist/__tests__/git-service.test.js +43 -0
- package/dist/__tests__/normalize.test.d.ts +1 -0
- package/dist/__tests__/normalize.test.js +161 -0
- package/dist/__tests__/store.test.d.ts +1 -0
- package/dist/__tests__/store.test.js +1153 -0
- package/dist/config-manager.d.ts +49 -0
- package/dist/config-manager.js +126 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +162 -0
- package/dist/crash-journal.d.ts +38 -0
- package/dist/crash-journal.js +198 -0
- package/dist/ephemeral-weights.json +1847 -0
- package/dist/ephemeral.d.ts +20 -0
- package/dist/ephemeral.js +516 -0
- package/dist/formatters.d.ts +10 -0
- package/dist/formatters.js +92 -0
- package/dist/git-service.d.ts +5 -0
- package/dist/git-service.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1197 -0
- package/dist/normalize.d.ts +2 -0
- package/dist/normalize.js +69 -0
- package/dist/store.d.ts +84 -0
- package/dist/store.js +813 -0
- package/dist/text-analyzer.d.ts +32 -0
- package/dist/text-analyzer.js +190 -0
- package/dist/thresholds.d.ts +39 -0
- package/dist/thresholds.js +75 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +33 -0
- package/package.json +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// Tests for ephemeral content detection — pure function tests.
|
|
2
|
+
// Each signal is tested independently with positive and negative cases.
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { detectEphemeralSignals, formatEphemeralWarning, classifyEphemeral } from '../ephemeral.js';
|
|
6
|
+
describe('ephemeral detection', () => {
|
|
7
|
+
// ── Temporal language ──────────────────────────────────────────────────
|
|
8
|
+
describe('temporal language signal', () => {
|
|
9
|
+
it('detects "currently" in content', () => {
|
|
10
|
+
const signals = detectEphemeralSignals('Build System', 'The build is currently broken due to a Gradle sync issue', 'gotchas');
|
|
11
|
+
const temporal = signals.find(s => s.id === 'temporal');
|
|
12
|
+
assert.ok(temporal, 'Should detect temporal language');
|
|
13
|
+
assert.ok(temporal.detail.includes('currently'));
|
|
14
|
+
});
|
|
15
|
+
it('detects "right now" in content', () => {
|
|
16
|
+
const signals = detectEphemeralSignals('Server Status', 'The dev server is not responding right now', 'architecture');
|
|
17
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
18
|
+
});
|
|
19
|
+
it('detects "today" in content', () => {
|
|
20
|
+
const signals = detectEphemeralSignals('Deployment', 'We deployed a hotfix today that changes the API', 'conventions');
|
|
21
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
22
|
+
});
|
|
23
|
+
it('does not flag durable content without temporal words', () => {
|
|
24
|
+
const signals = detectEphemeralSignals('MVI Architecture', 'The messaging feature uses MVI with standalone reducer classes and sealed interface events', 'architecture');
|
|
25
|
+
assert.ok(!signals.some(s => s.id === 'temporal'));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
// ── Fixed/resolved bugs ────────────────────────────────────────────────
|
|
29
|
+
describe('fixed-bug signal', () => {
|
|
30
|
+
it('detects "bug fixed" pattern', () => {
|
|
31
|
+
const signals = detectEphemeralSignals('Messaging Crash', 'The crash bug in the messaging reducer was fixed in the latest release', 'gotchas');
|
|
32
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
33
|
+
});
|
|
34
|
+
it('detects "issue resolved" pattern', () => {
|
|
35
|
+
const signals = detectEphemeralSignals('Build Issue', 'The Gradle sync issue has been resolved by updating the plugin version', 'gotchas');
|
|
36
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
37
|
+
});
|
|
38
|
+
it('detects "was broken" pattern', () => {
|
|
39
|
+
const signals = detectEphemeralSignals('CI Pipeline', 'The CI pipeline was broken but is working again after config fix', 'conventions');
|
|
40
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
41
|
+
});
|
|
42
|
+
it('detects "no longer fails" pattern', () => {
|
|
43
|
+
const signals = detectEphemeralSignals('Test Suite', 'The flaky test no longer fails after we added proper coroutine scope handling', 'gotchas');
|
|
44
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
45
|
+
});
|
|
46
|
+
it('detects "workaround no longer needed"', () => {
|
|
47
|
+
const signals = detectEphemeralSignals('Clean Build Workaround', 'The workaround of clean building after Tuist changes is no longer needed', 'gotchas');
|
|
48
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
49
|
+
});
|
|
50
|
+
it('does not flag active bugs or gotchas', () => {
|
|
51
|
+
const signals = detectEphemeralSignals('Build Cache Bug', 'Must clean build after Tuist changes or the build will fail with stale caches', 'gotchas');
|
|
52
|
+
assert.ok(!signals.some(s => s.id === 'fixed-bug'));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// ── Task/TODO language ─────────────────────────────────────────────────
|
|
56
|
+
describe('task-language signal', () => {
|
|
57
|
+
it('detects "need to" language', () => {
|
|
58
|
+
const signals = detectEphemeralSignals('Refactoring Plan', 'We need to refactor the messaging reducer to use sealed interfaces', 'architecture');
|
|
59
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
60
|
+
});
|
|
61
|
+
it('detects "TODO" in content', () => {
|
|
62
|
+
const signals = detectEphemeralSignals('Missing Tests', 'TODO: add unit tests for the new messaging flow reducer', 'conventions');
|
|
63
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
64
|
+
});
|
|
65
|
+
it('does not flag patterns or conventions', () => {
|
|
66
|
+
const signals = detectEphemeralSignals('Reducer Pattern', 'Reducers should implement the sealed interface pattern for exhaustive event handling', 'conventions');
|
|
67
|
+
assert.ok(!signals.some(s => s.id === 'task-language'));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ── Stack traces ───────────────────────────────────────────────────────
|
|
71
|
+
describe('stack-trace signal', () => {
|
|
72
|
+
it('detects Java/Kotlin stack traces', () => {
|
|
73
|
+
const signals = detectEphemeralSignals('NPE in Messaging', 'NullPointerException in messaging flow:\n at com.zillow.messaging.MessagingReducer.reduce(MessagingReducer.kt:42)\n at com.zillow.core.BaseViewModel.dispatch(BaseViewModel.kt:15)', 'gotchas');
|
|
74
|
+
assert.ok(signals.some(s => s.id === 'stack-trace'));
|
|
75
|
+
});
|
|
76
|
+
it('detects Python tracebacks', () => {
|
|
77
|
+
const signals = detectEphemeralSignals('Script Error', 'Traceback (most recent call last)\n File "build.py", line 42\n raise ValueError("bad config")', 'gotchas');
|
|
78
|
+
assert.ok(signals.some(s => s.id === 'stack-trace'));
|
|
79
|
+
});
|
|
80
|
+
it('detects Node.js error stacks', () => {
|
|
81
|
+
const signals = detectEphemeralSignals('Server Crash', 'Error: ENOENT: no such file or directory\n at Object.openSync (node:fs:600:3)', 'gotchas');
|
|
82
|
+
assert.ok(signals.some(s => s.id === 'stack-trace'));
|
|
83
|
+
});
|
|
84
|
+
it('does not flag normal code discussion', () => {
|
|
85
|
+
const signals = detectEphemeralSignals('File Structure', 'The messaging module lives at features/messaging/impl/ with the reducer at MessagingReducer.kt', 'architecture');
|
|
86
|
+
assert.ok(!signals.some(s => s.id === 'stack-trace'));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// ── Environment-specific values ────────────────────────────────────────
|
|
90
|
+
describe('environment-specific signal', () => {
|
|
91
|
+
it('detects multiple env-specific values (localhost + path)', () => {
|
|
92
|
+
const signals = detectEphemeralSignals('Dev Setup', 'Run the server at localhost:8080 and check logs at /users/etienne/logs/server.log with PID tracking', 'conventions');
|
|
93
|
+
assert.ok(signals.some(s => s.id === 'environment-specific'));
|
|
94
|
+
});
|
|
95
|
+
it('does not flag content with just one env-specific value', () => {
|
|
96
|
+
const signals = detectEphemeralSignals('API Endpoint', 'The staging API runs on port 8080 for local development', 'architecture');
|
|
97
|
+
// Single env value is not enough to trigger (threshold is 2)
|
|
98
|
+
assert.ok(!signals.some(s => s.id === 'environment-specific'));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// ── Verbatim code ──────────────────────────────────────────────────────
|
|
102
|
+
describe('verbatim-code signal', () => {
|
|
103
|
+
it('detects high code-character density', () => {
|
|
104
|
+
const codeBlock = 'fun reduce(state: State, event: Event): State { return when(event) { is Event.Load -> state.copy(loading = true); is Event.Success -> state.copy(loading = false, data = event.data); is Event.Error -> state.copy(loading = false, error = event.error); } }';
|
|
105
|
+
const signals = detectEphemeralSignals('Reducer Implementation', codeBlock, 'architecture');
|
|
106
|
+
assert.ok(signals.some(s => s.id === 'verbatim-code'));
|
|
107
|
+
});
|
|
108
|
+
it('detects fenced code blocks', () => {
|
|
109
|
+
const signals = detectEphemeralSignals('Example Code', 'Here is the reducer:\n```kotlin\nfun reduce(state: State, event: Event): State {\n return state\n}\n```\nThis is the pattern we follow for all features in the messaging module area.', 'conventions');
|
|
110
|
+
assert.ok(signals.some(s => s.id === 'verbatim-code'));
|
|
111
|
+
});
|
|
112
|
+
it('does not flag short prose content', () => {
|
|
113
|
+
const signals = detectEphemeralSignals('Reducer Pattern', 'Use standalone reducer classes with sealed interface events. Keep the reduce function pure.', 'conventions');
|
|
114
|
+
assert.ok(!signals.some(s => s.id === 'verbatim-code'));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ── Investigation language ─────────────────────────────────────────────
|
|
118
|
+
describe('investigation signal', () => {
|
|
119
|
+
it('detects "investigating" language', () => {
|
|
120
|
+
const signals = detectEphemeralSignals('Memory Leak', 'Investigating a potential memory leak in the messaging flow when switching tabs rapidly', 'gotchas');
|
|
121
|
+
assert.ok(signals.some(s => s.id === 'investigation'));
|
|
122
|
+
});
|
|
123
|
+
it('detects "trying to figure out" language', () => {
|
|
124
|
+
const signals = detectEphemeralSignals('Build Issue', 'Still trying to figure out why the Gradle cache invalidates on every clean build', 'gotchas');
|
|
125
|
+
assert.ok(signals.some(s => s.id === 'investigation'));
|
|
126
|
+
});
|
|
127
|
+
it('does not flag concluded findings', () => {
|
|
128
|
+
const signals = detectEphemeralSignals('Cache Invalidation', 'Gradle cache invalidates when the buildSrc directory changes. This is by design.', 'gotchas');
|
|
129
|
+
assert.ok(!signals.some(s => s.id === 'investigation'));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// ── Uncertainty / speculation ────────────────────────────────────────────
|
|
133
|
+
describe('uncertainty signal', () => {
|
|
134
|
+
it('detects "I think" language', () => {
|
|
135
|
+
const signals = detectEphemeralSignals('Possible Cause', 'I think the crash is caused by a race condition in the messaging flow coroutine scope', 'gotchas');
|
|
136
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
137
|
+
});
|
|
138
|
+
it('detects "maybe" language', () => {
|
|
139
|
+
const signals = detectEphemeralSignals('Architecture Guess', 'Maybe the best approach is to use a shared ViewModel for the messaging tabs', 'architecture');
|
|
140
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
141
|
+
});
|
|
142
|
+
it('detects "not sure" language', () => {
|
|
143
|
+
const signals = detectEphemeralSignals('DI Setup', 'Not sure if the Anvil scope should be AppScope or ActivityScope for this binding', 'conventions');
|
|
144
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
145
|
+
});
|
|
146
|
+
it('detects "might be because" language', () => {
|
|
147
|
+
const signals = detectEphemeralSignals('Flaky Test', 'The test failure might be because of the shared mutable state in the test fixtures', 'gotchas');
|
|
148
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
149
|
+
});
|
|
150
|
+
it('detects "seems like" language', () => {
|
|
151
|
+
const signals = detectEphemeralSignals('Memory Usage', 'It seems like the image cache grows unbounded after navigating between listings', 'gotchas');
|
|
152
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
153
|
+
});
|
|
154
|
+
it('does not flag definitive statements', () => {
|
|
155
|
+
const signals = detectEphemeralSignals('Coroutine Scope Rule', 'ViewModels must use viewModelScope for coroutine launching. Using GlobalScope causes memory leaks.', 'conventions');
|
|
156
|
+
assert.ok(!signals.some(s => s.id === 'uncertainty'));
|
|
157
|
+
});
|
|
158
|
+
it('does not flag factual architecture descriptions', () => {
|
|
159
|
+
const signals = detectEphemeralSignals('Event Handling', 'The reducer processes events through a sealed interface hierarchy. Each event maps to exactly one state transition.', 'architecture');
|
|
160
|
+
assert.ok(!signals.some(s => s.id === 'uncertainty'));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
// ── Self-correction / retraction ───────────────────────────────────────
|
|
164
|
+
describe('self-correction signal', () => {
|
|
165
|
+
it('detects "actually wait"', () => {
|
|
166
|
+
const signals = detectEphemeralSignals('Retraction', 'Actually wait, the leak is in the bitmap pool not the view cache as previously analyzed', 'gotchas');
|
|
167
|
+
assert.ok(signals.some(s => s.id === 'self-correction'));
|
|
168
|
+
});
|
|
169
|
+
it('detects "scratch that"', () => {
|
|
170
|
+
const signals = detectEphemeralSignals('Correction', "Scratch that, the timeout is on the server side not the client. The server closes idle connections.", 'gotchas');
|
|
171
|
+
assert.ok(signals.some(s => s.id === 'self-correction'));
|
|
172
|
+
});
|
|
173
|
+
it('detects "on second thought"', () => {
|
|
174
|
+
const signals = detectEphemeralSignals('Changed Mind', 'On second thought using a shared ViewModel would create tight coupling between tabs', 'architecture');
|
|
175
|
+
assert.ok(signals.some(s => s.id === 'self-correction'));
|
|
176
|
+
});
|
|
177
|
+
it('detects "I was wrong"', () => {
|
|
178
|
+
const signals = detectEphemeralSignals('Mistake', 'I was wrong about the ProGuard rule, the keep annotation does not cascade to nested classes', 'gotchas');
|
|
179
|
+
assert.ok(signals.some(s => s.id === 'self-correction'));
|
|
180
|
+
});
|
|
181
|
+
it('does not flag normal corrections/amendments', () => {
|
|
182
|
+
const signals = detectEphemeralSignals('ProGuard Convention', 'Add keep rules for all Kotlin serialization classes. The compiler plugin does not generate these automatically.', 'conventions');
|
|
183
|
+
assert.ok(!signals.some(s => s.id === 'self-correction'));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ── Meeting / conversation references ─────────────────────────────────
|
|
187
|
+
describe('meeting-reference signal', () => {
|
|
188
|
+
it('detects "as discussed in the meeting"', () => {
|
|
189
|
+
const signals = detectEphemeralSignals('Meeting Decision', 'As discussed in the meeting we are deprecating the REST endpoint and moving to GraphQL', 'architecture');
|
|
190
|
+
assert.ok(signals.some(s => s.id === 'meeting-reference'));
|
|
191
|
+
});
|
|
192
|
+
it('detects "per our discussion"', () => {
|
|
193
|
+
const signals = detectEphemeralSignals('Team Decision', 'Per our discussion the error handling will use sealed interfaces at all module boundaries', 'conventions');
|
|
194
|
+
assert.ok(signals.some(s => s.id === 'meeting-reference'));
|
|
195
|
+
});
|
|
196
|
+
it('detects "someone mentioned"', () => {
|
|
197
|
+
const signals = detectEphemeralSignals('Info from John', 'John mentioned that the backend will rate-limit the notification endpoint to 100 rpm per user', 'architecture');
|
|
198
|
+
assert.ok(signals.some(s => s.id === 'meeting-reference'));
|
|
199
|
+
});
|
|
200
|
+
it('does not flag ADR references', () => {
|
|
201
|
+
const signals = detectEphemeralSignals('ADR Reference', 'The decision to use sealed interfaces was documented in ADR-0023. The key argument is composability.', 'architecture');
|
|
202
|
+
assert.ok(!signals.some(s => s.id === 'meeting-reference'));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
// ── New pattern additions to existing signals ─────────────────────────
|
|
206
|
+
describe('new temporal patterns', () => {
|
|
207
|
+
it('detects "just tried"', () => {
|
|
208
|
+
const signals = detectEphemeralSignals('Attempt', 'Just tried the new Gradle wrapper and it still fails', 'gotchas');
|
|
209
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
210
|
+
});
|
|
211
|
+
it('detects "in this session"', () => {
|
|
212
|
+
const signals = detectEphemeralSignals('Session', 'In this session we found the image cache grows without bound', 'gotchas');
|
|
213
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
214
|
+
});
|
|
215
|
+
it('detects "as things stand"', () => {
|
|
216
|
+
const signals = detectEphemeralSignals('State', 'As things stand we have three auth flows coexisting', 'architecture');
|
|
217
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
218
|
+
});
|
|
219
|
+
it('detects "still pending"', () => {
|
|
220
|
+
const signals = detectEphemeralSignals('Status', 'Deployment is still pending approval from security', 'gotchas');
|
|
221
|
+
assert.ok(signals.some(s => s.id === 'temporal'));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('new uncertainty patterns', () => {
|
|
225
|
+
it('detects "as far as I know"', () => {
|
|
226
|
+
const signals = detectEphemeralSignals('Caveat', 'As far as I know the migration handles this automatically', 'gotchas');
|
|
227
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
228
|
+
});
|
|
229
|
+
it('detects "take this with a grain of salt"', () => {
|
|
230
|
+
const signals = detectEphemeralSignals('Caveat', 'Take this with a grain of salt but the profiling suggests main thread blocking', 'gotchas');
|
|
231
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
232
|
+
});
|
|
233
|
+
it('detects "TBD"', () => {
|
|
234
|
+
const signals = detectEphemeralSignals('Pending', 'The caching strategy is TBD, might go with disk caching or server push', 'architecture');
|
|
235
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
236
|
+
});
|
|
237
|
+
it('detects "subject to change"', () => {
|
|
238
|
+
const signals = detectEphemeralSignals('API', 'The response schema is subject to change, backend is still iterating', 'architecture');
|
|
239
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
240
|
+
});
|
|
241
|
+
it('detects "I could be wrong"', () => {
|
|
242
|
+
const signals = detectEphemeralSignals('Guess', 'I could be wrong but the interceptor retries automatically after token refresh', 'gotchas');
|
|
243
|
+
assert.ok(signals.some(s => s.id === 'uncertainty'));
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('new fixed-bug patterns', () => {
|
|
247
|
+
it('detects "works now"', () => {
|
|
248
|
+
const signals = detectEphemeralSignals('Fix', 'The reconnection works now after adding exponential backoff', 'gotchas');
|
|
249
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
250
|
+
});
|
|
251
|
+
it('detects "fixes #NNN"', () => {
|
|
252
|
+
const signals = detectEphemeralSignals('Fix', 'This fixes #4521 where the login showed a blank state on token expiry', 'gotchas');
|
|
253
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
254
|
+
});
|
|
255
|
+
it('detects "turns out it was"', () => {
|
|
256
|
+
const signals = detectEphemeralSignals('Root Cause', 'Turns out it was a threading issue with the database migration', 'gotchas');
|
|
257
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
258
|
+
});
|
|
259
|
+
it('detects "false alarm"', () => {
|
|
260
|
+
const signals = detectEphemeralSignals('Not A Bug', 'The slowdown was a false alarm, it was profiler overhead', 'gotchas');
|
|
261
|
+
assert.ok(signals.some(s => s.id === 'fixed-bug'));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('new investigation patterns', () => {
|
|
265
|
+
it('detects "can\'t reproduce"', () => {
|
|
266
|
+
const signals = detectEphemeralSignals('Repro', "Can't reproduce the crash on any test device", 'gotchas');
|
|
267
|
+
assert.ok(signals.some(s => s.id === 'investigation'));
|
|
268
|
+
});
|
|
269
|
+
it('detects "getting an error"', () => {
|
|
270
|
+
const signals = detectEphemeralSignals('Error', 'Getting a SocketTimeoutException every few minutes on staging', 'gotchas');
|
|
271
|
+
assert.ok(signals.some(s => s.id === 'investigation'));
|
|
272
|
+
});
|
|
273
|
+
it('detects "added logging"', () => {
|
|
274
|
+
const signals = detectEphemeralSignals('Debug', 'Added logging to the auth flow to understand the logout issue', 'gotchas');
|
|
275
|
+
assert.ok(signals.some(s => s.id === 'investigation'));
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
describe('new task-language patterns', () => {
|
|
279
|
+
it('detects "WIP"', () => {
|
|
280
|
+
const signals = detectEphemeralSignals('Feature', 'WIP implementation of notification grouping, missing DND support', 'architecture');
|
|
281
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
282
|
+
});
|
|
283
|
+
it('detects "FIXME"', () => {
|
|
284
|
+
const signals = detectEphemeralSignals('Code Issue', 'FIXME the error handling silently swallows network exceptions', 'gotchas');
|
|
285
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
286
|
+
});
|
|
287
|
+
it('detects "partial implementation"', () => {
|
|
288
|
+
const signals = detectEphemeralSignals('Incomplete', 'The search module has a partial implementation of autocomplete', 'architecture');
|
|
289
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
290
|
+
});
|
|
291
|
+
it('detects "doesn\'t support yet"', () => {
|
|
292
|
+
const signals = detectEphemeralSignals('Gap', "The analytics tracker doesn't support custom dimensions yet", 'gotchas');
|
|
293
|
+
assert.ok(signals.some(s => s.id === 'task-language'));
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
// ── Very short content ─────────────────────────────────────────────────
|
|
297
|
+
describe('too-short signal', () => {
|
|
298
|
+
it('detects very short content', () => {
|
|
299
|
+
const signals = detectEphemeralSignals('Note', 'Use sealed classes', 'conventions');
|
|
300
|
+
assert.ok(signals.some(s => s.id === 'too-short'));
|
|
301
|
+
});
|
|
302
|
+
it('does not flag content above threshold', () => {
|
|
303
|
+
const signals = detectEphemeralSignals('Sealed Class Pattern', 'Use sealed classes and interfaces for all event types in the reducer pattern', 'conventions');
|
|
304
|
+
assert.ok(!signals.some(s => s.id === 'too-short'));
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
// ── recent-work topic bypass ───────────────────────────────────────────
|
|
308
|
+
describe('recent-work bypass', () => {
|
|
309
|
+
it('skips all detection for recent-work topic', () => {
|
|
310
|
+
const signals = detectEphemeralSignals('Current Investigation', 'Currently debugging a crash that just happened today in the messaging reducer', 'recent-work');
|
|
311
|
+
// recent-work is handled by the store caller (topic !== 'recent-work'),
|
|
312
|
+
// but the detection function itself should still return signals for
|
|
313
|
+
// any topic — the caller decides to skip. So this tests the function directly.
|
|
314
|
+
// The store.ts code guards this: `topic !== 'recent-work' ? detect... : []`
|
|
315
|
+
// We'll verify the store-level bypass in store.test.ts
|
|
316
|
+
assert.ok(signals.length > 0, 'Detection function itself still fires for recent-work');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
// ── Multiple signals ───────────────────────────────────────────────────
|
|
320
|
+
describe('multiple signals', () => {
|
|
321
|
+
it('detects multiple signals in one entry', () => {
|
|
322
|
+
const signals = detectEphemeralSignals('Current Debug Session', 'Currently investigating a crash that was broken in the latest build:\n at com.zillow.messaging.Reducer.reduce(Reducer.kt:42)', 'gotchas');
|
|
323
|
+
assert.ok(signals.length >= 2, `Expected at least 2 signals, got ${signals.length}: ${signals.map(s => s.id).join(', ')}`);
|
|
324
|
+
const ids = signals.map(s => s.id);
|
|
325
|
+
assert.ok(ids.includes('temporal'), 'Should detect temporal');
|
|
326
|
+
assert.ok(ids.includes('stack-trace') || ids.includes('fixed-bug') || ids.includes('investigation'), 'Should detect at least one other signal');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
// ── formatEphemeralWarning ─────────────────────────────────────────────
|
|
330
|
+
describe('formatEphemeralWarning', () => {
|
|
331
|
+
it('returns undefined for empty signals', () => {
|
|
332
|
+
assert.strictEqual(formatEphemeralWarning([]), undefined);
|
|
333
|
+
});
|
|
334
|
+
it('formats a single high-confidence signal', () => {
|
|
335
|
+
const result = formatEphemeralWarning([
|
|
336
|
+
{ id: 'temporal', label: 'Temporal language', detail: 'contains "currently"', confidence: 'high' },
|
|
337
|
+
]);
|
|
338
|
+
assert.ok(result);
|
|
339
|
+
assert.ok(result.includes('possibly contains'), 'Single high = "possibly contains"');
|
|
340
|
+
assert.ok(result.includes('Temporal language'));
|
|
341
|
+
assert.ok(result.includes('currently'));
|
|
342
|
+
});
|
|
343
|
+
it('formats multiple high-confidence signals as "likely"', () => {
|
|
344
|
+
const result = formatEphemeralWarning([
|
|
345
|
+
{ id: 'temporal', label: 'Temporal language', detail: 'contains "right now"', confidence: 'high' },
|
|
346
|
+
{ id: 'stack-trace', label: 'Stack trace', detail: 'contains stack trace', confidence: 'high' },
|
|
347
|
+
]);
|
|
348
|
+
assert.ok(result);
|
|
349
|
+
assert.ok(result.includes('likely contains'), 'Two high = "likely contains"');
|
|
350
|
+
});
|
|
351
|
+
it('formats medium-only signals as "may contain"', () => {
|
|
352
|
+
const result = formatEphemeralWarning([
|
|
353
|
+
{ id: 'task-language', label: 'Task language', detail: 'contains "need to"', confidence: 'medium' },
|
|
354
|
+
]);
|
|
355
|
+
assert.ok(result);
|
|
356
|
+
assert.ok(result.includes('may contain'), 'Medium only = "may contain"');
|
|
357
|
+
});
|
|
358
|
+
it('includes actionable guidance scaled to confidence', () => {
|
|
359
|
+
// Single high-confidence: moderate advice
|
|
360
|
+
const singleHigh = formatEphemeralWarning([
|
|
361
|
+
{ id: 'temporal', label: 'Temporal language', detail: 'contains "today"', confidence: 'high' },
|
|
362
|
+
]);
|
|
363
|
+
assert.ok(singleHigh);
|
|
364
|
+
assert.ok(singleHigh.includes('lasting insight'), 'Single high should suggest keeping if lasting');
|
|
365
|
+
// Two high-confidence: strong advice
|
|
366
|
+
const twoHigh = formatEphemeralWarning([
|
|
367
|
+
{ id: 'temporal', label: 'Temporal', detail: 'contains "today"', confidence: 'high' },
|
|
368
|
+
{ id: 'stack-trace', label: 'Stack trace', detail: 'stack trace detected', confidence: 'high' },
|
|
369
|
+
]);
|
|
370
|
+
assert.ok(twoHigh);
|
|
371
|
+
assert.ok(twoHigh.includes('almost certainly session-specific'), 'Two high should strongly advise deletion');
|
|
372
|
+
// Medium-only: soft advice
|
|
373
|
+
const mediumOnly = formatEphemeralWarning([
|
|
374
|
+
{ id: 'uncertainty', label: 'Uncertain', detail: 'contains "maybe"', confidence: 'medium' },
|
|
375
|
+
]);
|
|
376
|
+
assert.ok(mediumOnly);
|
|
377
|
+
assert.ok(mediumOnly.includes('use your judgment'), 'Medium-only should defer to agent judgment');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
// ── Durable content produces no signals ────────────────────────────────
|
|
381
|
+
describe('durable content (negative cases)', () => {
|
|
382
|
+
it('produces no signals for a well-formed architecture entry', () => {
|
|
383
|
+
const signals = detectEphemeralSignals('MVI Architecture Pattern', 'The messaging feature uses MVI with standalone reducer classes. Events are modeled as sealed interfaces for exhaustive handling. ViewModels act as orchestrators, never containing business logic.', 'architecture');
|
|
384
|
+
assert.strictEqual(signals.length, 0, `Expected no signals, got: ${signals.map(s => s.id).join(', ')}`);
|
|
385
|
+
});
|
|
386
|
+
it('produces no signals for a well-formed gotcha', () => {
|
|
387
|
+
const signals = detectEphemeralSignals('Build Cache After Tuist Changes', 'Must clean build after Tuist changes or the build will fail with stale generated files. Run `tuist clean` then rebuild.', 'gotchas');
|
|
388
|
+
assert.strictEqual(signals.length, 0, `Expected no signals, got: ${signals.map(s => s.id).join(', ')}`);
|
|
389
|
+
});
|
|
390
|
+
it('produces no signals for a well-formed convention', () => {
|
|
391
|
+
const signals = detectEphemeralSignals('Dependency Injection Pattern', 'Use Anvil with @ContributesBinding(AppScope::class) for all production bindings. Constructor injection only, no field injection.', 'conventions');
|
|
392
|
+
assert.strictEqual(signals.length, 0, `Expected no signals, got: ${signals.map(s => s.id).join(', ')}`);
|
|
393
|
+
});
|
|
394
|
+
it('produces no signals for user identity', () => {
|
|
395
|
+
const signals = detectEphemeralSignals('Identity', 'Etienne, Senior Android Engineer at Zillow. Primary focus on messaging feature porting.', 'user');
|
|
396
|
+
assert.strictEqual(signals.length, 0, `Expected no signals, got: ${signals.map(s => s.id).join(', ')}`);
|
|
397
|
+
});
|
|
398
|
+
it('produces no signals for preferences', () => {
|
|
399
|
+
const signals = detectEphemeralSignals('Code Style Preferences', 'Prefer sealed interfaces over sealed classes. Always use immutable data classes for state. Avoid MutableStateFlow.', 'preferences');
|
|
400
|
+
assert.strictEqual(signals.length, 0, `Expected no signals, got: ${signals.map(s => s.id).join(', ')}`);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
// ── TF-IDF classifier ──────────────────────────────────────────────────
|
|
404
|
+
describe('TF-IDF classifier', () => {
|
|
405
|
+
it('loads the model and returns a score', () => {
|
|
406
|
+
const score = classifyEphemeral('Test Title', 'Some content about testing things');
|
|
407
|
+
assert.ok(score !== null, 'Model should load successfully');
|
|
408
|
+
assert.ok(score >= 0 && score <= 1, `Score should be between 0 and 1, got ${score}`);
|
|
409
|
+
});
|
|
410
|
+
it('scores ephemeral content higher than durable content', () => {
|
|
411
|
+
const ephScore = classifyEphemeral('Database Connection Pool Issue', 'The connection pool is hitting 95% utilization during peak hours. We patched the configuration but need to monitor it.');
|
|
412
|
+
const durScore = classifyEphemeral('Database Connection Pooling Convention', 'All database connections must use connection pooling with a maximum of 20 connections per service instance.');
|
|
413
|
+
assert.ok(ephScore !== null && durScore !== null);
|
|
414
|
+
assert.ok(ephScore > durScore, `Ephemeral score (${ephScore}) should be higher than durable (${durScore})`);
|
|
415
|
+
});
|
|
416
|
+
it('fires as supplementary signal when regex misses', () => {
|
|
417
|
+
// Content that regex misses but TF-IDF should catch (narrative, first-person-plural)
|
|
418
|
+
const signals = detectEphemeralSignals('Redis Key Naming Conflict', 'A naming collision occurred between cache keys and session keys in Redis, causing sessions to be evicted. We implemented a namespace prefix strategy to separate the keyspaces.', 'modules/database');
|
|
419
|
+
// If TF-IDF fires, it should be the only signal (regex didn't match)
|
|
420
|
+
const tfidfSignal = signals.find(s => s.id === 'tfidf-classifier');
|
|
421
|
+
if (tfidfSignal) {
|
|
422
|
+
assert.ok(tfidfSignal.confidence === 'low', 'TF-IDF signal should have low confidence');
|
|
423
|
+
assert.ok(tfidfSignal.detail.includes('model confidence'), 'Should include model confidence');
|
|
424
|
+
}
|
|
425
|
+
// Either TF-IDF caught it or it's a genuine false negative — both acceptable
|
|
426
|
+
});
|
|
427
|
+
it('does not fire when regex already matched', () => {
|
|
428
|
+
const signals = detectEphemeralSignals('Current Build Issue', 'The CI pipeline is currently broken due to a flaky test. We need to fix it right now.', 'gotchas');
|
|
429
|
+
// Regex should fire (temporal: "currently", "right now")
|
|
430
|
+
assert.ok(signals.some(s => s.id === 'temporal'), 'Regex should fire');
|
|
431
|
+
// TF-IDF should NOT also fire (it's supplementary only)
|
|
432
|
+
assert.ok(!signals.some(s => s.id === 'tfidf-classifier'), 'TF-IDF should not fire when regex matched');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Tests for git-service.ts — the injected git operations boundary.
|
|
2
|
+
// Tests both the real service (against this repo) and the fake service (deterministic).
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { realGitService, fakeGitService } from '../git-service.js';
|
|
7
|
+
describe('fakeGitService', () => {
|
|
8
|
+
it('returns configured branch', async () => {
|
|
9
|
+
const git = fakeGitService('feature/test');
|
|
10
|
+
assert.strictEqual(await git.getCurrentBranch('/any/path'), 'feature/test');
|
|
11
|
+
});
|
|
12
|
+
it('defaults to main', async () => {
|
|
13
|
+
const git = fakeGitService();
|
|
14
|
+
assert.strictEqual(await git.getCurrentBranch('/any/path'), 'main');
|
|
15
|
+
});
|
|
16
|
+
it('returns a deterministic SHA', async () => {
|
|
17
|
+
const git = fakeGitService();
|
|
18
|
+
assert.strictEqual(await git.getHeadSha('/any/path'), 'fake-sha-1234');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('realGitService', () => {
|
|
22
|
+
it('returns a branch name for a valid git repo', async () => {
|
|
23
|
+
// This test runs against the current repo — should always work in CI/dev
|
|
24
|
+
const branch = await realGitService.getCurrentBranch(process.cwd());
|
|
25
|
+
assert.ok(typeof branch === 'string', 'Should return a string');
|
|
26
|
+
assert.ok(branch.length > 0, 'Branch name should not be empty');
|
|
27
|
+
assert.notStrictEqual(branch, 'unknown', 'Should resolve the branch (not fallback)');
|
|
28
|
+
});
|
|
29
|
+
it('returns unknown for a non-git directory', async () => {
|
|
30
|
+
const branch = await realGitService.getCurrentBranch(os.tmpdir());
|
|
31
|
+
assert.strictEqual(branch, 'unknown');
|
|
32
|
+
});
|
|
33
|
+
it('returns a SHA for a valid git repo', async () => {
|
|
34
|
+
const sha = await realGitService.getHeadSha(process.cwd());
|
|
35
|
+
// This repo has no commits yet, so SHA may be undefined
|
|
36
|
+
// But the function should not throw
|
|
37
|
+
assert.ok(sha === undefined || (typeof sha === 'string' && sha.length > 0), 'Should return undefined or a valid SHA');
|
|
38
|
+
});
|
|
39
|
+
it('returns undefined for a non-git directory', async () => {
|
|
40
|
+
const sha = await realGitService.getHeadSha(os.tmpdir());
|
|
41
|
+
assert.strictEqual(sha, undefined);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|