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.
Files changed (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +459 -439
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +59 -16
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +3 -1
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. 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
- });