principles-disciple 1.97.0 → 1.99.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/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.99.0",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"eslint": "^10.4.1",
|
|
58
58
|
"jsdom": "^29.1.1",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
+
"vite": "^8.0.16",
|
|
60
61
|
"vitest": "^4.1.8",
|
|
61
62
|
"ws": "^8.18.0"
|
|
62
63
|
},
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,59 @@ const startedWorkspaces = new Set<string>();
|
|
|
70
70
|
// Used to complete shadow observations when subagent ends
|
|
71
71
|
const pendingShadowObservations = new Map<string, string>();
|
|
72
72
|
|
|
73
|
+
// ── Conversation Access Health Check (PRI-343) ────────────────────────────
|
|
74
|
+
// Pure function for checking whether OpenClaw plugin config has
|
|
75
|
+
// allowConversationAccess set to true. When missing, llm_output and
|
|
76
|
+
// trajectory hooks are silently blocked by OpenClaw, causing evidence
|
|
77
|
+
// to always be empty (PRI-338 root cause).
|
|
78
|
+
|
|
79
|
+
/** Keep in sync with @principles/core CONVERSATION_ACCESS_CONFIG_KEY */
|
|
80
|
+
const CONVERSATION_ACCESS_CONFIG_KEY = 'allowConversationAccess' as const;
|
|
81
|
+
|
|
82
|
+
export interface ConversationAccessCheckResult {
|
|
83
|
+
authorized: boolean;
|
|
84
|
+
reason?: string;
|
|
85
|
+
nextAction?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CONVERSATION_ACCESS_FIX_COMMAND =
|
|
89
|
+
'openclaw config set plugins.entries.principles-disciple.hooks.allowConversationAccess true --strict-json';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* PRI-343: Pure function — checks if pluginConfig has hooks.allowConversationAccess === true.
|
|
93
|
+
* Returns a structured result with reason and nextAction when not authorized (ERR-002).
|
|
94
|
+
*/
|
|
95
|
+
export function checkConversationAccessConfig(pluginConfig: unknown): ConversationAccessCheckResult {
|
|
96
|
+
if (pluginConfig === null || pluginConfig === undefined || typeof pluginConfig !== 'object' || Array.isArray(pluginConfig)) {
|
|
97
|
+
return {
|
|
98
|
+
authorized: false,
|
|
99
|
+
reason: 'pluginConfig is missing or invalid — conversation hooks cannot be registered',
|
|
100
|
+
nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const config = pluginConfig as Record<string, unknown>;
|
|
105
|
+
|
|
106
|
+
if (typeof config.hooks !== 'object' || config.hooks === null || Array.isArray(config.hooks)) {
|
|
107
|
+
return {
|
|
108
|
+
authorized: false,
|
|
109
|
+
reason: 'allowConversationAccess is not set to true',
|
|
110
|
+
nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const hooks = config.hooks as Record<string, unknown>;
|
|
115
|
+
if (hooks[CONVERSATION_ACCESS_CONFIG_KEY] !== true) {
|
|
116
|
+
return {
|
|
117
|
+
authorized: false,
|
|
118
|
+
reason: 'allowConversationAccess is not set to true',
|
|
119
|
+
nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { authorized: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
// ── Feature Flag Loader (plugin I/O boundary) ─────────────────────────────
|
|
74
127
|
// Reads workspace feature-flags.yaml and checks a specific flag.
|
|
75
128
|
// Returns the flag definition with effective enabled state.
|
|
@@ -161,6 +214,18 @@ const plugin = {
|
|
|
161
214
|
} else {
|
|
162
215
|
api.logger.info(`[PD:health] Tool hook workspaceDir OK: "${toolWorkspaceDir}"`);
|
|
163
216
|
}
|
|
217
|
+
|
|
218
|
+
// PRI-343: Check allowConversationAccess — warn if llm_output/trajectory hooks blocked
|
|
219
|
+
const accessCheck = checkConversationAccessConfig(api.pluginConfig);
|
|
220
|
+
if (!accessCheck.authorized) {
|
|
221
|
+
api.logger.error(
|
|
222
|
+
`[PD:health] conversation hooks (llm_output / trajectory) will be BLOCKED by OpenClaw.\n` +
|
|
223
|
+
` reason: ${accessCheck.reason}\n` +
|
|
224
|
+
` nextAction: ${accessCheck.nextAction}`,
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
api.logger.info(`[PD:health] conversation hooks (allowConversationAccess) OK`);
|
|
228
|
+
}
|
|
164
229
|
}, 1000);
|
|
165
230
|
healthCheckTimer.unref(); // Don't keep process alive for health check
|
|
166
231
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { evaluatePainDiagnosticGate, resetPainDiagnosticGateForTest } from '../../src/core/pain-diagnostic-gate.js';
|
|
2
|
+
import { evaluatePainDiagnosticGate, resetPainDiagnosticGateForTest, isCooldownActiveForEpisode } from '../../src/core/pain-diagnostic-gate.js';
|
|
3
3
|
|
|
4
4
|
describe('PainDiagnosticGate', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -496,3 +496,119 @@ describe('PainDiagnosticGate', () => {
|
|
|
496
496
|
}
|
|
497
497
|
});
|
|
498
498
|
});
|
|
499
|
+
|
|
500
|
+
// ── isCooldownActiveForEpisode ─────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
describe('isCooldownActiveForEpisode', () => {
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
resetPainDiagnosticGateForTest();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('returns false when no diagnosis has been recorded', () => {
|
|
508
|
+
const result = isCooldownActiveForEpisode('tool_failure', 's1', 'hash-abc');
|
|
509
|
+
expect(result).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('returns false when cooldownMs is 0 (disabled)', () => {
|
|
513
|
+
// Record diagnosis
|
|
514
|
+
evaluatePainDiagnosticGate({
|
|
515
|
+
source: 'tool_failure',
|
|
516
|
+
score: 50,
|
|
517
|
+
currentGfi: 72,
|
|
518
|
+
sessionId: 's1',
|
|
519
|
+
errorHash: 'hash-abc',
|
|
520
|
+
nowMs: 1_000,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// With cooldown disabled, should always return false
|
|
524
|
+
const noCooldown = isCooldownActiveForEpisode('tool_failure', 's1', 'hash-abc', 0);
|
|
525
|
+
expect(noCooldown).toBe(false);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('different sessionId does not share cooldown', () => {
|
|
529
|
+
// Record diagnosis for session s1
|
|
530
|
+
evaluatePainDiagnosticGate({
|
|
531
|
+
source: 'tool_failure',
|
|
532
|
+
score: 50,
|
|
533
|
+
currentGfi: 72,
|
|
534
|
+
sessionId: 's1',
|
|
535
|
+
errorHash: 'hash-abc',
|
|
536
|
+
nowMs: 1_000,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Check for different session s2 - should not be in cooldown
|
|
540
|
+
const differentSession = isCooldownActiveForEpisode('tool_failure', 's2', 'hash-abc');
|
|
541
|
+
expect(differentSession).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('different errorHash does not share cooldown', () => {
|
|
545
|
+
// Record diagnosis for hash-abc
|
|
546
|
+
evaluatePainDiagnosticGate({
|
|
547
|
+
source: 'tool_failure',
|
|
548
|
+
score: 50,
|
|
549
|
+
currentGfi: 72,
|
|
550
|
+
sessionId: 's1',
|
|
551
|
+
errorHash: 'hash-abc',
|
|
552
|
+
nowMs: 1_000,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Check for different hash - should not be in cooldown
|
|
556
|
+
const differentHash = isCooldownActiveForEpisode('tool_failure', 's1', 'hash-xyz');
|
|
557
|
+
expect(differentHash).toBe(false);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('different source does not share cooldown', () => {
|
|
561
|
+
// Record diagnosis for tool_failure
|
|
562
|
+
evaluatePainDiagnosticGate({
|
|
563
|
+
source: 'tool_failure',
|
|
564
|
+
score: 50,
|
|
565
|
+
currentGfi: 72,
|
|
566
|
+
sessionId: 's1',
|
|
567
|
+
errorHash: 'hash-abc',
|
|
568
|
+
nowMs: 1_000,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Check for different source dispatch_error - should not be in cooldown
|
|
572
|
+
const differentSource = isCooldownActiveForEpisode('dispatch_error', 's1', 'hash-abc');
|
|
573
|
+
expect(differentSource).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('undefined sessionId uses unknown as session identifier', () => {
|
|
577
|
+
// Record diagnosis with undefined sessionId
|
|
578
|
+
evaluatePainDiagnosticGate({
|
|
579
|
+
source: 'tool_failure',
|
|
580
|
+
score: 50,
|
|
581
|
+
currentGfi: 72,
|
|
582
|
+
errorHash: 'hash-abc',
|
|
583
|
+
nowMs: 1_000,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Check cooldown with undefined sessionId - should not be in cooldown
|
|
587
|
+
// because evaluate used Date.now() but isCooldownActiveForEpisode uses current Date.now()
|
|
588
|
+
// and 15 seconds haven't passed
|
|
589
|
+
const undefinedSession = isCooldownActiveForEpisode('tool_failure', undefined, 'hash-abc');
|
|
590
|
+
// The episodeKey built from undefined sessionId uses 'unknown'
|
|
591
|
+
// But we can't reliably test time-based behavior without mocking Date.now()
|
|
592
|
+
// So we just verify it doesn't throw
|
|
593
|
+
expect(typeof undefinedSession).toBe('boolean');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('episodeKey alignment: same inputs produce same cooldown state', () => {
|
|
597
|
+
// Use exact same inputs that would create an episodeKey
|
|
598
|
+
const episodeInput = {
|
|
599
|
+
source: 'manual' as const,
|
|
600
|
+
score: 100,
|
|
601
|
+
currentGfi: 0,
|
|
602
|
+
sessionId: 's-ep-test',
|
|
603
|
+
errorHash: 'hash-ep',
|
|
604
|
+
nowMs: 5_000,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// First diagnosis
|
|
608
|
+
evaluatePainDiagnosticGate(episodeInput);
|
|
609
|
+
|
|
610
|
+
// isCooldownActiveForEpisode should not throw with same inputs
|
|
611
|
+
const inCooldown = isCooldownActiveForEpisode('manual', 's-ep-test', 'hash-ep');
|
|
612
|
+
expect(typeof inCooldown).toBe('boolean');
|
|
613
|
+
});
|
|
614
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory Evidence Builder Tests — PRI-326
|
|
3
|
+
*
|
|
4
|
+
* Tests the pure data extraction function buildTrajectoryEvidence
|
|
5
|
+
* which reads from trajectory DB, sanitizes, and returns evidence entries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { buildTrajectoryEvidence } from '../../src/hooks/trajectory-evidence.js';
|
|
10
|
+
import type { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
11
|
+
import type { TrajectoryDatabase } from '../../src/core/trajectory.js';
|
|
12
|
+
|
|
13
|
+
// Mock sanitizeAssistantText to avoid testing message-sanitize here
|
|
14
|
+
vi.mock('../../src/hooks/message-sanitize.js', () => ({
|
|
15
|
+
sanitizeAssistantText: vi.fn((text: string) => text),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('buildTrajectoryEvidence', () => {
|
|
19
|
+
let mockTrajectory: Partial<TrajectoryDatabase>;
|
|
20
|
+
let mockWctx: Partial<WorkspaceContext>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockTrajectory = {
|
|
24
|
+
listUserTurnsForSession: vi.fn(),
|
|
25
|
+
listAssistantTurns: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
mockWctx = {
|
|
28
|
+
trajectory: mockTrajectory as TrajectoryDatabase,
|
|
29
|
+
workspaceDir: '/test/workspace',
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns unavailable evidence when trajectory is not available', () => {
|
|
34
|
+
mockWctx.trajectory = undefined;
|
|
35
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'unknown');
|
|
36
|
+
expect(result).toHaveLength(1);
|
|
37
|
+
expect(result[0].sourceRef).toBe('owner_message:unavailable');
|
|
38
|
+
expect(result[0].note).toContain('no_trajectory_db');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns unavailable evidence when sessionId is unknown', () => {
|
|
42
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'unknown');
|
|
43
|
+
expect(result).toHaveLength(1);
|
|
44
|
+
expect(result[0].sourceRef).toBe('owner_message:unavailable');
|
|
45
|
+
expect(result[0].note).toContain('unknown_session');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns last correction owner message as evidence', () => {
|
|
49
|
+
const mockUserTurn = {
|
|
50
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
51
|
+
correctionDetected: true,
|
|
52
|
+
rawExcerpt: 'Please fix this bug',
|
|
53
|
+
};
|
|
54
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([mockUserTurn as any]);
|
|
55
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
56
|
+
|
|
57
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
58
|
+
|
|
59
|
+
expect(result).toHaveLength(1);
|
|
60
|
+
expect(result[0].sourceRef).toContain('owner_message:');
|
|
61
|
+
expect(result[0].note).toBe('Please fix this bug');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles trajectory listUserTurnsForSession throwing an error gracefully', () => {
|
|
65
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockImplementation(() => {
|
|
66
|
+
throw new Error('Database error');
|
|
67
|
+
});
|
|
68
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
69
|
+
|
|
70
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
71
|
+
|
|
72
|
+
expect(result).toHaveLength(1);
|
|
73
|
+
expect(result[0].sourceRef).toBe('owner_message:unavailable');
|
|
74
|
+
expect(result[0].note).toContain('trajectory_user_turns_unavailable');
|
|
75
|
+
expect(result[0].note).toContain('Database error');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns recent assistant turns as evidence', () => {
|
|
79
|
+
const mockUserTurn = {
|
|
80
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
81
|
+
correctionDetected: false,
|
|
82
|
+
rawExcerpt: '',
|
|
83
|
+
};
|
|
84
|
+
const mockAssistantTurns = [
|
|
85
|
+
{ createdAt: '2024-01-15T09:58:00Z', sanitizedText: 'Turn 1' },
|
|
86
|
+
{ createdAt: '2024-01-15T09:59:00Z', sanitizedText: 'Turn 2' },
|
|
87
|
+
{ createdAt: '2024-01-15T10:00:00Z', sanitizedText: 'Turn 3' },
|
|
88
|
+
];
|
|
89
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([mockUserTurn as any]);
|
|
90
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue(mockAssistantTurns as any);
|
|
91
|
+
|
|
92
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
93
|
+
|
|
94
|
+
// Should have the last 3 assistant turns (MAX is 3 from core constants)
|
|
95
|
+
expect(result.length).toBeGreaterThan(0);
|
|
96
|
+
expect(result.some(e => e.note === 'Turn 3')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handles trajectory listAssistantTurns throwing an error gracefully', () => {
|
|
100
|
+
const mockUserTurn = {
|
|
101
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
102
|
+
correctionDetected: true,
|
|
103
|
+
rawExcerpt: 'Last correction',
|
|
104
|
+
};
|
|
105
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([mockUserTurn as any]);
|
|
106
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockImplementation(() => {
|
|
107
|
+
throw new Error('Trajectory DB error');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
111
|
+
|
|
112
|
+
// Should have owner message plus error entry
|
|
113
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
114
|
+
expect(result[0].sourceRef).toContain('owner_message:');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns empty trajectory notice when no user corrections or assistant turns', () => {
|
|
118
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([]);
|
|
119
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
120
|
+
|
|
121
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
122
|
+
|
|
123
|
+
expect(result).toHaveLength(1);
|
|
124
|
+
expect(result[0].sourceRef).toBe('trajectory:empty');
|
|
125
|
+
expect(result[0].note).toContain('trajectory_available_but_empty');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('respects MAX_EVIDENCE_ENTRIES limit', () => {
|
|
129
|
+
// Create many user turns with corrections
|
|
130
|
+
const manyUserTurns = Array.from({ length: 10 }, (_, i) => ({
|
|
131
|
+
createdAt: `2024-01-15T${String(i).padStart(2, '0')}:00:00Z`,
|
|
132
|
+
correctionDetected: true,
|
|
133
|
+
rawExcerpt: `Correction ${i}`,
|
|
134
|
+
}));
|
|
135
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue(manyUserTurns as any);
|
|
136
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
137
|
+
|
|
138
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
139
|
+
|
|
140
|
+
// MAX_EVIDENCE_ENTRIES from core is 5, so should be capped
|
|
141
|
+
expect(result.length).toBeLessThanOrEqual(5);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('uses last correction turn (most recent) when multiple corrections exist', () => {
|
|
145
|
+
const olderCorrection = {
|
|
146
|
+
createdAt: '2024-01-15T09:00:00Z',
|
|
147
|
+
correctionDetected: true,
|
|
148
|
+
rawExcerpt: 'Older correction',
|
|
149
|
+
};
|
|
150
|
+
const newerCorrection = {
|
|
151
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
152
|
+
correctionDetected: true,
|
|
153
|
+
rawExcerpt: 'Newer correction',
|
|
154
|
+
};
|
|
155
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([olderCorrection, newerCorrection] as any);
|
|
156
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
157
|
+
|
|
158
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
159
|
+
|
|
160
|
+
// Should use the newer correction (last in reverse order)
|
|
161
|
+
expect(result[0].note).toBe('Newer correction');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('sanitizes owner message text', () => {
|
|
165
|
+
const mockUserTurn = {
|
|
166
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
167
|
+
correctionDetected: true,
|
|
168
|
+
rawExcerpt: 'Text with [EMOTIONAL_DAMAGE_DETECTED:mild] internal tags',
|
|
169
|
+
};
|
|
170
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([mockUserTurn] as any);
|
|
171
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
172
|
+
|
|
173
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
174
|
+
|
|
175
|
+
// The mock sanitizeAssistantText returns text as-is
|
|
176
|
+
expect(result[0].note).toContain('Text with [EMOTIONAL_DAMAGE_DETECTED:mild] internal tags');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('truncates long notes to MAX_EVIDENCE_NOTE_CHARS', () => {
|
|
180
|
+
const longText = 'A'.repeat(10000);
|
|
181
|
+
const mockUserTurn = {
|
|
182
|
+
createdAt: '2024-01-15T10:00:00Z',
|
|
183
|
+
correctionDetected: true,
|
|
184
|
+
rawExcerpt: longText,
|
|
185
|
+
};
|
|
186
|
+
vi.mocked(mockTrajectory.listUserTurnsForSession!).mockReturnValue([mockUserTurn] as any);
|
|
187
|
+
vi.mocked(mockTrajectory.listAssistantTurns!).mockReturnValue([]);
|
|
188
|
+
|
|
189
|
+
const result = buildTrajectoryEvidence(mockWctx as WorkspaceContext, 'session-123');
|
|
190
|
+
|
|
191
|
+
// Note should be truncated to MAX_EVIDENCE_NOTE_CHARS (1000 from core)
|
|
192
|
+
expect(result[0].note.length).toBeLessThanOrEqual(1000);
|
|
193
|
+
});
|
|
194
|
+
});
|
package/tests/index.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import plugin from '../src/index';
|
|
3
|
+
import { checkConversationAccessConfig } from '../src/index';
|
|
3
4
|
import type { PluginCommandDefinition, OpenClawPluginApi, PluginCommandContext } from '../src/openclaw-sdk.js';
|
|
4
5
|
|
|
5
6
|
function createMockApi(): { registeredCommands: PluginCommandDefinition[]; api: OpenClawPluginApi } {
|
|
@@ -100,3 +101,42 @@ describe('Command Registration', () => {
|
|
|
100
101
|
expect(ctx.workspaceDir).toBe('/mock/workspace');
|
|
101
102
|
});
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
describe('checkConversationAccessConfig — PRI-343', () => {
|
|
106
|
+
it('returns authorized:false with reason and nextAction when allowConversationAccess is not true', () => {
|
|
107
|
+
const result = checkConversationAccessConfig({ hooks: { allowConversationAccess: false } });
|
|
108
|
+
expect(result.authorized).toBe(false);
|
|
109
|
+
expect(result.reason).toBeDefined();
|
|
110
|
+
expect(typeof result.reason).toBe('string');
|
|
111
|
+
expect(result.reason!.length).toBeGreaterThan(0);
|
|
112
|
+
expect(result.nextAction).toBeDefined();
|
|
113
|
+
expect(typeof result.nextAction).toBe('string');
|
|
114
|
+
expect(result.nextAction!.length).toBeGreaterThan(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns authorized:true when allowConversationAccess is true', () => {
|
|
118
|
+
const result = checkConversationAccessConfig({ hooks: { allowConversationAccess: true } });
|
|
119
|
+
expect(result.authorized).toBe(true);
|
|
120
|
+
expect(result.reason).toBeUndefined();
|
|
121
|
+
expect(result.nextAction).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns authorized:false when hooks object is missing', () => {
|
|
125
|
+
const result = checkConversationAccessConfig({ enabled: true });
|
|
126
|
+
expect(result.authorized).toBe(false);
|
|
127
|
+
expect(result.reason).toBeDefined();
|
|
128
|
+
expect(result.nextAction).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns authorized:false when pluginConfig is null', () => {
|
|
132
|
+
const result = checkConversationAccessConfig(null);
|
|
133
|
+
expect(result.authorized).toBe(false);
|
|
134
|
+
expect(result.reason).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns authorized:false when pluginConfig is undefined', () => {
|
|
138
|
+
const result = checkConversationAccessConfig(undefined);
|
|
139
|
+
expect(result.authorized).toBe(false);
|
|
140
|
+
expect(result.reason).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
});
|