@soleri/core 2.4.0 → 2.5.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 +7 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +56 -9
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/types.d.ts +2 -2
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cognee/client.d.ts +3 -0
- package/dist/cognee/client.d.ts.map +1 -1
- package/dist/cognee/client.js +17 -0
- package/dist/cognee/client.js.map +1 -1
- package/dist/cognee/sync-manager.d.ts +94 -0
- package/dist/cognee/sync-manager.d.ts.map +1 -0
- package/dist/cognee/sync-manager.js +293 -0
- package/dist/cognee/sync-manager.js.map +1 -0
- package/dist/curator/curator.d.ts +8 -1
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +64 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/errors/classify.d.ts +13 -0
- package/dist/errors/classify.d.ts.map +1 -0
- package/dist/errors/classify.js +97 -0
- package/dist/errors/classify.js.map +1 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/retry.d.ts +40 -0
- package/dist/errors/retry.d.ts.map +1 -0
- package/dist/errors/retry.js +97 -0
- package/dist/errors/retry.js.map +1 -0
- package/dist/errors/types.d.ts +48 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/errors/types.js +59 -0
- package/dist/errors/types.js.map +1 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +14 -0
- package/dist/intake/content-classifier.d.ts.map +1 -0
- package/dist/intake/content-classifier.js +125 -0
- package/dist/intake/content-classifier.js.map +1 -0
- package/dist/intake/dedup-gate.d.ts +17 -0
- package/dist/intake/dedup-gate.d.ts.map +1 -0
- package/dist/intake/dedup-gate.js +66 -0
- package/dist/intake/dedup-gate.js.map +1 -0
- package/dist/intake/intake-pipeline.d.ts +63 -0
- package/dist/intake/intake-pipeline.d.ts.map +1 -0
- package/dist/intake/intake-pipeline.js +373 -0
- package/dist/intake/intake-pipeline.js.map +1 -0
- package/dist/intake/types.d.ts +65 -0
- package/dist/intake/types.d.ts.map +1 -0
- package/dist/intake/types.js +3 -0
- package/dist/intake/types.js.map +1 -0
- package/dist/intelligence/loader.js +1 -1
- package/dist/intelligence/loader.js.map +1 -1
- package/dist/intelligence/types.d.ts +3 -1
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/loop/loop-manager.d.ts +58 -7
- package/dist/loop/loop-manager.d.ts.map +1 -1
- package/dist/loop/loop-manager.js +280 -6
- package/dist/loop/loop-manager.js.map +1 -1
- package/dist/loop/types.d.ts +69 -1
- package/dist/loop/types.d.ts.map +1 -1
- package/dist/loop/types.js +4 -1
- package/dist/loop/types.js.map +1 -1
- package/dist/persistence/index.d.ts +3 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +2 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/sqlite-provider.d.ts +25 -0
- package/dist/persistence/sqlite-provider.d.ts.map +1 -0
- package/dist/persistence/sqlite-provider.js +59 -0
- package/dist/persistence/sqlite-provider.js.map +1 -0
- package/dist/persistence/types.d.ts +36 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +8 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +47 -4
- package/dist/planning/gap-analysis.d.ts.map +1 -1
- package/dist/planning/gap-analysis.js +190 -13
- package/dist/planning/gap-analysis.js.map +1 -1
- package/dist/planning/gap-types.d.ts +1 -1
- package/dist/planning/gap-types.d.ts.map +1 -1
- package/dist/planning/gap-types.js.map +1 -1
- package/dist/planning/planner.d.ts +277 -9
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +611 -46
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/generic/brainstorming.d.ts +9 -0
- package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
- package/dist/playbooks/generic/brainstorming.js +105 -0
- package/dist/playbooks/generic/brainstorming.js.map +1 -0
- package/dist/playbooks/generic/code-review.d.ts +11 -0
- package/dist/playbooks/generic/code-review.d.ts.map +1 -0
- package/dist/playbooks/generic/code-review.js +176 -0
- package/dist/playbooks/generic/code-review.js.map +1 -0
- package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
- package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
- package/dist/playbooks/generic/subagent-execution.js +68 -0
- package/dist/playbooks/generic/subagent-execution.js.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
- package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
- package/dist/playbooks/generic/systematic-debugging.js +87 -0
- package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
- package/dist/playbooks/generic/tdd.d.ts +9 -0
- package/dist/playbooks/generic/tdd.d.ts.map +1 -0
- package/dist/playbooks/generic/tdd.js +70 -0
- package/dist/playbooks/generic/tdd.js.map +1 -0
- package/dist/playbooks/generic/verification.d.ts +9 -0
- package/dist/playbooks/generic/verification.d.ts.map +1 -0
- package/dist/playbooks/generic/verification.js +74 -0
- package/dist/playbooks/generic/verification.js.map +1 -0
- package/dist/playbooks/index.d.ts +4 -0
- package/dist/playbooks/index.d.ts.map +1 -0
- package/dist/playbooks/index.js +5 -0
- package/dist/playbooks/index.js.map +1 -0
- package/dist/playbooks/playbook-registry.d.ts +42 -0
- package/dist/playbooks/playbook-registry.d.ts.map +1 -0
- package/dist/playbooks/playbook-registry.js +227 -0
- package/dist/playbooks/playbook-registry.js.map +1 -0
- package/dist/playbooks/playbook-seeder.d.ts +47 -0
- package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
- package/dist/playbooks/playbook-seeder.js +104 -0
- package/dist/playbooks/playbook-seeder.js.map +1 -0
- package/dist/playbooks/playbook-types.d.ts +132 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -0
- package/dist/playbooks/playbook-types.js +12 -0
- package/dist/playbooks/playbook-types.js.map +1 -0
- package/dist/project/project-registry.d.ts.map +1 -1
- package/dist/project/project-registry.js +9 -11
- package/dist/project/project-registry.js.map +1 -1
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/parser.d.ts +17 -0
- package/dist/prompts/parser.d.ts.map +1 -0
- package/dist/prompts/parser.js +47 -0
- package/dist/prompts/parser.js.map +1 -0
- package/dist/prompts/template-manager.d.ts +25 -0
- package/dist/prompts/template-manager.d.ts.map +1 -0
- package/dist/prompts/template-manager.js +71 -0
- package/dist/prompts/template-manager.js.map +1 -0
- package/dist/prompts/types.d.ts +26 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +5 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +5 -3
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +322 -11
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +10 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +20 -2
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/cognee-sync-ops.d.ts +12 -0
- package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
- package/dist/runtime/cognee-sync-ops.js +55 -0
- package/dist/runtime/cognee-sync-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +8 -6
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +226 -9
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +2 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +15 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/domain-ops.js +2 -2
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts.map +1 -1
- package/dist/runtime/grading-ops.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts +14 -0
- package/dist/runtime/intake-ops.d.ts.map +1 -0
- package/dist/runtime/intake-ops.js +110 -0
- package/dist/runtime/intake-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +5 -4
- package/dist/runtime/loop-ops.d.ts.map +1 -1
- package/dist/runtime/loop-ops.js +84 -12
- package/dist/runtime/loop-ops.js.map +1 -1
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
- package/dist/runtime/memory-cross-project-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.js +5 -5
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +8 -2
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts +13 -5
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +381 -18
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.d.ts +14 -0
- package/dist/runtime/playbook-ops.d.ts.map +1 -0
- package/dist/runtime/playbook-ops.js +141 -0
- package/dist/runtime/playbook-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts.map +1 -1
- package/dist/runtime/project-ops.js +7 -2
- package/dist/runtime/project-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +27 -8
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +8 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +3 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +345 -4
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- package/dist/vault/playbook.d.ts +34 -0
- package/dist/vault/playbook.d.ts.map +1 -0
- package/dist/vault/playbook.js +60 -0
- package/dist/vault/playbook.js.map +1 -0
- package/dist/vault/vault.d.ts +31 -32
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +201 -181
- package/dist/vault/vault.js.map +1 -1
- package/package.json +7 -3
- package/src/__tests__/admin-extra-ops.test.ts +62 -15
- package/src/__tests__/admin-ops.test.ts +2 -2
- package/src/__tests__/brain.test.ts +3 -3
- package/src/__tests__/cognee-integration.test.ts +80 -0
- package/src/__tests__/cognee-sync-manager.test.ts +103 -0
- package/src/__tests__/core-ops.test.ts +30 -4
- package/src/__tests__/curator-extra-ops.test.ts +24 -2
- package/src/__tests__/errors.test.ts +388 -0
- package/src/__tests__/grading-ops.test.ts +28 -7
- package/src/__tests__/intake-pipeline.test.ts +162 -0
- package/src/__tests__/loop-ops.test.ts +74 -3
- package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
- package/src/__tests__/orchestrate-ops.test.ts +8 -3
- package/src/__tests__/persistence.test.ts +225 -0
- package/src/__tests__/planner.test.ts +99 -21
- package/src/__tests__/planning-extra-ops.test.ts +168 -10
- package/src/__tests__/playbook-registry.test.ts +326 -0
- package/src/__tests__/playbook-seeder.test.ts +163 -0
- package/src/__tests__/playbook.test.ts +389 -0
- package/src/__tests__/project-ops.test.ts +18 -4
- package/src/__tests__/template-manager.test.ts +222 -0
- package/src/__tests__/vault-extra-ops.test.ts +82 -7
- package/src/brain/brain.ts +71 -9
- package/src/brain/types.ts +2 -2
- package/src/cognee/client.ts +18 -0
- package/src/cognee/sync-manager.ts +389 -0
- package/src/curator/curator.ts +88 -7
- package/src/errors/classify.ts +102 -0
- package/src/errors/index.ts +5 -0
- package/src/errors/retry.ts +132 -0
- package/src/errors/types.ts +81 -0
- package/src/index.ts +114 -3
- package/src/intake/content-classifier.ts +146 -0
- package/src/intake/dedup-gate.ts +92 -0
- package/src/intake/intake-pipeline.ts +503 -0
- package/src/intake/types.ts +69 -0
- package/src/intelligence/loader.ts +1 -1
- package/src/intelligence/types.ts +3 -1
- package/src/loop/loop-manager.ts +325 -7
- package/src/loop/types.ts +72 -1
- package/src/persistence/index.ts +7 -0
- package/src/persistence/sqlite-provider.ts +62 -0
- package/src/persistence/types.ts +44 -0
- package/src/planning/gap-analysis.ts +286 -17
- package/src/planning/gap-types.ts +4 -1
- package/src/planning/planner.ts +828 -55
- package/src/playbooks/generic/brainstorming.ts +110 -0
- package/src/playbooks/generic/code-review.ts +181 -0
- package/src/playbooks/generic/subagent-execution.ts +74 -0
- package/src/playbooks/generic/systematic-debugging.ts +92 -0
- package/src/playbooks/generic/tdd.ts +75 -0
- package/src/playbooks/generic/verification.ts +79 -0
- package/src/playbooks/index.ts +27 -0
- package/src/playbooks/playbook-registry.ts +284 -0
- package/src/playbooks/playbook-seeder.ts +119 -0
- package/src/playbooks/playbook-types.ts +162 -0
- package/src/project/project-registry.ts +29 -17
- package/src/prompts/index.ts +3 -0
- package/src/prompts/parser.ts +59 -0
- package/src/prompts/template-manager.ts +77 -0
- package/src/prompts/types.ts +28 -0
- package/src/runtime/admin-extra-ops.ts +358 -13
- package/src/runtime/admin-ops.ts +17 -6
- package/src/runtime/capture-ops.ts +25 -6
- package/src/runtime/cognee-sync-ops.ts +63 -0
- package/src/runtime/core-ops.ts +258 -8
- package/src/runtime/curator-extra-ops.ts +17 -3
- package/src/runtime/domain-ops.ts +2 -2
- package/src/runtime/grading-ops.ts +11 -2
- package/src/runtime/intake-ops.ts +126 -0
- package/src/runtime/loop-ops.ts +96 -13
- package/src/runtime/memory-cross-project-ops.ts +1 -2
- package/src/runtime/memory-extra-ops.ts +5 -5
- package/src/runtime/orchestrate-ops.ts +8 -2
- package/src/runtime/planning-extra-ops.ts +414 -23
- package/src/runtime/playbook-ops.ts +169 -0
- package/src/runtime/project-ops.ts +9 -3
- package/src/runtime/runtime.ts +35 -9
- package/src/runtime/types.ts +8 -0
- package/src/runtime/vault-extra-ops.ts +385 -4
- package/src/vault/playbook.ts +87 -0
- package/src/vault/vault.ts +301 -235
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Vault } from '../vault/vault.js';
|
|
3
|
+
import {
|
|
4
|
+
seedDefaultPlaybooks,
|
|
5
|
+
playbookDefinitionToEntry,
|
|
6
|
+
entryToPlaybookDefinition,
|
|
7
|
+
} from '../playbooks/playbook-seeder.js';
|
|
8
|
+
import { getAllBuiltinPlaybooks } from '../playbooks/playbook-registry.js';
|
|
9
|
+
|
|
10
|
+
describe('playbookDefinitionToEntry', () => {
|
|
11
|
+
it('should convert a PlaybookDefinition to IntelligenceEntry', () => {
|
|
12
|
+
const def = getAllBuiltinPlaybooks()[0];
|
|
13
|
+
const entry = playbookDefinitionToEntry(def);
|
|
14
|
+
|
|
15
|
+
expect(entry.id).toBe(def.id);
|
|
16
|
+
expect(entry.type).toBe('playbook');
|
|
17
|
+
expect(entry.domain).toBe(def.category);
|
|
18
|
+
expect(entry.title).toBe(def.title);
|
|
19
|
+
expect(entry.severity).toBe('suggestion');
|
|
20
|
+
expect(entry.description).toBe(def.description);
|
|
21
|
+
expect(entry.example).toBe(def.steps);
|
|
22
|
+
expect(entry.why).toBe(def.expectedOutcome);
|
|
23
|
+
expect(entry.tags).toEqual(def.tags);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should embed the full definition in context', () => {
|
|
27
|
+
const def = getAllBuiltinPlaybooks()[0];
|
|
28
|
+
const entry = playbookDefinitionToEntry(def);
|
|
29
|
+
expect(entry.context).toContain('__PLAYBOOK_DEF__');
|
|
30
|
+
expect(entry.context).toContain('__END_DEF__');
|
|
31
|
+
expect(entry.context).toContain(def.trigger);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('entryToPlaybookDefinition', () => {
|
|
36
|
+
it('should round-trip through entry and back', () => {
|
|
37
|
+
const original = getAllBuiltinPlaybooks()[0];
|
|
38
|
+
const entry = playbookDefinitionToEntry(original);
|
|
39
|
+
const restored = entryToPlaybookDefinition(entry);
|
|
40
|
+
|
|
41
|
+
expect(restored).not.toBeNull();
|
|
42
|
+
expect(restored!.id).toBe(original.id);
|
|
43
|
+
expect(restored!.tier).toBe(original.tier);
|
|
44
|
+
expect(restored!.title).toBe(original.title);
|
|
45
|
+
expect(restored!.matchIntents).toEqual(original.matchIntents);
|
|
46
|
+
expect(restored!.matchKeywords).toEqual(original.matchKeywords);
|
|
47
|
+
expect(restored!.gates).toEqual(original.gates);
|
|
48
|
+
expect(restored!.taskTemplates).toEqual(original.taskTemplates);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return null for non-playbook entry', () => {
|
|
52
|
+
const result = entryToPlaybookDefinition({
|
|
53
|
+
id: 'pat-1',
|
|
54
|
+
type: 'pattern',
|
|
55
|
+
domain: 'test',
|
|
56
|
+
title: 'Test',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
description: 'Test',
|
|
59
|
+
tags: [],
|
|
60
|
+
});
|
|
61
|
+
expect(result).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return null for playbook entry without metadata', () => {
|
|
65
|
+
const result = entryToPlaybookDefinition({
|
|
66
|
+
id: 'pb-1',
|
|
67
|
+
type: 'playbook',
|
|
68
|
+
domain: 'test',
|
|
69
|
+
title: 'Test',
|
|
70
|
+
severity: 'suggestion',
|
|
71
|
+
description: 'Test',
|
|
72
|
+
context: 'plain text without markers',
|
|
73
|
+
tags: [],
|
|
74
|
+
});
|
|
75
|
+
expect(result).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return null for malformed JSON in metadata', () => {
|
|
79
|
+
const result = entryToPlaybookDefinition({
|
|
80
|
+
id: 'pb-1',
|
|
81
|
+
type: 'playbook',
|
|
82
|
+
domain: 'test',
|
|
83
|
+
title: 'Test',
|
|
84
|
+
severity: 'suggestion',
|
|
85
|
+
description: 'Test',
|
|
86
|
+
context: '__PLAYBOOK_DEF__{not valid json}__END_DEF__',
|
|
87
|
+
tags: [],
|
|
88
|
+
});
|
|
89
|
+
expect(result).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('seedDefaultPlaybooks', () => {
|
|
94
|
+
let vault: Vault;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vault = new Vault(':memory:');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vault.close();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should seed 6 built-in playbooks into empty vault', () => {
|
|
105
|
+
const result = seedDefaultPlaybooks(vault);
|
|
106
|
+
expect(result.seeded).toBe(6);
|
|
107
|
+
expect(result.skipped).toBe(0);
|
|
108
|
+
expect(result.errors).toBe(0);
|
|
109
|
+
expect(result.details).toHaveLength(6);
|
|
110
|
+
expect(result.details.every((d) => d.action === 'seeded')).toBe(true);
|
|
111
|
+
|
|
112
|
+
// Verify they're in the vault
|
|
113
|
+
const entries = vault.list({ type: 'playbook' });
|
|
114
|
+
expect(entries).toHaveLength(6);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should be idempotent — second call skips all', () => {
|
|
118
|
+
seedDefaultPlaybooks(vault);
|
|
119
|
+
const result = seedDefaultPlaybooks(vault);
|
|
120
|
+
|
|
121
|
+
expect(result.seeded).toBe(0);
|
|
122
|
+
expect(result.skipped).toBe(6);
|
|
123
|
+
expect(result.errors).toBe(0);
|
|
124
|
+
expect(result.details.every((d) => d.action === 'skipped')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should not overwrite user modifications', () => {
|
|
128
|
+
// Seed first
|
|
129
|
+
seedDefaultPlaybooks(vault);
|
|
130
|
+
|
|
131
|
+
// Simulate user modifying a playbook by removing and re-adding with different content
|
|
132
|
+
const builtins = getAllBuiltinPlaybooks();
|
|
133
|
+
vault.remove(builtins[0].id);
|
|
134
|
+
vault.add({
|
|
135
|
+
id: builtins[0].id,
|
|
136
|
+
type: 'playbook',
|
|
137
|
+
domain: 'user-custom',
|
|
138
|
+
title: 'User Modified Playbook',
|
|
139
|
+
severity: 'suggestion',
|
|
140
|
+
description: 'Modified by user',
|
|
141
|
+
tags: ['custom'],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Re-seed should skip this one
|
|
145
|
+
const result = seedDefaultPlaybooks(vault);
|
|
146
|
+
expect(result.skipped).toBe(6); // all exist, including user-modified one
|
|
147
|
+
|
|
148
|
+
// Verify user's version is preserved
|
|
149
|
+
const entry = vault.get(builtins[0].id);
|
|
150
|
+
expect(entry?.title).toBe('User Modified Playbook');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should report correct counts in details', () => {
|
|
154
|
+
const result = seedDefaultPlaybooks(vault);
|
|
155
|
+
const ids = result.details.map((d) => d.id);
|
|
156
|
+
expect(ids).toContain('generic-tdd');
|
|
157
|
+
expect(ids).toContain('generic-brainstorming');
|
|
158
|
+
expect(ids).toContain('generic-code-review');
|
|
159
|
+
expect(ids).toContain('generic-subagent-execution');
|
|
160
|
+
expect(ids).toContain('generic-systematic-debugging');
|
|
161
|
+
expect(ids).toContain('generic-verification');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,389 @@
|
|
|
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 { Vault } from '../vault/vault.js';
|
|
6
|
+
import { validatePlaybook, parsePlaybookFromEntry } from '../vault/playbook.js';
|
|
7
|
+
import type { Playbook } from '../vault/playbook.js';
|
|
8
|
+
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
9
|
+
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
10
|
+
import { createCoreOps } from '../runtime/core-ops.js';
|
|
11
|
+
import type { AgentRuntime, OpDefinition } from '../runtime/types.js';
|
|
12
|
+
|
|
13
|
+
function makePlaybook(overrides: Partial<Playbook> = {}): Playbook {
|
|
14
|
+
return {
|
|
15
|
+
id: overrides.id ?? 'pb-1',
|
|
16
|
+
title: overrides.title ?? 'Deploy Checklist',
|
|
17
|
+
domain: overrides.domain ?? 'ops',
|
|
18
|
+
description: overrides.description ?? 'Standard deploy procedure.',
|
|
19
|
+
steps: overrides.steps ?? [
|
|
20
|
+
{
|
|
21
|
+
order: 1,
|
|
22
|
+
title: 'Run tests',
|
|
23
|
+
description: 'Execute full test suite',
|
|
24
|
+
validation: 'All tests pass',
|
|
25
|
+
},
|
|
26
|
+
{ order: 2, title: 'Build', description: 'Run production build' },
|
|
27
|
+
{
|
|
28
|
+
order: 3,
|
|
29
|
+
title: 'Deploy',
|
|
30
|
+
description: 'Push to production',
|
|
31
|
+
validation: 'Health check passes',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
tags: overrides.tags ?? ['deploy', 'checklist'],
|
|
35
|
+
createdAt: overrides.createdAt ?? Date.now(),
|
|
36
|
+
updatedAt: overrides.updatedAt ?? Date.now(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function playbookToEntry(pb: Playbook): IntelligenceEntry {
|
|
41
|
+
return {
|
|
42
|
+
id: pb.id,
|
|
43
|
+
type: 'playbook',
|
|
44
|
+
domain: pb.domain,
|
|
45
|
+
title: pb.title,
|
|
46
|
+
severity: 'suggestion',
|
|
47
|
+
description: pb.description,
|
|
48
|
+
context: JSON.stringify({ steps: pb.steps }),
|
|
49
|
+
tags: pb.tags,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('validatePlaybook', () => {
|
|
54
|
+
it('should pass for a valid playbook', () => {
|
|
55
|
+
const result = validatePlaybook(makePlaybook());
|
|
56
|
+
expect(result.valid).toBe(true);
|
|
57
|
+
expect(result.errors).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should fail for empty title', () => {
|
|
61
|
+
const result = validatePlaybook(makePlaybook({ title: '' }));
|
|
62
|
+
expect(result.valid).toBe(false);
|
|
63
|
+
expect(result.errors).toContain('Playbook title must not be empty');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should fail for no steps', () => {
|
|
67
|
+
const result = validatePlaybook(makePlaybook({ steps: [] }));
|
|
68
|
+
expect(result.valid).toBe(false);
|
|
69
|
+
expect(result.errors).toContain('Playbook must have at least one step');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should fail for non-sequential step orders', () => {
|
|
73
|
+
const result = validatePlaybook(
|
|
74
|
+
makePlaybook({
|
|
75
|
+
steps: [
|
|
76
|
+
{ order: 1, title: 'Step A', description: 'Desc A' },
|
|
77
|
+
{ order: 3, title: 'Step B', description: 'Desc B' },
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(result.valid).toBe(false);
|
|
82
|
+
expect(result.errors.some((e) => e.includes('order 3, expected 2'))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('parsePlaybookFromEntry', () => {
|
|
87
|
+
it('should parse a valid playbook entry', () => {
|
|
88
|
+
const pb = makePlaybook();
|
|
89
|
+
const entry = playbookToEntry(pb);
|
|
90
|
+
const parsed = parsePlaybookFromEntry(entry);
|
|
91
|
+
expect(parsed).not.toBeNull();
|
|
92
|
+
expect(parsed!.id).toBe(pb.id);
|
|
93
|
+
expect(parsed!.title).toBe(pb.title);
|
|
94
|
+
expect(parsed!.steps).toHaveLength(3);
|
|
95
|
+
expect(parsed!.steps[0].title).toBe('Run tests');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return null for a non-playbook entry', () => {
|
|
99
|
+
const entry: IntelligenceEntry = {
|
|
100
|
+
id: 'pat-1',
|
|
101
|
+
type: 'pattern',
|
|
102
|
+
domain: 'testing',
|
|
103
|
+
title: 'Some Pattern',
|
|
104
|
+
severity: 'warning',
|
|
105
|
+
description: 'A pattern.',
|
|
106
|
+
tags: [],
|
|
107
|
+
};
|
|
108
|
+
expect(parsePlaybookFromEntry(entry)).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return null for invalid context JSON', () => {
|
|
112
|
+
const entry: IntelligenceEntry = {
|
|
113
|
+
id: 'pb-bad',
|
|
114
|
+
type: 'playbook',
|
|
115
|
+
domain: 'ops',
|
|
116
|
+
title: 'Bad Playbook',
|
|
117
|
+
severity: 'suggestion',
|
|
118
|
+
description: 'Has bad context.',
|
|
119
|
+
context: 'not valid json',
|
|
120
|
+
tags: [],
|
|
121
|
+
};
|
|
122
|
+
expect(parsePlaybookFromEntry(entry)).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return null when context has no steps array', () => {
|
|
126
|
+
const entry: IntelligenceEntry = {
|
|
127
|
+
id: 'pb-nosteps',
|
|
128
|
+
type: 'playbook',
|
|
129
|
+
domain: 'ops',
|
|
130
|
+
title: 'No Steps',
|
|
131
|
+
severity: 'suggestion',
|
|
132
|
+
description: 'Missing steps.',
|
|
133
|
+
context: JSON.stringify({ something: 'else' }),
|
|
134
|
+
tags: [],
|
|
135
|
+
};
|
|
136
|
+
expect(parsePlaybookFromEntry(entry)).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('Vault playbook integration', () => {
|
|
141
|
+
let vault: Vault;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vault = new Vault(':memory:');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
vault.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should insert and retrieve a playbook via vault.list', () => {
|
|
152
|
+
const pb = makePlaybook();
|
|
153
|
+
const entry = playbookToEntry(pb);
|
|
154
|
+
vault.seed([entry]);
|
|
155
|
+
|
|
156
|
+
const results = vault.list({ type: 'playbook' });
|
|
157
|
+
expect(results).toHaveLength(1);
|
|
158
|
+
expect(results[0].id).toBe('pb-1');
|
|
159
|
+
expect(results[0].type).toBe('playbook');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should find a playbook via FTS5 search', () => {
|
|
163
|
+
const pb = makePlaybook({ title: 'Deploy Checklist Procedure' });
|
|
164
|
+
const entry = playbookToEntry(pb);
|
|
165
|
+
vault.seed([entry]);
|
|
166
|
+
|
|
167
|
+
const results = vault.search('deploy checklist');
|
|
168
|
+
expect(results.length).toBeGreaterThan(0);
|
|
169
|
+
expect(results[0].entry.id).toBe(pb.id);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('playbook_create op', () => {
|
|
174
|
+
let runtime: AgentRuntime;
|
|
175
|
+
let ops: OpDefinition[];
|
|
176
|
+
let plannerDir: string;
|
|
177
|
+
|
|
178
|
+
function findOp(name: string): OpDefinition {
|
|
179
|
+
const op = ops.find((o) => o.name === name);
|
|
180
|
+
if (!op) throw new Error(`Op "${name}" not found`);
|
|
181
|
+
return op;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
plannerDir = join(tmpdir(), 'playbook-create-test-' + Date.now());
|
|
186
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
187
|
+
runtime = createAgentRuntime({
|
|
188
|
+
agentId: 'test',
|
|
189
|
+
vaultPath: ':memory:',
|
|
190
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
191
|
+
});
|
|
192
|
+
ops = createCoreOps(runtime);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
runtime.close();
|
|
197
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should create a playbook and store it in the vault', async () => {
|
|
201
|
+
const result = (await findOp('playbook_create').handler({
|
|
202
|
+
title: 'PR Review',
|
|
203
|
+
domain: 'engineering',
|
|
204
|
+
description: 'Standard PR review procedure.',
|
|
205
|
+
steps: [
|
|
206
|
+
{ title: 'Read diff', description: 'Review all changed files' },
|
|
207
|
+
{ title: 'Run tests', description: 'Ensure CI passes', validation: 'All green' },
|
|
208
|
+
{ title: 'Approve', description: 'Leave approval comment' },
|
|
209
|
+
],
|
|
210
|
+
tags: ['review', 'pr'],
|
|
211
|
+
})) as { created: boolean; id: string; steps: number };
|
|
212
|
+
|
|
213
|
+
expect(result.created).toBe(true);
|
|
214
|
+
expect(result.steps).toBe(3);
|
|
215
|
+
|
|
216
|
+
// Verify it's in the vault
|
|
217
|
+
const entry = runtime.vault.get(result.id);
|
|
218
|
+
expect(entry).not.toBeNull();
|
|
219
|
+
expect(entry!.type).toBe('playbook');
|
|
220
|
+
expect(entry!.domain).toBe('engineering');
|
|
221
|
+
|
|
222
|
+
// Verify it parses back correctly
|
|
223
|
+
const playbook = parsePlaybookFromEntry(entry!);
|
|
224
|
+
expect(playbook).not.toBeNull();
|
|
225
|
+
expect(playbook!.steps).toHaveLength(3);
|
|
226
|
+
expect(playbook!.steps[0].order).toBe(1);
|
|
227
|
+
expect(playbook!.steps[2].order).toBe(3);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should reject a playbook with empty steps', async () => {
|
|
231
|
+
const result = (await findOp('playbook_create').handler({
|
|
232
|
+
title: 'Empty',
|
|
233
|
+
domain: 'test',
|
|
234
|
+
description: 'No steps.',
|
|
235
|
+
steps: [],
|
|
236
|
+
tags: [],
|
|
237
|
+
})) as { created: boolean; errors: string[] };
|
|
238
|
+
|
|
239
|
+
expect(result.created).toBe(false);
|
|
240
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should reject a playbook with empty title', async () => {
|
|
244
|
+
const result = (await findOp('playbook_create').handler({
|
|
245
|
+
title: '',
|
|
246
|
+
domain: 'test',
|
|
247
|
+
description: 'Has steps but no title.',
|
|
248
|
+
steps: [{ title: 'Step 1', description: 'Do something' }],
|
|
249
|
+
tags: [],
|
|
250
|
+
})) as { created: boolean; errors: string[] };
|
|
251
|
+
|
|
252
|
+
expect(result.created).toBe(false);
|
|
253
|
+
expect(result.errors).toContain('Playbook title must not be empty');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should auto-generate ID when not provided', async () => {
|
|
257
|
+
const result = (await findOp('playbook_create').handler({
|
|
258
|
+
title: 'Auto ID Test',
|
|
259
|
+
domain: 'test',
|
|
260
|
+
description: 'Should get an auto ID.',
|
|
261
|
+
steps: [{ title: 'Step', description: 'Do it' }],
|
|
262
|
+
tags: [],
|
|
263
|
+
})) as { created: boolean; id: string };
|
|
264
|
+
|
|
265
|
+
expect(result.created).toBe(true);
|
|
266
|
+
expect(result.id).toMatch(/^playbook-test-/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should use provided ID when given', async () => {
|
|
270
|
+
const result = (await findOp('playbook_create').handler({
|
|
271
|
+
id: 'my-custom-id',
|
|
272
|
+
title: 'Custom ID Test',
|
|
273
|
+
domain: 'test',
|
|
274
|
+
description: 'Has a custom ID.',
|
|
275
|
+
steps: [{ title: 'Step', description: 'Do it' }],
|
|
276
|
+
tags: [],
|
|
277
|
+
})) as { created: boolean; id: string };
|
|
278
|
+
|
|
279
|
+
expect(result.created).toBe(true);
|
|
280
|
+
expect(result.id).toBe('my-custom-id');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('playbook_match op', () => {
|
|
285
|
+
let runtime: AgentRuntime;
|
|
286
|
+
let ops: OpDefinition[];
|
|
287
|
+
let plannerDir: string;
|
|
288
|
+
|
|
289
|
+
function findOp(name: string): OpDefinition {
|
|
290
|
+
const op = ops.find((o) => o.name === name);
|
|
291
|
+
if (!op) throw new Error(`Op "${name}" not found`);
|
|
292
|
+
return op;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
plannerDir = join(tmpdir(), 'playbook-match-test-' + Date.now());
|
|
297
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
298
|
+
runtime = createAgentRuntime({
|
|
299
|
+
agentId: 'test',
|
|
300
|
+
vaultPath: ':memory:',
|
|
301
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
302
|
+
});
|
|
303
|
+
ops = createCoreOps(runtime);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
afterEach(() => {
|
|
307
|
+
runtime.close();
|
|
308
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should match TDD playbook for BUILD intent', async () => {
|
|
312
|
+
const result = (await findOp('playbook_match').handler({
|
|
313
|
+
intent: 'BUILD',
|
|
314
|
+
text: 'implement a new feature',
|
|
315
|
+
})) as { playbook: { label: string } | null; genericMatch?: { id: string } };
|
|
316
|
+
|
|
317
|
+
expect(result.playbook).not.toBeNull();
|
|
318
|
+
expect(result.genericMatch?.id).toBe('generic-tdd');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should match debugging playbook for FIX intent', async () => {
|
|
322
|
+
const result = (await findOp('playbook_match').handler({
|
|
323
|
+
intent: 'FIX',
|
|
324
|
+
text: 'fix the broken bug',
|
|
325
|
+
})) as { playbook: { label: string } | null; genericMatch?: { id: string } };
|
|
326
|
+
|
|
327
|
+
expect(result.playbook).not.toBeNull();
|
|
328
|
+
expect(result.genericMatch?.id).toBe('generic-systematic-debugging');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return null playbook for unrelated text', async () => {
|
|
332
|
+
const result = (await findOp('playbook_match').handler({
|
|
333
|
+
text: 'random unrelated xyz',
|
|
334
|
+
})) as { playbook: null };
|
|
335
|
+
|
|
336
|
+
expect(result.playbook).toBeNull();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('playbook_seed op', () => {
|
|
341
|
+
let runtime: AgentRuntime;
|
|
342
|
+
let ops: OpDefinition[];
|
|
343
|
+
let plannerDir: string;
|
|
344
|
+
|
|
345
|
+
function findOp(name: string): OpDefinition {
|
|
346
|
+
const op = ops.find((o) => o.name === name);
|
|
347
|
+
if (!op) throw new Error(`Op "${name}" not found`);
|
|
348
|
+
return op;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
beforeEach(() => {
|
|
352
|
+
plannerDir = join(tmpdir(), 'playbook-seed-test-' + Date.now());
|
|
353
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
354
|
+
runtime = createAgentRuntime({
|
|
355
|
+
agentId: 'test',
|
|
356
|
+
vaultPath: ':memory:',
|
|
357
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
358
|
+
});
|
|
359
|
+
ops = createCoreOps(runtime);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
afterEach(() => {
|
|
363
|
+
runtime.close();
|
|
364
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should seed built-in playbooks', async () => {
|
|
368
|
+
const result = (await findOp('playbook_seed').handler({})) as {
|
|
369
|
+
seeded: number;
|
|
370
|
+
skipped: number;
|
|
371
|
+
errors: number;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
expect(result.seeded).toBe(6);
|
|
375
|
+
expect(result.skipped).toBe(0);
|
|
376
|
+
expect(result.errors).toBe(0);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should be idempotent', async () => {
|
|
380
|
+
await findOp('playbook_seed').handler({});
|
|
381
|
+
const result = (await findOp('playbook_seed').handler({})) as {
|
|
382
|
+
seeded: number;
|
|
383
|
+
skipped: number;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
expect(result.seeded).toBe(0);
|
|
387
|
+
expect(result.skipped).toBe(6);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -143,7 +143,10 @@ describe('createProjectOps', () => {
|
|
|
143
143
|
category: 'behavior',
|
|
144
144
|
text: 'Always use semantic tokens',
|
|
145
145
|
priority: 10,
|
|
146
|
-
})) as {
|
|
146
|
+
})) as {
|
|
147
|
+
added: boolean;
|
|
148
|
+
rule: { id: string; category: string; text: string; priority: number };
|
|
149
|
+
};
|
|
147
150
|
|
|
148
151
|
expect(addResult.added).toBe(true);
|
|
149
152
|
expect(addResult.rule.category).toBe('behavior');
|
|
@@ -197,8 +200,16 @@ describe('createProjectOps', () => {
|
|
|
197
200
|
const p1 = runtime.projectRegistry.register('/tmp/p1', 'P1');
|
|
198
201
|
const p2 = runtime.projectRegistry.register('/tmp/p2', 'P2');
|
|
199
202
|
runtime.projectRegistry.addRule(p1.id, { category: 'behavior', text: 'Rule A', priority: 0 });
|
|
200
|
-
runtime.projectRegistry.addRule(p1.id, {
|
|
201
|
-
|
|
203
|
+
runtime.projectRegistry.addRule(p1.id, {
|
|
204
|
+
category: 'convention',
|
|
205
|
+
text: 'Rule B',
|
|
206
|
+
priority: 1,
|
|
207
|
+
});
|
|
208
|
+
runtime.projectRegistry.addRule(p2.id, {
|
|
209
|
+
category: 'preference',
|
|
210
|
+
text: 'Rule C',
|
|
211
|
+
priority: 0,
|
|
212
|
+
});
|
|
202
213
|
|
|
203
214
|
const result = (await findOp('project_list_rules').handler({})) as {
|
|
204
215
|
count: number;
|
|
@@ -225,7 +236,10 @@ describe('createProjectOps', () => {
|
|
|
225
236
|
sourceId: p1.id,
|
|
226
237
|
targetId: p2.id,
|
|
227
238
|
linkType: 'related',
|
|
228
|
-
})) as {
|
|
239
|
+
})) as {
|
|
240
|
+
linked: boolean;
|
|
241
|
+
link: { sourceProjectId: string; targetProjectId: string; linkType: string };
|
|
242
|
+
};
|
|
229
243
|
|
|
230
244
|
expect(linkResult.linked).toBe(true);
|
|
231
245
|
expect(linkResult.link.sourceProjectId).toBe(p1.id);
|