principles-disciple 1.72.0 → 1.74.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/INSTALL.md +1 -3
- 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/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 +29 -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/migration.ts +0 -1
- 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/path-resolver.ts +0 -1
- package/src/core/paths.ts +0 -1
- 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 +71 -64
- package/src/hooks/gate.ts +183 -31
- 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 +400 -440
- 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 +38 -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 +8 -20
- 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 +7 -7
- package/templates/langs/en/core/BOOT.md +1 -1
- package/templates/langs/en/core/HEARTBEAT.md +2 -2
- package/templates/langs/en/principles/THINKING_OS.md +3 -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/scripts/run.mjs +51 -15
- package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
- 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 +2 -3
- 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 +7 -7
- package/templates/langs/zh/core/BOOT.md +1 -1
- package/templates/langs/zh/core/HEARTBEAT.md +2 -2
- package/templates/langs/zh/principles/THINKING_OS.md +3 -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/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 +4 -4
- 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 +2 -3
- 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/migration.test.ts +7 -7
- 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/path-resolver.test.ts +1 -1
- package/tests/core/paths-refactor.test.ts +0 -22
- 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-context.test.ts +2 -2
- package/tests/core/workspace-dir-validation.test.ts +8 -1
- package/tests/core-anti-growth.test.ts +191 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
- package/tests/hooks/confirm-first-removal.test.ts +188 -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-no-path-write-tool.test.ts +172 -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/templates/langs/en/skills/plan-script/SKILL.md +0 -32
- package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
- 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
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pain → Diagnostician Loop E2E Tests
|
|
3
|
-
*
|
|
4
|
-
* PURPOSE: Verify business invariants in the Pain → Diagnostician loop.
|
|
5
|
-
* These tests are designed to DISCOVER bugs, not just confirm existing behavior.
|
|
6
|
-
*
|
|
7
|
-
* DESIGN PRINCIPLES:
|
|
8
|
-
* 1. Use real file system (no mocks for I/O)
|
|
9
|
-
* 2. Test business invariants, not implementation details
|
|
10
|
-
* 3. Use independent Oracle data sources for verification
|
|
11
|
-
* 4. Test resilience (corruption recovery, concurrency safety)
|
|
12
|
-
*
|
|
13
|
-
* DATA FLOW:
|
|
14
|
-
* after_tool_call (hooks/pain.ts)
|
|
15
|
-
* → writePainFlag → .state/.pain_flag
|
|
16
|
-
* Evolution Worker (service/evolution-worker.ts)
|
|
17
|
-
* → enqueues pain_diagnosis task → evolution_queue.json
|
|
18
|
-
* → addDiagnosticianTask → diagnostician_tasks.json
|
|
19
|
-
* before_prompt_build (hooks/prompt.ts)
|
|
20
|
-
* → getPendingDiagnosticianTasks → inject into prompt
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
24
|
-
import * as fs from 'fs';
|
|
25
|
-
import * as os from 'os';
|
|
26
|
-
import * as path from 'path';
|
|
27
|
-
import {
|
|
28
|
-
buildPainFlag,
|
|
29
|
-
writePainFlag,
|
|
30
|
-
readPainFlagData,
|
|
31
|
-
validatePainFlag,
|
|
32
|
-
} from '../../src/core/pain.js';
|
|
33
|
-
import { getPendingDiagnosticianTasks, addDiagnosticianTask } from '../../src/core/diagnostician-task-store.js';
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
36
|
-
// Helper functions
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function createTestWorkspace(): { workspaceDir: string; stateDir: string } {
|
|
40
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-pain-'));
|
|
41
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
42
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
43
|
-
fs.mkdirSync(path.join(workspaceDir, '.principles'), { recursive: true });
|
|
44
|
-
return { workspaceDir, stateDir };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function cleanupWorkspace(workspaceDir: string): void {
|
|
48
|
-
try {
|
|
49
|
-
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
50
|
-
} catch {
|
|
51
|
-
// ignore
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
56
|
-
// PART 1: Business Invariants
|
|
57
|
-
// Tests that verify system MUST maintain these rules
|
|
58
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
describe('Pain → Diagnostician: Business Invariants', () => {
|
|
61
|
-
let workspaceDir: string;
|
|
62
|
-
let stateDir: string;
|
|
63
|
-
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
const ws = createTestWorkspace();
|
|
66
|
-
workspaceDir = ws.workspaceDir;
|
|
67
|
-
stateDir = ws.stateDir;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
cleanupWorkspace(workspaceDir);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── INVARIANT 1: Pain flag format contract ──
|
|
75
|
-
describe('INVARIANT: Pain flag format contract', () => {
|
|
76
|
-
it('MUST contain all required fields after writePainFlag', () => {
|
|
77
|
-
const data = buildPainFlag({
|
|
78
|
-
source: 'tool_failure',
|
|
79
|
-
score: '70',
|
|
80
|
-
reason: 'Command failed with exit code 1',
|
|
81
|
-
session_id: 'test-session-123',
|
|
82
|
-
agent_id: 'main',
|
|
83
|
-
is_risky: true,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
writePainFlag(workspaceDir, data);
|
|
87
|
-
|
|
88
|
-
// Independent verification: read file directly, don't trust writePainFlag
|
|
89
|
-
const painFlagPath = path.join(stateDir, '.pain_flag');
|
|
90
|
-
expect(fs.existsSync(painFlagPath)).toBe(true);
|
|
91
|
-
|
|
92
|
-
const content = fs.readFileSync(painFlagPath, 'utf-8');
|
|
93
|
-
|
|
94
|
-
// INVARIANT: All required fields MUST be present
|
|
95
|
-
expect(content).toContain('source: tool_failure');
|
|
96
|
-
expect(content).toContain('score: 70');
|
|
97
|
-
expect(content).toMatch(/time: \d{4}-\d{2}-\d{2}T/); // ISO timestamp with space
|
|
98
|
-
expect(content).toContain('reason:');
|
|
99
|
-
expect(content).toContain('is_risky: true');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('MUST NOT write empty optional fields to disk', () => {
|
|
103
|
-
const data = buildPainFlag({
|
|
104
|
-
source: 'human_intervention',
|
|
105
|
-
score: '50',
|
|
106
|
-
reason: 'User feedback',
|
|
107
|
-
// Optional fields omitted
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
writePainFlag(workspaceDir, data);
|
|
111
|
-
|
|
112
|
-
const content = fs.readFileSync(path.join(stateDir, '.pain_flag'), 'utf-8');
|
|
113
|
-
|
|
114
|
-
// INVARIANT: Empty optional fields MUST NOT appear in file
|
|
115
|
-
// This prevents confusion when reading the file
|
|
116
|
-
expect(content).not.toMatch(/trace_id:\s*$/m);
|
|
117
|
-
expect(content).not.toMatch(/trigger_text_preview:\s*$/m);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('score MUST be in valid range 0-100', () => {
|
|
121
|
-
// Test boundary values
|
|
122
|
-
const scores = ['0', '50', '100'];
|
|
123
|
-
|
|
124
|
-
for (const score of scores) {
|
|
125
|
-
const data = buildPainFlag({
|
|
126
|
-
source: 'test',
|
|
127
|
-
score,
|
|
128
|
-
reason: 'Test',
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
writePainFlag(workspaceDir, data);
|
|
132
|
-
|
|
133
|
-
const read = readPainFlagData(workspaceDir);
|
|
134
|
-
const numScore = Number(read.score);
|
|
135
|
-
|
|
136
|
-
// INVARIANT: Score MUST be in valid range
|
|
137
|
-
expect(numScore).toBeGreaterThanOrEqual(0);
|
|
138
|
-
expect(numScore).toBeLessThanOrEqual(100);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ── INVARIANT 2: Pain flag validation contract ──
|
|
144
|
-
describe('INVARIANT: Pain flag validation', () => {
|
|
145
|
-
it('validatePainFlag MUST reject flags missing required fields', () => {
|
|
146
|
-
const invalidFlags = [
|
|
147
|
-
{ source: '', score: '50', time: '2024-01-01', reason: 'test' }, // empty source
|
|
148
|
-
{ source: 'test', score: '', time: '2024-01-01', reason: 'test' }, // empty score
|
|
149
|
-
{ source: 'test', score: '50', time: '', reason: 'test' }, // empty time
|
|
150
|
-
{ source: 'test', score: '50', time: '2024-01-01', reason: '' }, // empty reason
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
for (const flag of invalidFlags) {
|
|
154
|
-
const missing = validatePainFlag(flag);
|
|
155
|
-
// INVARIANT: Missing required fields MUST be detected
|
|
156
|
-
expect(missing.length).toBeGreaterThan(0);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('validatePainFlag MUST accept valid flags', () => {
|
|
161
|
-
const validFlag = {
|
|
162
|
-
source: 'tool_failure',
|
|
163
|
-
score: '70',
|
|
164
|
-
time: new Date().toISOString(),
|
|
165
|
-
reason: 'Command failed',
|
|
166
|
-
session_id: 'test',
|
|
167
|
-
agent_id: 'main',
|
|
168
|
-
is_risky: 'false',
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const missing = validatePainFlag(validFlag);
|
|
172
|
-
// INVARIANT: Valid flags MUST pass validation
|
|
173
|
-
expect(missing).toEqual([]);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ── INVARIANT 3: Diagnostician task store contract ──
|
|
178
|
-
describe('INVARIANT: Diagnostician task store', () => {
|
|
179
|
-
it('MUST persist tasks with correct structure', async () => {
|
|
180
|
-
const taskId = `task-${Date.now()}`;
|
|
181
|
-
const prompt = 'Diagnose the following pain signal:\n- source: tool_failure\n- score: 70';
|
|
182
|
-
|
|
183
|
-
await addDiagnosticianTask(stateDir, taskId, prompt);
|
|
184
|
-
|
|
185
|
-
// Independent verification: read file directly
|
|
186
|
-
const tasksPath = path.join(stateDir, 'diagnostician_tasks.json');
|
|
187
|
-
expect(fs.existsSync(tasksPath)).toBe(true);
|
|
188
|
-
|
|
189
|
-
const store = JSON.parse(fs.readFileSync(tasksPath, 'utf-8'));
|
|
190
|
-
|
|
191
|
-
// INVARIANT: Task MUST be in store with correct structure
|
|
192
|
-
expect(store.tasks).toBeDefined();
|
|
193
|
-
expect(store.tasks[taskId]).toBeDefined();
|
|
194
|
-
expect(store.tasks[taskId].prompt).toBe(prompt);
|
|
195
|
-
expect(store.tasks[taskId].status).toBe('pending');
|
|
196
|
-
expect(store.tasks[taskId].createdAt).toBeDefined();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('getPendingDiagnosticianTasks MUST only return pending tasks', async () => {
|
|
200
|
-
// Add pending task
|
|
201
|
-
await addDiagnosticianTask(stateDir, 'pending-task', 'Test prompt');
|
|
202
|
-
|
|
203
|
-
// Add completed task manually
|
|
204
|
-
const tasksPath = path.join(stateDir, 'diagnostician_tasks.json');
|
|
205
|
-
const store = { tasks: {} };
|
|
206
|
-
store.tasks['completed-task'] = {
|
|
207
|
-
prompt: 'Completed',
|
|
208
|
-
status: 'completed',
|
|
209
|
-
createdAt: new Date().toISOString(),
|
|
210
|
-
};
|
|
211
|
-
fs.writeFileSync(tasksPath, JSON.stringify(store));
|
|
212
|
-
|
|
213
|
-
// Add pending task to the store
|
|
214
|
-
const existingStore = JSON.parse(fs.readFileSync(tasksPath, 'utf-8'));
|
|
215
|
-
existingStore.tasks['pending-task'] = {
|
|
216
|
-
prompt: 'Pending',
|
|
217
|
-
status: 'pending',
|
|
218
|
-
createdAt: new Date().toISOString(),
|
|
219
|
-
};
|
|
220
|
-
fs.writeFileSync(tasksPath, JSON.stringify(existingStore));
|
|
221
|
-
|
|
222
|
-
const pending = getPendingDiagnosticianTasks(stateDir);
|
|
223
|
-
|
|
224
|
-
// INVARIANT: Only pending tasks MUST be returned
|
|
225
|
-
expect(pending.some(t => t.id === 'pending-task')).toBe(true);
|
|
226
|
-
expect(pending.some(t => t.id === 'completed-task')).toBe(false);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
232
|
-
// PART 2: Resilience Tests
|
|
233
|
-
// Tests that verify system behavior under abnormal conditions
|
|
234
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
describe('Pain → Diagnostician: Resilience', () => {
|
|
237
|
-
let workspaceDir: string;
|
|
238
|
-
let stateDir: string;
|
|
239
|
-
|
|
240
|
-
beforeEach(() => {
|
|
241
|
-
const ws = createTestWorkspace();
|
|
242
|
-
workspaceDir = ws.workspaceDir;
|
|
243
|
-
stateDir = ws.stateDir;
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
afterEach(() => {
|
|
247
|
-
cleanupWorkspace(workspaceDir);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// ── RESILIENCE 1: Corruption recovery ──
|
|
251
|
-
describe('RESILIENCE: Corruption recovery', () => {
|
|
252
|
-
it('readPainFlagData MUST NOT crash on corrupted file', () => {
|
|
253
|
-
// Write corrupted content
|
|
254
|
-
fs.writeFileSync(path.join(stateDir, '.pain_flag'), 'invalid {{{ json');
|
|
255
|
-
|
|
256
|
-
// This should NOT throw
|
|
257
|
-
expect(() => readPainFlagData(workspaceDir)).not.toThrow();
|
|
258
|
-
|
|
259
|
-
const data = readPainFlagData(workspaceDir);
|
|
260
|
-
|
|
261
|
-
// INVARIANT: Should return safe default, not undefined/null
|
|
262
|
-
expect(data).toBeDefined();
|
|
263
|
-
expect(typeof data).toBe('object');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('getPendingDiagnosticianTasks MUST NOT crash on missing file', () => {
|
|
267
|
-
// Don't create diagnostician_tasks.json
|
|
268
|
-
|
|
269
|
-
// This should NOT throw
|
|
270
|
-
expect(() => getPendingDiagnosticianTasks(stateDir)).not.toThrow();
|
|
271
|
-
|
|
272
|
-
const pending = getPendingDiagnosticianTasks(stateDir);
|
|
273
|
-
|
|
274
|
-
// INVARIANT: Should return empty array, not crash
|
|
275
|
-
expect(Array.isArray(pending)).toBe(true);
|
|
276
|
-
expect(pending.length).toBe(0);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('getPendingDiagnosticianTasks MUST NOT crash on corrupted JSON', () => {
|
|
280
|
-
fs.writeFileSync(
|
|
281
|
-
path.join(stateDir, 'diagnostician_tasks.json'),
|
|
282
|
-
'not valid json {{{'
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// This should NOT throw
|
|
286
|
-
expect(() => getPendingDiagnosticianTasks(stateDir)).not.toThrow();
|
|
287
|
-
|
|
288
|
-
const pending = getPendingDiagnosticianTasks(stateDir);
|
|
289
|
-
|
|
290
|
-
// INVARIANT: Should return empty array as fallback
|
|
291
|
-
expect(Array.isArray(pending)).toBe(true);
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// ── RESILIENCE 2: Concurrent access safety ──
|
|
296
|
-
describe('RESILIENCE: Concurrent access', () => {
|
|
297
|
-
it('writePainFlag MUST handle rapid sequential writes', () => {
|
|
298
|
-
// Simulate rapid consecutive writes
|
|
299
|
-
for (let i = 0; i < 10; i++) {
|
|
300
|
-
const data = buildPainFlag({
|
|
301
|
-
source: `test-${i}`,
|
|
302
|
-
score: String(i * 10),
|
|
303
|
-
reason: `Test ${i}`,
|
|
304
|
-
});
|
|
305
|
-
writePainFlag(workspaceDir, data);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// INVARIANT: File must exist and be valid after rapid writes
|
|
309
|
-
const painFlagPath = path.join(stateDir, '.pain_flag');
|
|
310
|
-
expect(fs.existsSync(painFlagPath)).toBe(true);
|
|
311
|
-
|
|
312
|
-
const content = fs.readFileSync(painFlagPath, 'utf-8');
|
|
313
|
-
|
|
314
|
-
// Should not have corruption artifacts
|
|
315
|
-
expect(content).not.toContain('undefined');
|
|
316
|
-
expect(content).not.toContain('[object Object]');
|
|
317
|
-
expect(content).not.toContain('null');
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
323
|
-
// PART 3: Round-trip Tests
|
|
324
|
-
// Tests that verify data survives the full write → read cycle
|
|
325
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
describe('Pain → Diagnostician: Round-trip', () => {
|
|
328
|
-
let workspaceDir: string;
|
|
329
|
-
let stateDir: string;
|
|
330
|
-
|
|
331
|
-
beforeEach(() => {
|
|
332
|
-
const ws = createTestWorkspace();
|
|
333
|
-
workspaceDir = ws.workspaceDir;
|
|
334
|
-
stateDir = ws.stateDir;
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
afterEach(() => {
|
|
338
|
-
cleanupWorkspace(workspaceDir);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('Pain flag round-trip: write → read → verify', () => {
|
|
342
|
-
const original = buildPainFlag({
|
|
343
|
-
source: 'tool_failure',
|
|
344
|
-
score: '75',
|
|
345
|
-
reason: 'npm test failed with exit code 1',
|
|
346
|
-
session_id: 'session-abc123',
|
|
347
|
-
agent_id: 'main',
|
|
348
|
-
is_risky: false,
|
|
349
|
-
trace_id: 'trace-xyz789',
|
|
350
|
-
trigger_text_preview: 'npm test',
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
writePainFlag(workspaceDir, original);
|
|
354
|
-
const read = readPainFlagData(workspaceDir);
|
|
355
|
-
|
|
356
|
-
// INVARIANT: All fields MUST survive round-trip
|
|
357
|
-
expect(read.source).toBe(original.source);
|
|
358
|
-
expect(read.score).toBe(original.score);
|
|
359
|
-
expect(read.reason).toBe(original.reason);
|
|
360
|
-
expect(read.session_id).toBe(original.session_id);
|
|
361
|
-
expect(read.agent_id).toBe(original.agent_id);
|
|
362
|
-
expect(read.is_risky).toBe(original.is_risky);
|
|
363
|
-
expect(read.trace_id).toBe(original.trace_id);
|
|
364
|
-
expect(read.trigger_text_preview).toBe(original.trigger_text_preview);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('Diagnostician task round-trip: add → get → verify', async () => {
|
|
368
|
-
const taskId = 'round-trip-task';
|
|
369
|
-
const prompt = 'Analyze the following error:\n```\nError: ENOENT\n```';
|
|
370
|
-
|
|
371
|
-
await addDiagnosticianTask(stateDir, taskId, prompt);
|
|
372
|
-
const pending = getPendingDiagnosticianTasks(stateDir);
|
|
373
|
-
const task = pending.find(t => t.id === taskId);
|
|
374
|
-
|
|
375
|
-
// INVARIANT: Task MUST survive round-trip
|
|
376
|
-
expect(task).toBeDefined();
|
|
377
|
-
expect(task!.task.prompt).toBe(prompt);
|
|
378
|
-
expect(task!.task.status).toBe('pending');
|
|
379
|
-
});
|
|
380
|
-
});
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { ControlUiQueryService } from '../../src/service/control-ui-query-service.js';
|
|
3
|
-
|
|
4
|
-
const mocks = vi.hoisted(() => ({
|
|
5
|
-
fromHookContext: vi.fn(),
|
|
6
|
-
clearCache: vi.fn(),
|
|
7
|
-
controlUiDb: {
|
|
8
|
-
all: vi.fn(),
|
|
9
|
-
get: vi.fn(),
|
|
10
|
-
dispose: vi.fn(),
|
|
11
|
-
},
|
|
12
|
-
trajectory: {
|
|
13
|
-
getDataStats: vi.fn(),
|
|
14
|
-
},
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('../../src/core/workspace-context.js', () => ({
|
|
18
|
-
WorkspaceContext: {
|
|
19
|
-
fromHookContext: mocks.fromHookContext,
|
|
20
|
-
clearCache: mocks.clearCache,
|
|
21
|
-
},
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock('../../src/core/control-ui-db.js', () => ({
|
|
25
|
-
ControlUiDatabase: class {
|
|
26
|
-
constructor() {
|
|
27
|
-
return mocks.controlUiDb as any;
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
describe('ControlUiQueryService', () => {
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
vi.clearAllMocks();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('returns null for unknown thinking models', () => {
|
|
38
|
-
mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
|
|
39
|
-
const service = new ControlUiQueryService('/mock/workspace');
|
|
40
|
-
|
|
41
|
-
expect(service.getThinkingModelDetail('UNKNOWN')).toBeNull();
|
|
42
|
-
service.dispose();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('labels overview responses as trajectory analytics rather than runtime control state', () => {
|
|
46
|
-
mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
|
|
47
|
-
mocks.trajectory.getDataStats.mockReturnValue({
|
|
48
|
-
dbPath: '/mock/trajectory.db',
|
|
49
|
-
dbSizeBytes: 0,
|
|
50
|
-
assistantTurns: 0,
|
|
51
|
-
userTurns: 0,
|
|
52
|
-
toolCalls: 0,
|
|
53
|
-
painEvents: 0,
|
|
54
|
-
pendingSamples: 0,
|
|
55
|
-
approvedSamples: 0,
|
|
56
|
-
blobBytes: 0,
|
|
57
|
-
lastIngestAt: null,
|
|
58
|
-
});
|
|
59
|
-
mocks.controlUiDb.get.mockImplementation((sql: string) => {
|
|
60
|
-
if (sql.includes('FROM gate_blocks')) return { count: 0 };
|
|
61
|
-
if (sql.includes('FROM task_outcomes')) return { count: 0 };
|
|
62
|
-
return { count: 0 };
|
|
63
|
-
});
|
|
64
|
-
mocks.controlUiDb.all.mockImplementation((sql: string) => {
|
|
65
|
-
if (sql.includes('v_error_clusters')) return [];
|
|
66
|
-
if (sql.includes('v_sample_queue')) return [];
|
|
67
|
-
if (sql.includes('correction_samples')) return [];
|
|
68
|
-
if (sql.includes('v_thinking_model_effectiveness')) return [];
|
|
69
|
-
if (sql.includes('v_thinking_model_daily_trend')) return [];
|
|
70
|
-
if (sql.includes('v_thinking_model_scenarios')) return [];
|
|
71
|
-
return [];
|
|
72
|
-
});
|
|
73
|
-
const service = new ControlUiQueryService('/mock/workspace');
|
|
74
|
-
const overview = service.getOverview();
|
|
75
|
-
|
|
76
|
-
expect(overview.dataSource).toBe('trajectory_db_analytics');
|
|
77
|
-
expect(overview.runtimeControlPlaneSource).toBe('pd_evolution_status');
|
|
78
|
-
expect(overview.summary.gateBlocks).toBe(0);
|
|
79
|
-
expect(overview.summary.taskOutcomes).toBe(0);
|
|
80
|
-
service.dispose();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('surfaces gate block and task outcome counts from trajectory analytics', () => {
|
|
84
|
-
mocks.fromHookContext.mockReturnValue({ trajectory: mocks.trajectory } as any);
|
|
85
|
-
mocks.trajectory.getDataStats.mockReturnValue({
|
|
86
|
-
dbPath: '/mock/trajectory.db',
|
|
87
|
-
dbSizeBytes: 0,
|
|
88
|
-
assistantTurns: 0,
|
|
89
|
-
userTurns: 0,
|
|
90
|
-
toolCalls: 0,
|
|
91
|
-
painEvents: 0,
|
|
92
|
-
pendingSamples: 0,
|
|
93
|
-
approvedSamples: 0,
|
|
94
|
-
blobBytes: 0,
|
|
95
|
-
lastIngestAt: null,
|
|
96
|
-
});
|
|
97
|
-
mocks.controlUiDb.get.mockImplementation((sql: string) => {
|
|
98
|
-
if (sql.includes('FROM gate_blocks')) return { count: 1 };
|
|
99
|
-
if (sql.includes('FROM task_outcomes')) return { count: 1 };
|
|
100
|
-
return { count: 0 };
|
|
101
|
-
});
|
|
102
|
-
mocks.controlUiDb.all.mockImplementation((sql: string) => {
|
|
103
|
-
if (sql.includes('v_error_clusters')) return [];
|
|
104
|
-
if (sql.includes('v_sample_queue')) return [];
|
|
105
|
-
if (sql.includes('correction_samples')) return [];
|
|
106
|
-
if (sql.includes('v_thinking_model_effectiveness')) return [];
|
|
107
|
-
if (sql.includes('v_thinking_model_daily_trend')) return [];
|
|
108
|
-
if (sql.includes('v_thinking_model_scenarios')) return [];
|
|
109
|
-
return [];
|
|
110
|
-
});
|
|
111
|
-
const service = new ControlUiQueryService('/mock/workspace');
|
|
112
|
-
const overview = service.getOverview();
|
|
113
|
-
|
|
114
|
-
expect(overview.summary.gateBlocks).toBe(1);
|
|
115
|
-
expect(overview.summary.taskOutcomes).toBe(1);
|
|
116
|
-
expect(overview.dataSource).toBe('trajectory_db_analytics');
|
|
117
|
-
expect(overview.runtimeControlPlaneSource).toBe('pd_evolution_status');
|
|
118
|
-
|
|
119
|
-
service.dispose();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
|
|
6
|
-
import { recordPersistentFailure, resetFailureState, isTaskKindInCooldown } from '../../src/service/cooldown-strategy.js';
|
|
7
|
-
import { readState } from '../../src/service/nocturnal-runtime.js';
|
|
8
|
-
|
|
9
|
-
let tmpDir: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cooldown-test-'));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('cooldown-strategy', () => {
|
|
20
|
-
describe('recordPersistentFailure', () => {
|
|
21
|
-
it('sets Tier 1 (30min) on first call', async () => {
|
|
22
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
23
|
-
const state = await readState(tmpDir);
|
|
24
|
-
|
|
25
|
-
expect(state.taskFailureState).toBeDefined();
|
|
26
|
-
expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(1);
|
|
27
|
-
expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(1);
|
|
28
|
-
|
|
29
|
-
const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
|
|
30
|
-
const expectedEnd = Date.now() + 30 * 60 * 1000;
|
|
31
|
-
expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('sets Tier 2 (4h) on second call', async () => {
|
|
35
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
36
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
37
|
-
const state = await readState(tmpDir);
|
|
38
|
-
|
|
39
|
-
expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(2);
|
|
40
|
-
expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(2);
|
|
41
|
-
|
|
42
|
-
const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
|
|
43
|
-
const expectedEnd = Date.now() + 4 * 60 * 60 * 1000;
|
|
44
|
-
expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('caps at Tier 3 (24h) on fourth+ call', async () => {
|
|
48
|
-
for (let i = 0; i < 4; i++) {
|
|
49
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
50
|
-
}
|
|
51
|
-
const state = await readState(tmpDir);
|
|
52
|
-
|
|
53
|
-
expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(4);
|
|
54
|
-
expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(3);
|
|
55
|
-
|
|
56
|
-
const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
|
|
57
|
-
const expectedEnd = Date.now() + 24 * 60 * 60 * 1000;
|
|
58
|
-
expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(5000);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('independent state per task kind', async () => {
|
|
62
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
63
|
-
await new Promise((r) => setTimeout(r, 10)); // ensure distinct timestamps
|
|
64
|
-
await recordPersistentFailure(tmpDir, 'keyword_optimization');
|
|
65
|
-
const state = await readState(tmpDir);
|
|
66
|
-
|
|
67
|
-
expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(1);
|
|
68
|
-
expect(state.taskFailureState!['keyword_optimization'].escalationTier).toBe(1);
|
|
69
|
-
expect(state.taskFailureState!['sleep_reflection'].cooldownUntil).not.toBe(
|
|
70
|
-
state.taskFailureState!['keyword_optimization'].cooldownUntil,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('resetFailureState', () => {
|
|
76
|
-
it('clears failures after escalation', async () => {
|
|
77
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
78
|
-
await resetFailureState(tmpDir, 'sleep_reflection');
|
|
79
|
-
const state = await readState(tmpDir);
|
|
80
|
-
|
|
81
|
-
expect(state.taskFailureState!['sleep_reflection'].consecutiveFailures).toBe(0);
|
|
82
|
-
expect(state.taskFailureState!['sleep_reflection'].escalationTier).toBe(0);
|
|
83
|
-
expect(state.taskFailureState!['sleep_reflection'].cooldownUntil).toBeUndefined();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('is idempotent with no prior state', async () => {
|
|
87
|
-
await expect(resetFailureState(tmpDir, 'sleep_reflection')).resolves.not.toThrow();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('isTaskKindInCooldown', () => {
|
|
92
|
-
it('returns inCooldown=true after recordPersistentFailure', async () => {
|
|
93
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
94
|
-
const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
|
|
95
|
-
|
|
96
|
-
expect(result.inCooldown).toBe(true);
|
|
97
|
-
expect(result.remainingMs).toBeGreaterThan(0);
|
|
98
|
-
expect(result.cooldownUntil).not.toBeNull();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('returns inCooldown=false when cooldownUntil is in the past', async () => {
|
|
102
|
-
// Manually set expired cooldown
|
|
103
|
-
const state = await readState(tmpDir);
|
|
104
|
-
state.taskFailureState = {
|
|
105
|
-
sleep_reflection: {
|
|
106
|
-
consecutiveFailures: 1,
|
|
107
|
-
escalationTier: 1,
|
|
108
|
-
cooldownUntil: new Date(Date.now() - 60000).toISOString(),
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
const { writeState } = await import('../../src/service/nocturnal-runtime.js');
|
|
112
|
-
await writeState(tmpDir, state);
|
|
113
|
-
|
|
114
|
-
const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
|
|
115
|
-
expect(result.inCooldown).toBe(false);
|
|
116
|
-
expect(result.remainingMs).toBe(0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('returns inCooldown=false when no state exists', () => {
|
|
120
|
-
const result = isTaskKindInCooldown(tmpDir, 'sleep_reflection');
|
|
121
|
-
expect(result.inCooldown).toBe(false);
|
|
122
|
-
expect(result.remainingMs).toBe(0);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('returns inCooldown=false for untracked task kind', async () => {
|
|
126
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
127
|
-
const result = isTaskKindInCooldown(tmpDir, 'keyword_optimization');
|
|
128
|
-
|
|
129
|
-
expect(result.inCooldown).toBe(false);
|
|
130
|
-
expect(result.remainingMs).toBe(0);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('state persistence', () => {
|
|
135
|
-
it('state survives to disk after recordPersistentFailure', async () => {
|
|
136
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection');
|
|
137
|
-
|
|
138
|
-
// Read directly from file to verify persistence
|
|
139
|
-
const filePath = path.join(tmpDir, 'nocturnal-runtime.json');
|
|
140
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
141
|
-
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
142
|
-
expect(raw.taskFailureState).toBeDefined();
|
|
143
|
-
expect(raw.taskFailureState.sleep_reflection).toBeDefined();
|
|
144
|
-
expect(raw.taskFailureState.sleep_reflection.consecutiveFailures).toBe(1);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('custom config', () => {
|
|
149
|
-
it('uses custom tier durations', async () => {
|
|
150
|
-
const customConfig = {
|
|
151
|
-
tier1_ms: 1000,
|
|
152
|
-
tier2_ms: 5000,
|
|
153
|
-
tier3_ms: 10000,
|
|
154
|
-
consecutive_threshold: 3,
|
|
155
|
-
};
|
|
156
|
-
await recordPersistentFailure(tmpDir, 'sleep_reflection', customConfig);
|
|
157
|
-
const state = await readState(tmpDir);
|
|
158
|
-
|
|
159
|
-
const cooldownUntil = new Date(state.taskFailureState!['sleep_reflection'].cooldownUntil!).getTime();
|
|
160
|
-
const expectedEnd = Date.now() + 1000;
|
|
161
|
-
expect(Math.abs(cooldownUntil - expectedEnd)).toBeLessThan(1000);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
});
|