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.
Files changed (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  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 +29 -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/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -1,118 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
-
6
- const { mockExecuteNocturnalReflectionAsync } = vi.hoisted(() => ({
7
- mockExecuteNocturnalReflectionAsync: vi.fn(),
8
- }));
9
-
10
- vi.mock('../../src/service/nocturnal-service.js', () => ({
11
- executeNocturnalReflectionAsync: mockExecuteNocturnalReflectionAsync,
12
- }));
13
-
14
- vi.mock('../../src/utils/subagent-probe.js', () => ({
15
- isSubagentRuntimeAvailable: vi.fn(() => true),
16
- }));
17
-
18
- vi.mock('../../src/core/nocturnal-paths.js', () => ({
19
- resolveNocturnalDir: vi.fn((workspaceDir: string) => path.join(workspaceDir, '.state', 'nocturnal', 'samples')),
20
- }));
21
-
22
- import { NocturnalWorkflowManager, nocturnalWorkflowSpec } from '../../src/service/subagent-workflow/nocturnal-workflow-manager.js';
23
- import { safeRmDir } from '../test-utils.js';
24
-
25
- describe('NocturnalWorkflowManager runtime hardening', () => {
26
- let workspaceDir: string;
27
- let stateDir: string;
28
-
29
- beforeEach(() => {
30
- vi.clearAllMocks();
31
- workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-wf-'));
32
- stateDir = path.join(workspaceDir, '.state');
33
- fs.mkdirSync(stateDir, { recursive: true });
34
- });
35
-
36
- afterEach(() => {
37
- safeRmDir(workspaceDir);
38
- });
39
-
40
- it('marks workflow terminal_error when async pipeline throws a gateway-only runtime error', async () => {
41
- mockExecuteNocturnalReflectionAsync.mockRejectedValue(
42
- new Error('Plugin runtime subagent methods are only available during a gateway request.')
43
- );
44
-
45
- const manager = new NocturnalWorkflowManager({
46
- workspaceDir,
47
- stateDir,
48
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
49
- runtimeAdapter: {} as any,
50
- });
51
-
52
- const handle = await manager.startWorkflow(nocturnalWorkflowSpec, {
53
- parentSessionId: 'sleep_reflection:test',
54
- taskInput: {},
55
- metadata: {
56
- snapshot: {
57
- sessionId: 'session-1',
58
- startedAt: '2026-04-10T00:00:00.000Z',
59
- updatedAt: '2026-04-10T00:01:00.000Z',
60
- assistantTurns: [],
61
- userTurns: [],
62
- toolCalls: [],
63
- painEvents: [],
64
- gateBlocks: [],
65
- stats: {
66
- totalAssistantTurns: 1,
67
- totalToolCalls: 1,
68
- totalPainEvents: 0,
69
- totalGateBlocks: 0,
70
- failureCount: 0,
71
- },
72
- },
73
- },
74
- });
75
-
76
- await Promise.resolve();
77
- await Promise.resolve();
78
-
79
- const summary = await manager.getWorkflowDebugSummary(handle.workflowId);
80
- expect(summary?.state).toBe('terminal_error');
81
- expect(summary?.recentEvents.some((event) => event.eventType === 'nocturnal_failed')).toBe(true);
82
-
83
- manager.dispose();
84
- });
85
-
86
- it('rejects malformed snapshot ingress before starting the async pipeline', async () => {
87
- const manager = new NocturnalWorkflowManager({
88
- workspaceDir,
89
- stateDir,
90
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
91
- runtimeAdapter: {} as any,
92
- });
93
-
94
- const handle = await manager.startWorkflow(nocturnalWorkflowSpec, {
95
- parentSessionId: 'sleep_reflection:test',
96
- taskInput: {},
97
- metadata: {
98
- snapshot: {
99
- sessionId: 'session-1',
100
- stats: {
101
- totalAssistantTurns: 1,
102
- totalToolCalls: 1,
103
- totalPainEvents: 0,
104
- totalGateBlocks: 0,
105
- failureCount: 0,
106
- },
107
- },
108
- },
109
- });
110
-
111
- const summary = await manager.getWorkflowDebugSummary(handle.workflowId);
112
- expect(summary?.state).toBe('terminal_error');
113
- expect(summary?.recentEvents.some((event) => event.eventType === 'nocturnal_failed')).toBe(true);
114
- expect(mockExecuteNocturnalReflectionAsync).not.toHaveBeenCalled();
115
-
116
- manager.dispose();
117
- });
118
- });
@@ -1,473 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
- import {
6
- checkWorkspaceIdle,
7
- checkCooldown,
8
- checkPreflight,
9
- recordRunStart,
10
- recordRunEnd,
11
- clearAllCooldowns,
12
- getRuntimeState,
13
- DEFAULT_IDLE_THRESHOLD_MS,
14
- DEFAULT_GLOBAL_COOLDOWN_MS,
15
- DEFAULT_PRINCIPLE_COOLDOWN_MS,
16
- DEFAULT_ABANDONED_THRESHOLD_MS,
17
- NOCTURNAL_RUNTIME_FILE,
18
- } from '../../src/service/nocturnal-runtime.js';
19
- import { initPersistence, trackToolRead, clearSession, listSessions } from '../../src/core/session-tracker.js';
20
- import { safeRmDir } from '../test-utils.js';
21
-
22
- describe('NocturnalRuntime', () => {
23
- let tempDir: string;
24
- let workspaceDir: string;
25
-
26
- beforeEach(() => {
27
- vi.useFakeTimers();
28
- // Use a fixed "now" for deterministic testing
29
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
30
-
31
- workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-ws-'));
32
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-'));
33
-
34
- // Initialize session tracker persistence for the temp workspace
35
- initPersistence(tempDir);
36
- });
37
-
38
- afterEach(() => {
39
- vi.useRealTimers();
40
- vi.clearAllMocks();
41
- safeRmDir(workspaceDir);
42
- safeRmDir(tempDir);
43
- clearSession('session-active');
44
- clearSession('session-stale');
45
- clearSession('session-abandoned');
46
- clearSession('session-ancient');
47
- });
48
-
49
- // -------------------------------------------------------------------------
50
- // Idle Detection Tests
51
- // -------------------------------------------------------------------------
52
-
53
- describe('checkWorkspaceIdle', () => {
54
- it('should return isIdle=true when no sessions exist', () => {
55
- const result = checkWorkspaceIdle(workspaceDir);
56
- expect(result.isIdle).toBe(true);
57
- expect(result.userActiveSessions).toBe(0);
58
- expect(result.abandonedSessionIds).toEqual([]);
59
- expect(result.reason).toContain('No active sessions');
60
- });
61
-
62
- it('should return isIdle=false when a session is recent (within threshold)', () => {
63
- // Create an active session with recent activity
64
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
65
-
66
- const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
67
- expect(result.isIdle).toBe(false);
68
- expect(result.userActiveSessions).toBe(1);
69
- expect(result.abandonedSessionIds).toEqual([]);
70
- });
71
-
72
- it('should return isIdle=true when session is older than idle threshold', () => {
73
- // Create a stale session (activity 45 min ago)
74
- vi.setSystemTime(new Date('2026-03-27T11:15:00.000Z')); // 45 min before "now"
75
- trackToolRead('session-stale', 'src/main.ts', workspaceDir);
76
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z')); // reset to "now"
77
-
78
- const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
79
- expect(result.isIdle).toBe(true);
80
- expect(result.idleForMs).toBeGreaterThan(30 * 60 * 1000);
81
- });
82
-
83
- // TODO: Fix - abandonedSessionIds not being populated correctly
84
- it.skip('should treat abandoned sessions as not contributing to idle check', () => {
85
- // Session active 3 hours ago — should be treated as abandoned
86
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours before "now"
87
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
88
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z')); // reset to "now"
89
-
90
- const result = checkWorkspaceIdle(workspaceDir, {
91
- idleThresholdMs: 30 * 60 * 1000,
92
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
93
- });
94
-
95
- expect(result.isIdle).toBe(true); // No active sessions, so idle
96
- expect(result.abandonedSessionIds).toContain('session-abandoned');
97
- expect(result.userActiveSessions).toBe(0);
98
- expect(result.reason).toContain('abandoned session(s) ignored');
99
- });
100
-
101
- // TODO: Fix - abandonedSessionIds not being populated correctly
102
- it.skip('should ignore ancient sessions but still detect recent activity from other sessions', () => {
103
- // Ancient session (4 hours ago — abandoned)
104
- vi.setSystemTime(new Date('2026-03-27T08:00:00.000Z'));
105
- trackToolRead('session-ancient', 'src/main.ts', workspaceDir);
106
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
107
-
108
- // Recent session (5 minutes ago — still active)
109
- vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
110
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
111
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
112
-
113
- const result = checkWorkspaceIdle(workspaceDir, {
114
- idleThresholdMs: 30 * 60 * 1000,
115
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
116
- });
117
-
118
- expect(result.isIdle).toBe(false); // Recent activity 5 min ago
119
- expect(result.abandonedSessionIds).toContain('session-ancient');
120
- expect(result.userActiveSessions).toBe(1);
121
- });
122
-
123
- it('should use trajectory timestamp as secondary guardrail', () => {
124
- // No sessions, trajectory shows recent activity
125
- const trajectoryRecent = Date.now() - 5 * 60 * 1000; // 5 min ago
126
-
127
- const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryRecent);
128
- expect(result.isIdle).toBe(true); // Still idle (no sessions is primary)
129
- expect(result.trajectoryGuardrailConfirmsIdle).toBe(false); // But trajectory disagrees
130
- });
131
-
132
- it('should confirm idle when both session state and trajectory agree', () => {
133
- // No sessions, trajectory also shows idle (>80% of threshold)
134
- const trajectoryOld = Date.now() - 40 * 60 * 1000; // 40 min ago (>24 min = 80% of 30min)
135
-
136
- const result = checkWorkspaceIdle(workspaceDir, {}, trajectoryOld);
137
- expect(result.isIdle).toBe(true);
138
- expect(result.trajectoryGuardrailConfirmsIdle).toBe(true);
139
- });
140
-
141
- it('should report idleForMs correctly', () => {
142
- // Session active 15 min ago
143
- vi.setSystemTime(new Date('2026-03-27T11:45:00.000Z'));
144
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
145
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
146
-
147
- const result = checkWorkspaceIdle(workspaceDir, { idleThresholdMs: 30 * 60 * 1000 });
148
- expect(result.idleForMs).toBe(15 * 60 * 1000);
149
- expect(result.isIdle).toBe(false);
150
- });
151
- });
152
-
153
- // -------------------------------------------------------------------------
154
- // Cooldown Management Tests
155
- // -------------------------------------------------------------------------
156
-
157
- describe('checkCooldown', () => {
158
- it('should return no active cooldowns when state is empty', () => {
159
- const result = checkCooldown(tempDir);
160
- expect(result.globalCooldownActive).toBe(false);
161
- expect(result.principleCooldownActive).toBe(false);
162
- expect(result.quotaExhausted).toBe(false);
163
- expect(result.runsRemaining).toBe(3); // DEFAULT_MAX_RUNS_PER_WINDOW
164
- });
165
-
166
- it('should detect active global cooldown', async () => {
167
- await recordRunStart(tempDir, 'T-01');
168
-
169
- const result = checkCooldown(tempDir);
170
- expect(result.globalCooldownActive).toBe(true);
171
- expect(result.globalCooldownRemainingMs).toBe(DEFAULT_GLOBAL_COOLDOWN_MS);
172
- expect(result.globalCooldownUntil).toBeTruthy();
173
- });
174
-
175
- it('should detect expired global cooldown', async () => {
176
- await recordRunStart(tempDir, 'T-01');
177
-
178
- // Advance time past the global cooldown
179
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
180
-
181
- const result = checkCooldown(tempDir);
182
- expect(result.globalCooldownActive).toBe(false);
183
- expect(result.globalCooldownRemainingMs).toBe(0);
184
- });
185
-
186
- it('should detect principle-specific cooldown after successful run', async () => {
187
- await recordRunStart(tempDir, 'T-01');
188
- await recordRunEnd(tempDir, 'success', { sampleCount: 5 });
189
-
190
- const result = checkCooldown(tempDir, 'T-01');
191
- expect(result.principleCooldownActive).toBe(true);
192
- expect(result.principleCooldownRemainingMs).toBe(DEFAULT_PRINCIPLE_COOLDOWN_MS);
193
- });
194
-
195
- it('should not trigger principle cooldown on failed run', async () => {
196
- await recordRunStart(tempDir, 'T-01');
197
- await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions' });
198
-
199
- // Global cooldown still active, but no principle cooldown
200
- const result = checkCooldown(tempDir, 'T-01');
201
- expect(result.globalCooldownActive).toBe(true);
202
- expect(result.principleCooldownActive).toBe(false);
203
- });
204
-
205
- it('should not trigger principle cooldown on skipped run', async () => {
206
- await recordRunStart(tempDir, 'T-01');
207
- await recordRunEnd(tempDir, 'skipped', { reason: 'Idle check failed' });
208
-
209
- const result = checkCooldown(tempDir, 'T-01');
210
- expect(result.principleCooldownActive).toBe(false);
211
- });
212
-
213
- it('should enforce quota limit', async () => {
214
- // Run max number of times
215
- for (let i = 0; i < 3; i++) {
216
- await recordRunStart(tempDir, 'T-01');
217
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
218
- // Advance past global cooldown for each run
219
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
220
- }
221
-
222
- const result = checkCooldown(tempDir);
223
- expect(result.quotaExhausted).toBe(true);
224
- expect(result.runsRemaining).toBe(0);
225
- });
226
-
227
- it('should reset quota after window expires', async () => {
228
- // Run max times
229
- for (let i = 0; i < 3; i++) {
230
- await recordRunStart(tempDir, 'T-01');
231
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
232
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
233
- }
234
-
235
- // Advance past the quota window
236
- vi.advanceTimersByTime(24 * 60 * 60 * 1000 + 1000);
237
-
238
- const result = checkCooldown(tempDir);
239
- expect(result.quotaExhausted).toBe(false);
240
- expect(result.runsRemaining).toBe(3);
241
- });
242
-
243
- it('should only cooldown specific principles, not others', async () => {
244
- await recordRunStart(tempDir, 'T-01');
245
- await recordRunEnd(tempDir, 'success');
246
-
247
- const resultT01 = checkCooldown(tempDir, 'T-01');
248
- const resultT02 = checkCooldown(tempDir, 'T-02');
249
-
250
- expect(resultT01.principleCooldownActive).toBe(true);
251
- expect(resultT02.principleCooldownActive).toBe(false);
252
- });
253
- });
254
-
255
- // -------------------------------------------------------------------------
256
- // Run Recording Tests
257
- // -------------------------------------------------------------------------
258
-
259
- describe('recordRunStart / recordRunEnd', () => {
260
- it('should record run start timestamp', async () => {
261
- await recordRunStart(tempDir, 'T-01');
262
- const state = await getRuntimeState(tempDir);
263
-
264
- expect(state.lastRunAt).toBeTruthy();
265
- expect(state.lastRunMeta?.targetPrincipleId).toBe('T-01');
266
- expect(state.lastRunMeta?.status).toBe('skipped');
267
- expect(state.globalCooldownUntil).toBeTruthy();
268
- });
269
-
270
- it('should record successful run with sample count', async () => {
271
- await recordRunStart(tempDir, 'T-01');
272
- await recordRunEnd(tempDir, 'success', { sampleCount: 7 });
273
-
274
- const state = await getRuntimeState(tempDir);
275
- expect(state.lastSuccessfulRunAt).toBeTruthy();
276
- expect(state.lastRunMeta?.status).toBe('success');
277
- expect(state.lastRunMeta?.sampleCount).toBe(7);
278
- expect(state.principleCooldowns['T-01']).toBeTruthy();
279
- });
280
-
281
- it('should preserve failed run reason without setting successful timestamp', async () => {
282
- await recordRunStart(tempDir, 'T-02');
283
- await recordRunEnd(tempDir, 'failed', { reason: 'No violating sessions found' });
284
-
285
- const state = await getRuntimeState(tempDir);
286
- expect(state.lastSuccessfulRunAt).toBeUndefined();
287
- expect(state.lastRunMeta?.status).toBe('failed');
288
- expect(state.lastRunMeta?.reason).toBe('No violating sessions found');
289
- expect(state.principleCooldowns['T-02']).toBeUndefined(); // No principle cooldown on failure
290
- });
291
-
292
- it('should add timestamp to recentRunTimestamps for quota tracking', async () => {
293
- await recordRunStart(tempDir, 'T-01');
294
- const state = await getRuntimeState(tempDir);
295
-
296
- expect(state.recentRunTimestamps.length).toBe(1);
297
- });
298
- });
299
-
300
- // -------------------------------------------------------------------------
301
- // clearAllCooldowns Tests
302
- // -------------------------------------------------------------------------
303
-
304
- describe('clearAllCooldowns', () => {
305
- it('should clear all cooldown state', async () => {
306
- await recordRunStart(tempDir, 'T-01');
307
- await recordRunEnd(tempDir, 'success');
308
-
309
- await clearAllCooldowns(tempDir);
310
-
311
- const state = await getRuntimeState(tempDir);
312
- expect(state.globalCooldownUntil).toBeUndefined();
313
- expect(state.principleCooldowns).toEqual({});
314
- expect(state.recentRunTimestamps).toEqual([]);
315
- expect(state.lastRunMeta).toBeUndefined();
316
- });
317
- });
318
-
319
- // -------------------------------------------------------------------------
320
- // Preflight Check Tests
321
- // -------------------------------------------------------------------------
322
-
323
- describe('checkPreflight', () => {
324
- it('should return canRun=true when workspace is idle and no cooldowns', () => {
325
- // No sessions = idle
326
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
327
- expect(result.canRun).toBe(true);
328
- expect(result.blockers).toEqual([]);
329
- });
330
-
331
- it('should block when workspace is not idle', () => {
332
- // Create recent session
333
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
334
-
335
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
336
- expect(result.canRun).toBe(false);
337
- expect(result.blockers.some(b => b.includes('not idle'))).toBe(true);
338
- });
339
-
340
- it('should block when global cooldown is active', async () => {
341
- await recordRunStart(tempDir, 'T-01');
342
-
343
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
344
- expect(result.canRun).toBe(false);
345
- expect(result.blockers.some(b => b.includes('Global cooldown'))).toBe(true);
346
- });
347
-
348
- it('should block when quota is exhausted', async () => {
349
- // Exhaust quota
350
- for (let i = 0; i < 3; i++) {
351
- await recordRunStart(tempDir, 'T-01');
352
- await recordRunEnd(tempDir, 'success', { sampleCount: 1 });
353
- vi.advanceTimersByTime(DEFAULT_GLOBAL_COOLDOWN_MS + 1000);
354
- }
355
-
356
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
357
- expect(result.canRun).toBe(false);
358
- expect(result.blockers.some(b => b.includes('Quota exhausted'))).toBe(true);
359
- });
360
-
361
- it('should report all blockers when multiple conditions block', async () => {
362
- // Create recent session AND set global cooldown
363
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
364
- await recordRunStart(tempDir, 'T-01');
365
-
366
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
367
- expect(result.canRun).toBe(false);
368
- expect(result.blockers.length).toBeGreaterThanOrEqual(2);
369
- });
370
-
371
- it('should include idle info in preflight result', () => {
372
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
373
- expect(result.idle).toBeDefined();
374
- expect(result.idle.isIdle).toBe(true); // No sessions
375
- });
376
-
377
- it('should include cooldown info in preflight result', () => {
378
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
379
- expect(result.cooldown).toBeDefined();
380
- expect(result.cooldown.globalCooldownActive).toBe(false);
381
- });
382
- });
383
-
384
- // -------------------------------------------------------------------------
385
- // Abandoned Session Tests (dedicated)
386
- // -------------------------------------------------------------------------
387
-
388
- describe('abandoned sessions', () => {
389
- it('should not block nocturnal flow when all sessions are abandoned but workspace otherwise empty', () => {
390
- // Create only abandoned sessions (no recent activity)
391
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z')); // 3 hours ago
392
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
393
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
394
-
395
- const result = checkWorkspaceIdle(workspaceDir, {
396
- idleThresholdMs: 30 * 60 * 1000,
397
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
398
- });
399
-
400
- // Workspace should be considered idle (all sessions abandoned = effectively no sessions)
401
- expect(result.isIdle).toBe(true);
402
- expect(result.userActiveSessions).toBe(0);
403
- });
404
-
405
- // TODO: Fix - abandonedSessionIds not being populated correctly
406
- it.skip('should not incorrectly block when there are abandoned AND active sessions', () => {
407
- // Abandoned session (3 hours ago)
408
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
409
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
410
-
411
- // Recent session (5 min ago)
412
- vi.setSystemTime(new Date('2026-03-27T11:55:00.000Z'));
413
- trackToolRead('session-active', 'src/main.ts', workspaceDir);
414
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
415
-
416
- const idleResult = checkWorkspaceIdle(workspaceDir, {
417
- idleThresholdMs: 30 * 60 * 1000,
418
- abandonedThresholdMs: 2 * 60 * 60 * 1000,
419
- });
420
-
421
- // Should NOT be idle because there's a recent active session
422
- expect(idleResult.isIdle).toBe(false);
423
- expect(idleResult.userActiveSessions).toBe(1);
424
- expect(idleResult.abandonedSessionIds).toContain('session-abandoned');
425
- });
426
-
427
- it('should persist abandoned sessions do not create cooldown state', async () => {
428
- // Create abandoned session
429
- vi.setSystemTime(new Date('2026-03-27T09:00:00.000Z'));
430
- trackToolRead('session-abandoned', 'src/main.ts', workspaceDir);
431
- vi.setSystemTime(new Date('2026-03-27T12:00:00.000Z'));
432
-
433
- // Workspace is idle, preflight should pass
434
- const result = checkPreflight(workspaceDir, tempDir, 'T-01');
435
- expect(result.canRun).toBe(true);
436
- });
437
- });
438
-
439
- // -------------------------------------------------------------------------
440
- // File Persistence Tests
441
- // -------------------------------------------------------------------------
442
-
443
- describe('file persistence', () => {
444
- it('should create nocturnal-runtime.json on first write', async () => {
445
- const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
446
- expect(fs.existsSync(filePath)).toBe(false);
447
-
448
- await recordRunStart(tempDir, 'T-01');
449
- expect(fs.existsSync(filePath)).toBe(true);
450
- });
451
-
452
- it('should survive corrupted JSON (start fresh)', async () => {
453
- const filePath = path.join(tempDir, NOCTURNAL_RUNTIME_FILE);
454
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
455
- fs.writeFileSync(filePath, '{ corrupted json }', 'utf-8');
456
-
457
- const state = await getRuntimeState(tempDir);
458
- // Should return default state, not throw
459
- expect(state.principleCooldowns).toEqual({});
460
- expect(state.recentRunTimestamps).toEqual([]);
461
- });
462
-
463
- it('should read persisted cooldown on restart', async () => {
464
- await recordRunStart(tempDir, 'T-01');
465
- await recordRunEnd(tempDir, 'success');
466
-
467
- // Simulate restart by re-reading
468
- const state = await getRuntimeState(tempDir);
469
- expect(state.lastSuccessfulRunAt).toBeTruthy();
470
- expect(state.principleCooldowns['T-01']).toBeTruthy();
471
- });
472
- });
473
- });