principles-disciple 1.51.0 → 1.53.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.
@@ -11,6 +11,7 @@ import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js
11
11
  import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
12
12
  import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
13
13
  import { PathResolver } from '../core/path-resolver.js';
14
+ import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/principle-injection.js';
14
15
  import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
15
16
  import { getPendingDiagnosticianTasks } from '../core/diagnostician-task-store.js';
16
17
  import {
@@ -533,8 +534,11 @@ The empathy observer subagent handles pain detection independently.
533
534
 
534
535
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
535
536
  logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
537
+
538
+ // Track if we should inject behavioral constraints (will be added to appendSystemContext later)
539
+ let shouldInjectBehavioralConstraints = false;
536
540
  if (empathyEnabled && isUserInteraction && sessionId && api && !isAgentToAgent) {
537
- prependContext = '### BEHAVIORAL_CONSTRAINTS\n' + empathySilenceConstraint + '\n\n' + prependContext;
541
+ shouldInjectBehavioralConstraints = true;
538
542
 
539
543
  // ── Empathy Hybrid Matching (keyword + subagent sampling) ──
540
544
  // Fast keyword scan on every turn, with strategic subagent sampling
@@ -898,12 +902,26 @@ ${taskBlocks}${processingNote}
898
902
  }
899
903
 
900
904
 
901
- // Evolution principles injection (active + probation summary)
905
+ // Evolution principles injection budget-aware selection (SDK-QUAL-04)
902
906
  let evolutionPrinciplesContent = '';
903
907
  try {
904
908
  const reducer = wctx.evolutionReducer;
905
- const active = reducer.getActivePrinciples().slice(-3);
906
- const probation = reducer.getProbationPrinciples().slice(0, 5);
909
+ const allActive = reducer.getActivePrinciples();
910
+ const allProbation = reducer.getProbationPrinciples();
911
+
912
+ // Budget-aware selection: prioritize P0>P1>P2 and recency
913
+ const activeSelection = selectPrinciplesForInjection(allActive, DEFAULT_PRINCIPLE_BUDGET);
914
+ const active = activeSelection.selected;
915
+
916
+ // Probation principles get a smaller sub-budget (1000 chars)
917
+ const probationBudget = 1000;
918
+ const probationSelection = selectPrinciplesForInjection(allProbation, probationBudget);
919
+ const probation = probationSelection.selected;
920
+
921
+ if (activeSelection.wasTruncated || probationSelection.wasTruncated) {
922
+ logger?.info?.(`[PD:Prompt] Principles truncated: active=${activeSelection.breakdown.p0 + activeSelection.breakdown.p1 + activeSelection.breakdown.p2}/${allActive.length} (${activeSelection.totalChars}c), probation=${probation.length}/${allProbation.length} (${probationSelection.totalChars}c)`);
923
+ }
924
+
907
925
  if (ctx.sessionId) {
908
926
  if (probation.length > 0) {
909
927
  setInjectedProbationIds(ctx.sessionId, probation.map((p) => p.id), workspaceDir);
@@ -935,9 +953,18 @@ ${taskBlocks}${processingNote}
935
953
  }
936
954
 
937
955
  // Build appendSystemContext with recency effect
938
- // Content order (most important last): project_context -> working_memory -> reflection_log -> thinking_os -> principles
956
+ // Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
939
957
  const appendParts: string[] = [];
940
958
 
959
+ // 0. Behavioral Constraints (empathy observer coordination)
960
+ // Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
961
+ // See: https://github.com/csuzngjh/principles/issues/XXX
962
+ if (shouldInjectBehavioralConstraints) {
963
+ appendParts.push(`<behavioral_constraints>
964
+ ${empathySilenceConstraint}
965
+ </behavioral_constraints>`);
966
+ }
967
+
941
968
  // 1. Project Context (lowest priority, goes first)
942
969
  if (projectContextContent) {
943
970
  appendParts.push(`<project_context>\n${projectContextContent}\n</project_context>`);
@@ -1087,6 +1114,7 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
1087
1114
  ---
1088
1115
 
1089
1116
  **【EXECUTION RULES】** (Priority: Low → High):
1117
+ - \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
1090
1118
  - \`<project_context>\` - Current priorities (can be overridden)
1091
1119
  - \`<reflection_log>\` - Past lessons (inform your approach)
1092
1120
  - \`<thinking_os>\` - Thinking models (guide your reasoning)
@@ -1,9 +1,9 @@
1
1
  /**
2
- * EventLog Auditor Search and verify events across all .state directories
3
- *
2
+ * EventLog Auditor - Search and verify events across all .state directories
3
+ *
4
4
  * This tool addresses a common debugging issue where hook events may be
5
5
  * written to the wrong .state directory due to workspaceDir resolution bugs.
6
- *
6
+ *
7
7
  * Usage:
8
8
  * const report = await auditEventLogs(openclawDir, ['after_tool_call', 'before_tool_call']);
9
9
  * console.log(report.summary);
@@ -42,7 +42,7 @@ interface AuditReport {
42
42
  */
43
43
  function findEventLogs(baseDir: string, maxDepth = 4): string[] {
44
44
  const results: string[] = [];
45
-
45
+
46
46
  function scan(dir: string, depth: number): void {
47
47
  if (depth > maxDepth) return;
48
48
  try {
@@ -58,37 +58,50 @@ function findEventLogs(baseDir: string, maxDepth = 4): string[] {
58
58
  // Permission denied or directory doesn't exist
59
59
  }
60
60
  }
61
-
61
+
62
62
  scan(baseDir, 0);
63
63
  return results;
64
64
  }
65
65
 
66
66
  /**
67
67
  * Find events.jsonl in well-known locations.
68
+ * Dynamically discovers workspace directories instead of hardcoding names.
68
69
  */
69
70
  function findKnownEventLogPaths(): string[] {
70
71
  const homeDir = os.homedir();
71
72
  const candidates: string[] = [];
72
-
73
- // Common patterns
74
- const patterns = [
73
+
74
+ // Common patterns (legacy, non-workspace paths)
75
+ const legacyPatterns = [
75
76
  path.join(homeDir, '.state', 'logs', 'events.jsonl'),
76
77
  path.join(homeDir, '.openclaw', '.state', 'logs', 'events.jsonl'),
77
- path.join(homeDir, '.openclaw', 'workspace-main', '.state', 'logs', 'events.jsonl'),
78
- path.join(homeDir, '.openclaw', 'workspace-builder', '.state', 'logs', 'events.jsonl'),
79
- path.join(homeDir, '.openclaw', 'workspace-pm', '.state', 'logs', 'events.jsonl'),
80
- path.join(homeDir, '.openclaw', 'workspace-hr', '.state', 'logs', 'events.jsonl'),
81
- path.join(homeDir, '.openclaw', 'workspace-repair', '.state', 'logs', 'events.jsonl'),
82
- path.join(homeDir, '.openclaw', 'workspace-research', '.state', 'logs', 'events.jsonl'),
83
- path.join(homeDir, '.openclaw', 'workspace-scout', '.state', 'logs', 'events.jsonl'),
84
78
  ];
85
-
86
- for (const p of patterns) {
79
+
80
+ for (const p of legacyPatterns) {
87
81
  if (fs.existsSync(p)) {
88
82
  candidates.push(p);
89
83
  }
90
84
  }
91
-
85
+
86
+ // Dynamically discover workspace directories under ~/.openclaw/
87
+ const openclawDir = path.join(homeDir, '.openclaw');
88
+ if (fs.existsSync(openclawDir)) {
89
+ try {
90
+ const entries = fs.readdirSync(openclawDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (!entry.isDirectory()) continue;
93
+ // Skip known non-workspace directories
94
+ if (entry.name.startsWith('.') || entry.name === 'extensions' || entry.name === 'memory') continue;
95
+ const eventLogPath = path.join(openclawDir, entry.name, '.state', 'logs', 'events.jsonl');
96
+ if (fs.existsSync(eventLogPath)) {
97
+ candidates.push(eventLogPath);
98
+ }
99
+ }
100
+ } catch {
101
+ // Directory read failed, skip dynamic discovery
102
+ }
103
+ }
104
+
92
105
  return candidates;
93
106
  }
94
107
 
@@ -133,25 +146,25 @@ function countAllHooks(filePath: string): Record<string, number> {
133
146
 
134
147
  /**
135
148
  * Audit all events.jsonl files.
136
- *
149
+ *
137
150
  * @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
138
151
  * @param expectedToolHooks - Hook names that should appear in the primary workspace
139
152
  */
140
-
153
+
141
154
  export async function auditEventLogs(
142
155
  openclawDir: string,
143
156
  expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
144
157
  ): Promise<AuditReport> {
145
158
  const homeDir = os.homedir();
146
-
159
+
147
160
  // Find all event logs
148
161
  const knownPaths = findKnownEventLogPaths();
149
162
  const scannedPaths = findEventLogs(homeDir, 4);
150
163
  const allPaths = [...new Set([...knownPaths, ...scannedPaths])];
151
-
164
+
152
165
  const locations: LocationReport[] = [];
153
166
  let primaryPath: string | null = null;
154
-
167
+
155
168
  for (const filePath of allPaths) {
156
169
  try {
157
170
  const stat = fs.statSync(filePath);
@@ -165,7 +178,7 @@ export async function auditEventLogs(
165
178
  hookCounts: allCounts,
166
179
  recentEntries: recent,
167
180
  });
168
-
181
+
169
182
  // Determine primary path (workspace-main or most recent)
170
183
  if (filePath.includes('workspace-main') || filePath.includes('workspace-main')) {
171
184
  primaryPath = filePath;
@@ -174,7 +187,7 @@ export async function auditEventLogs(
174
187
  // Skip unreadable files
175
188
  }
176
189
  }
177
-
190
+
178
191
  // If no primary found, use most recent
179
192
  if (!primaryPath && locations.length > 0) {
180
193
  locations.sort((a, b) => {
@@ -184,21 +197,21 @@ export async function auditEventLogs(
184
197
  });
185
198
  primaryPath = locations[0].path;
186
199
  }
187
-
200
+
188
201
  // Detect misplaced tool hook events
189
202
  const misplacedEvents: { path: string; entries: EventLogEntry[] }[] = [];
190
203
  for (const loc of locations) {
191
204
  if (loc.path === primaryPath) continue;
192
-
193
- const toolHookEntries = loc.recentEntries.filter(e =>
205
+
206
+ const toolHookEntries = loc.recentEntries.filter(e =>
194
207
  e.type === 'hook_execution' && expectedToolHooks.includes(e.data?.hook as string)
195
208
  );
196
-
209
+
197
210
  if (toolHookEntries.length > 0) {
198
211
  misplacedEvents.push({ path: loc.path, entries: toolHookEntries });
199
212
  }
200
213
  }
201
-
214
+
202
215
  return {
203
216
  searchedPaths: allPaths,
204
217
  locations,
@@ -210,37 +223,37 @@ export async function auditEventLogs(
210
223
  /**
211
224
  * Format audit report for display.
212
225
  */
213
-
226
+
214
227
  export function formatAuditReport(report: AuditReport): string {
215
228
  const lines: string[] = [];
216
-
229
+
217
230
  lines.push('=== Event Log Audit Report ===\n');
218
-
231
+
219
232
  lines.push(`Searched ${report.searchedPaths.length} paths:\n`);
220
233
  for (const p of report.searchedPaths) {
221
234
  lines.push(` ${p}`);
222
235
  }
223
236
  lines.push('');
224
-
237
+
225
238
  lines.push(`Primary: ${report.primaryPath ?? 'NOT FOUND'}\n`);
226
-
239
+
227
240
  for (const loc of report.locations) {
228
241
  const isPrimary = loc.path === report.primaryPath;
229
242
  lines.push(`─── ${isPrimary ? '[PRIMARY]' : '[OTHER] '}${loc.path}`);
230
243
  lines.push(` Last modified: ${loc.lastModified?.toISOString() ?? 'never'}`);
231
244
  lines.push(` Hook counts:`);
232
-
245
+
233
246
  const hooks = Object.entries(loc.hookCounts).sort((a, b) => b[1] - a[1]);
234
247
  for (const [hook, count] of hooks) {
235
248
  lines.push(` ${hook}: ${count}`);
236
249
  }
237
-
250
+
238
251
  if (hooks.length === 0) {
239
252
  lines.push(` (no hooks recorded)`);
240
253
  }
241
254
  lines.push('');
242
255
  }
243
-
256
+
244
257
  if (report.misplacedEvents.length > 0) {
245
258
  lines.push('⚠️ MISPLACED tool hook events detected:');
246
259
  for (const me of report.misplacedEvents) {
@@ -258,6 +271,6 @@ export function formatAuditReport(report: AuditReport): string {
258
271
  } else {
259
272
  lines.push('✅ No misplaced tool hook events detected.');
260
273
  }
261
-
274
+
262
275
  return lines.join('\n');
263
276
  }
@@ -18,6 +18,7 @@ import type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
18
18
  import type { PrincipleEvaluability } from '../types/principle-tree-schema.js';
19
19
  export type { TaskKind, TaskPriority } from '../core/trajectory-types.js';
20
20
  import { atomicWriteFileSync } from '../utils/io.js';
21
+ import { validatePainSignal, type PainSignalValidationResult } from '../core/pain-signal.js';
21
22
 
22
23
  // Re-export queue I/O (extracted to queue-io.ts)
23
24
  export { loadEvolutionQueue, saveEvolutionQueue, withQueueLock, acquireQueueLock, requireQueueLock } from './queue-io.js';
@@ -330,6 +331,26 @@ async function doEnqueuePainTask(
330
331
  return result;
331
332
  }
332
333
 
334
+ // Validate pain signal through TypeBox schema before enqueuing.
335
+ // Malformed signals are logged and skipped — they never enter the queue.
336
+ const signalInput = {
337
+ source: v.source,
338
+ score: v.score,
339
+ timestamp: new Date().toISOString(),
340
+ reason: v.reason,
341
+ sessionId: v.sessionId || '',
342
+ agentId: v.agentId || '',
343
+ traceId: v.traceId || '',
344
+ triggerTextPreview: v.preview,
345
+ };
346
+ const validation: PainSignalValidationResult = validatePainSignal(signalInput);
347
+ if (!validation.valid) {
348
+ result.skipped_reason = `invalid_pain_signal (${validation.errors.join('; ')})`;
349
+ if (logger) logger.warn(`[PD:EvolutionWorker] Pain signal validation failed, skipping enqueue: ${validation.errors.join('; ')}`);
350
+ SystemLogger.log(wctx.workspaceDir, 'PAIN_SIGNAL_INVALID', `Validation errors: ${validation.errors.join('; ')} | source=${v.source} score=${v.score}`);
351
+ return result;
352
+ }
353
+
333
354
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
334
355
  const releaseLock = await requireQueueLock(queuePath, logger, 'checkPainFlag');
335
356
  try {
@@ -763,9 +784,38 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
763
784
  }
764
785
 
765
786
  // V2: Migrate queue to current schema if needed
766
- const queue: EvolutionQueueItem[] = migrateQueueToV2(rawQueue) as unknown as EvolutionQueueItem[];
787
+ let queue: EvolutionQueueItem[] = migrateQueueToV2(rawQueue) as unknown as EvolutionQueueItem[];
788
+
789
+ // Validate queue items — filter out malformed entries before processing.
790
+ // Malformed items are logged + skipped; they never crash the evolution cycle.
791
+ const beforeValidation = queue.length;
792
+ queue = queue.filter((item) => {
793
+ const errors: string[] = [];
794
+ if (!item.id || typeof item.id !== 'string') errors.push('missing/invalid id');
795
+ if (!item.source || typeof item.source !== 'string') errors.push('missing/invalid source');
796
+ if (typeof item.score !== 'number') errors.push('missing/invalid score');
797
+ if (!item.status || typeof item.status !== 'string') errors.push('missing/invalid status');
798
+ if (!item.taskKind || typeof item.taskKind !== 'string') errors.push('missing/invalid taskKind');
799
+ else {
800
+ const validTaskKinds = ['pain_diagnosis', 'sleep_reflection', 'model_eval', 'keyword_optimization'];
801
+ if (!validTaskKinds.includes(item.taskKind)) {
802
+ errors.push(`invalid taskKind value '${item.taskKind}' (expected one of: ${validTaskKinds.join(', ')})`);
803
+ }
804
+ }
805
+ if (typeof item.retryCount !== 'number') errors.push('missing/invalid retryCount');
806
+ if (typeof item.maxRetries !== 'number') errors.push('missing/invalid maxRetries');
807
+ if (errors.length > 0) {
808
+ logger?.warn?.(`[PD:EvolutionWorker] Skipping malformed queue item: ${errors.join(', ')} | ${JSON.stringify(item).slice(0, 200)}`);
809
+ SystemLogger.log(wctx.workspaceDir, 'QUEUE_ITEM_MALFORMED', `Skipped: ${errors.join(', ')} | id=${item.id || 'N/A'}`);
810
+ return false;
811
+ }
812
+ return true;
813
+ });
814
+ if (queue.length < beforeValidation) {
815
+ logger?.info?.(`[PD:EvolutionWorker] Filtered ${beforeValidation - queue.length} malformed queue item(s)`);
816
+ }
767
817
 
768
- let queueChanged = rawQueue.some(isLegacyQueueItem);
818
+ let queueChanged = rawQueue.some(isLegacyQueueItem) || queue.length < beforeValidation;
769
819
 
770
820
  // Guard: Skip keyword_optimization if one is already pending/in-progress (CORR-08)
771
821
  if (hasPendingTask(queue, 'keyword_optimization')) {
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { EvolutionHook, PrincipleCreatedEvent, PrinciplePromotedEvent } from '../../src/core/evolution-hook.js';
3
+ import { noOpEvolutionHook } from '../../src/core/evolution-hook.js';
4
+ import type { PainSignal } from '../../src/core/pain-signal.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function validPainSignal(overrides: Partial<PainSignal> = {}): PainSignal {
11
+ return {
12
+ source: 'tool_failure',
13
+ score: 75,
14
+ timestamp: '2026-04-17T00:00:00.000Z',
15
+ reason: 'File not found',
16
+ sessionId: 'session-001',
17
+ agentId: 'main',
18
+ traceId: 'trace-001',
19
+ triggerTextPreview: 'File not found: test.ts',
20
+ domain: 'coding',
21
+ severity: 'high',
22
+ context: {},
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Tests
29
+ // ---------------------------------------------------------------------------
30
+
31
+ describe('EvolutionHook', () => {
32
+ it('implements all 3 methods', () => {
33
+ const calls: string[] = [];
34
+ const hook: EvolutionHook = {
35
+ onPainDetected(signal: PainSignal): void { calls.push(`pain:${signal.source}`); },
36
+ onPrincipleCreated(event: PrincipleCreatedEvent): void { calls.push(`created:${event.id}`); },
37
+ onPrinciplePromoted(event: PrinciplePromotedEvent): void { calls.push(`promoted:${event.id}`); },
38
+ };
39
+
40
+ hook.onPainDetected(validPainSignal());
41
+ hook.onPrincipleCreated({ id: 'p-1', text: 'Test principle', trigger: 'tool failure' });
42
+ hook.onPrinciplePromoted({ id: 'p-1', from: 'candidate', to: 'active' });
43
+
44
+ expect(calls).toEqual(['pain:tool_failure', 'created:p-1', 'promoted:p-1']);
45
+ });
46
+
47
+ it('onPainDetected receives a PainSignal', () => {
48
+ let received: PainSignal | undefined;
49
+ const hook: EvolutionHook = {
50
+ ...noOpEvolutionHook,
51
+ onPainDetected(signal: PainSignal): void { received = signal; },
52
+ };
53
+
54
+ const signal = validPainSignal();
55
+ hook.onPainDetected(signal);
56
+ expect(received).toEqual(signal);
57
+ });
58
+
59
+ it('onPrincipleCreated receives a PrincipleCreatedEvent', () => {
60
+ let received: PrincipleCreatedEvent | undefined;
61
+ const hook: EvolutionHook = {
62
+ ...noOpEvolutionHook,
63
+ onPrincipleCreated(event: PrincipleCreatedEvent): void { received = event; },
64
+ };
65
+
66
+ const event = { id: 'p-1', text: 'Test principle', trigger: 'tool failure' };
67
+ hook.onPrincipleCreated(event);
68
+ expect(received).toEqual(event);
69
+ });
70
+
71
+ it('onPrinciplePromoted receives a PrinciplePromotedEvent', () => {
72
+ let received: PrinciplePromotedEvent | undefined;
73
+ const hook: EvolutionHook = {
74
+ ...noOpEvolutionHook,
75
+ onPrinciplePromoted(event: PrinciplePromotedEvent): void { received = event; },
76
+ };
77
+
78
+ const event = { id: 'p-1', from: 'candidate', to: 'active' };
79
+ hook.onPrinciplePromoted(event);
80
+ expect(received).toEqual(event);
81
+ });
82
+ });
83
+
84
+ describe('noOpEvolutionHook', () => {
85
+ it('implements all 3 methods as no-ops', () => {
86
+ expect(() => {
87
+ noOpEvolutionHook.onPainDetected(validPainSignal());
88
+ noOpEvolutionHook.onPrincipleCreated({ id: 'p-1', text: 'Test', trigger: 'test' });
89
+ noOpEvolutionHook.onPrinciplePromoted({ id: 'p-1', from: 'candidate', to: 'active' });
90
+ }).not.toThrow();
91
+ });
92
+
93
+ it('can be spread to override individual methods', () => {
94
+ const calls: string[] = [];
95
+ const hook: EvolutionHook = {
96
+ ...noOpEvolutionHook,
97
+ onPainDetected(_signal: PainSignal): void { calls.push('pain'); },
98
+ };
99
+
100
+ hook.onPainDetected(validPainSignal());
101
+ hook.onPrincipleCreated({ id: 'p-1', text: 'Test', trigger: 'test' });
102
+
103
+ expect(calls).toEqual(['pain']);
104
+ });
105
+ });
106
+
107
+ describe('PrincipleCreatedEvent', () => {
108
+ it('has required fields: id, text, trigger', () => {
109
+ const event: PrincipleCreatedEvent = { id: 'p-1', text: 'Always verify', trigger: 'tool failure' };
110
+ expect(event.id).toBe('p-1');
111
+ expect(event.text).toBe('Always verify');
112
+ expect(event.trigger).toBe('tool failure');
113
+ });
114
+ });
115
+
116
+ describe('PrinciplePromotedEvent', () => {
117
+ it('has required fields: id, from, to', () => {
118
+ const event: PrinciplePromotedEvent = { id: 'p-1', from: 'candidate', to: 'active' };
119
+ expect(event.id).toBe('p-1');
120
+ expect(event.from).toBe('candidate');
121
+ expect(event.to).toBe('active');
122
+ });
123
+ });