principles-disciple 1.11.0 → 1.13.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ControlUiQueryService } from '../../src/service/control-ui-query-service.js';
|
|
3
|
+
import { CentralOverviewService } from '../../src/service/central-overview-service.js';
|
|
4
|
+
import { CentralHealthService } from '../../src/service/central-health-service.js';
|
|
5
|
+
import { HealthQueryService } from '../../src/service/health-query-service.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Shared mock infrastructure
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const trajectoryMock = vi.hoisted(() => ({
|
|
12
|
+
getDataStats: vi.fn(() => ({
|
|
13
|
+
dbPath: '/mock/trajectory.db',
|
|
14
|
+
dbSizeBytes: 0,
|
|
15
|
+
assistantTurns: 10,
|
|
16
|
+
userTurns: 5,
|
|
17
|
+
toolCalls: 20,
|
|
18
|
+
painEvents: 2,
|
|
19
|
+
pendingSamples: 1,
|
|
20
|
+
approvedSamples: 0,
|
|
21
|
+
blobBytes: 0,
|
|
22
|
+
lastIngestAt: '2026-04-09T00:00:00Z',
|
|
23
|
+
})),
|
|
24
|
+
reviewCorrectionSample: vi.fn(),
|
|
25
|
+
exportCorrections: vi.fn(),
|
|
26
|
+
listEvolutionEvents: vi.fn(() => []),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const configMock = vi.hoisted(() => ({
|
|
30
|
+
get: vi.fn((key: string) => {
|
|
31
|
+
if (key === 'gfi_gate.thresholds.low_risk_block') return 70;
|
|
32
|
+
return null;
|
|
33
|
+
}),
|
|
34
|
+
has: vi.fn(() => false),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const eventLogMock = vi.hoisted(() => ({
|
|
38
|
+
getBufferedEvents: vi.fn(() => []),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const evolutionReducerMock = vi.hoisted(() => ({
|
|
42
|
+
getStats: vi.fn(() => ({
|
|
43
|
+
candidateCount: 1,
|
|
44
|
+
probationCount: 0,
|
|
45
|
+
activeCount: 2,
|
|
46
|
+
deprecatedCount: 0,
|
|
47
|
+
})),
|
|
48
|
+
getPrincipleById: vi.fn(() => null),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const controlUiDbMock = vi.hoisted(() => ({
|
|
52
|
+
all: vi.fn((..._args: unknown[]) => []) as any,
|
|
53
|
+
get: vi.fn((..._args: unknown[]) => null) as any,
|
|
54
|
+
run: vi.fn(),
|
|
55
|
+
execute: vi.fn(),
|
|
56
|
+
dispose: vi.fn(),
|
|
57
|
+
restoreRawText: vi.fn((rawText: string | null) => rawText ?? ''),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
vi.mock('../../src/core/workspace-context.js', () => ({
|
|
61
|
+
WorkspaceContext: {
|
|
62
|
+
fromHookContext: vi.fn(() => ({
|
|
63
|
+
workspaceDir: '/mock/workspace',
|
|
64
|
+
stateDir: '/mock/workspace/.openclaw',
|
|
65
|
+
trajectory: trajectoryMock,
|
|
66
|
+
config: configMock,
|
|
67
|
+
eventLog: eventLogMock,
|
|
68
|
+
evolutionReducer: evolutionReducerMock,
|
|
69
|
+
})),
|
|
70
|
+
clearCache: vi.fn(),
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
vi.mock('../../src/core/control-ui-db.js', () => ({
|
|
75
|
+
ControlUiDatabase: class {
|
|
76
|
+
all = controlUiDbMock.all;
|
|
77
|
+
get = controlUiDbMock.get;
|
|
78
|
+
run = controlUiDbMock.run;
|
|
79
|
+
execute = controlUiDbMock.execute;
|
|
80
|
+
dispose = controlUiDbMock.dispose;
|
|
81
|
+
restoreRawText = controlUiDbMock.restoreRawText;
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
vi.mock('../../src/core/event-log.js', () => ({
|
|
86
|
+
EventLog: vi.fn(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock('../../src/core/trajectory.js', () => ({
|
|
90
|
+
TrajectoryRegistry: {
|
|
91
|
+
get: vi.fn(() => trajectoryMock),
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// CentralDatabase mock for CentralOverviewService
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
const centralDbMethods = {
|
|
100
|
+
getOverviewStats: vi.fn(() => ({
|
|
101
|
+
totalToolCalls: 10,
|
|
102
|
+
totalFailures: 1,
|
|
103
|
+
totalCorrections: 0,
|
|
104
|
+
totalThinkingEvents: 5,
|
|
105
|
+
totalPainEvents: 1,
|
|
106
|
+
pendingSamples: 1,
|
|
107
|
+
approvedSamples: 0,
|
|
108
|
+
rejectedSamples: 0,
|
|
109
|
+
workspaceCount: 2,
|
|
110
|
+
enabledWorkspaceCount: 2,
|
|
111
|
+
workspaceNames: ['ws1', 'ws2'],
|
|
112
|
+
enabledWorkspaceNames: ['ws1', 'ws2'],
|
|
113
|
+
})),
|
|
114
|
+
getDailyTrend: vi.fn(() => []),
|
|
115
|
+
getTopRegressions: vi.fn(() => []),
|
|
116
|
+
getThinkingModelStats: vi.fn(() => ({
|
|
117
|
+
totalModels: 3,
|
|
118
|
+
activeModels: 2,
|
|
119
|
+
models: [],
|
|
120
|
+
})),
|
|
121
|
+
getEnabledWorkspaces: vi.fn(() => [
|
|
122
|
+
{ name: 'ws1', path: '/mock/workspace1', lastSync: '2026-04-09T00:00:00Z' },
|
|
123
|
+
{ name: 'ws2', path: '/mock/workspace2', lastSync: '2026-04-09T00:00:00Z' },
|
|
124
|
+
]),
|
|
125
|
+
getSamplePreview: vi.fn(() => []),
|
|
126
|
+
getMostRecentSync: vi.fn(() => '2026-04-09T00:00:00Z'),
|
|
127
|
+
getTaskOutcomes: vi.fn(() => 0),
|
|
128
|
+
getPrincipleEventCount: vi.fn(() => 0),
|
|
129
|
+
getSampleCountersByStatus: vi.fn(() => ({})),
|
|
130
|
+
dispose: vi.fn(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
vi.mock('../../src/service/central-database.js', () => ({
|
|
134
|
+
getCentralDatabase: vi.fn(() => centralDbMethods),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Tests
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
describe('Data Endpoints Regression Tests', () => {
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
vi.clearAllMocks();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ===== Overview Page Tests =====
|
|
147
|
+
|
|
148
|
+
describe('Overview Page - /api/overview', () => {
|
|
149
|
+
it('getOverview returns correct response shape with all required fields', () => {
|
|
150
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
151
|
+
const result = service.getOverview(30);
|
|
152
|
+
|
|
153
|
+
// Top-level fields from OverviewResponse
|
|
154
|
+
expect(result).toHaveProperty('workspaceDir');
|
|
155
|
+
expect(result).toHaveProperty('generatedAt');
|
|
156
|
+
expect(result).toHaveProperty('dataFreshness');
|
|
157
|
+
expect(result).toHaveProperty('summary');
|
|
158
|
+
expect(result).toHaveProperty('dailyTrend');
|
|
159
|
+
expect(result).toHaveProperty('topRegressions');
|
|
160
|
+
expect(result).toHaveProperty('sampleQueue');
|
|
161
|
+
expect(result).toHaveProperty('thinkingSummary');
|
|
162
|
+
|
|
163
|
+
// dataSource fields from ControlUiQueryService
|
|
164
|
+
expect(result.dataSource).toBe('trajectory_db_analytics');
|
|
165
|
+
expect(result.runtimeControlPlaneSource).toBe('pd_evolution_status');
|
|
166
|
+
|
|
167
|
+
// Summary fields
|
|
168
|
+
expect(result.summary).toHaveProperty('repeatErrorRate');
|
|
169
|
+
expect(result.summary).toHaveProperty('userCorrectionRate');
|
|
170
|
+
expect(result.summary).toHaveProperty('pendingSamples');
|
|
171
|
+
expect(result.summary).toHaveProperty('approvedSamples');
|
|
172
|
+
expect(result.summary).toHaveProperty('thinkingCoverageRate');
|
|
173
|
+
expect(result.summary).toHaveProperty('painEvents');
|
|
174
|
+
expect(result.summary).toHaveProperty('principleEventCount');
|
|
175
|
+
expect(result.summary).toHaveProperty('gateBlocks');
|
|
176
|
+
expect(result.summary).toHaveProperty('taskOutcomes');
|
|
177
|
+
|
|
178
|
+
// SampleQueue structure
|
|
179
|
+
expect(result.sampleQueue).toHaveProperty('counters');
|
|
180
|
+
expect(result.sampleQueue).toHaveProperty('preview');
|
|
181
|
+
expect(typeof result.sampleQueue.counters).toBe('object');
|
|
182
|
+
|
|
183
|
+
// ThinkingSummary structure
|
|
184
|
+
expect(result.thinkingSummary).toHaveProperty('activeModels');
|
|
185
|
+
expect(result.thinkingSummary).toHaveProperty('dormantModels');
|
|
186
|
+
expect(result.thinkingSummary).toHaveProperty('effectiveModels');
|
|
187
|
+
expect(result.thinkingSummary).toHaveProperty('coverageRate');
|
|
188
|
+
|
|
189
|
+
service.dispose();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('getOverview returns array fields (dailyTrend, topRegressions, sampleQueue.preview) as arrays', () => {
|
|
193
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
194
|
+
const result = service.getOverview(30);
|
|
195
|
+
|
|
196
|
+
expect(Array.isArray(result.dailyTrend)).toBe(true);
|
|
197
|
+
expect(Array.isArray(result.topRegressions)).toBe(true);
|
|
198
|
+
expect(Array.isArray(result.sampleQueue.preview)).toBe(true);
|
|
199
|
+
|
|
200
|
+
service.dispose();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('getOverview dailyTrend items have correct shape when populated', () => {
|
|
204
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
205
|
+
if (sql.includes('v_daily_metrics')) {
|
|
206
|
+
return [
|
|
207
|
+
{ day: '2026-04-08', tool_calls: 10, failures: 1, user_corrections: 0, thinking_turns: 5 },
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
return [];
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
214
|
+
const result = service.getOverview(30);
|
|
215
|
+
|
|
216
|
+
if (result.dailyTrend.length > 0) {
|
|
217
|
+
const item = result.dailyTrend[0];
|
|
218
|
+
expect(item).toHaveProperty('day');
|
|
219
|
+
expect(item).toHaveProperty('toolCalls');
|
|
220
|
+
expect(item).toHaveProperty('failures');
|
|
221
|
+
expect(item).toHaveProperty('userCorrections');
|
|
222
|
+
expect(item).toHaveProperty('thinkingTurns');
|
|
223
|
+
expect(typeof item.day).toBe('string');
|
|
224
|
+
expect(typeof item.toolCalls).toBe('number');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
service.dispose();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('getOverview topRegressions items have correct shape when populated', () => {
|
|
231
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
232
|
+
if (sql.includes('v_error_clusters')) {
|
|
233
|
+
return [{ tool_name: 'Read', error_type: 'file_not_found', occurrences: 3 }];
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
});
|
|
237
|
+
controlUiDbMock.get.mockImplementation((sql: string) => {
|
|
238
|
+
if (sql.includes('gate_blocks')) return { count: 0 };
|
|
239
|
+
if (sql.includes('task_outcomes')) return { count: 0 };
|
|
240
|
+
if (sql.includes('user_turns')) return { count: 0 };
|
|
241
|
+
if (sql.includes('principle_events')) return { count: 0 };
|
|
242
|
+
return { count: 0 };
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
246
|
+
const result = service.getOverview(30);
|
|
247
|
+
|
|
248
|
+
if (result.topRegressions.length > 0) {
|
|
249
|
+
const item = result.topRegressions[0];
|
|
250
|
+
expect(item).toHaveProperty('toolName');
|
|
251
|
+
expect(item).toHaveProperty('errorType');
|
|
252
|
+
expect(item).toHaveProperty('occurrences');
|
|
253
|
+
expect(typeof item.toolName).toBe('string');
|
|
254
|
+
expect(typeof item.errorType).toBe('string');
|
|
255
|
+
expect(typeof item.occurrences).toBe('number');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
service.dispose();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('Overview Page - /api/central/overview', () => {
|
|
263
|
+
it('CentralOverviewService.getOverview returns correct response shape', () => {
|
|
264
|
+
const service = new CentralOverviewService();
|
|
265
|
+
const result = service.getOverview(30);
|
|
266
|
+
|
|
267
|
+
// Top-level fields
|
|
268
|
+
expect(result).toHaveProperty('workspaceDir');
|
|
269
|
+
expect(result).toHaveProperty('generatedAt');
|
|
270
|
+
expect(result).toHaveProperty('dataFreshness');
|
|
271
|
+
expect(result).toHaveProperty('summary');
|
|
272
|
+
expect(result).toHaveProperty('dailyTrend');
|
|
273
|
+
expect(result).toHaveProperty('topRegressions');
|
|
274
|
+
expect(result).toHaveProperty('sampleQueue');
|
|
275
|
+
expect(result).toHaveProperty('thinkingSummary');
|
|
276
|
+
|
|
277
|
+
// Central-specific fields
|
|
278
|
+
expect(result.dataSource).toBe('central_aggregated_db');
|
|
279
|
+
expect(result.runtimeControlPlaneSource).toBe('all_workspaces');
|
|
280
|
+
expect(result).toHaveProperty('centralInfo');
|
|
281
|
+
|
|
282
|
+
// Summary structure matches OverviewResponse
|
|
283
|
+
expect(result.summary).toHaveProperty('repeatErrorRate');
|
|
284
|
+
expect(result.summary).toHaveProperty('userCorrectionRate');
|
|
285
|
+
expect(result.summary).toHaveProperty('pendingSamples');
|
|
286
|
+
expect(result.summary).toHaveProperty('approvedSamples');
|
|
287
|
+
expect(result.summary).toHaveProperty('thinkingCoverageRate');
|
|
288
|
+
expect(result.summary).toHaveProperty('painEvents');
|
|
289
|
+
expect(result.summary).toHaveProperty('principleEventCount');
|
|
290
|
+
expect(result.summary).toHaveProperty('gateBlocks');
|
|
291
|
+
expect(result.summary).toHaveProperty('taskOutcomes');
|
|
292
|
+
|
|
293
|
+
// centralInfo structure
|
|
294
|
+
expect(result.centralInfo).toHaveProperty('workspaceCount');
|
|
295
|
+
expect(result.centralInfo).toHaveProperty('enabledWorkspaceCount');
|
|
296
|
+
expect(result.centralInfo).toHaveProperty('workspaces');
|
|
297
|
+
expect(result.centralInfo).toHaveProperty('enabledWorkspaces');
|
|
298
|
+
expect(Array.isArray(result.centralInfo.workspaces)).toBe(true);
|
|
299
|
+
|
|
300
|
+
service.dispose();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('Overview Page - /api/overview/health', () => {
|
|
305
|
+
it('getOverviewHealth returns correct response shape', () => {
|
|
306
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
307
|
+
const result = service.getOverviewHealth();
|
|
308
|
+
|
|
309
|
+
// Top-level fields from OverviewHealthResponse
|
|
310
|
+
expect(result).toHaveProperty('gfi');
|
|
311
|
+
expect(result).toHaveProperty('trust');
|
|
312
|
+
expect(result).toHaveProperty('evolution');
|
|
313
|
+
expect(result).toHaveProperty('painFlag');
|
|
314
|
+
expect(result).toHaveProperty('principles');
|
|
315
|
+
expect(result).toHaveProperty('queue');
|
|
316
|
+
expect(result).toHaveProperty('activeStage');
|
|
317
|
+
|
|
318
|
+
// GFI structure
|
|
319
|
+
expect(result.gfi).toHaveProperty('current');
|
|
320
|
+
expect(result.gfi).toHaveProperty('peakToday');
|
|
321
|
+
expect(result.gfi).toHaveProperty('threshold');
|
|
322
|
+
expect(typeof result.gfi.current).toBe('number');
|
|
323
|
+
expect(typeof result.gfi.peakToday).toBe('number');
|
|
324
|
+
expect(typeof result.gfi.threshold).toBe('number');
|
|
325
|
+
|
|
326
|
+
// Trust structure
|
|
327
|
+
expect(result.trust).toHaveProperty('stage');
|
|
328
|
+
expect(result.trust).toHaveProperty('stageLabel');
|
|
329
|
+
expect(result.trust).toHaveProperty('score');
|
|
330
|
+
expect(typeof result.trust.stage).toBe('number');
|
|
331
|
+
expect(typeof result.trust.stageLabel).toBe('string');
|
|
332
|
+
expect(typeof result.trust.score).toBe('number');
|
|
333
|
+
|
|
334
|
+
// Evolution structure
|
|
335
|
+
expect(result.evolution).toHaveProperty('tier');
|
|
336
|
+
expect(result.evolution).toHaveProperty('points');
|
|
337
|
+
expect(typeof result.evolution.tier).toBe('string');
|
|
338
|
+
expect(typeof result.evolution.points).toBe('number');
|
|
339
|
+
|
|
340
|
+
// PainFlag structure
|
|
341
|
+
expect(result.painFlag).toHaveProperty('active');
|
|
342
|
+
expect(result.painFlag).toHaveProperty('source');
|
|
343
|
+
expect(result.painFlag).toHaveProperty('score');
|
|
344
|
+
|
|
345
|
+
// Principles structure
|
|
346
|
+
expect(result.principles).toHaveProperty('candidate');
|
|
347
|
+
expect(result.principles).toHaveProperty('probation');
|
|
348
|
+
expect(result.principles).toHaveProperty('active');
|
|
349
|
+
expect(result.principles).toHaveProperty('deprecated');
|
|
350
|
+
|
|
351
|
+
// Queue structure
|
|
352
|
+
expect(result.queue).toHaveProperty('pending');
|
|
353
|
+
expect(result.queue).toHaveProperty('inProgress');
|
|
354
|
+
expect(result.queue).toHaveProperty('completed');
|
|
355
|
+
|
|
356
|
+
service.dispose();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ===== Samples Page Tests =====
|
|
361
|
+
|
|
362
|
+
describe('Samples Page - /api/samples', () => {
|
|
363
|
+
it('listSamples returns correct response shape', () => {
|
|
364
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
365
|
+
const result = service.listSamples({});
|
|
366
|
+
|
|
367
|
+
// Top-level fields
|
|
368
|
+
expect(result).toHaveProperty('counters');
|
|
369
|
+
expect(result).toHaveProperty('items');
|
|
370
|
+
expect(result).toHaveProperty('pagination');
|
|
371
|
+
|
|
372
|
+
// Counters is an object
|
|
373
|
+
expect(typeof result.counters).toBe('object');
|
|
374
|
+
|
|
375
|
+
// Items array
|
|
376
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
377
|
+
|
|
378
|
+
// Pagination fields
|
|
379
|
+
expect(result.pagination).toHaveProperty('page');
|
|
380
|
+
expect(result.pagination).toHaveProperty('pageSize');
|
|
381
|
+
expect(result.pagination).toHaveProperty('total');
|
|
382
|
+
expect(result.pagination).toHaveProperty('totalPages');
|
|
383
|
+
expect(typeof result.pagination.page).toBe('number');
|
|
384
|
+
expect(typeof result.pagination.pageSize).toBe('number');
|
|
385
|
+
expect(typeof result.pagination.total).toBe('number');
|
|
386
|
+
expect(typeof result.pagination.totalPages).toBe('number');
|
|
387
|
+
|
|
388
|
+
service.dispose();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('listSamples items have all required fields when populated', () => {
|
|
392
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
393
|
+
if (sql.includes('correction_samples')) {
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
sample_id: 'sample-1',
|
|
397
|
+
session_id: 'session-1',
|
|
398
|
+
review_status: 'pending',
|
|
399
|
+
quality_score: 0.6,
|
|
400
|
+
created_at: '2026-04-09T00:00:00Z',
|
|
401
|
+
updated_at: '2026-04-09T00:00:00Z',
|
|
402
|
+
diff_excerpt: '...',
|
|
403
|
+
failure_mode: 'Read',
|
|
404
|
+
related_thinking_count: 2,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
return [];
|
|
409
|
+
});
|
|
410
|
+
controlUiDbMock.get.mockReturnValue({ count: 1 });
|
|
411
|
+
|
|
412
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
413
|
+
const result = service.listSamples({ pageSize: 1 });
|
|
414
|
+
|
|
415
|
+
if (result.items.length > 0) {
|
|
416
|
+
const item = result.items[0];
|
|
417
|
+
expect(item).toHaveProperty('sampleId');
|
|
418
|
+
expect(item).toHaveProperty('sessionId');
|
|
419
|
+
expect(item).toHaveProperty('reviewStatus');
|
|
420
|
+
expect(item).toHaveProperty('qualityScore');
|
|
421
|
+
expect(item).toHaveProperty('failureMode');
|
|
422
|
+
expect(item).toHaveProperty('relatedThinkingCount');
|
|
423
|
+
expect(item).toHaveProperty('createdAt');
|
|
424
|
+
expect(item).toHaveProperty('updatedAt');
|
|
425
|
+
expect(item).toHaveProperty('diffExcerpt');
|
|
426
|
+
expect(typeof item.sampleId).toBe('string');
|
|
427
|
+
expect(typeof item.sessionId).toBe('string');
|
|
428
|
+
expect(typeof item.reviewStatus).toBe('string');
|
|
429
|
+
expect(typeof item.qualityScore).toBe('number');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
service.dispose();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('Samples Page - /api/samples/:id', () => {
|
|
437
|
+
it('getSampleDetail returns null for non-existent sample', () => {
|
|
438
|
+
controlUiDbMock.get.mockReturnValue(null);
|
|
439
|
+
|
|
440
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
441
|
+
const result = service.getSampleDetail('non-existent-id');
|
|
442
|
+
|
|
443
|
+
expect(result).toBeNull();
|
|
444
|
+
service.dispose();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('getSampleDetail returns correct shape when sample exists', () => {
|
|
448
|
+
controlUiDbMock.get.mockImplementation((sql: string) => {
|
|
449
|
+
if (sql.includes('correction_samples')) {
|
|
450
|
+
return {
|
|
451
|
+
sample_id: 'sample-1',
|
|
452
|
+
session_id: 'session-1',
|
|
453
|
+
review_status: 'pending',
|
|
454
|
+
quality_score: 0.6,
|
|
455
|
+
created_at: '2026-04-09T00:00:00Z',
|
|
456
|
+
updated_at: '2026-04-09T00:00:00Z',
|
|
457
|
+
recovery_tool_span_json: '[{"id":1,"toolName":"Read"}]',
|
|
458
|
+
principle_ids_json: '[]',
|
|
459
|
+
bad_turn_id: 1,
|
|
460
|
+
bad_raw_text: null,
|
|
461
|
+
bad_blob_ref: null,
|
|
462
|
+
bad_sanitized_text: 'bad code',
|
|
463
|
+
bad_created_at: '2026-04-09T00:00:00Z',
|
|
464
|
+
user_turn_id: 2,
|
|
465
|
+
user_raw_text: null,
|
|
466
|
+
user_blob_ref: null,
|
|
467
|
+
user_correction_cue: null,
|
|
468
|
+
user_created_at: '2026-04-09T00:01:00Z',
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
});
|
|
473
|
+
controlUiDbMock.all.mockReturnValue([]);
|
|
474
|
+
|
|
475
|
+
const service = new ControlUiQueryService('/mock/workspace');
|
|
476
|
+
const result = service.getSampleDetail('sample-1');
|
|
477
|
+
|
|
478
|
+
expect(result).not.toBeNull();
|
|
479
|
+
if (result) {
|
|
480
|
+
expect(result).toHaveProperty('sampleId');
|
|
481
|
+
expect(result).toHaveProperty('sessionId');
|
|
482
|
+
expect(result).toHaveProperty('reviewStatus');
|
|
483
|
+
expect(result).toHaveProperty('qualityScore');
|
|
484
|
+
expect(result).toHaveProperty('createdAt');
|
|
485
|
+
expect(result).toHaveProperty('updatedAt');
|
|
486
|
+
expect(result).toHaveProperty('badAttempt');
|
|
487
|
+
expect(result).toHaveProperty('userCorrection');
|
|
488
|
+
expect(result).toHaveProperty('recoveryToolSpan');
|
|
489
|
+
expect(result).toHaveProperty('relatedPrinciples');
|
|
490
|
+
expect(result).toHaveProperty('relatedThinkingHits');
|
|
491
|
+
expect(result).toHaveProperty('reviewHistory');
|
|
492
|
+
|
|
493
|
+
// badAttempt structure
|
|
494
|
+
expect(result.badAttempt).toHaveProperty('assistantTurnId');
|
|
495
|
+
expect(result.badAttempt).toHaveProperty('rawText');
|
|
496
|
+
expect(result.badAttempt).toHaveProperty('sanitizedText');
|
|
497
|
+
expect(result.badAttempt).toHaveProperty('createdAt');
|
|
498
|
+
|
|
499
|
+
// userCorrection structure
|
|
500
|
+
expect(result.userCorrection).toHaveProperty('userTurnId');
|
|
501
|
+
expect(result.userCorrection).toHaveProperty('rawText');
|
|
502
|
+
expect(result.userCorrection).toHaveProperty('correctionCue');
|
|
503
|
+
expect(result.userCorrection).toHaveProperty('createdAt');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
service.dispose();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// ===== Feedback Page Tests =====
|
|
511
|
+
|
|
512
|
+
describe('Feedback Page - /api/feedback/gfi', () => {
|
|
513
|
+
it('getFeedbackGfi returns correct response shape', () => {
|
|
514
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
515
|
+
const result = service.getFeedbackGfi();
|
|
516
|
+
|
|
517
|
+
// Top-level fields from FeedbackGfiResponse
|
|
518
|
+
expect(result).toHaveProperty('current');
|
|
519
|
+
expect(result).toHaveProperty('peakToday');
|
|
520
|
+
expect(result).toHaveProperty('threshold');
|
|
521
|
+
expect(result).toHaveProperty('trend');
|
|
522
|
+
expect(result).toHaveProperty('sources');
|
|
523
|
+
|
|
524
|
+
// Type validation
|
|
525
|
+
expect(typeof result.current).toBe('number');
|
|
526
|
+
expect(typeof result.peakToday).toBe('number');
|
|
527
|
+
expect(typeof result.threshold).toBe('number');
|
|
528
|
+
expect(Array.isArray(result.trend)).toBe(true);
|
|
529
|
+
expect(typeof result.sources).toBe('object');
|
|
530
|
+
|
|
531
|
+
service.dispose();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('getFeedbackGfi trend items have correct shape when populated', () => {
|
|
535
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
536
|
+
if (sql.includes('pain_events')) {
|
|
537
|
+
return [{ hour: '2026-04-09T10:00:00Z', value: 15 }];
|
|
538
|
+
}
|
|
539
|
+
return [];
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
543
|
+
const result = service.getFeedbackGfi();
|
|
544
|
+
|
|
545
|
+
if (result.trend.length > 0) {
|
|
546
|
+
const trendItem = result.trend[0];
|
|
547
|
+
expect(trendItem).toHaveProperty('hour');
|
|
548
|
+
expect(trendItem).toHaveProperty('value');
|
|
549
|
+
expect(typeof trendItem.hour).toBe('string');
|
|
550
|
+
expect(typeof trendItem.value).toBe('number');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
service.dispose();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('Feedback Page - /api/feedback/empathy-events', () => {
|
|
558
|
+
it('getFeedbackEmpathyEvents returns correct response shape', () => {
|
|
559
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
560
|
+
const result = service.getFeedbackEmpathyEvents();
|
|
561
|
+
|
|
562
|
+
expect(Array.isArray(result)).toBe(true);
|
|
563
|
+
|
|
564
|
+
service.dispose();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('getFeedbackEmpathyEvents items have correct fields when populated', () => {
|
|
568
|
+
eventLogMock.getBufferedEvents.mockReturnValue([
|
|
569
|
+
{
|
|
570
|
+
ts: '2026-04-09T10:00:00Z',
|
|
571
|
+
type: 'pain_signal',
|
|
572
|
+
data: { source: 'user_empathy', severity: 'high', score: 8, reason: 'tool failure', origin: 'agent', gfiAfter: 75 },
|
|
573
|
+
},
|
|
574
|
+
]);
|
|
575
|
+
|
|
576
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
577
|
+
const result = service.getFeedbackEmpathyEvents();
|
|
578
|
+
|
|
579
|
+
if (result.length > 0) {
|
|
580
|
+
const event = result[0];
|
|
581
|
+
expect(event).toHaveProperty('timestamp');
|
|
582
|
+
expect(event).toHaveProperty('severity');
|
|
583
|
+
expect(event).toHaveProperty('score');
|
|
584
|
+
expect(event).toHaveProperty('reason');
|
|
585
|
+
expect(event).toHaveProperty('origin');
|
|
586
|
+
expect(event).toHaveProperty('gfiAfter');
|
|
587
|
+
|
|
588
|
+
// Type validation
|
|
589
|
+
expect(typeof event.timestamp).toBe('string');
|
|
590
|
+
expect(typeof event.severity).toBe('string');
|
|
591
|
+
expect(typeof event.score).toBe('number');
|
|
592
|
+
expect(typeof event.reason).toBe('string');
|
|
593
|
+
expect(typeof event.origin).toBe('string');
|
|
594
|
+
expect(typeof event.gfiAfter).toBe('number');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
service.dispose();
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
describe('Feedback Page - /api/feedback/gate-blocks', () => {
|
|
602
|
+
it('getFeedbackGateBlocks returns correct response shape', () => {
|
|
603
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
604
|
+
const result = service.getFeedbackGateBlocks();
|
|
605
|
+
|
|
606
|
+
expect(Array.isArray(result)).toBe(true);
|
|
607
|
+
|
|
608
|
+
service.dispose();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('getFeedbackGateBlocks items have correct fields when populated', () => {
|
|
612
|
+
// Mock gate_blocks table read
|
|
613
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
614
|
+
if (sql.includes('gate_blocks')) {
|
|
615
|
+
return [
|
|
616
|
+
{
|
|
617
|
+
created_at: '2026-04-09T10:00:00Z',
|
|
618
|
+
tool_name: 'Write',
|
|
619
|
+
reason: 'gfi block',
|
|
620
|
+
gfi: 75,
|
|
621
|
+
gfi_after: 80,
|
|
622
|
+
trust_stage: 2,
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
return [];
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
630
|
+
const result = service.getFeedbackGateBlocks();
|
|
631
|
+
|
|
632
|
+
if (result.length > 0) {
|
|
633
|
+
const block = result[0];
|
|
634
|
+
expect(block).toHaveProperty('timestamp');
|
|
635
|
+
expect(block).toHaveProperty('toolName');
|
|
636
|
+
expect(block).toHaveProperty('reason');
|
|
637
|
+
expect(block).toHaveProperty('gfi');
|
|
638
|
+
expect(block).toHaveProperty('trustStage');
|
|
639
|
+
|
|
640
|
+
// Type validation
|
|
641
|
+
expect(typeof block.timestamp).toBe('string');
|
|
642
|
+
expect(typeof block.toolName).toBe('string');
|
|
643
|
+
expect(typeof block.reason).toBe('string');
|
|
644
|
+
expect(typeof block.gfi).toBe('number');
|
|
645
|
+
expect(typeof block.trustStage).toBe('number');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
service.dispose();
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ===== Gate Monitor Page Tests =====
|
|
653
|
+
|
|
654
|
+
describe('Gate Monitor Page - /api/gate/stats', () => {
|
|
655
|
+
it('getGateStats returns correct response shape', () => {
|
|
656
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
657
|
+
const result = service.getGateStats();
|
|
658
|
+
|
|
659
|
+
// Top-level fields from GateStatsResponse
|
|
660
|
+
expect(result).toHaveProperty('today');
|
|
661
|
+
expect(result).toHaveProperty('trust');
|
|
662
|
+
expect(result).toHaveProperty('evolution');
|
|
663
|
+
|
|
664
|
+
// Today structure
|
|
665
|
+
expect(result.today).toHaveProperty('gfiBlocks');
|
|
666
|
+
expect(result.today).toHaveProperty('stageBlocks');
|
|
667
|
+
expect(result.today).toHaveProperty('p03Blocks');
|
|
668
|
+
expect(result.today).toHaveProperty('bypassAttempts');
|
|
669
|
+
expect(result.today).toHaveProperty('p16Exemptions');
|
|
670
|
+
|
|
671
|
+
// Type validation for today counts
|
|
672
|
+
expect(typeof result.today.gfiBlocks).toBe('number');
|
|
673
|
+
expect(typeof result.today.stageBlocks).toBe('number');
|
|
674
|
+
expect(typeof result.today.p03Blocks).toBe('number');
|
|
675
|
+
expect(typeof result.today.bypassAttempts).toBe('number');
|
|
676
|
+
expect(typeof result.today.p16Exemptions).toBe('number');
|
|
677
|
+
|
|
678
|
+
// Trust structure
|
|
679
|
+
expect(result.trust).toHaveProperty('stage');
|
|
680
|
+
expect(result.trust).toHaveProperty('score');
|
|
681
|
+
expect(result.trust).toHaveProperty('status');
|
|
682
|
+
expect(typeof result.trust.stage).toBe('number');
|
|
683
|
+
expect(typeof result.trust.score).toBe('number');
|
|
684
|
+
expect(typeof result.trust.status).toBe('string');
|
|
685
|
+
|
|
686
|
+
// Evolution structure
|
|
687
|
+
expect(result.evolution).toHaveProperty('tier');
|
|
688
|
+
expect(result.evolution).toHaveProperty('points');
|
|
689
|
+
expect(result.evolution).toHaveProperty('status');
|
|
690
|
+
expect(typeof result.evolution.tier).toBe('string');
|
|
691
|
+
expect(typeof result.evolution.points).toBe('number');
|
|
692
|
+
expect(typeof result.evolution.status).toBe('string');
|
|
693
|
+
|
|
694
|
+
service.dispose();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('Gate Monitor Page - /api/gate/blocks', () => {
|
|
699
|
+
it('getGateBlocks returns correct response shape', () => {
|
|
700
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
701
|
+
const result = service.getGateBlocks();
|
|
702
|
+
|
|
703
|
+
expect(Array.isArray(result)).toBe(true);
|
|
704
|
+
|
|
705
|
+
service.dispose();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('getGateBlocks items have all 7 required fields when populated', () => {
|
|
709
|
+
// Mock hasTableColumn to return true for all optional columns
|
|
710
|
+
controlUiDbMock.all.mockImplementation((sql: string) => {
|
|
711
|
+
if (sql.includes('PRAGMA table_info')) {
|
|
712
|
+
return [{ name: 'created_at' }, { name: 'tool_name' }, { name: 'file_path' }, { name: 'reason' }, { name: 'gfi' }, { name: 'gfi_after' }, { name: 'trust_stage' }, { name: 'gate_type' }];
|
|
713
|
+
}
|
|
714
|
+
if (sql.includes('gate_blocks')) {
|
|
715
|
+
return [
|
|
716
|
+
{
|
|
717
|
+
created_at: '2026-04-09T10:00:00Z',
|
|
718
|
+
tool_name: 'Write',
|
|
719
|
+
file_path: '/test/file.ts',
|
|
720
|
+
reason: 'gfi block',
|
|
721
|
+
gfi: 75,
|
|
722
|
+
gfi_after: 80,
|
|
723
|
+
trust_stage: 2,
|
|
724
|
+
gate_type: 'gfi',
|
|
725
|
+
},
|
|
726
|
+
];
|
|
727
|
+
}
|
|
728
|
+
return [];
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const service = new HealthQueryService('/mock/workspace');
|
|
732
|
+
const result = service.getGateBlocks();
|
|
733
|
+
|
|
734
|
+
if (result.length > 0) {
|
|
735
|
+
const block = result[0];
|
|
736
|
+
// 7 required fields from GateBlockItem
|
|
737
|
+
expect(block).toHaveProperty('timestamp');
|
|
738
|
+
expect(block).toHaveProperty('toolName');
|
|
739
|
+
expect(block).toHaveProperty('filePath');
|
|
740
|
+
expect(block).toHaveProperty('reason');
|
|
741
|
+
expect(block).toHaveProperty('gateType');
|
|
742
|
+
expect(block).toHaveProperty('gfi');
|
|
743
|
+
expect(block).toHaveProperty('trustStage');
|
|
744
|
+
|
|
745
|
+
// Type validation
|
|
746
|
+
expect(typeof block.timestamp).toBe('string');
|
|
747
|
+
expect(typeof block.toolName).toBe('string');
|
|
748
|
+
// filePath can be string or null
|
|
749
|
+
expect(typeof block.filePath === 'string' || block.filePath === null).toBe(true);
|
|
750
|
+
expect(typeof block.reason).toBe('string');
|
|
751
|
+
expect(typeof block.gateType).toBe('string');
|
|
752
|
+
expect(typeof block.gfi).toBe('number');
|
|
753
|
+
expect(typeof block.trustStage).toBe('number');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
service.dispose();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// ===== Central Health Page - /api/central/health =====
|
|
761
|
+
|
|
762
|
+
describe('Central Health - /api/central/health', () => {
|
|
763
|
+
it('CentralHealthService returns response with workspaces array and generatedAt', () => {
|
|
764
|
+
const service = new CentralHealthService();
|
|
765
|
+
const result = service.getAllWorkspaceHealth();
|
|
766
|
+
|
|
767
|
+
expect(result).toHaveProperty('workspaces');
|
|
768
|
+
expect(result).toHaveProperty('generatedAt');
|
|
769
|
+
expect(Array.isArray(result.workspaces)).toBe(true);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('CentralHealthService returns one entry per enabled workspace', () => {
|
|
773
|
+
const service = new CentralHealthService();
|
|
774
|
+
const result = service.getAllWorkspaceHealth();
|
|
775
|
+
|
|
776
|
+
const enabledWorkspaces = centralDbMethods.getEnabledWorkspaces();
|
|
777
|
+
expect(result.workspaces.length).toBe(enabledWorkspaces.length);
|
|
778
|
+
|
|
779
|
+
// Each entry has workspaceName and health
|
|
780
|
+
for (const entry of result.workspaces) {
|
|
781
|
+
expect(entry).toHaveProperty('workspaceName');
|
|
782
|
+
expect(entry).toHaveProperty('health');
|
|
783
|
+
expect(typeof entry.workspaceName).toBe('string');
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('CentralHealthService health entries have all required OverviewHealthResponse fields', () => {
|
|
788
|
+
const service = new CentralHealthService();
|
|
789
|
+
const result = service.getAllWorkspaceHealth();
|
|
790
|
+
|
|
791
|
+
for (const entry of result.workspaces) {
|
|
792
|
+
const h = entry.health;
|
|
793
|
+
// GFI fields
|
|
794
|
+
expect(h.gfi).toHaveProperty('current');
|
|
795
|
+
expect(h.gfi).toHaveProperty('peakToday');
|
|
796
|
+
expect(h.gfi).toHaveProperty('threshold');
|
|
797
|
+
expect(typeof h.gfi.current).toBe('number');
|
|
798
|
+
expect(typeof h.gfi.threshold).toBe('number');
|
|
799
|
+
|
|
800
|
+
// Trust fields
|
|
801
|
+
expect(h.trust).toHaveProperty('stage');
|
|
802
|
+
expect(h.trust).toHaveProperty('stageLabel');
|
|
803
|
+
expect(h.trust).toHaveProperty('score');
|
|
804
|
+
expect(typeof h.trust.stage).toBe('number');
|
|
805
|
+
expect(typeof h.trust.stageLabel).toBe('string');
|
|
806
|
+
|
|
807
|
+
// Evolution fields
|
|
808
|
+
expect(h.evolution).toHaveProperty('tier');
|
|
809
|
+
expect(h.evolution).toHaveProperty('points');
|
|
810
|
+
expect(typeof h.evolution.points).toBe('number');
|
|
811
|
+
|
|
812
|
+
// PainFlag fields
|
|
813
|
+
expect(h.painFlag).toHaveProperty('active');
|
|
814
|
+
expect(h.painFlag).toHaveProperty('source');
|
|
815
|
+
expect(typeof h.painFlag.active).toBe('boolean');
|
|
816
|
+
|
|
817
|
+
// Principles fields
|
|
818
|
+
expect(h.principles).toHaveProperty('candidate');
|
|
819
|
+
expect(h.principles).toHaveProperty('probation');
|
|
820
|
+
expect(h.principles).toHaveProperty('active');
|
|
821
|
+
expect(h.principles).toHaveProperty('deprecated');
|
|
822
|
+
|
|
823
|
+
// Queue fields
|
|
824
|
+
expect(h.queue).toHaveProperty('pending');
|
|
825
|
+
expect(h.queue).toHaveProperty('inProgress');
|
|
826
|
+
expect(h.queue).toHaveProperty('completed');
|
|
827
|
+
|
|
828
|
+
// activeStage
|
|
829
|
+
expect(h).toHaveProperty('activeStage');
|
|
830
|
+
expect(typeof h.activeStage).toBe('string');
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
});
|