principles-disciple 1.72.0 → 1.73.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 +10 -5
- package/package.json +17 -19
- package/scripts/acceptance-test.mjs +16 -73
- package/scripts/sync-plugin.mjs +382 -77
- package/src/commands/archive-impl.ts +2 -1
- package/src/commands/capabilities.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/disable-impl.ts +2 -1
- package/src/commands/evolution-status.ts +16 -16
- package/src/commands/export.ts +12 -67
- package/src/commands/pain.ts +91 -1
- package/src/commands/principle-rollback.ts +2 -1
- package/src/commands/promote-impl.ts +7 -43
- package/src/commands/rollback-impl.ts +2 -1
- package/src/commands/rollback.ts +2 -1
- package/src/commands/samples.ts +2 -1
- package/src/commands/thinking-os.ts +2 -1
- package/src/config/errors.ts +18 -2
- package/src/constants/diagnostician.ts +2 -2
- package/src/constants/tools.ts +2 -1
- package/src/core/__tests__/focus-history.test.ts +210 -0
- package/src/core/config.ts +1 -1
- package/src/core/confirm-first-gate.ts +255 -0
- package/src/core/correction-cue-learner.ts +2 -136
- package/src/core/correction-types.ts +16 -88
- package/src/core/dictionary.ts +19 -20
- package/src/core/empathy-keyword-matcher.ts +17 -289
- package/src/core/empathy-types.ts +18 -229
- package/src/core/event-log.ts +38 -132
- package/src/core/evolution-reducer.ts +21 -2
- package/src/core/evolution-types.ts +76 -464
- package/src/core/file-store.ts +80 -0
- package/src/core/focus-history.ts +228 -955
- package/src/core/local-worker-routing.ts +34 -314
- package/src/core/merge-gate-audit.ts +0 -195
- package/src/core/pain-diagnostic-gate.ts +154 -0
- package/src/core/pain-signal.ts +21 -138
- package/src/core/pain.ts +15 -88
- package/src/core/pd-task-reconciler.ts +26 -115
- package/src/core/pd-task-service.ts +9 -9
- package/src/core/pd-task-types.ts +23 -127
- package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
- package/src/core/principle-compiler/code-validator.ts +15 -42
- package/src/core/principle-compiler/compiler.ts +100 -15
- package/src/core/principle-compiler/index.ts +5 -2
- package/src/core/principle-compiler/template-generator.ts +4 -104
- package/src/core/principle-injection.ts +10 -202
- package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
- package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
- package/src/core/principle-tree-ledger-adapter.ts +145 -0
- package/src/core/principle-tree-ledger.ts +8 -6
- package/src/core/reflection/reflection-context.ts +14 -109
- package/src/core/replay-engine.ts +8 -500
- package/src/core/rule-host-helpers.ts +5 -35
- package/src/core/rule-host-types.ts +10 -82
- package/src/core/rule-host.ts +6 -63
- package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
- package/src/core/session-tracker.ts +87 -101
- package/src/core/shadow-observation-registry.ts +19 -48
- package/src/core/trajectory.ts +3 -1
- package/src/core/workflow-funnel-loader.ts +62 -68
- package/src/core/workspace-context.ts +46 -0
- package/src/core/workspace-dir-service.ts +1 -1
- package/src/core/workspace-dir-validation.ts +18 -9
- package/src/hooks/AGENTS.md +1 -1
- package/src/hooks/gate-block-helper.ts +46 -44
- package/src/hooks/gate.ts +207 -7
- package/src/hooks/lifecycle.ts +30 -32
- package/src/hooks/llm.ts +60 -32
- package/src/hooks/pain.ts +297 -103
- package/src/hooks/prompt.ts +459 -439
- package/src/hooks/subagent.ts +2 -29
- package/src/i18n/commands.ts +2 -10
- package/src/index.ts +95 -85
- package/src/openclaw-sdk.ts +311 -0
- package/src/service/central-database.ts +8 -4
- package/src/service/evolution-queue-migration.ts +2 -1
- package/src/service/evolution-worker.ts +163 -1786
- package/src/service/internalization-trigger-adapter.ts +302 -0
- package/src/service/keyword-optimization-service.ts +4 -4
- package/src/service/monitoring-query-service.ts +1 -215
- package/src/service/queue-io.ts +60 -331
- package/src/service/runtime-summary-service.ts +59 -16
- package/src/service/subagent-workflow/index.ts +0 -41
- package/src/service/subagent-workflow/types.ts +9 -120
- package/src/service/subagent-workflow/workflow-store.ts +2 -119
- package/src/service/workflow-watchdog.ts +0 -43
- package/src/types/event-payload.ts +16 -74
- package/src/types/event-types.ts +39 -547
- package/src/types/hygiene-types.ts +7 -30
- package/src/types/principle-tree-schema.ts +20 -222
- package/src/types/queue.ts +15 -70
- package/src/types/runtime-summary.ts +5 -49
- package/src/utils/io.ts +10 -0
- package/src/utils/retry.ts +1 -1
- package/src/utils/shadow-fingerprint.ts +2 -2
- package/src/utils/workspace-resolver.ts +50 -0
- package/templates/langs/en/core/AGENTS.md +2 -2
- package/templates/langs/en/core/BOOT.md +1 -1
- package/templates/langs/en/core/HEARTBEAT.md +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
- package/templates/langs/zh/core/AGENTS.md +2 -2
- package/templates/langs/zh/core/BOOT.md +1 -1
- package/templates/langs/zh/core/HEARTBEAT.md +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
- package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
- package/tests/build-artifacts.test.ts +1 -3
- package/tests/commands/evolution-status.test.ts +0 -118
- package/tests/core/bootstrap-rules.test.ts +1 -1
- package/tests/core/config.test.ts +1 -1
- package/tests/core/event-log.test.ts +35 -0
- package/tests/core/evolution-engine.test.ts +610 -0
- package/tests/core/file-store.test.ts +102 -0
- package/tests/core/focus-history.test.ts +203 -11
- package/tests/core/merge-gate-audit.test.ts +2 -169
- package/tests/core/model-deployment-registry.test.ts +7 -1
- package/tests/core/model-training-registry.test.ts +19 -0
- package/tests/core/observability.test.ts +0 -1
- package/tests/core/pain-diagnostic-gate.test.ts +498 -0
- package/tests/core/pain.test.ts +0 -1
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
- package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
- package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
- package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
- package/tests/core/reflection-context.test.ts +0 -14
- package/tests/core/replay-engine.test.ts +127 -215
- package/tests/core/rule-host-helpers.test.ts +2 -2
- package/tests/core/rule-implementation-runtime.test.ts +0 -27
- package/tests/core/workflow-funnel-loader.test.ts +162 -0
- package/tests/core/workspace-dir-validation.test.ts +8 -1
- package/tests/core-anti-growth.test.ts +192 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
- package/tests/hooks/confirm-first-gate.test.ts +333 -0
- package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
- package/tests/hooks/gate-auto-correct.test.ts +665 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
- package/tests/hooks/pain.test.ts +269 -12
- package/tests/hooks/prompt-characterization.test.ts +500 -0
- package/tests/hooks/prompt-size-guard.test.ts +32 -17
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
- package/tests/index.test.ts +94 -1
- package/tests/integration/auto-entry-gate.test.ts +248 -0
- package/tests/integration/internalization-trigger-guard.test.ts +69 -0
- package/tests/integration/m8-legacy-paths.test.ts +63 -0
- package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
- package/tests/plugin-config-resolution-cutover.test.ts +359 -0
- package/tests/runtime-v2-discovery-guard.test.ts +154 -0
- package/tests/service/central-database.test.ts +457 -0
- package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
- package/tests/service/evolution-worker.timeout.test.ts +11 -129
- package/tests/service/internalization-trigger-adapter.test.ts +251 -0
- package/tests/service/monitoring-query-service.test.ts +1 -47
- package/tests/service/queue-io.test.ts +1 -62
- package/tests/service/runtime-summary-service.test.ts +3 -1
- package/tests/service/workflow-watchdog.test.ts +0 -91
- package/tests/utils/file-lock.test.ts +5 -3
- package/tests/utils/session-key.test.ts +52 -0
- package/tests/utils/subagent-probe.test.ts +48 -1
- package/vitest.config.ts +4 -11
- package/.planning/codebase/ARCHITECTURE.md +0 -157
- package/.planning/codebase/CONCERNS.md +0 -145
- package/.planning/codebase/CONVENTIONS.md +0 -148
- package/.planning/codebase/INTEGRATIONS.md +0 -81
- package/.planning/codebase/STACK.md +0 -87
- package/.planning/codebase/STRUCTURE.md +0 -193
- package/.planning/codebase/TESTING.md +0 -243
- package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
- package/docs/COMMAND_REFERENCE.md +0 -76
- package/docs/COMMAND_REFERENCE_EN.md +0 -79
- package/scripts/build-web.mjs +0 -46
- package/scripts/diagnose-nocturnal.mjs +0 -537
- package/scripts/seed-nocturnal-scenarios.mjs +0 -384
- package/src/commands/nocturnal-review.ts +0 -322
- package/src/commands/nocturnal-rollout.ts +0 -790
- package/src/commands/nocturnal-train.ts +0 -986
- package/src/commands/pd-reflect.ts +0 -88
- package/src/core/adaptive-thresholds.ts +0 -478
- package/src/core/diagnostician-task-store.ts +0 -192
- package/src/core/nocturnal-arbiter.ts +0 -715
- package/src/core/nocturnal-artifact-lineage.ts +0 -116
- package/src/core/nocturnal-artificer.ts +0 -257
- package/src/core/nocturnal-candidate-scoring.ts +0 -530
- package/src/core/nocturnal-compliance.ts +0 -1146
- package/src/core/nocturnal-dataset.ts +0 -763
- package/src/core/nocturnal-executability.ts +0 -428
- package/src/core/nocturnal-export.ts +0 -499
- package/src/core/nocturnal-paths.ts +0 -240
- package/src/core/nocturnal-reasoning-deriver.ts +0 -343
- package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
- package/src/core/nocturnal-snapshot-contract.ts +0 -99
- package/src/core/nocturnal-trajectory-extractor.ts +0 -512
- package/src/core/nocturnal-trinity-types.ts +0 -218
- package/src/core/nocturnal-trinity.ts +0 -2680
- package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
- package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
- package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
- package/src/http/principles-console-route.ts +0 -709
- package/src/service/central-health-service.ts +0 -49
- package/src/service/central-overview-service.ts +0 -138
- package/src/service/control-ui-query-service.ts +0 -900
- package/src/service/cooldown-strategy.ts +0 -97
- package/src/service/evolution-pain-context.ts +0 -79
- package/src/service/evolution-query-service.ts +0 -407
- package/src/service/health-query-service.ts +0 -1038
- package/src/service/nocturnal-config.ts +0 -214
- package/src/service/nocturnal-runtime.ts +0 -734
- package/src/service/nocturnal-service.ts +0 -1605
- package/src/service/nocturnal-target-selector.ts +0 -545
- package/src/service/sleep-cycle.ts +0 -157
- package/src/service/startup-reconciler.ts +0 -112
- package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
- package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
- package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
- package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
- package/src/tools/write-pain-flag.ts +0 -215
- package/tests/commands/nocturnal-review.test.ts +0 -448
- package/tests/commands/nocturnal-train.test.ts +0 -97
- package/tests/commands/pd-reflect.test.ts +0 -49
- package/tests/core/adaptive-thresholds.test.ts +0 -261
- package/tests/core/nocturnal-arbiter.test.ts +0 -559
- package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
- package/tests/core/nocturnal-artificer.test.ts +0 -241
- package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
- package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
- package/tests/core/nocturnal-compliance.test.ts +0 -646
- package/tests/core/nocturnal-dataset.test.ts +0 -892
- package/tests/core/nocturnal-e2e.test.ts +0 -234
- package/tests/core/nocturnal-executability.test.ts +0 -357
- package/tests/core/nocturnal-export.test.ts +0 -517
- package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
- package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
- package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
- package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
- package/tests/core/nocturnal-trinity.test.ts +0 -2053
- package/tests/core/pain-auto-repair.test.ts +0 -96
- package/tests/core/pain-integration.test.ts +0 -510
- package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
- package/tests/http/principles-console-route.test.ts +0 -162
- package/tests/integration/chaos-resilience.test.ts +0 -348
- package/tests/integration/empathy-workflow-integration.test.ts +0 -626
- package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
- package/tests/service/control-ui-query-service.test.ts +0 -121
- package/tests/service/cooldown-strategy.test.ts +0 -164
- package/tests/service/data-endpoints-regression.test.ts +0 -834
- package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
- package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
- package/tests/service/nocturnal-runtime.test.ts +0 -473
- package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
- package/tests/service/nocturnal-target-selector.test.ts +0 -615
- package/tests/service/startup-reconciler.test.ts +0 -148
- package/tests/tools/write-pain-flag.test.ts +0 -358
- package/ui/src/App.tsx +0 -45
- package/ui/src/api.ts +0 -220
- package/ui/src/charts.tsx +0 -955
- package/ui/src/components/ErrorState.tsx +0 -6
- package/ui/src/components/Loading.tsx +0 -13
- package/ui/src/components/ProtectedRoute.tsx +0 -12
- package/ui/src/components/Shell.tsx +0 -91
- package/ui/src/components/WorkspaceConfig.tsx +0 -178
- package/ui/src/components/index.ts +0 -5
- package/ui/src/context/auth.tsx +0 -80
- package/ui/src/context/theme.tsx +0 -66
- package/ui/src/hooks/useAutoRefresh.ts +0 -39
- package/ui/src/i18n/ui.ts +0 -473
- package/ui/src/main.tsx +0 -16
- package/ui/src/pages/EvolutionPage.tsx +0 -333
- package/ui/src/pages/FeedbackPage.tsx +0 -138
- package/ui/src/pages/GateMonitorPage.tsx +0 -136
- package/ui/src/pages/LoginPage.tsx +0 -89
- package/ui/src/pages/OverviewPage.tsx +0 -599
- package/ui/src/pages/SamplesPage.tsx +0 -174
- package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
- package/ui/src/styles.css +0 -2020
- package/ui/src/types.ts +0 -384
- package/ui/src/utils/format.ts +0 -15
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { rmSync, mkdirSync, readdirSync, existsSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
|
|
7
|
+
import { CentralDatabase, getCentralDatabase, resetCentralDatabase } from '../../src/service/central-database.js';
|
|
8
|
+
|
|
9
|
+
// ── Home dir ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const REAL_HOME = os.homedir();
|
|
12
|
+
const OPENCLAW_DIR = join(REAL_HOME, '.openclaw');
|
|
13
|
+
const CENTRAL_DB_DIR = '.central';
|
|
14
|
+
|
|
15
|
+
/** Each test gets its own DB file via PD_CENTRAL_DB_PATH env var. */
|
|
16
|
+
let tempDbDir = '';
|
|
17
|
+
|
|
18
|
+
function makeTempDbPath(): string {
|
|
19
|
+
const dir = join(os.tmpdir(), `pd-cdb-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
return join(dir, 'aggregated.db');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cleanupTestDirs() {
|
|
25
|
+
// Clean up workspace dirs (cdb-test-* names) and .central/
|
|
26
|
+
if (existsSync(OPENCLAW_DIR)) {
|
|
27
|
+
for (const entry of readdirSync(OPENCLAW_DIR)) {
|
|
28
|
+
if (entry.startsWith('cdb-test-') || entry === '.central') {
|
|
29
|
+
try {
|
|
30
|
+
rmSync(join(OPENCLAW_DIR, entry), { recursive: true, force: true });
|
|
31
|
+
} catch { /* noop */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Clean up temp DB dirs from this run
|
|
36
|
+
if (tempDbDir) {
|
|
37
|
+
try { rmSync(tempDbDir, { recursive: true, force: true }); } catch { /* noop */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
cleanupTestDirs();
|
|
43
|
+
const dbPath = makeTempDbPath();
|
|
44
|
+
tempDbDir = dirname(dbPath);
|
|
45
|
+
process.env.PD_CENTRAL_DB_PATH = dbPath;
|
|
46
|
+
resetCentralDatabase();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
resetCentralDatabase();
|
|
51
|
+
delete process.env.PD_CENTRAL_DB_PATH;
|
|
52
|
+
cleanupTestDirs();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeWorkspaceName(prefix: string) {
|
|
56
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Trajectory DB helper ────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function makeTrajectoryDb(workspaceName: string) {
|
|
62
|
+
// openclawDir = ~/.openclaw, so workspace dir is ~/.openclaw/<workspaceName>
|
|
63
|
+
const wsDir = join(OPENCLAW_DIR, workspaceName);
|
|
64
|
+
mkdirSync(wsDir, { recursive: true });
|
|
65
|
+
const stateDir = join(wsDir, '.state');
|
|
66
|
+
mkdirSync(stateDir, { recursive: true });
|
|
67
|
+
const db = new Database(join(stateDir, 'trajectory.db'));
|
|
68
|
+
db.exec(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS tool_calls (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, outcome TEXT NOT NULL, duration_ms INTEGER, error_type TEXT, error_message TEXT, created_at TEXT NOT NULL);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS pain_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, source TEXT NOT NULL, score REAL NOT NULL, reason TEXT, created_at TEXT NOT NULL);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS user_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, correction_cue TEXT, correction_detected INTEGER DEFAULT 0, created_at TEXT NOT NULL);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS principle_events (id INTEGER PRIMARY KEY AUTOINCREMENT, principle_id TEXT, event_type TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS thinking_model_events (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, model_id TEXT NOT NULL, matched_pattern TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
75
|
+
CREATE TABLE IF NOT EXISTS correction_samples (sample_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, bad_assistant_turn_id INTEGER NOT NULL, quality_score REAL, review_status TEXT, created_at TEXT NOT NULL);
|
|
76
|
+
CREATE TABLE IF NOT EXISTS task_outcomes (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, task_id TEXT, outcome TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
77
|
+
`);
|
|
78
|
+
return { db, wsDir };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function seedSessions(db: Database.Database, count: number) {
|
|
82
|
+
for (let i = 0; i < count; i++) {
|
|
83
|
+
db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
|
|
84
|
+
.run(`sess-${i}`, '2026-04-01T00:00:00Z', '2026-04-01T00:10:00Z');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function seedToolCalls(db: Database.Database, successes = 0, failures = 0, errorType = 'ENOENT') {
|
|
89
|
+
for (let i = 0; i < successes; i++) {
|
|
90
|
+
db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
91
|
+
.run('sess-0', 'write_file', 'success', 100, '2026-04-01T00:00:00Z');
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < failures; i++) {
|
|
94
|
+
db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, error_type, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
95
|
+
.run('sess-0', 'bash', 'failure', 100, errorType, '2026-04-01T00:00:00Z');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function seedPainEvents(db: Database.Database, count: number) {
|
|
100
|
+
for (let i = 0; i < count; i++) {
|
|
101
|
+
db.prepare('INSERT INTO pain_events (session_id, source, score, reason, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
102
|
+
.run('sess-0', 'tool_failure', 75, 'test', '2026-04-01T00:00:00Z');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function seedCorrections(db: Database.Database, count: number) {
|
|
107
|
+
for (let i = 0; i < count; i++) {
|
|
108
|
+
db.prepare('INSERT INTO user_turns (session_id, correction_cue, correction_detected, created_at) VALUES (?, ?, 1, ?)')
|
|
109
|
+
.run('sess-0', 'stop doing that', '2026-04-01T00:00:00Z');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function seedPrincipleEvents(db: Database.Database, count: number) {
|
|
114
|
+
for (let i = 0; i < count; i++) {
|
|
115
|
+
db.prepare('INSERT INTO principle_events (principle_id, event_type, created_at) VALUES (?, ?, ?)')
|
|
116
|
+
.run(`p-${i}`, 'candidate_created', '2026-04-01T00:00:00Z');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function seedThinkingEvents(db: Database.Database, count: number) {
|
|
121
|
+
for (let i = 0; i < count; i++) {
|
|
122
|
+
db.prepare('INSERT INTO thinking_model_events (session_id, model_id, matched_pattern, created_at) VALUES (?, ?, ?, ?)')
|
|
123
|
+
.run('sess-0', 'claude-3-5-sonnet', 'error_pattern', '2026-04-01T00:00:00Z');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function seedSamples(db: Database.Database) {
|
|
128
|
+
db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
129
|
+
.run('s1', 'sess-0', 1, 0.9, 'pending', '2026-04-01T00:00:00Z');
|
|
130
|
+
db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
131
|
+
.run('s2', 'sess-0', 1, 0.7, 'approved', '2026-04-01T00:00:00Z');
|
|
132
|
+
db.prepare('INSERT INTO correction_samples (sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
133
|
+
.run('s3', 'sess-0', 1, 0.5, 'rejected', '2026-04-01T00:00:00Z');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function seedTaskOutcomes(db: Database.Database, count: number) {
|
|
137
|
+
for (let i = 0; i < count; i++) {
|
|
138
|
+
db.prepare('INSERT INTO task_outcomes (session_id, task_id, outcome, created_at) VALUES (?, ?, ?, ?)')
|
|
139
|
+
.run('sess-0', `task-${i}`, 'success', '2026-04-01T00:00:00Z');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe('CentralDatabase', () => {
|
|
146
|
+
test('creates schema on construction', () => {
|
|
147
|
+
const db = new CentralDatabase();
|
|
148
|
+
expect(() => db.getOverviewStats()).not.toThrow();
|
|
149
|
+
db.dispose();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('isClosed reflects disposal state', () => {
|
|
153
|
+
const db = new CentralDatabase();
|
|
154
|
+
expect(db.isClosed).toBe(false);
|
|
155
|
+
db.dispose();
|
|
156
|
+
expect(db.isClosed).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('dispose prevents further operations', () => {
|
|
160
|
+
const db = new CentralDatabase();
|
|
161
|
+
db.dispose();
|
|
162
|
+
expect(() => db.getOverviewStats()).toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('getOverviewStats returns zeros when no data', () => {
|
|
166
|
+
const db = new CentralDatabase();
|
|
167
|
+
const stats = db.getOverviewStats();
|
|
168
|
+
expect(stats.totalSessions).toBe(0);
|
|
169
|
+
expect(stats.totalToolCalls).toBe(0);
|
|
170
|
+
expect(stats.totalFailures).toBe(0);
|
|
171
|
+
expect(stats.totalPainEvents).toBe(0);
|
|
172
|
+
db.dispose();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('getTopRegressions returns empty array when no failures', () => {
|
|
176
|
+
const db = new CentralDatabase();
|
|
177
|
+
expect(db.getTopRegressions()).toEqual([]);
|
|
178
|
+
db.dispose();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('addCustomWorkspace inserts and enables workspace', () => {
|
|
182
|
+
const db = new CentralDatabase();
|
|
183
|
+
db.addCustomWorkspace('my-ws', '/custom/path');
|
|
184
|
+
const configs = db.getWorkspaceConfigs();
|
|
185
|
+
const found = configs.find(c => c.workspaceName === 'my-ws');
|
|
186
|
+
expect(found).toBeDefined();
|
|
187
|
+
expect(found?.enabled).toBe(true);
|
|
188
|
+
expect(found?.syncEnabled).toBe(true);
|
|
189
|
+
db.dispose();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('updateWorkspaceConfig updates displayName and enabled', () => {
|
|
193
|
+
const db = new CentralDatabase();
|
|
194
|
+
db.addCustomWorkspace('cfg-ws', '/path');
|
|
195
|
+
db.updateWorkspaceConfig('cfg-ws', { displayName: 'Display', enabled: false });
|
|
196
|
+
const configs = db.getWorkspaceConfigs();
|
|
197
|
+
const found = configs.find(c => c.workspaceName === 'cfg-ws');
|
|
198
|
+
expect(found?.displayName).toBe('Display');
|
|
199
|
+
expect(found?.enabled).toBe(false);
|
|
200
|
+
db.dispose();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('isWorkspaceEnabled returns true when no config', () => {
|
|
204
|
+
const db = new CentralDatabase();
|
|
205
|
+
expect(db.isWorkspaceEnabled('unknown')).toBe(true);
|
|
206
|
+
db.dispose();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('isWorkspaceEnabled returns false when disabled', () => {
|
|
210
|
+
const db = new CentralDatabase();
|
|
211
|
+
db.addCustomWorkspace('dis-ws', '/path');
|
|
212
|
+
db.updateWorkspaceConfig('dis-ws', { enabled: false });
|
|
213
|
+
expect(db.isWorkspaceEnabled('dis-ws')).toBe(false);
|
|
214
|
+
db.dispose();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('removeWorkspace disables workspace', () => {
|
|
218
|
+
const db = new CentralDatabase();
|
|
219
|
+
db.addCustomWorkspace('rem-ws', '/path');
|
|
220
|
+
db.removeWorkspace('rem-ws');
|
|
221
|
+
expect(db.isWorkspaceEnabled('rem-ws')).toBe(false);
|
|
222
|
+
db.dispose();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('setGlobalConfig and getGlobalConfig round-trip', () => {
|
|
226
|
+
const db = new CentralDatabase();
|
|
227
|
+
db.setGlobalConfig('key', 'value');
|
|
228
|
+
expect(db.getGlobalConfig('key')).toBe('value');
|
|
229
|
+
db.setGlobalConfig('key', 'value2');
|
|
230
|
+
expect(db.getGlobalConfig('key')).toBe('value2');
|
|
231
|
+
expect(db.getGlobalConfig('missing')).toBeNull();
|
|
232
|
+
db.dispose();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('clearAll deletes all aggregated data', () => {
|
|
236
|
+
const db = new CentralDatabase();
|
|
237
|
+
db.clearAll();
|
|
238
|
+
expect(db.getOverviewStats().totalSessions).toBe(0);
|
|
239
|
+
db.dispose();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── syncWorkspace ─────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
test('syncWorkspace throws for unknown workspace', () => {
|
|
245
|
+
const db = new CentralDatabase();
|
|
246
|
+
expect(() => db.syncWorkspace('nonexistent')).toThrow();
|
|
247
|
+
db.dispose();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('syncWorkspace returns 0 when no trajectory.db', () => {
|
|
251
|
+
const wsName = makeWorkspaceName('cdb-test-empty');
|
|
252
|
+
const db = new CentralDatabase();
|
|
253
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
254
|
+
// Create workspace dir without trajectory.db
|
|
255
|
+
mkdirSync(join(OPENCLAW_DIR, wsName, '.state'), { recursive: true });
|
|
256
|
+
expect(db.syncWorkspace(wsName)).toBe(0);
|
|
257
|
+
db.dispose();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('syncWorkspace syncs sessions', () => {
|
|
261
|
+
const wsName = makeWorkspaceName('cdb-test-sessions');
|
|
262
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
263
|
+
seedSessions(trajDb, 5);
|
|
264
|
+
trajDb.close();
|
|
265
|
+
const db = new CentralDatabase();
|
|
266
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
267
|
+
db.syncWorkspace(wsName);
|
|
268
|
+
expect(db.getOverviewStats().totalSessions).toBeGreaterThanOrEqual(5);
|
|
269
|
+
db.dispose();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('syncWorkspace syncs tool calls with failures', () => {
|
|
273
|
+
const wsName = makeWorkspaceName('cdb-test-tools');
|
|
274
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
275
|
+
seedSessions(trajDb, 1);
|
|
276
|
+
seedToolCalls(trajDb, 3, 2, 'ENOENT');
|
|
277
|
+
trajDb.close();
|
|
278
|
+
const db = new CentralDatabase();
|
|
279
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
280
|
+
db.syncWorkspace(wsName);
|
|
281
|
+
const stats = db.getOverviewStats();
|
|
282
|
+
expect(stats.totalToolCalls).toBeGreaterThanOrEqual(5);
|
|
283
|
+
expect(stats.totalFailures).toBeGreaterThanOrEqual(2);
|
|
284
|
+
db.dispose();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('syncWorkspace syncs pain events', () => {
|
|
288
|
+
const wsName = makeWorkspaceName('cdb-test-pain');
|
|
289
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
290
|
+
seedSessions(trajDb, 1);
|
|
291
|
+
seedPainEvents(trajDb, 4);
|
|
292
|
+
trajDb.close();
|
|
293
|
+
const db = new CentralDatabase();
|
|
294
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
295
|
+
db.syncWorkspace(wsName);
|
|
296
|
+
expect(db.getOverviewStats().totalPainEvents).toBeGreaterThanOrEqual(4);
|
|
297
|
+
db.dispose();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('syncWorkspace syncs user corrections', () => {
|
|
301
|
+
const wsName = makeWorkspaceName('cdb-test-corr');
|
|
302
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
303
|
+
seedSessions(trajDb, 1);
|
|
304
|
+
seedCorrections(trajDb, 3);
|
|
305
|
+
trajDb.close();
|
|
306
|
+
const db = new CentralDatabase();
|
|
307
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
308
|
+
db.syncWorkspace(wsName);
|
|
309
|
+
expect(db.getOverviewStats().totalCorrections).toBeGreaterThanOrEqual(3);
|
|
310
|
+
db.dispose();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('syncWorkspace syncs principle events', () => {
|
|
314
|
+
const wsName = makeWorkspaceName('cdb-test-principle');
|
|
315
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
316
|
+
seedSessions(trajDb, 1);
|
|
317
|
+
seedPrincipleEvents(trajDb, 2);
|
|
318
|
+
trajDb.close();
|
|
319
|
+
const db = new CentralDatabase();
|
|
320
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
321
|
+
db.syncWorkspace(wsName);
|
|
322
|
+
expect(db.getPrincipleEventCount()).toBeGreaterThanOrEqual(2);
|
|
323
|
+
db.dispose();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('syncWorkspace syncs thinking model events', () => {
|
|
327
|
+
const wsName = makeWorkspaceName('cdb-test-thinking');
|
|
328
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
329
|
+
seedSessions(trajDb, 1);
|
|
330
|
+
seedThinkingEvents(trajDb, 3);
|
|
331
|
+
trajDb.close();
|
|
332
|
+
const db = new CentralDatabase();
|
|
333
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
334
|
+
db.syncWorkspace(wsName);
|
|
335
|
+
expect(db.getThinkingModelStats().totalModels).toBeGreaterThanOrEqual(1);
|
|
336
|
+
db.dispose();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('syncWorkspace syncs correction samples with review_status', () => {
|
|
340
|
+
const wsName = makeWorkspaceName('cdb-test-samples');
|
|
341
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
342
|
+
seedSessions(trajDb, 1);
|
|
343
|
+
seedSamples(trajDb);
|
|
344
|
+
trajDb.close();
|
|
345
|
+
const db = new CentralDatabase();
|
|
346
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
347
|
+
db.syncWorkspace(wsName);
|
|
348
|
+
const counters = db.getSampleCountersByStatus();
|
|
349
|
+
expect(counters['pending']).toBeGreaterThanOrEqual(1);
|
|
350
|
+
expect(counters['approved']).toBeGreaterThanOrEqual(1);
|
|
351
|
+
expect(counters['rejected']).toBeGreaterThanOrEqual(1);
|
|
352
|
+
db.dispose();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('syncWorkspace syncs task outcomes', () => {
|
|
356
|
+
const wsName = makeWorkspaceName('cdb-test-tasks');
|
|
357
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
358
|
+
seedSessions(trajDb, 1);
|
|
359
|
+
seedTaskOutcomes(trajDb, 4);
|
|
360
|
+
trajDb.close();
|
|
361
|
+
const db = new CentralDatabase();
|
|
362
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
363
|
+
db.syncWorkspace(wsName);
|
|
364
|
+
expect(db.getTaskOutcomes()).toBeGreaterThanOrEqual(4);
|
|
365
|
+
db.dispose();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('syncWorkspace updates last_sync timestamp', () => {
|
|
369
|
+
const wsName = makeWorkspaceName('cdb-test-sync');
|
|
370
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
371
|
+
seedSessions(trajDb, 1);
|
|
372
|
+
trajDb.close();
|
|
373
|
+
const db = new CentralDatabase();
|
|
374
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
375
|
+
db.syncWorkspace(wsName);
|
|
376
|
+
const ws = db.getWorkspaces().find(w => w.name === wsName);
|
|
377
|
+
expect(ws?.lastSync).not.toBeNull();
|
|
378
|
+
db.dispose();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('getTopRegressions returns error types ordered by occurrences', () => {
|
|
382
|
+
const wsName = makeWorkspaceName('cdb-test-regr');
|
|
383
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
384
|
+
seedSessions(trajDb, 1);
|
|
385
|
+
trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
386
|
+
.run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:00:00Z');
|
|
387
|
+
trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
388
|
+
.run('sess-0', 'bash', 'failure', 'ENOENT', 100, '2026-04-01T00:01:00Z');
|
|
389
|
+
trajDb.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, error_type, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
390
|
+
.run('sess-0', 'bash', 'failure', 'EPERM', 100, '2026-04-01T00:02:00Z');
|
|
391
|
+
trajDb.close();
|
|
392
|
+
const db = new CentralDatabase();
|
|
393
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
394
|
+
db.syncWorkspace(wsName);
|
|
395
|
+
const regressions = db.getTopRegressions(5);
|
|
396
|
+
expect(regressions[0].errorType).toBe('ENOENT');
|
|
397
|
+
expect(regressions[0].occurrences).toBe(2);
|
|
398
|
+
db.dispose();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('getSamplePreview returns recent pending/approved samples', () => {
|
|
402
|
+
const wsName = makeWorkspaceName('cdb-test-preview');
|
|
403
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
404
|
+
seedSessions(trajDb, 1);
|
|
405
|
+
seedSamples(trajDb);
|
|
406
|
+
trajDb.close();
|
|
407
|
+
const db = new CentralDatabase();
|
|
408
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
409
|
+
db.syncWorkspace(wsName);
|
|
410
|
+
const preview = db.getSamplePreview(5);
|
|
411
|
+
expect(preview.length).toBeGreaterThanOrEqual(2);
|
|
412
|
+
db.dispose();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('getMostRecentSync returns null when no sync', () => {
|
|
416
|
+
const db = new CentralDatabase();
|
|
417
|
+
expect(db.getMostRecentSync()).toBeNull();
|
|
418
|
+
db.dispose();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('getMostRecentSync returns timestamp after sync', () => {
|
|
422
|
+
const wsName = makeWorkspaceName('cdb-test-recent');
|
|
423
|
+
const { db: trajDb } = makeTrajectoryDb(wsName);
|
|
424
|
+
seedSessions(trajDb, 1);
|
|
425
|
+
trajDb.close();
|
|
426
|
+
const db = new CentralDatabase();
|
|
427
|
+
db.addCustomWorkspace(wsName, join(OPENCLAW_DIR, wsName));
|
|
428
|
+
db.syncWorkspace(wsName);
|
|
429
|
+
expect(db.getMostRecentSync()).not.toBeNull();
|
|
430
|
+
db.dispose();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ── Singleton ────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
test('getCentralDatabase returns same instance', () => {
|
|
436
|
+
const db1 = getCentralDatabase();
|
|
437
|
+
const db2 = getCentralDatabase();
|
|
438
|
+
expect(db1).toBe(db2);
|
|
439
|
+
resetCentralDatabase();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('resetCentralDatabase clears instance', () => {
|
|
443
|
+
const db1 = getCentralDatabase();
|
|
444
|
+
resetCentralDatabase();
|
|
445
|
+
const db2 = getCentralDatabase();
|
|
446
|
+
expect(db1).not.toBe(db2);
|
|
447
|
+
resetCentralDatabase();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('getCentralDatabase reopens after close', () => {
|
|
451
|
+
const db1 = getCentralDatabase();
|
|
452
|
+
db1.dispose();
|
|
453
|
+
const db2 = getCentralDatabase();
|
|
454
|
+
expect(db2.isClosed).toBe(false);
|
|
455
|
+
resetCentralDatabase();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
6
|
+
import { ConfigService } from '../../src/core/config-service.js';
|
|
7
|
+
|
|
8
|
+
// Shared mocks
|
|
9
|
+
const mockLearner = {
|
|
10
|
+
getStore: vi.fn(() => ({ keywords: [{ term: 'wrong', weight: 0.5 }] })),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const mockDb = {
|
|
14
|
+
listRecentSessions: vi.fn(() => [{ sessionId: 'session-1' }]),
|
|
15
|
+
listUserTurnsForSession: vi.fn(() => [{ rawExcerpt: 'User said wrong input', correctionDetected: true, correctionCue: 'wrong' }]),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockOptimizationService = {
|
|
19
|
+
buildTrajectoryHistory: vi.fn(async () => [
|
|
20
|
+
{ sessionId: 'session-1', timestamp: 'now', term: 'wrong', userMessage: '' }
|
|
21
|
+
]),
|
|
22
|
+
applyResult: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Mock dependencies
|
|
26
|
+
vi.mock('../../src/core/correction-cue-learner.js', () => ({
|
|
27
|
+
CorrectionCueLearner: { get: vi.fn(() => mockLearner) },
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('../../src/core/trajectory.js', () => ({
|
|
31
|
+
TrajectoryRegistry: {
|
|
32
|
+
get: vi.fn(() => mockDb),
|
|
33
|
+
clear: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('../../src/service/keyword-optimization-service.js', () => ({
|
|
38
|
+
KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock principles-core runtime-v2 observer/scheduler classes
|
|
42
|
+
const mockDispatch = vi.fn().mockResolvedValue({
|
|
43
|
+
updated: true,
|
|
44
|
+
summary: 'Keyword store optimized',
|
|
45
|
+
updates: { wrong: { action: 'update', weight: 0.4, reasoning: 'slightly high FP' } }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const mockRegister = vi.fn();
|
|
49
|
+
|
|
50
|
+
vi.mock('@principles/core/runtime-v2', () => {
|
|
51
|
+
return {
|
|
52
|
+
WorkflowFunnelLoader: class {
|
|
53
|
+
getFunnel = vi.fn(() => ({
|
|
54
|
+
policy: {
|
|
55
|
+
runtimeKind: 'pi-ai',
|
|
56
|
+
provider: 'anthropic',
|
|
57
|
+
model: 'anthropic/claude-3-5-sonnet',
|
|
58
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
59
|
+
timeoutMs: 30000,
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
PiAiRuntimeAdapter: class {},
|
|
64
|
+
CorrectionObserver: class {},
|
|
65
|
+
AgentScheduler: class {
|
|
66
|
+
register = mockRegister;
|
|
67
|
+
dispatch = mockDispatch;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Import EvolutionWorkerService
|
|
73
|
+
import { EvolutionWorkerService } from '../../src/service/evolution-worker.js';
|
|
74
|
+
import { safeRmDir } from '../test-utils.js';
|
|
75
|
+
|
|
76
|
+
function createMockApi() {
|
|
77
|
+
return {
|
|
78
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
79
|
+
runtime: {
|
|
80
|
+
agent: { runEmbeddedPiAgent: vi.fn() },
|
|
81
|
+
system: {
|
|
82
|
+
requestHeartbeatNow: vi.fn(),
|
|
83
|
+
runHeartbeatOnce: vi.fn(),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as any;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fastPollConfig = { get: (k: string) => k === 'intervals.worker_poll_ms' ? 1000 : undefined };
|
|
90
|
+
|
|
91
|
+
describe('EvolutionWorkerService Correction Observer Integration', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
EvolutionWorkerService.api = null;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
vi.useRealTimers();
|
|
100
|
+
EvolutionWorkerService.api = null;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('runs Correction Observer on heartbeat and applies updates when configured', async () => {
|
|
104
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-worker-corr-'));
|
|
105
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
106
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// Initialize an empty queue to avoid processing actual queue items
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(stateDir, 'evolution_queue.json'),
|
|
111
|
+
JSON.stringify([], null, 2),
|
|
112
|
+
'utf8'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Initialize pain settings with fast poll interval
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(stateDir, 'pain_settings.json'),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
intervals: {
|
|
120
|
+
worker_poll_ms: 1000
|
|
121
|
+
}
|
|
122
|
+
}, null, 2),
|
|
123
|
+
'utf8'
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Invalidate workspace cache to load the newly written settings
|
|
127
|
+
WorkspaceContext.clearCache();
|
|
128
|
+
ConfigService.reset();
|
|
129
|
+
|
|
130
|
+
const mockApi = createMockApi();
|
|
131
|
+
EvolutionWorkerService.api = mockApi;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
EvolutionWorkerService.start({
|
|
135
|
+
workspaceDir,
|
|
136
|
+
stateDir,
|
|
137
|
+
logger: mockApi.logger,
|
|
138
|
+
config: fastPollConfig,
|
|
139
|
+
api: mockApi,
|
|
140
|
+
} as any);
|
|
141
|
+
|
|
142
|
+
// 1. Advance to startup timer (5000ms) and wait for microtasks to settle
|
|
143
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
144
|
+
for (let i = 0; i < 20; i++) {
|
|
145
|
+
await Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. Advance past the poll interval (1000ms) to trigger runCycle
|
|
149
|
+
await vi.advanceTimersByTimeAsync(1050);
|
|
150
|
+
for (let i = 0; i < 20; i++) {
|
|
151
|
+
await Promise.resolve();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Verify that the scheduler dispatch was called with correct payload
|
|
155
|
+
expect(mockRegister).toHaveBeenCalled();
|
|
156
|
+
expect(mockDispatch).toHaveBeenCalledWith('correction-observer', expect.objectContaining({
|
|
157
|
+
parentSessionId: 'evolution-worker',
|
|
158
|
+
workspaceDir,
|
|
159
|
+
recentMessages: ['User said wrong input'],
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Verify updates were applied using KeywordOptimizationService
|
|
163
|
+
expect(mockOptimizationService.applyResult).toHaveBeenCalledWith(expect.objectContaining({
|
|
164
|
+
updated: true,
|
|
165
|
+
summary: 'Keyword store optimized',
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
} finally {
|
|
169
|
+
EvolutionWorkerService.stop!({ workspaceDir, stateDir, logger: console } as any);
|
|
170
|
+
safeRmDir(workspaceDir);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|