@soleri/core 2.0.2 → 2.4.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/dist/brain/brain.d.ts +14 -50
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +207 -16
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +86 -0
- package/dist/brain/intelligence.d.ts.map +1 -0
- package/dist/brain/intelligence.js +771 -0
- package/dist/brain/intelligence.js.map +1 -0
- package/dist/brain/types.d.ts +197 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +2 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/cognee/client.d.ts +35 -0
- package/dist/cognee/client.d.ts.map +1 -0
- package/dist/cognee/client.js +291 -0
- package/dist/cognee/client.js.map +1 -0
- package/dist/cognee/types.d.ts +46 -0
- package/dist/cognee/types.d.ts.map +1 -0
- package/dist/cognee/types.js +3 -0
- package/dist/cognee/types.js.map +1 -0
- package/dist/control/identity-manager.d.ts +22 -0
- package/dist/control/identity-manager.d.ts.map +1 -0
- package/dist/control/identity-manager.js +233 -0
- package/dist/control/identity-manager.js.map +1 -0
- package/dist/control/intent-router.d.ts +32 -0
- package/dist/control/intent-router.d.ts.map +1 -0
- package/dist/control/intent-router.js +242 -0
- package/dist/control/intent-router.js.map +1 -0
- package/dist/control/types.d.ts +68 -0
- package/dist/control/types.d.ts.map +1 -0
- package/dist/control/types.js +9 -0
- package/dist/control/types.js.map +1 -0
- package/dist/curator/curator.d.ts +29 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +142 -5
- package/dist/curator/curator.js.map +1 -1
- package/dist/facades/types.d.ts +1 -1
- package/dist/governance/governance.d.ts +42 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/governance.js +488 -0
- package/dist/governance/governance.js.map +1 -0
- package/dist/governance/index.d.ts +3 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +2 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/types.d.ts +102 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/governance/types.js +3 -0
- package/dist/governance/types.js.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +9 -2
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/logging/logger.d.ts +37 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +145 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/types.d.ts +19 -0
- package/dist/logging/types.d.ts.map +1 -0
- package/dist/logging/types.js +2 -0
- package/dist/logging/types.js.map +1 -0
- package/dist/loop/loop-manager.d.ts +49 -0
- package/dist/loop/loop-manager.d.ts.map +1 -0
- package/dist/loop/loop-manager.js +105 -0
- package/dist/loop/loop-manager.js.map +1 -0
- package/dist/loop/types.d.ts +35 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +8 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +29 -0
- package/dist/planning/gap-analysis.d.ts.map +1 -0
- package/dist/planning/gap-analysis.js +265 -0
- package/dist/planning/gap-analysis.js.map +1 -0
- package/dist/planning/gap-types.d.ts +29 -0
- package/dist/planning/gap-types.d.ts.map +1 -0
- package/dist/planning/gap-types.js +28 -0
- package/dist/planning/gap-types.js.map +1 -0
- package/dist/planning/planner.d.ts +150 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +365 -2
- package/dist/planning/planner.js.map +1 -1
- package/dist/project/project-registry.d.ts +79 -0
- package/dist/project/project-registry.d.ts.map +1 -0
- package/dist/project/project-registry.js +276 -0
- package/dist/project/project-registry.js.map +1 -0
- package/dist/project/types.d.ts +28 -0
- package/dist/project/types.d.ts.map +1 -0
- package/dist/project/types.js +5 -0
- package/dist/project/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +13 -0
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
- package/dist/runtime/admin-extra-ops.js +284 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -0
- package/dist/runtime/admin-ops.d.ts +15 -0
- package/dist/runtime/admin-ops.d.ts.map +1 -0
- package/dist/runtime/admin-ops.js +322 -0
- package/dist/runtime/admin-ops.js.map +1 -0
- package/dist/runtime/capture-ops.d.ts +15 -0
- package/dist/runtime/capture-ops.d.ts.map +1 -0
- package/dist/runtime/capture-ops.js +345 -0
- package/dist/runtime/capture-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +7 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +646 -15
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +9 -0
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
- package/dist/runtime/curator-extra-ops.js +59 -0
- package/dist/runtime/curator-extra-ops.js.map +1 -0
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +59 -13
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts +14 -0
- package/dist/runtime/grading-ops.d.ts.map +1 -0
- package/dist/runtime/grading-ops.js +105 -0
- package/dist/runtime/grading-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +13 -0
- package/dist/runtime/loop-ops.d.ts.map +1 -0
- package/dist/runtime/loop-ops.js +179 -0
- package/dist/runtime/loop-ops.js.map +1 -0
- package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
- package/dist/runtime/memory-cross-project-ops.js +165 -0
- package/dist/runtime/memory-cross-project-ops.js.map +1 -0
- package/dist/runtime/memory-extra-ops.d.ts +13 -0
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
- package/dist/runtime/memory-extra-ops.js +173 -0
- package/dist/runtime/memory-extra-ops.js.map +1 -0
- package/dist/runtime/orchestrate-ops.d.ts +17 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
- package/dist/runtime/orchestrate-ops.js +240 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts +17 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
- package/dist/runtime/planning-extra-ops.js +300 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts +15 -0
- package/dist/runtime/project-ops.d.ts.map +1 -0
- package/dist/runtime/project-ops.js +181 -0
- package/dist/runtime/project-ops.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +48 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +9 -0
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
- package/dist/runtime/vault-extra-ops.js +195 -0
- package/dist/runtime/vault-extra-ops.js.map +1 -0
- package/dist/telemetry/telemetry.d.ts +48 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/telemetry/telemetry.js +87 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/vault/vault.d.ts +94 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +340 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/admin-extra-ops.test.ts +420 -0
- package/src/__tests__/admin-ops.test.ts +271 -0
- package/src/__tests__/brain-intelligence.test.ts +828 -0
- package/src/__tests__/brain.test.ts +396 -27
- package/src/__tests__/capture-ops.test.ts +509 -0
- package/src/__tests__/cognee-client.test.ts +524 -0
- package/src/__tests__/core-ops.test.ts +341 -49
- package/src/__tests__/curator-extra-ops.test.ts +359 -0
- package/src/__tests__/curator.test.ts +126 -31
- package/src/__tests__/domain-ops.test.ts +111 -9
- package/src/__tests__/governance.test.ts +522 -0
- package/src/__tests__/grading-ops.test.ts +340 -0
- package/src/__tests__/identity-manager.test.ts +243 -0
- package/src/__tests__/intent-router.test.ts +222 -0
- package/src/__tests__/logger.test.ts +200 -0
- package/src/__tests__/loop-ops.test.ts +398 -0
- package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
- package/src/__tests__/memory-extra-ops.test.ts +352 -0
- package/src/__tests__/orchestrate-ops.test.ts +284 -0
- package/src/__tests__/planner.test.ts +331 -0
- package/src/__tests__/planning-extra-ops.test.ts +548 -0
- package/src/__tests__/project-ops.test.ts +367 -0
- package/src/__tests__/runtime.test.ts +13 -11
- package/src/__tests__/vault-extra-ops.test.ts +407 -0
- package/src/brain/brain.ts +308 -72
- package/src/brain/intelligence.ts +1230 -0
- package/src/brain/types.ts +214 -0
- package/src/cognee/client.ts +352 -0
- package/src/cognee/types.ts +62 -0
- package/src/control/identity-manager.ts +354 -0
- package/src/control/intent-router.ts +326 -0
- package/src/control/types.ts +102 -0
- package/src/curator/curator.ts +265 -15
- package/src/governance/governance.ts +698 -0
- package/src/governance/index.ts +18 -0
- package/src/governance/types.ts +111 -0
- package/src/index.ts +128 -3
- package/src/llm/llm-client.ts +18 -24
- package/src/logging/logger.ts +154 -0
- package/src/logging/types.ts +21 -0
- package/src/loop/loop-manager.ts +130 -0
- package/src/loop/types.ts +44 -0
- package/src/planning/gap-analysis.ts +506 -0
- package/src/planning/gap-types.ts +58 -0
- package/src/planning/planner.ts +478 -2
- package/src/project/project-registry.ts +358 -0
- package/src/project/types.ts +31 -0
- package/src/runtime/admin-extra-ops.ts +307 -0
- package/src/runtime/admin-ops.ts +329 -0
- package/src/runtime/capture-ops.ts +385 -0
- package/src/runtime/core-ops.ts +747 -26
- package/src/runtime/curator-extra-ops.ts +71 -0
- package/src/runtime/domain-ops.ts +65 -13
- package/src/runtime/grading-ops.ts +121 -0
- package/src/runtime/loop-ops.ts +194 -0
- package/src/runtime/memory-cross-project-ops.ts +192 -0
- package/src/runtime/memory-extra-ops.ts +186 -0
- package/src/runtime/orchestrate-ops.ts +272 -0
- package/src/runtime/planning-extra-ops.ts +327 -0
- package/src/runtime/project-ops.ts +196 -0
- package/src/runtime/runtime.ts +54 -1
- package/src/runtime/types.ts +23 -0
- package/src/runtime/vault-extra-ops.ts +225 -0
- package/src/telemetry/telemetry.ts +118 -0
- package/src/vault/vault.ts +412 -1
|
@@ -41,12 +41,28 @@ describe('createDomainFacade', () => {
|
|
|
41
41
|
|
|
42
42
|
it('get_patterns should scope to domain', async () => {
|
|
43
43
|
runtime.vault.seed([
|
|
44
|
-
{
|
|
45
|
-
|
|
44
|
+
{
|
|
45
|
+
id: 'sec-1',
|
|
46
|
+
type: 'pattern',
|
|
47
|
+
domain: 'security',
|
|
48
|
+
title: 'Auth',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
description: 'Auth.',
|
|
51
|
+
tags: ['auth'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'api-1',
|
|
55
|
+
type: 'pattern',
|
|
56
|
+
domain: 'api-design',
|
|
57
|
+
title: 'REST',
|
|
58
|
+
severity: 'warning',
|
|
59
|
+
description: 'REST.',
|
|
60
|
+
tags: ['rest'],
|
|
61
|
+
},
|
|
46
62
|
]);
|
|
47
63
|
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
48
64
|
const op = facade.ops.find((o) => o.name === 'get_patterns')!;
|
|
49
|
-
const results = await op.handler({}) as IntelligenceEntry[];
|
|
65
|
+
const results = (await op.handler({})) as IntelligenceEntry[];
|
|
50
66
|
expect(results.every((e) => e.domain === 'security')).toBe(true);
|
|
51
67
|
});
|
|
52
68
|
|
|
@@ -68,29 +84,111 @@ describe('createDomainFacade', () => {
|
|
|
68
84
|
|
|
69
85
|
it('remove should delete entry', async () => {
|
|
70
86
|
runtime.vault.seed([
|
|
71
|
-
{
|
|
87
|
+
{
|
|
88
|
+
id: 'rm-1',
|
|
89
|
+
type: 'pattern',
|
|
90
|
+
domain: 'security',
|
|
91
|
+
title: 'Remove me',
|
|
92
|
+
severity: 'warning',
|
|
93
|
+
description: 'Remove.',
|
|
94
|
+
tags: ['test'],
|
|
95
|
+
},
|
|
72
96
|
]);
|
|
73
97
|
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
74
98
|
const removeOp = facade.ops.find((o) => o.name === 'remove')!;
|
|
75
|
-
const result = await removeOp.handler({ id: 'rm-1' }) as { removed: boolean };
|
|
99
|
+
const result = (await removeOp.handler({ id: 'rm-1' })) as { removed: boolean };
|
|
76
100
|
expect(result.removed).toBe(true);
|
|
77
101
|
expect(runtime.vault.get('rm-1')).toBeNull();
|
|
78
102
|
});
|
|
79
103
|
|
|
104
|
+
it('capture should include governance action on default (moderate) preset', async () => {
|
|
105
|
+
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
106
|
+
const captureOp = facade.ops.find((o) => o.name === 'capture')!;
|
|
107
|
+
const result = (await captureOp.handler({
|
|
108
|
+
id: 'gov-cap-1',
|
|
109
|
+
type: 'pattern',
|
|
110
|
+
title: 'Governed Pattern',
|
|
111
|
+
severity: 'warning',
|
|
112
|
+
description: 'Test governance capture.',
|
|
113
|
+
tags: ['gov'],
|
|
114
|
+
})) as { captured: boolean; governance: { action: string } };
|
|
115
|
+
expect(result.captured).toBe(true);
|
|
116
|
+
expect(result.governance.action).toBe('capture');
|
|
117
|
+
expect(runtime.vault.get('gov-cap-1')).not.toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('capture should create proposal under strict preset', async () => {
|
|
121
|
+
runtime.governance.applyPreset('.', 'strict', 'test');
|
|
122
|
+
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
123
|
+
const captureOp = facade.ops.find((o) => o.name === 'capture')!;
|
|
124
|
+
const result = (await captureOp.handler({
|
|
125
|
+
id: 'gov-prop-1',
|
|
126
|
+
type: 'pattern',
|
|
127
|
+
title: 'Needs Review',
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
description: 'Should be proposed.',
|
|
130
|
+
tags: ['gov'],
|
|
131
|
+
})) as {
|
|
132
|
+
captured: boolean;
|
|
133
|
+
governance: { action: string; proposalId: number; reason?: string };
|
|
134
|
+
};
|
|
135
|
+
expect(result.captured).toBe(false);
|
|
136
|
+
expect(result.governance.action).toBe('propose');
|
|
137
|
+
expect(result.governance.proposalId).toBeGreaterThan(0);
|
|
138
|
+
// Entry should NOT be in vault
|
|
139
|
+
expect(runtime.vault.get('gov-prop-1')).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('capture should reject when total quota exceeded', async () => {
|
|
143
|
+
runtime.governance.setPolicy('.', 'quota', { maxEntriesTotal: 1 }, 'test');
|
|
144
|
+
runtime.vault.seed([
|
|
145
|
+
{
|
|
146
|
+
id: 'existing-1',
|
|
147
|
+
type: 'pattern',
|
|
148
|
+
domain: 'security',
|
|
149
|
+
title: 'Existing',
|
|
150
|
+
severity: 'warning',
|
|
151
|
+
description: 'Takes the slot.',
|
|
152
|
+
tags: ['fill'],
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
156
|
+
const captureOp = facade.ops.find((o) => o.name === 'capture')!;
|
|
157
|
+
const result = (await captureOp.handler({
|
|
158
|
+
id: 'gov-rej-1',
|
|
159
|
+
type: 'pattern',
|
|
160
|
+
title: 'Over Quota',
|
|
161
|
+
severity: 'warning',
|
|
162
|
+
description: 'Should be rejected.',
|
|
163
|
+
tags: ['gov'],
|
|
164
|
+
})) as { captured: boolean; governance: { action: string; reason?: string } };
|
|
165
|
+
expect(result.captured).toBe(false);
|
|
166
|
+
expect(result.governance.action).toBe('reject');
|
|
167
|
+
expect(runtime.vault.get('gov-rej-1')).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
80
170
|
it('get_entry should return specific entry', async () => {
|
|
81
171
|
runtime.vault.seed([
|
|
82
|
-
{
|
|
172
|
+
{
|
|
173
|
+
id: 'ge-1',
|
|
174
|
+
type: 'pattern',
|
|
175
|
+
domain: 'security',
|
|
176
|
+
title: 'Get me',
|
|
177
|
+
severity: 'warning',
|
|
178
|
+
description: 'Get.',
|
|
179
|
+
tags: ['test'],
|
|
180
|
+
},
|
|
83
181
|
]);
|
|
84
182
|
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
85
183
|
const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
|
|
86
|
-
const result = await getOp.handler({ id: 'ge-1' }) as IntelligenceEntry;
|
|
184
|
+
const result = (await getOp.handler({ id: 'ge-1' })) as IntelligenceEntry;
|
|
87
185
|
expect(result.id).toBe('ge-1');
|
|
88
186
|
});
|
|
89
187
|
|
|
90
188
|
it('get_entry should return error for missing entry', async () => {
|
|
91
189
|
const facade = createDomainFacade(runtime, 'test-domain', 'security');
|
|
92
190
|
const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
|
|
93
|
-
const result = await getOp.handler({ id: 'nope' }) as { error: string };
|
|
191
|
+
const result = (await getOp.handler({ id: 'nope' })) as { error: string };
|
|
94
192
|
expect(result.error).toBeDefined();
|
|
95
193
|
});
|
|
96
194
|
});
|
|
@@ -110,7 +208,11 @@ describe('createDomainFacades', () => {
|
|
|
110
208
|
});
|
|
111
209
|
|
|
112
210
|
it('should create one facade per domain', () => {
|
|
113
|
-
const facades = createDomainFacades(runtime, 'test-multi', [
|
|
211
|
+
const facades = createDomainFacades(runtime, 'test-multi', [
|
|
212
|
+
'security',
|
|
213
|
+
'api-design',
|
|
214
|
+
'testing',
|
|
215
|
+
]);
|
|
114
216
|
expect(facades.length).toBe(3);
|
|
115
217
|
expect(facades[0].name).toBe('test-multi_security');
|
|
116
218
|
expect(facades[1].name).toBe('test-multi_api_design');
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
6
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
7
|
+
|
|
8
|
+
describe('Governance', () => {
|
|
9
|
+
let runtime: AgentRuntime;
|
|
10
|
+
let plannerDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
plannerDir = join(tmpdir(), 'governance-test-' + Date.now());
|
|
14
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
15
|
+
runtime = createAgentRuntime({
|
|
16
|
+
agentId: 'test-governance',
|
|
17
|
+
vaultPath: ':memory:',
|
|
18
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
runtime.close();
|
|
24
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ─── Policy CRUD ──────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe('Policy CRUD', () => {
|
|
30
|
+
it('should return moderate defaults for unknown project', () => {
|
|
31
|
+
const policy = runtime.governance.getPolicy('/unknown');
|
|
32
|
+
expect(policy.projectPath).toBe('/unknown');
|
|
33
|
+
expect(policy.quotas.maxEntriesTotal).toBe(500);
|
|
34
|
+
expect(policy.retention.archiveAfterDays).toBe(90);
|
|
35
|
+
expect(policy.autoCapture.enabled).toBe(true);
|
|
36
|
+
expect(policy.autoCapture.requireReview).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should persist setPolicy changes', () => {
|
|
40
|
+
runtime.governance.setPolicy('/test', 'quota', {
|
|
41
|
+
maxEntriesTotal: 100,
|
|
42
|
+
maxEntriesPerCategory: 30,
|
|
43
|
+
maxEntriesPerType: 50,
|
|
44
|
+
warnAtPercent: 75,
|
|
45
|
+
});
|
|
46
|
+
const policy = runtime.governance.getPolicy('/test');
|
|
47
|
+
expect(policy.quotas.maxEntriesTotal).toBe(100);
|
|
48
|
+
expect(policy.quotas.warnAtPercent).toBe(75);
|
|
49
|
+
// Other policies should remain default
|
|
50
|
+
expect(policy.retention.archiveAfterDays).toBe(90);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should applyPreset for all 3 policy types', () => {
|
|
54
|
+
runtime.governance.applyPreset('/test', 'strict', 'admin');
|
|
55
|
+
const policy = runtime.governance.getPolicy('/test');
|
|
56
|
+
expect(policy.quotas.maxEntriesTotal).toBe(200);
|
|
57
|
+
expect(policy.retention.archiveAfterDays).toBe(30);
|
|
58
|
+
expect(policy.autoCapture.requireReview).toBe(true);
|
|
59
|
+
expect(policy.autoCapture.maxPendingProposals).toBe(10);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ─── Evaluation Cascade ───────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('Evaluation Cascade', () => {
|
|
66
|
+
it('should reject when auto-capture is disabled', () => {
|
|
67
|
+
runtime.governance.setPolicy('/test', 'auto-capture', {
|
|
68
|
+
enabled: false,
|
|
69
|
+
requireReview: false,
|
|
70
|
+
maxPendingProposals: 25,
|
|
71
|
+
autoExpireDays: 14,
|
|
72
|
+
});
|
|
73
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
74
|
+
type: 'pattern',
|
|
75
|
+
category: 'testing',
|
|
76
|
+
});
|
|
77
|
+
expect(decision.allowed).toBe(false);
|
|
78
|
+
expect(decision.action).toBe('reject');
|
|
79
|
+
expect(decision.reason).toContain('disabled');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should propose when review is required', () => {
|
|
83
|
+
runtime.governance.applyPreset('/test', 'strict'); // requireReview: true
|
|
84
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
85
|
+
type: 'pattern',
|
|
86
|
+
category: 'testing',
|
|
87
|
+
});
|
|
88
|
+
expect(decision.allowed).toBe(false);
|
|
89
|
+
expect(decision.action).toBe('propose');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should reject when total quota exceeded', () => {
|
|
93
|
+
// Set very low quota
|
|
94
|
+
runtime.governance.setPolicy('/test', 'quota', {
|
|
95
|
+
maxEntriesTotal: 1,
|
|
96
|
+
maxEntriesPerCategory: 100,
|
|
97
|
+
maxEntriesPerType: 100,
|
|
98
|
+
warnAtPercent: 80,
|
|
99
|
+
});
|
|
100
|
+
// Seed entries to exceed quota
|
|
101
|
+
runtime.vault.seed([
|
|
102
|
+
{
|
|
103
|
+
id: 'q1',
|
|
104
|
+
type: 'pattern',
|
|
105
|
+
domain: 'testing',
|
|
106
|
+
title: 'Test',
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
description: 'Test',
|
|
109
|
+
tags: ['t'],
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
113
|
+
type: 'pattern',
|
|
114
|
+
category: 'testing',
|
|
115
|
+
});
|
|
116
|
+
expect(decision.allowed).toBe(false);
|
|
117
|
+
expect(decision.action).toBe('reject');
|
|
118
|
+
expect(decision.reason).toContain('Total quota exceeded');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should quarantine when category quota exceeded', () => {
|
|
122
|
+
runtime.governance.setPolicy('/test', 'quota', {
|
|
123
|
+
maxEntriesTotal: 1000,
|
|
124
|
+
maxEntriesPerCategory: 1,
|
|
125
|
+
maxEntriesPerType: 1000,
|
|
126
|
+
warnAtPercent: 80,
|
|
127
|
+
});
|
|
128
|
+
runtime.vault.seed([
|
|
129
|
+
{
|
|
130
|
+
id: 'cq1',
|
|
131
|
+
type: 'pattern',
|
|
132
|
+
domain: 'testing',
|
|
133
|
+
title: 'Test',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
description: 'Test',
|
|
136
|
+
tags: ['t'],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
140
|
+
type: 'pattern',
|
|
141
|
+
category: 'testing',
|
|
142
|
+
});
|
|
143
|
+
expect(decision.allowed).toBe(false);
|
|
144
|
+
expect(decision.action).toBe('quarantine');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should quarantine when type quota exceeded', () => {
|
|
148
|
+
runtime.governance.setPolicy('/test', 'quota', {
|
|
149
|
+
maxEntriesTotal: 1000,
|
|
150
|
+
maxEntriesPerCategory: 1000,
|
|
151
|
+
maxEntriesPerType: 1,
|
|
152
|
+
warnAtPercent: 80,
|
|
153
|
+
});
|
|
154
|
+
runtime.vault.seed([
|
|
155
|
+
{
|
|
156
|
+
id: 'tq1',
|
|
157
|
+
type: 'pattern',
|
|
158
|
+
domain: 'testing',
|
|
159
|
+
title: 'Test',
|
|
160
|
+
severity: 'warning',
|
|
161
|
+
description: 'Test',
|
|
162
|
+
tags: ['t'],
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
166
|
+
type: 'pattern',
|
|
167
|
+
category: 'testing',
|
|
168
|
+
});
|
|
169
|
+
expect(decision.allowed).toBe(false);
|
|
170
|
+
expect(decision.action).toBe('quarantine');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should allow capture when within all quotas', () => {
|
|
174
|
+
const decision = runtime.governance.evaluateCapture('/test', {
|
|
175
|
+
type: 'pattern',
|
|
176
|
+
category: 'testing',
|
|
177
|
+
title: 'A good pattern',
|
|
178
|
+
});
|
|
179
|
+
expect(decision.allowed).toBe(true);
|
|
180
|
+
expect(decision.action).toBe('capture');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ─── Batch Evaluation ─────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe('Batch Evaluation', () => {
|
|
187
|
+
it('should evaluate multiple entries with running state', () => {
|
|
188
|
+
const results = runtime.governance.evaluateBatch('/test', [
|
|
189
|
+
{ type: 'pattern', category: 'a' },
|
|
190
|
+
{ type: 'anti-pattern', category: 'b' },
|
|
191
|
+
{ type: 'rule', category: 'c' },
|
|
192
|
+
]);
|
|
193
|
+
expect(results).toHaveLength(3);
|
|
194
|
+
expect(results.every((r) => r.decision.action === 'capture')).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ─── Proposal Lifecycle ───────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe('Proposal Lifecycle', () => {
|
|
201
|
+
it('should create and approve a proposal', () => {
|
|
202
|
+
const id = runtime.governance.propose('/test', {
|
|
203
|
+
title: 'New pattern',
|
|
204
|
+
type: 'pattern',
|
|
205
|
+
category: 'testing',
|
|
206
|
+
data: { description: 'A discovered pattern' },
|
|
207
|
+
});
|
|
208
|
+
expect(id).toBeGreaterThan(0);
|
|
209
|
+
|
|
210
|
+
const approved = runtime.governance.approveProposal(id, 'admin');
|
|
211
|
+
expect(approved).not.toBeNull();
|
|
212
|
+
expect(approved!.status).toBe('approved');
|
|
213
|
+
expect(approved!.decidedBy).toBe('admin');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should auto-capture entry into vault on approval', () => {
|
|
217
|
+
const id = runtime.governance.propose('/test', {
|
|
218
|
+
entryId: 'approved-entry-1',
|
|
219
|
+
title: 'Approved pattern',
|
|
220
|
+
type: 'pattern',
|
|
221
|
+
category: 'testing',
|
|
222
|
+
data: {
|
|
223
|
+
severity: 'warning',
|
|
224
|
+
description: 'A pattern that was reviewed and approved.',
|
|
225
|
+
tags: ['governance', 'approved'],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Before approval — not in vault
|
|
230
|
+
expect(runtime.vault.get('approved-entry-1')).toBeNull();
|
|
231
|
+
|
|
232
|
+
runtime.governance.approveProposal(id, 'admin');
|
|
233
|
+
|
|
234
|
+
// After approval — entry is in vault
|
|
235
|
+
const entry = runtime.vault.get('approved-entry-1');
|
|
236
|
+
expect(entry).not.toBeNull();
|
|
237
|
+
expect(entry!.domain).toBe('testing');
|
|
238
|
+
expect(entry!.title).toBe('Approved pattern');
|
|
239
|
+
expect(entry!.tags).toContain('governance');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should generate entry id from proposal id when entryId is missing', () => {
|
|
243
|
+
const id = runtime.governance.propose('/test', {
|
|
244
|
+
title: 'No entry id',
|
|
245
|
+
type: 'rule',
|
|
246
|
+
category: 'styling',
|
|
247
|
+
data: { severity: 'suggestion', description: 'Auto-id test.' },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
runtime.governance.approveProposal(id);
|
|
251
|
+
|
|
252
|
+
const entry = runtime.vault.get(`proposal-${id}`);
|
|
253
|
+
expect(entry).not.toBeNull();
|
|
254
|
+
expect(entry!.type).toBe('rule');
|
|
255
|
+
expect(entry!.domain).toBe('styling');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should reject a proposal with a note', () => {
|
|
259
|
+
const id = runtime.governance.propose('/test', {
|
|
260
|
+
title: 'Bad pattern',
|
|
261
|
+
type: 'pattern',
|
|
262
|
+
category: 'testing',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const rejected = runtime.governance.rejectProposal(id, 'admin', 'Not useful');
|
|
266
|
+
expect(rejected).not.toBeNull();
|
|
267
|
+
expect(rejected!.status).toBe('rejected');
|
|
268
|
+
expect(rejected!.modificationNote).toBe('Not useful');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should modify a proposal and mark as modified', () => {
|
|
272
|
+
const id = runtime.governance.propose('/test', {
|
|
273
|
+
title: 'Draft pattern',
|
|
274
|
+
type: 'pattern',
|
|
275
|
+
category: 'testing',
|
|
276
|
+
data: { description: 'Original' },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const modified = runtime.governance.modifyProposal(
|
|
280
|
+
id,
|
|
281
|
+
{ description: 'Updated description', severity: 'critical' },
|
|
282
|
+
'editor',
|
|
283
|
+
);
|
|
284
|
+
expect(modified).not.toBeNull();
|
|
285
|
+
expect(modified!.status).toBe('modified');
|
|
286
|
+
expect(modified!.proposedData.description).toBe('Updated description');
|
|
287
|
+
expect(modified!.proposedData.severity).toBe('critical');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should auto-capture into vault on modify with merged data', () => {
|
|
291
|
+
const id = runtime.governance.propose('/test', {
|
|
292
|
+
entryId: 'mod-cap-1',
|
|
293
|
+
title: 'Modify me',
|
|
294
|
+
type: 'pattern',
|
|
295
|
+
category: 'testing',
|
|
296
|
+
data: { severity: 'warning', description: 'Original desc', tags: ['test'] },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Before modify — not in vault
|
|
300
|
+
expect(runtime.vault.get('mod-cap-1')).toBeNull();
|
|
301
|
+
|
|
302
|
+
runtime.governance.modifyProposal(id, { description: 'Improved desc' }, 'editor');
|
|
303
|
+
|
|
304
|
+
// After modify — captured with merged data
|
|
305
|
+
const entry = runtime.vault.get('mod-cap-1');
|
|
306
|
+
expect(entry).not.toBeNull();
|
|
307
|
+
expect(entry!.description).toBe('Improved desc');
|
|
308
|
+
expect(entry!.domain).toBe('testing');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should return null when approving nonexistent proposal', () => {
|
|
312
|
+
const result = runtime.governance.approveProposal(999);
|
|
313
|
+
expect(result).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should not allow double-approval', () => {
|
|
317
|
+
const id = runtime.governance.propose('/test', {
|
|
318
|
+
title: 'Test',
|
|
319
|
+
type: 'pattern',
|
|
320
|
+
category: 'testing',
|
|
321
|
+
});
|
|
322
|
+
runtime.governance.approveProposal(id);
|
|
323
|
+
const second = runtime.governance.approveProposal(id);
|
|
324
|
+
expect(second).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should list pending proposals', () => {
|
|
328
|
+
runtime.governance.propose('/test', { title: 'P1', type: 'pattern', category: 'a' });
|
|
329
|
+
runtime.governance.propose('/test', { title: 'P2', type: 'rule', category: 'b' });
|
|
330
|
+
runtime.governance.propose('/other', { title: 'P3', type: 'pattern', category: 'c' });
|
|
331
|
+
|
|
332
|
+
const all = runtime.governance.listPendingProposals();
|
|
333
|
+
expect(all).toHaveLength(3);
|
|
334
|
+
|
|
335
|
+
const testOnly = runtime.governance.listPendingProposals('/test');
|
|
336
|
+
expect(testOnly).toHaveLength(2);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ─── Proposal Stats ──────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
describe('Proposal Stats', () => {
|
|
343
|
+
it('should compute counts and acceptance rate', () => {
|
|
344
|
+
const id1 = runtime.governance.propose('/test', {
|
|
345
|
+
title: 'P1',
|
|
346
|
+
type: 'pattern',
|
|
347
|
+
category: 'a',
|
|
348
|
+
});
|
|
349
|
+
const id2 = runtime.governance.propose('/test', {
|
|
350
|
+
title: 'P2',
|
|
351
|
+
type: 'pattern',
|
|
352
|
+
category: 'a',
|
|
353
|
+
});
|
|
354
|
+
runtime.governance.propose('/test', { title: 'P3', type: 'rule', category: 'b' });
|
|
355
|
+
|
|
356
|
+
runtime.governance.approveProposal(id1);
|
|
357
|
+
runtime.governance.rejectProposal(id2);
|
|
358
|
+
// id3 remains pending
|
|
359
|
+
|
|
360
|
+
const stats = runtime.governance.getProposalStats('/test');
|
|
361
|
+
expect(stats.total).toBe(3);
|
|
362
|
+
expect(stats.approved).toBe(1);
|
|
363
|
+
expect(stats.rejected).toBe(1);
|
|
364
|
+
expect(stats.pending).toBe(1);
|
|
365
|
+
expect(stats.acceptanceRate).toBe(0.5); // 1 approved / 2 decided
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should compute byCategory breakdown', () => {
|
|
369
|
+
const id1 = runtime.governance.propose('/test', {
|
|
370
|
+
title: 'P1',
|
|
371
|
+
type: 'pattern',
|
|
372
|
+
category: 'styling',
|
|
373
|
+
});
|
|
374
|
+
const id2 = runtime.governance.propose('/test', {
|
|
375
|
+
title: 'P2',
|
|
376
|
+
type: 'pattern',
|
|
377
|
+
category: 'styling',
|
|
378
|
+
});
|
|
379
|
+
runtime.governance.approveProposal(id1);
|
|
380
|
+
runtime.governance.rejectProposal(id2);
|
|
381
|
+
|
|
382
|
+
const stats = runtime.governance.getProposalStats('/test');
|
|
383
|
+
expect(stats.byCategory.styling).toBeDefined();
|
|
384
|
+
expect(stats.byCategory.styling.total).toBe(2);
|
|
385
|
+
expect(stats.byCategory.styling.accepted).toBe(1);
|
|
386
|
+
expect(stats.byCategory.styling.rate).toBe(0.5);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ─── Audit Trail ──────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
describe('Audit Trail', () => {
|
|
393
|
+
it('should log policy changes', () => {
|
|
394
|
+
runtime.governance.setPolicy(
|
|
395
|
+
'/test',
|
|
396
|
+
'quota',
|
|
397
|
+
{
|
|
398
|
+
maxEntriesTotal: 100,
|
|
399
|
+
maxEntriesPerCategory: 30,
|
|
400
|
+
maxEntriesPerType: 50,
|
|
401
|
+
warnAtPercent: 75,
|
|
402
|
+
},
|
|
403
|
+
'admin',
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const trail = runtime.governance.getAuditTrail('/test');
|
|
407
|
+
expect(trail.length).toBeGreaterThan(0);
|
|
408
|
+
expect(trail[0].policyType).toBe('quota');
|
|
409
|
+
expect(trail[0].changedBy).toBe('admin');
|
|
410
|
+
expect(trail[0].oldConfig).toBeNull(); // First set, no previous
|
|
411
|
+
expect(trail[0].newConfig).toHaveProperty('maxEntriesTotal', 100);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should record old config on policy update', () => {
|
|
415
|
+
runtime.governance.setPolicy('/test', 'quota', { maxEntriesTotal: 100 } as Record<
|
|
416
|
+
string,
|
|
417
|
+
unknown
|
|
418
|
+
>);
|
|
419
|
+
runtime.governance.setPolicy('/test', 'quota', { maxEntriesTotal: 200 } as Record<
|
|
420
|
+
string,
|
|
421
|
+
unknown
|
|
422
|
+
>);
|
|
423
|
+
|
|
424
|
+
const trail = runtime.governance.getAuditTrail('/test');
|
|
425
|
+
expect(trail.length).toBe(2);
|
|
426
|
+
// Both entries present — find the second change (the one with oldConfig)
|
|
427
|
+
const updateEntry = trail.find((t) => t.oldConfig !== null);
|
|
428
|
+
expect(updateEntry).toBeDefined();
|
|
429
|
+
expect(updateEntry!.newConfig).toHaveProperty('maxEntriesTotal', 200);
|
|
430
|
+
expect(updateEntry!.oldConfig).toHaveProperty('maxEntriesTotal', 100);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ─── Dashboard ────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
describe('Dashboard', () => {
|
|
437
|
+
it('should return combined health view', () => {
|
|
438
|
+
const dashboard = runtime.governance.getDashboard('/test');
|
|
439
|
+
expect(dashboard.vaultSize).toBe(0);
|
|
440
|
+
expect(dashboard.quotaPercent).toBe(0);
|
|
441
|
+
expect(dashboard.pendingProposals).toBe(0);
|
|
442
|
+
expect(dashboard.policySummary.maxEntries).toBe(500);
|
|
443
|
+
expect(dashboard.policySummary.requireReview).toBe(false);
|
|
444
|
+
expect(typeof dashboard.acceptanceRate).toBe('number');
|
|
445
|
+
expect(typeof dashboard.evaluationTrend).toBe('object');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should reflect vault entries in quota percent', () => {
|
|
449
|
+
// Set low quota for easy percentage
|
|
450
|
+
runtime.governance.setPolicy('/test', 'quota', {
|
|
451
|
+
maxEntriesTotal: 10,
|
|
452
|
+
maxEntriesPerCategory: 100,
|
|
453
|
+
maxEntriesPerType: 100,
|
|
454
|
+
warnAtPercent: 80,
|
|
455
|
+
});
|
|
456
|
+
runtime.vault.seed([
|
|
457
|
+
{
|
|
458
|
+
id: 'dq1',
|
|
459
|
+
type: 'pattern',
|
|
460
|
+
domain: 'd',
|
|
461
|
+
title: 'T',
|
|
462
|
+
severity: 'warning',
|
|
463
|
+
description: 'D',
|
|
464
|
+
tags: ['t'],
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'dq2',
|
|
468
|
+
type: 'pattern',
|
|
469
|
+
domain: 'd',
|
|
470
|
+
title: 'T',
|
|
471
|
+
severity: 'warning',
|
|
472
|
+
description: 'D',
|
|
473
|
+
tags: ['t'],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: 'dq3',
|
|
477
|
+
type: 'pattern',
|
|
478
|
+
domain: 'd',
|
|
479
|
+
title: 'T',
|
|
480
|
+
severity: 'warning',
|
|
481
|
+
description: 'D',
|
|
482
|
+
tags: ['t'],
|
|
483
|
+
},
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
const dashboard = runtime.governance.getDashboard('/test');
|
|
487
|
+
expect(dashboard.vaultSize).toBe(3);
|
|
488
|
+
expect(dashboard.quotaPercent).toBe(30);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// ─── Edge Cases ───────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
describe('Edge Cases', () => {
|
|
495
|
+
it('should handle empty vault gracefully', () => {
|
|
496
|
+
const status = runtime.governance.getQuotaStatus('/empty');
|
|
497
|
+
expect(status.total).toBe(0);
|
|
498
|
+
expect(status.isWarning).toBe(false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should handle unknown project defaults', () => {
|
|
502
|
+
const policy = runtime.governance.getPolicy('/nonexistent');
|
|
503
|
+
expect(policy.quotas.maxEntriesTotal).toBe(500);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should return null for approving nonexistent proposal', () => {
|
|
507
|
+
expect(runtime.governance.approveProposal(9999)).toBeNull();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should return 0 expired when no stale proposals', () => {
|
|
511
|
+
const expired = runtime.governance.expireStaleProposals(1);
|
|
512
|
+
expect(expired).toBe(0);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should return empty stats when no proposals exist', () => {
|
|
516
|
+
const stats = runtime.governance.getProposalStats('/test');
|
|
517
|
+
expect(stats.total).toBe(0);
|
|
518
|
+
expect(stats.acceptanceRate).toBe(0);
|
|
519
|
+
expect(Object.keys(stats.byCategory)).toHaveLength(0);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|