principles-disciple 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for bug fixes in v1.9.1
|
|
3
|
+
*
|
|
4
|
+
* Validates fixes for:
|
|
5
|
+
* - #208/#209: isExpectedSubagentError expanded + terminal_error classification
|
|
6
|
+
* - #212: evaluability defaults to weak_heuristic, principles are evaluable
|
|
7
|
+
* - #213: fire-and-forget Promise has .catch()
|
|
8
|
+
* - #214: sleep_reflection timeout expires nocturnal workflow
|
|
9
|
+
* - #207/#210: stateDir properly passed through WorkspaceContext
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
16
|
+
import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
|
|
17
|
+
import {
|
|
18
|
+
listEvaluablePrinciples,
|
|
19
|
+
transitionInternalizationStatus,
|
|
20
|
+
getPrincipleState,
|
|
21
|
+
} from '../../src/core/principle-training-state.js';
|
|
22
|
+
import { isExpectedSubagentError } from '../../src/service/subagent-workflow/subagent-error-utils.js';
|
|
23
|
+
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
24
|
+
|
|
25
|
+
const tempDirs: string[] = [];
|
|
26
|
+
|
|
27
|
+
function makeTempDir(): string {
|
|
28
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-regression-'));
|
|
29
|
+
tempDirs.push(dir);
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const dir of tempDirs.splice(0)) {
|
|
35
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── #208/#209: isExpectedSubagentError covers daemon-mode errors ──
|
|
40
|
+
|
|
41
|
+
describe('#208/#209: isExpectedSubagentError daemon-mode coverage', () => {
|
|
42
|
+
it('matches original gateway request errors', () => {
|
|
43
|
+
expect(isExpectedSubagentError('Plugin runtime subagent methods are only available during a gateway request')).toBe(true);
|
|
44
|
+
expect(isExpectedSubagentError('cannot start workflow for boot session')).toBe(true);
|
|
45
|
+
expect(isExpectedSubagentError('subagent runtime unavailable')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('matches NocturnalWorkflowManager specific error', () => {
|
|
49
|
+
expect(isExpectedSubagentError('NocturnalWorkflowManager: subagent runtime unavailable')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('matches daemon-mode connection errors', () => {
|
|
53
|
+
expect(isExpectedSubagentError('subagent is not available')).toBe(true);
|
|
54
|
+
expect(isExpectedSubagentError('gateway is not running')).toBe(true);
|
|
55
|
+
expect(isExpectedSubagentError('process isolation error: ECONNREFUSED')).toBe(true);
|
|
56
|
+
// #3 review fix: connection errors now require 'subagent' in message to reduce false positives
|
|
57
|
+
expect(isExpectedSubagentError('subagent connection refused')).toBe(true);
|
|
58
|
+
expect(isExpectedSubagentError('subagent connection reset by peer')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not match unrelated errors', () => {
|
|
62
|
+
expect(isExpectedSubagentError('file not found')).toBe(false);
|
|
63
|
+
expect(isExpectedSubagentError('syntax error in config')).toBe(false);
|
|
64
|
+
expect(isExpectedSubagentError('network timeout to external API')).toBe(false);
|
|
65
|
+
// #3 review fix: generic connection errors without 'subagent' should NOT match
|
|
66
|
+
expect(isExpectedSubagentError('connection refused')).toBe(false);
|
|
67
|
+
expect(isExpectedSubagentError('connection reset by peer')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── #212: evaluability defaults to weak_heuristic ──
|
|
72
|
+
|
|
73
|
+
describe('#212: principle evaluability and internalization status', () => {
|
|
74
|
+
it('new principle without explicit evaluability defaults to weak_heuristic', () => {
|
|
75
|
+
const workspace = makeTempDir();
|
|
76
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
77
|
+
|
|
78
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
79
|
+
painId: 'regression-212-1',
|
|
80
|
+
painType: 'tool_failure',
|
|
81
|
+
triggerPattern: 'api call times out after 30s',
|
|
82
|
+
action: 'implement retry with exponential backoff',
|
|
83
|
+
source: 'test',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(id).not.toBeNull();
|
|
87
|
+
const principle = reducer.getPrincipleById(id!);
|
|
88
|
+
expect(principle).not.toBeNull();
|
|
89
|
+
expect(principle!.evaluability).toBe('weak_heuristic');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('new principle with weak_heuristic evaluability gets needs_training status', () => {
|
|
93
|
+
const workspace = makeTempDir();
|
|
94
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
95
|
+
|
|
96
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
97
|
+
painId: 'regression-212-2',
|
|
98
|
+
painType: 'tool_failure',
|
|
99
|
+
triggerPattern: 'database connection pool exhausted',
|
|
100
|
+
action: 'queue requests and retry with timeout',
|
|
101
|
+
source: 'test',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(id).not.toBeNull();
|
|
105
|
+
const trainingState = getPrincipleState(workspace, id!);
|
|
106
|
+
expect(trainingState.internalizationStatus).toBe('needs_training');
|
|
107
|
+
expect(trainingState.evaluability).toBe('weak_heuristic');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('new principle with manual_only evaluability gets prompt_only status', () => {
|
|
111
|
+
const workspace = makeTempDir();
|
|
112
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
113
|
+
|
|
114
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
115
|
+
painId: 'regression-212-3',
|
|
116
|
+
painType: 'user_frustration',
|
|
117
|
+
triggerPattern: 'ambiguous user intent',
|
|
118
|
+
action: 'ask clarifying questions before acting',
|
|
119
|
+
source: 'test',
|
|
120
|
+
evaluability: 'manual_only',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(id).not.toBeNull();
|
|
124
|
+
const trainingState = getPrincipleState(workspace, id!);
|
|
125
|
+
expect(trainingState.internalizationStatus).toBe('prompt_only');
|
|
126
|
+
expect(trainingState.evaluability).toBe('manual_only');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('listEvaluablePrinciples includes weak_heuristic principles', () => {
|
|
130
|
+
const workspace = makeTempDir();
|
|
131
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
132
|
+
|
|
133
|
+
// Create a weak_heuristic principle (should be evaluable)
|
|
134
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
135
|
+
painId: 'regression-212-eval-1',
|
|
136
|
+
painType: 'tool_failure',
|
|
137
|
+
triggerPattern: 'regex parse error on empty input',
|
|
138
|
+
action: 'validate input before regex processing',
|
|
139
|
+
source: 'test',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(id).not.toBeNull();
|
|
143
|
+
const evaluable = listEvaluablePrinciples(workspace);
|
|
144
|
+
expect(evaluable.length).toBeGreaterThan(0);
|
|
145
|
+
// Principle IDs are auto-generated (P_001, P_002...), check by the returned ID
|
|
146
|
+
expect(evaluable.some(p => p.principleId === id)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('listEvaluablePrinciples excludes manual_only + prompt_only principles', () => {
|
|
150
|
+
const workspace = makeTempDir();
|
|
151
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
152
|
+
|
|
153
|
+
// Create a manual_only principle (should NOT be evaluable)
|
|
154
|
+
reducer.createPrincipleFromDiagnosis({
|
|
155
|
+
painId: 'regression-212-noeval-1',
|
|
156
|
+
painType: 'user_frustration',
|
|
157
|
+
triggerPattern: 'user confused about feature',
|
|
158
|
+
action: 'provide contextual help',
|
|
159
|
+
source: 'test',
|
|
160
|
+
evaluability: 'manual_only',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const evaluable = listEvaluablePrinciples(workspace);
|
|
164
|
+
expect(evaluable.some(p => p.principleId.includes('regression-212-noeval'))).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── #212: internalization status transition mechanism ──
|
|
169
|
+
|
|
170
|
+
describe('#212: internalization status transitions', () => {
|
|
171
|
+
it('transitions prompt_only → needs_training', () => {
|
|
172
|
+
const workspace = makeTempDir();
|
|
173
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
174
|
+
|
|
175
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
176
|
+
painId: 'regression-212-transition-1',
|
|
177
|
+
painType: 'user_frustration',
|
|
178
|
+
triggerPattern: 'user error on complex form',
|
|
179
|
+
action: 'simplify form with progressive disclosure',
|
|
180
|
+
source: 'test',
|
|
181
|
+
evaluability: 'manual_only',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(id).not.toBeNull();
|
|
185
|
+
const before = getPrincipleState(workspace, id!);
|
|
186
|
+
expect(before.internalizationStatus).toBe('prompt_only');
|
|
187
|
+
|
|
188
|
+
transitionInternalizationStatus(workspace, id!, 'needs_training');
|
|
189
|
+
const after = getPrincipleState(workspace, id!);
|
|
190
|
+
expect(after.internalizationStatus).toBe('needs_training');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects invalid transition prompt_only → in_training', () => {
|
|
194
|
+
const workspace = makeTempDir();
|
|
195
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
196
|
+
|
|
197
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
198
|
+
painId: 'regression-212-invalid-transition',
|
|
199
|
+
painType: 'user_frustration',
|
|
200
|
+
triggerPattern: 'user error on complex form',
|
|
201
|
+
action: 'simplify form',
|
|
202
|
+
source: 'test',
|
|
203
|
+
evaluability: 'manual_only',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(() => transitionInternalizationStatus(workspace, id!, 'in_training')).toThrow(
|
|
207
|
+
'Invalid transition: prompt_only → in_training'
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('transitions regressed → needs_training', () => {
|
|
212
|
+
const workspace = makeTempDir();
|
|
213
|
+
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace, stateDir: workspace });
|
|
214
|
+
|
|
215
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
216
|
+
painId: 'regression-212-regress',
|
|
217
|
+
painType: 'tool_failure',
|
|
218
|
+
triggerPattern: 'timeout on slow endpoint',
|
|
219
|
+
action: 'add pagination and caching',
|
|
220
|
+
source: 'test',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Transition chain: needs_training → in_training → deployed_pending_eval → regressed → needs_training
|
|
224
|
+
transitionInternalizationStatus(workspace, id!, 'in_training');
|
|
225
|
+
transitionInternalizationStatus(workspace, id!, 'deployed_pending_eval');
|
|
226
|
+
|
|
227
|
+
const before = getPrincipleState(workspace, id!);
|
|
228
|
+
expect(before.internalizationStatus).toBe('deployed_pending_eval');
|
|
229
|
+
|
|
230
|
+
// Can transition to regressed from deployed_pending_eval
|
|
231
|
+
transitionInternalizationStatus(workspace, id!, 'regressed');
|
|
232
|
+
const regressed = getPrincipleState(workspace, id!);
|
|
233
|
+
expect(regressed.internalizationStatus).toBe('regressed');
|
|
234
|
+
|
|
235
|
+
// Can transition back to needs_training from regressed
|
|
236
|
+
transitionInternalizationStatus(workspace, id!, 'needs_training');
|
|
237
|
+
const after = getPrincipleState(workspace, id!);
|
|
238
|
+
expect(after.internalizationStatus).toBe('needs_training');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── #207/#210: stateDir via WorkspaceContext ──
|
|
243
|
+
|
|
244
|
+
describe('#207/#210: WorkspaceContext provides stateDir to evolutionReducer', () => {
|
|
245
|
+
it('evolutionReducer from WorkspaceContext has stateDir', () => {
|
|
246
|
+
const workspace = makeTempDir();
|
|
247
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir: workspace });
|
|
248
|
+
|
|
249
|
+
// The reducer should have stateDir set via WorkspaceContext
|
|
250
|
+
// We can verify by creating a principle and checking training store
|
|
251
|
+
const reducer = wctx.evolutionReducer;
|
|
252
|
+
const id = reducer.createPrincipleFromDiagnosis({
|
|
253
|
+
painId: 'regression-207-1',
|
|
254
|
+
painType: 'tool_failure',
|
|
255
|
+
triggerPattern: 'null pointer on empty list',
|
|
256
|
+
action: 'check for empty list before iteration',
|
|
257
|
+
source: 'test',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(id).not.toBeNull();
|
|
261
|
+
// If stateDir was properly passed, training store should have been written
|
|
262
|
+
const trainingState = getPrincipleState(workspace, id!);
|
|
263
|
+
expect(trainingState.principleId).toBe(id);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ReplayEngine } from '../../src/core/replay-engine.js';
|
|
6
|
+
import { TrajectoryDatabase, TrajectoryRegistry } from '../../src/core/trajectory.js';
|
|
7
|
+
import { registerSample } from '../../src/core/nocturnal-dataset.js';
|
|
8
|
+
import { createImplementationAssetDir, getImplementationAssetRoot } from '../../src/core/code-implementation-storage.js';
|
|
9
|
+
import { saveLedger, type LedgerPrinciple, type LedgerRule } from '../../src/core/principle-tree-ledger.js';
|
|
10
|
+
import type { Implementation } from '../../src/types/principle-tree-schema.js';
|
|
11
|
+
import { safeRmDir } from '../test-utils.js';
|
|
12
|
+
|
|
13
|
+
function makePrinciple(): LedgerPrinciple {
|
|
14
|
+
return {
|
|
15
|
+
id: 'P-1',
|
|
16
|
+
version: 1,
|
|
17
|
+
text: 'Protect risky writes without READY plans',
|
|
18
|
+
triggerPattern: 'write',
|
|
19
|
+
action: 'require approval before risky write',
|
|
20
|
+
status: 'active',
|
|
21
|
+
priority: 'P1',
|
|
22
|
+
scope: 'general',
|
|
23
|
+
evaluability: 'deterministic',
|
|
24
|
+
valueScore: 0,
|
|
25
|
+
adherenceRate: 0,
|
|
26
|
+
painPreventedCount: 0,
|
|
27
|
+
derivedFromPainIds: [],
|
|
28
|
+
ruleIds: ['R-1'],
|
|
29
|
+
conflictsWithPrincipleIds: [],
|
|
30
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
31
|
+
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeRule(): LedgerRule {
|
|
36
|
+
return {
|
|
37
|
+
id: 'R-1',
|
|
38
|
+
version: 1,
|
|
39
|
+
name: 'Require approval for risky writes',
|
|
40
|
+
description: 'Risky write operations without a READY plan need approval.',
|
|
41
|
+
type: 'gate',
|
|
42
|
+
triggerCondition: 'tool=write',
|
|
43
|
+
enforcement: 'block',
|
|
44
|
+
action: 'require approval for risky write',
|
|
45
|
+
principleId: 'P-1',
|
|
46
|
+
status: 'implemented',
|
|
47
|
+
coverageRate: 0,
|
|
48
|
+
falsePositiveRate: 0,
|
|
49
|
+
implementationIds: ['IMPL-1'],
|
|
50
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
51
|
+
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeImplementation(): Implementation {
|
|
56
|
+
return {
|
|
57
|
+
id: 'IMPL-1',
|
|
58
|
+
ruleId: 'R-1',
|
|
59
|
+
type: 'code',
|
|
60
|
+
path: path.join('virtual', 'IMPL-1.js'),
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
coversCondition: 'risky write',
|
|
63
|
+
coveragePercentage: 100,
|
|
64
|
+
lifecycleState: 'candidate',
|
|
65
|
+
createdAt: '2026-04-08T00:00:00.000Z',
|
|
66
|
+
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('ReplayEngine', () => {
|
|
71
|
+
let tempDir: string;
|
|
72
|
+
let workspaceDir: string;
|
|
73
|
+
let stateDir: string;
|
|
74
|
+
let trajectory: TrajectoryDatabase;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-replay-engine-'));
|
|
78
|
+
workspaceDir = path.join(tempDir, 'workspace');
|
|
79
|
+
stateDir = path.join(tempDir, '.state');
|
|
80
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
81
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
path.join(workspaceDir, 'PROFILE.json'),
|
|
84
|
+
JSON.stringify({ risk_paths: ['src/**'] }, null, 2),
|
|
85
|
+
'utf-8',
|
|
86
|
+
);
|
|
87
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
try {
|
|
92
|
+
trajectory.dispose();
|
|
93
|
+
} catch {
|
|
94
|
+
// Best effort cleanup.
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
TrajectoryRegistry.dispose(workspaceDir);
|
|
98
|
+
} catch {
|
|
99
|
+
// Best effort cleanup.
|
|
100
|
+
}
|
|
101
|
+
safeRmDir(tempDir);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function seedLedgerAndImplementation(): void {
|
|
105
|
+
saveLedger(stateDir, {
|
|
106
|
+
trainingStore: {
|
|
107
|
+
'P-1': {
|
|
108
|
+
principleId: 'P-1',
|
|
109
|
+
evaluability: 'deterministic',
|
|
110
|
+
applicableOpportunityCount: 1,
|
|
111
|
+
observedViolationCount: 0,
|
|
112
|
+
complianceRate: 1,
|
|
113
|
+
violationTrend: 0,
|
|
114
|
+
generatedSampleCount: 0,
|
|
115
|
+
approvedSampleCount: 0,
|
|
116
|
+
includedTrainRunIds: [],
|
|
117
|
+
deployedCheckpointIds: [],
|
|
118
|
+
internalizationStatus: 'internalized',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
tree: {
|
|
122
|
+
principles: { 'P-1': makePrinciple() },
|
|
123
|
+
rules: { 'R-1': makeRule() },
|
|
124
|
+
implementations: { 'IMPL-1': makeImplementation() },
|
|
125
|
+
metrics: {},
|
|
126
|
+
lastUpdated: '2026-04-08T00:00:00.000Z',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
createImplementationAssetDir(stateDir, 'IMPL-1', '1.0.0', {
|
|
131
|
+
entrySource: [
|
|
132
|
+
'export const meta = {',
|
|
133
|
+
' name: "risky-write-guard",',
|
|
134
|
+
' version: "1.0.0",',
|
|
135
|
+
' ruleId: "R-1",',
|
|
136
|
+
' coversCondition: "risky write",',
|
|
137
|
+
'};',
|
|
138
|
+
'export function evaluate(input, helpers) {',
|
|
139
|
+
" if (helpers.isRiskPath() && helpers.getToolName() === 'write' && helpers.getPlanStatus() !== 'READY') {",
|
|
140
|
+
" return { decision: 'requireApproval', matched: true, reason: 'plan required' };",
|
|
141
|
+
' }',
|
|
142
|
+
" return { decision: 'allow', matched: false, reason: 'not-applicable' };",
|
|
143
|
+
'}',
|
|
144
|
+
].join('\n'),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function seedPainNegativeSample(): void {
|
|
149
|
+
trajectory.recordSession({
|
|
150
|
+
sessionId: 'session-1',
|
|
151
|
+
startedAt: '2026-04-08T00:00:00.000Z',
|
|
152
|
+
});
|
|
153
|
+
trajectory.recordToolCall({
|
|
154
|
+
sessionId: 'session-1',
|
|
155
|
+
toolName: 'write',
|
|
156
|
+
outcome: 'blocked',
|
|
157
|
+
errorMessage: 'risky write requires approval',
|
|
158
|
+
paramsJson: { filePath: 'src/app.ts' },
|
|
159
|
+
createdAt: '2026-04-08T00:01:00.000Z',
|
|
160
|
+
});
|
|
161
|
+
trajectory.recordPainEvent({
|
|
162
|
+
sessionId: 'session-1',
|
|
163
|
+
source: 'gate',
|
|
164
|
+
score: 75,
|
|
165
|
+
reason: 'risky write requires approval',
|
|
166
|
+
createdAt: '2026-04-08T00:01:01.000Z',
|
|
167
|
+
});
|
|
168
|
+
trajectory.recordGateBlock({
|
|
169
|
+
sessionId: 'session-1',
|
|
170
|
+
toolName: 'write',
|
|
171
|
+
filePath: 'src/app.ts',
|
|
172
|
+
reason: 'risky write requires approval',
|
|
173
|
+
planStatus: 'NONE',
|
|
174
|
+
createdAt: '2026-04-08T00:01:02.000Z',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const artifactPath = path.join(stateDir, 'nocturnal', 'samples', 'artifact-1.json');
|
|
178
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
artifactPath,
|
|
181
|
+
JSON.stringify(
|
|
182
|
+
{
|
|
183
|
+
artifactId: 'artifact-1',
|
|
184
|
+
sessionId: 'session-1',
|
|
185
|
+
principleId: 'P-1',
|
|
186
|
+
sourceSnapshotRef: 'snapshot-session-1',
|
|
187
|
+
badDecision: 'Retried the write without checking approval requirements',
|
|
188
|
+
betterDecision: 'Check the approval requirements before writing',
|
|
189
|
+
rationale: 'Riskiest paths should not be written without a plan',
|
|
190
|
+
createdAt: '2026-04-08T00:02:00.000Z',
|
|
191
|
+
},
|
|
192
|
+
null,
|
|
193
|
+
2,
|
|
194
|
+
),
|
|
195
|
+
'utf-8',
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
registerSample(
|
|
199
|
+
workspaceDir,
|
|
200
|
+
{
|
|
201
|
+
artifactId: 'artifact-1',
|
|
202
|
+
sessionId: 'session-1',
|
|
203
|
+
principleId: 'P-1',
|
|
204
|
+
sourceSnapshotRef: 'snapshot-session-1',
|
|
205
|
+
badDecision: 'Retried the write without checking approval requirements',
|
|
206
|
+
betterDecision: 'Check the approval requirements before writing',
|
|
207
|
+
rationale: 'Riskiest paths should not be written without a plan',
|
|
208
|
+
createdAt: '2026-04-08T00:02:00.000Z',
|
|
209
|
+
},
|
|
210
|
+
artifactPath,
|
|
211
|
+
null,
|
|
212
|
+
'pain-negative',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it('persists replay reports under the implementation asset root', () => {
|
|
217
|
+
seedLedgerAndImplementation();
|
|
218
|
+
seedPainNegativeSample();
|
|
219
|
+
|
|
220
|
+
const engine = new ReplayEngine(workspaceDir, stateDir);
|
|
221
|
+
const report = engine.runReplayForImplementation('IMPL-1', ['pain-negative']);
|
|
222
|
+
|
|
223
|
+
expect(report.overallDecision).toBe('pass');
|
|
224
|
+
expect(report.replayResults.painNegative.passed).toBe(1);
|
|
225
|
+
expect(report.replayResults.painNegative.failed).toBe(0);
|
|
226
|
+
|
|
227
|
+
const reportDir = path.join(
|
|
228
|
+
getImplementationAssetRoot(stateDir, 'IMPL-1'),
|
|
229
|
+
'replays',
|
|
230
|
+
);
|
|
231
|
+
expect(fs.existsSync(reportDir)).toBe(true);
|
|
232
|
+
expect(fs.readdirSync(reportDir).some((file) => file.endsWith('.json'))).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Host Helpers Tests
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Verify that the helper whitelist:
|
|
5
|
+
* - Returns correct values from the frozen input snapshot
|
|
6
|
+
* - Is a frozen object that cannot be mutated
|
|
7
|
+
* - All helpers are pure functions with no side effects
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { createRuleHostHelpers } from '../../src/core/rule-host-helpers.js';
|
|
12
|
+
import type { RuleHostInput } from '../../src/core/rule-host-types.js';
|
|
13
|
+
|
|
14
|
+
function makeInput(overrides?: Partial<RuleHostInput>): RuleHostInput {
|
|
15
|
+
return {
|
|
16
|
+
action: {
|
|
17
|
+
toolName: 'write',
|
|
18
|
+
normalizedPath: 'src/test.ts',
|
|
19
|
+
paramsSummary: {},
|
|
20
|
+
},
|
|
21
|
+
workspace: {
|
|
22
|
+
isRiskPath: false,
|
|
23
|
+
planStatus: 'NONE',
|
|
24
|
+
hasPlanFile: false,
|
|
25
|
+
},
|
|
26
|
+
session: {
|
|
27
|
+
sessionId: 'session-123',
|
|
28
|
+
currentGfi: 10,
|
|
29
|
+
recentThinking: false,
|
|
30
|
+
},
|
|
31
|
+
evolution: {
|
|
32
|
+
epTier: 2,
|
|
33
|
+
},
|
|
34
|
+
derived: {
|
|
35
|
+
estimatedLineChanges: 50,
|
|
36
|
+
bashRisk: 'normal',
|
|
37
|
+
},
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('createRuleHostHelpers', () => {
|
|
43
|
+
it('should return a frozen object', () => {
|
|
44
|
+
const helpers = createRuleHostHelpers(makeInput());
|
|
45
|
+
expect(Object.isFrozen(helpers)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw TypeError when attempting to mutate helpers', () => {
|
|
49
|
+
const helpers = createRuleHostHelpers(makeInput()) as Record<string, unknown>;
|
|
50
|
+
expect(() => {
|
|
51
|
+
(helpers as any).isRiskPath = () => true;
|
|
52
|
+
}).toThrow(TypeError);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return correct isRiskPath from input snapshot', () => {
|
|
56
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
57
|
+
workspace: { isRiskPath: true, planStatus: 'READY', hasPlanFile: true },
|
|
58
|
+
}));
|
|
59
|
+
expect(helpers.isRiskPath()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return false isRiskPath by default', () => {
|
|
63
|
+
const helpers = createRuleHostHelpers(makeInput());
|
|
64
|
+
expect(helpers.isRiskPath()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return correct toolName from input snapshot', () => {
|
|
68
|
+
const helpers = createRuleHostHelpers(makeInput());
|
|
69
|
+
expect(helpers.getToolName()).toBe('write');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return correct estimatedLineChanges from input snapshot', () => {
|
|
73
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
74
|
+
derived: { estimatedLineChanges: 200, bashRisk: 'dangerous' },
|
|
75
|
+
}));
|
|
76
|
+
expect(helpers.getEstimatedLineChanges()).toBe(200);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return correct bashRisk from input snapshot', () => {
|
|
80
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
81
|
+
derived: { estimatedLineChanges: 50, bashRisk: 'safe' },
|
|
82
|
+
}));
|
|
83
|
+
expect(helpers.getBashRisk()).toBe('safe');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return correct hasPlanFile from input snapshot', () => {
|
|
87
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
88
|
+
workspace: { isRiskPath: false, planStatus: 'READY', hasPlanFile: true },
|
|
89
|
+
}));
|
|
90
|
+
expect(helpers.hasPlanFile()).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return correct planStatus from input snapshot', () => {
|
|
94
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
95
|
+
workspace: { isRiskPath: false, planStatus: 'DRAFT', hasPlanFile: true },
|
|
96
|
+
}));
|
|
97
|
+
expect(helpers.getPlanStatus()).toBe('DRAFT');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return correct epTier from input snapshot', () => {
|
|
101
|
+
const helpers = createRuleHostHelpers(makeInput({
|
|
102
|
+
evolution: { epTier: 4 },
|
|
103
|
+
}));
|
|
104
|
+
expect(helpers.getCurrentEpiTier()).toBe(4);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('all helpers should be pure functions with no side effects', () => {
|
|
108
|
+
const input = makeInput();
|
|
109
|
+
const helpers = createRuleHostHelpers(input);
|
|
110
|
+
|
|
111
|
+
// Call each helper multiple times — should always return the same value
|
|
112
|
+
expect(helpers.isRiskPath()).toBe(helpers.isRiskPath());
|
|
113
|
+
expect(helpers.getToolName()).toBe(helpers.getToolName());
|
|
114
|
+
expect(helpers.getEstimatedLineChanges()).toBe(helpers.getEstimatedLineChanges());
|
|
115
|
+
expect(helpers.getBashRisk()).toBe(helpers.getBashRisk());
|
|
116
|
+
expect(helpers.hasPlanFile()).toBe(helpers.hasPlanFile());
|
|
117
|
+
expect(helpers.getPlanStatus()).toBe(helpers.getPlanStatus());
|
|
118
|
+
expect(helpers.getCurrentEpiTier()).toBe(helpers.getCurrentEpiTier());
|
|
119
|
+
});
|
|
120
|
+
});
|