agentic-workflow-manager 2.0.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/README.md +62 -0
- package/dist/src/commands/doctor.js +77 -0
- package/dist/src/commands/hooks/index.js +118 -0
- package/dist/src/commands/hooks/install.js +113 -0
- package/dist/src/commands/hooks/resync.js +50 -0
- package/dist/src/commands/hooks/status.js +74 -0
- package/dist/src/commands/hooks/uninstall.js +59 -0
- package/dist/src/commands/init.js +181 -0
- package/dist/src/commands/ledger/index.js +73 -0
- package/dist/src/commands/pin.js +75 -0
- package/dist/src/commands/registry/add.js +62 -0
- package/dist/src/commands/registry/index.js +166 -0
- package/dist/src/commands/registry/install-bundles.js +36 -0
- package/dist/src/commands/registry/remove.js +18 -0
- package/dist/src/commands/registry/status.js +31 -0
- package/dist/src/commands/sensors/baseline.js +93 -0
- package/dist/src/commands/sensors/formatters/eslint.js +29 -0
- package/dist/src/commands/sensors/formatters/generic.js +8 -0
- package/dist/src/commands/sensors/formatters/semgrep.js +18 -0
- package/dist/src/commands/sensors/formatters/test.js +12 -0
- package/dist/src/commands/sensors/formatters/tsc.js +23 -0
- package/dist/src/commands/sensors/index.js +94 -0
- package/dist/src/commands/sensors/init.js +112 -0
- package/dist/src/commands/sensors/install.js +78 -0
- package/dist/src/commands/sensors/run.js +217 -0
- package/dist/src/commands/sensors/status.js +85 -0
- package/dist/src/commands/sensors/types.js +2 -0
- package/dist/src/core/bundle-install.js +116 -0
- package/dist/src/core/bundles.js +122 -0
- package/dist/src/core/cli-version.js +30 -0
- package/dist/src/core/context/materializer.js +30 -0
- package/dist/src/core/context/orchestrator.js +75 -0
- package/dist/src/core/context/project-constitution-inject.js +47 -0
- package/dist/src/core/context/provider.js +29 -0
- package/dist/src/core/context/regenerate.js +61 -0
- package/dist/src/core/context/strategies/config-instructions.js +73 -0
- package/dist/src/core/context/strategies/hook-merge.js +23 -0
- package/dist/src/core/context/strategies/strategy.js +2 -0
- package/dist/src/core/context/types.js +2 -0
- package/dist/src/core/diagnostics/checks.js +141 -0
- package/dist/src/core/diagnostics/context.js +181 -0
- package/dist/src/core/diagnostics/types.js +2 -0
- package/dist/src/core/discovery.js +135 -0
- package/dist/src/core/executor.js +41 -0
- package/dist/src/core/init/detector.js +67 -0
- package/dist/src/core/init/orchestrator.js +54 -0
- package/dist/src/core/init/steps.js +279 -0
- package/dist/src/core/init/types.js +2 -0
- package/dist/src/core/ledger/store.js +73 -0
- package/dist/src/core/ledger/types.js +2 -0
- package/dist/src/core/miro.js +314 -0
- package/dist/src/core/profile-pins.js +36 -0
- package/dist/src/core/profile.js +146 -0
- package/dist/src/core/registries.js +205 -0
- package/dist/src/core/registry.js +28 -0
- package/dist/src/core/skill-integrity.js +97 -0
- package/dist/src/core/story-map-parser.js +83 -0
- package/dist/src/core/update-check-worker.js +10 -0
- package/dist/src/core/update-check.js +106 -0
- package/dist/src/core/versioning.js +112 -0
- package/dist/src/index.js +689 -0
- package/dist/src/providers/index.js +63 -0
- package/dist/src/utils/config.js +35 -0
- package/dist/src/utils/grouping.js +51 -0
- package/dist/src/utils/registry-view.js +154 -0
- package/dist/tests/commands/doctor.test.js +98 -0
- package/dist/tests/commands/hooks/install.test.js +149 -0
- package/dist/tests/commands/hooks/resync.test.js +135 -0
- package/dist/tests/commands/hooks/router.test.js +26 -0
- package/dist/tests/commands/hooks/status.test.js +88 -0
- package/dist/tests/commands/hooks/uninstall.test.js +104 -0
- package/dist/tests/commands/init.test.js +87 -0
- package/dist/tests/commands/ledger/index.test.js +64 -0
- package/dist/tests/commands/pin.test.js +72 -0
- package/dist/tests/commands/registry/add.test.js +223 -0
- package/dist/tests/commands/registry/install-bundles.test.js +87 -0
- package/dist/tests/commands/registry/remove.test.js +49 -0
- package/dist/tests/commands/registry/status.test.js +43 -0
- package/dist/tests/commands/sensors/baseline.test.js +106 -0
- package/dist/tests/commands/sensors/formatters/eslint.test.js +32 -0
- package/dist/tests/commands/sensors/formatters/semgrep.test.js +38 -0
- package/dist/tests/commands/sensors/formatters/tsc.test.js +25 -0
- package/dist/tests/commands/sensors/index.test.js +18 -0
- package/dist/tests/commands/sensors/init.test.js +137 -0
- package/dist/tests/commands/sensors/install.test.js +60 -0
- package/dist/tests/commands/sensors/router.test.js +23 -0
- package/dist/tests/commands/sensors/run.test.js +353 -0
- package/dist/tests/commands/sensors/status.test.js +98 -0
- package/dist/tests/core/base-remote.test.js +70 -0
- package/dist/tests/core/bundle-install.test.js +198 -0
- package/dist/tests/core/bundles-multiroot.test.js +68 -0
- package/dist/tests/core/bundles-overrides.test.js +49 -0
- package/dist/tests/core/bundles.test.js +96 -0
- package/dist/tests/core/cli-version.test.js +17 -0
- package/dist/tests/core/context/materializer.test.js +46 -0
- package/dist/tests/core/context/orchestrator.test.js +127 -0
- package/dist/tests/core/context/project-constitution-inject.test.js +82 -0
- package/dist/tests/core/context/provider.test.js +45 -0
- package/dist/tests/core/context/regenerate.test.js +106 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +88 -0
- package/dist/tests/core/context/strategies/hook-merge.test.js +71 -0
- package/dist/tests/core/context/types.test.js +15 -0
- package/dist/tests/core/diagnostics/checks.test.js +208 -0
- package/dist/tests/core/diagnostics/context.test.js +170 -0
- package/dist/tests/core/discovery-multiroot.test.js +63 -0
- package/dist/tests/core/discovery-overrides.test.js +105 -0
- package/dist/tests/core/discovery.test.js +113 -0
- package/dist/tests/core/executor.test.js +44 -0
- package/dist/tests/core/init/detector.test.js +56 -0
- package/dist/tests/core/init/orchestrator.test.js +122 -0
- package/dist/tests/core/init/steps.test.js +363 -0
- package/dist/tests/core/ledger/gitignore.test.js +13 -0
- package/dist/tests/core/ledger/store.test.js +122 -0
- package/dist/tests/core/miro-layout.test.js +150 -0
- package/dist/tests/core/profile-pins.test.js +131 -0
- package/dist/tests/core/profile-registries.test.js +59 -0
- package/dist/tests/core/profile.test.js +110 -0
- package/dist/tests/core/registries-capability.test.js +52 -0
- package/dist/tests/core/registries-seed.test.js +66 -0
- package/dist/tests/core/registries-sync.test.js +205 -0
- package/dist/tests/core/registries.test.js +91 -0
- package/dist/tests/core/registry-manifest.test.js +95 -0
- package/dist/tests/core/registry-versioned-sync.test.js +113 -0
- package/dist/tests/core/registry.test.js +51 -0
- package/dist/tests/core/skill-integrity.test.js +126 -0
- package/dist/tests/core/story-map-parser.test.js +136 -0
- package/dist/tests/core/sync-gates.test.js +97 -0
- package/dist/tests/core/update-check.test.js +106 -0
- package/dist/tests/core/versioning.test.js +169 -0
- package/dist/tests/integration/pack-e2e.test.js +50 -0
- package/dist/tests/providers/hooks-config.test.js +45 -0
- package/dist/tests/providers/index.test.js +58 -0
- package/dist/tests/providers/injection-config.test.js +25 -0
- package/dist/tests/registry/b3-ledger-wiring.test.js +74 -0
- package/dist/tests/registry/catalog-consistency.test.js +47 -0
- package/dist/tests/registry/design-skills.test.js +146 -0
- package/dist/tests/registry/prose-agnostic.test.js +18 -0
- package/dist/tests/registry/sensor-packs.test.js +45 -0
- package/dist/tests/registry/skill-versions.test.js +26 -0
- package/dist/tests/registry/using-awm.test.js +39 -0
- package/dist/tests/utils/config.test.js +31 -0
- package/dist/tests/utils/grouping.test.js +43 -0
- package/dist/tests/utils/registry-view-overrides.test.js +41 -0
- package/dist/tests/utils/registry-view.test.js +156 -0
- package/package.json +49 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const store_1 = require("../../../src/core/ledger/store");
|
|
10
|
+
function mkTmp() {
|
|
11
|
+
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ledger-'));
|
|
12
|
+
}
|
|
13
|
+
function entry(over = {}) {
|
|
14
|
+
return {
|
|
15
|
+
ts: '2026-06-06T00:00:00.000Z',
|
|
16
|
+
branch: 'feat-x',
|
|
17
|
+
phase: 'post-qa',
|
|
18
|
+
source_skill: 'post-implementation-qa',
|
|
19
|
+
polarity: 'finding',
|
|
20
|
+
class: 'logica',
|
|
21
|
+
signature: 'public-fn-returns-infinity',
|
|
22
|
+
severity: 'blocker',
|
|
23
|
+
desc: 'splitBill(100,0) returns Infinity',
|
|
24
|
+
ref: 'src/split.ts:12',
|
|
25
|
+
...over,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe('ledger store — add/list', () => {
|
|
29
|
+
let cwd;
|
|
30
|
+
beforeEach(() => { cwd = mkTmp(); });
|
|
31
|
+
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
|
|
32
|
+
test('ledgerPath sanitizes branch slashes into the filename', () => {
|
|
33
|
+
expect((0, store_1.ledgerPath)(cwd, 'feature/foo')).toBe(path_1.default.join(cwd, '.awm', 'ledger', 'feature__foo.jsonl'));
|
|
34
|
+
});
|
|
35
|
+
test('addEntry creates .awm/ledger/ and appends one jsonl line', () => {
|
|
36
|
+
(0, store_1.addEntry)(cwd, entry());
|
|
37
|
+
const raw = fs_1.default.readFileSync((0, store_1.ledgerPath)(cwd, 'feat-x'), 'utf-8');
|
|
38
|
+
expect(raw.trim().split('\n')).toHaveLength(1);
|
|
39
|
+
expect(JSON.parse(raw.trim())).toMatchObject({ signature: 'public-fn-returns-infinity', polarity: 'finding' });
|
|
40
|
+
});
|
|
41
|
+
test('addEntry appends without clobbering prior entries', () => {
|
|
42
|
+
(0, store_1.addEntry)(cwd, entry());
|
|
43
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'second', desc: 'another' }));
|
|
44
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toHaveLength(2);
|
|
45
|
+
});
|
|
46
|
+
test('listEntries on a branch with no ledger returns []', () => {
|
|
47
|
+
expect((0, store_1.listEntries)(cwd, 'never-touched')).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
test('listEntries skips a malformed line without throwing', () => {
|
|
50
|
+
const p = (0, store_1.ledgerPath)(cwd, 'feat-x');
|
|
51
|
+
fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
|
|
52
|
+
fs_1.default.writeFileSync(p, JSON.stringify(entry()) + '\n' + 'NOT JSON\n' + JSON.stringify(entry({ signature: 's2' })) + '\n');
|
|
53
|
+
const got = (0, store_1.listEntries)(cwd, 'feat-x');
|
|
54
|
+
expect(got).toHaveLength(2);
|
|
55
|
+
expect(got.map(e => e.signature)).toEqual(['public-fn-returns-infinity', 's2']);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('ledger store — detectBranch', () => {
|
|
59
|
+
test('falls back to _no-branch outside a git repo', () => {
|
|
60
|
+
const tmp = mkTmp();
|
|
61
|
+
try {
|
|
62
|
+
expect((0, store_1.detectBranch)(tmp)).toBe('_no-branch');
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
test('returns the current branch name from an actual git repo', () => {
|
|
69
|
+
const { execSync: exec } = require('child_process');
|
|
70
|
+
const tmp = mkTmp();
|
|
71
|
+
try {
|
|
72
|
+
exec('git init && git -c user.email=test@test.com -c user.name=Test commit --allow-empty -m init && git checkout -b test-ledger-branch', { cwd: tmp, stdio: 'ignore' });
|
|
73
|
+
expect((0, store_1.detectBranch)(tmp)).toBe('test-ledger-branch');
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('ledger store — recurring', () => {
|
|
81
|
+
let cwd;
|
|
82
|
+
beforeEach(() => { cwd = mkTmp(); });
|
|
83
|
+
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
|
|
84
|
+
test('groups by signature and reports clusters with count >= min', () => {
|
|
85
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'dup' }));
|
|
86
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'dup' }));
|
|
87
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'solo' }));
|
|
88
|
+
const clusters = (0, store_1.recurring)(cwd, 'feat-x', 2);
|
|
89
|
+
expect(clusters).toHaveLength(1);
|
|
90
|
+
expect(clusters[0]).toMatchObject({ signature: 'dup', count: 2 });
|
|
91
|
+
expect(clusters[0].entries).toHaveLength(2);
|
|
92
|
+
});
|
|
93
|
+
test('respects --min: count 2 is excluded when min is 3', () => {
|
|
94
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'dup' }));
|
|
95
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'dup' }));
|
|
96
|
+
expect((0, store_1.recurring)(cwd, 'feat-x', 3)).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
test('sorts clusters by count descending', () => {
|
|
99
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'a' }));
|
|
100
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'a' }));
|
|
101
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'b' }));
|
|
102
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'b' }));
|
|
103
|
+
(0, store_1.addEntry)(cwd, entry({ signature: 'b' }));
|
|
104
|
+
const clusters = (0, store_1.recurring)(cwd, 'feat-x', 2);
|
|
105
|
+
expect(clusters.map(c => c.signature)).toEqual(['b', 'a']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('ledger store — archive', () => {
|
|
109
|
+
let cwd;
|
|
110
|
+
beforeEach(() => { cwd = mkTmp(); });
|
|
111
|
+
afterEach(() => { fs_1.default.rmSync(cwd, { recursive: true, force: true }); });
|
|
112
|
+
test('moves the branch ledger into archive/ and leaves no active ledger', () => {
|
|
113
|
+
(0, store_1.addEntry)(cwd, entry());
|
|
114
|
+
const moved = (0, store_1.archiveLedger)(cwd, 'feat-x', '20260606T000000');
|
|
115
|
+
expect(moved).toBe(true);
|
|
116
|
+
expect(fs_1.default.existsSync((0, store_1.ledgerPath)(cwd, 'feat-x'))).toBe(false);
|
|
117
|
+
expect(fs_1.default.existsSync(path_1.default.join(cwd, '.awm', 'ledger', 'archive', 'feat-x-20260606T000000.jsonl'))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
test('archiving a non-existent ledger is a no-op returning false', () => {
|
|
120
|
+
expect((0, store_1.archiveLedger)(cwd, 'feat-x', '20260606T000000')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const miro_1 = require("../../src/core/miro");
|
|
4
|
+
const SIMPLE_MAP = {
|
|
5
|
+
project: 'Test Project',
|
|
6
|
+
goal: 'Test goal',
|
|
7
|
+
activities: [
|
|
8
|
+
{
|
|
9
|
+
title: 'Actividad 1',
|
|
10
|
+
tasks: [
|
|
11
|
+
{
|
|
12
|
+
title: 'Task 1.1',
|
|
13
|
+
stories: [
|
|
14
|
+
{ release: 'MVP', title: 'Story A' },
|
|
15
|
+
{ release: 'Release 2', title: 'Story B' },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'Actividad 2',
|
|
22
|
+
tasks: [
|
|
23
|
+
{
|
|
24
|
+
title: 'Task 2.1',
|
|
25
|
+
stories: [
|
|
26
|
+
{ release: 'MVP', title: 'Story C' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Task 2.2',
|
|
31
|
+
stories: [],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
// New layout: column = Task. Total task columns = 3 (1 + 2)
|
|
38
|
+
// CARD_W=260, COL_W=280, PADDING=40
|
|
39
|
+
// frameWidth = 40*2 + 3*280 - 20 = 900
|
|
40
|
+
// Activity 1 spans 1 col (width=260), Activity 2 spans 2 cols (width=540)
|
|
41
|
+
describe('computeLayout', () => {
|
|
42
|
+
it('returns one card per activity', () => {
|
|
43
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
44
|
+
const activities = items.filter(i => i.kind === 'activity');
|
|
45
|
+
expect(activities).toHaveLength(2);
|
|
46
|
+
});
|
|
47
|
+
it('returns one card per task', () => {
|
|
48
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
49
|
+
const tasks = items.filter(i => i.kind === 'task');
|
|
50
|
+
expect(tasks).toHaveLength(3);
|
|
51
|
+
});
|
|
52
|
+
it('returns one card per story', () => {
|
|
53
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
54
|
+
const stories = items.filter(i => i.kind === 'story');
|
|
55
|
+
expect(stories).toHaveLength(3);
|
|
56
|
+
});
|
|
57
|
+
it('returns one swimlane label per unique release', () => {
|
|
58
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
59
|
+
const swimlanes = items.filter(i => i.kind === 'swimlane');
|
|
60
|
+
expect(swimlanes).toHaveLength(2);
|
|
61
|
+
expect(swimlanes.map(s => s.title)).toContain('MVP');
|
|
62
|
+
expect(swimlanes.map(s => s.title)).toContain('Release 2');
|
|
63
|
+
});
|
|
64
|
+
it('activity with 1 task has same x as its task (centered over single column)', () => {
|
|
65
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
66
|
+
const act1 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 1');
|
|
67
|
+
const task11 = items.find(i => i.kind === 'task' && i.title === 'Task 1.1');
|
|
68
|
+
expect(act1.x).toBe(task11.x);
|
|
69
|
+
});
|
|
70
|
+
it('activity with 2 tasks is centered between its task columns', () => {
|
|
71
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
72
|
+
const act2 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 2');
|
|
73
|
+
const task21 = items.find(i => i.kind === 'task' && i.title === 'Task 2.1');
|
|
74
|
+
const task22 = items.find(i => i.kind === 'task' && i.title === 'Task 2.2');
|
|
75
|
+
// Activity center should be midpoint of its task columns
|
|
76
|
+
expect(act2.x).toBe((task21.x + task22.x) / 2);
|
|
77
|
+
});
|
|
78
|
+
it('activity with 2 tasks is wider than activity with 1 task', () => {
|
|
79
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
80
|
+
const act1 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 1');
|
|
81
|
+
const act2 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 2');
|
|
82
|
+
expect(act2.width).toBeGreaterThan(act1.width);
|
|
83
|
+
});
|
|
84
|
+
it('activities in different positions have different x values', () => {
|
|
85
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
86
|
+
const act1 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 1');
|
|
87
|
+
const act2 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 2');
|
|
88
|
+
expect(act1.x).not.toBe(act2.x);
|
|
89
|
+
});
|
|
90
|
+
it('frame width scales with total number of task columns', () => {
|
|
91
|
+
const { frameWidth } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
92
|
+
// 3 task columns
|
|
93
|
+
const oneTask = { ...SIMPLE_MAP, activities: [SIMPLE_MAP.activities[0]] };
|
|
94
|
+
const { frameWidth: oneWidth } = (0, miro_1.computeLayout)(oneTask);
|
|
95
|
+
// 1 task column
|
|
96
|
+
expect(frameWidth).toBeGreaterThan(oneWidth);
|
|
97
|
+
});
|
|
98
|
+
it('frame height is positive', () => {
|
|
99
|
+
const { frameHeight } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
100
|
+
expect(frameHeight).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
it('assigns correct colors by kind', () => {
|
|
103
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
104
|
+
const act = items.find(i => i.kind === 'activity');
|
|
105
|
+
const task = items.find(i => i.kind === 'task');
|
|
106
|
+
const story = items.find(i => i.kind === 'story');
|
|
107
|
+
const swimlane = items.find(i => i.kind === 'swimlane');
|
|
108
|
+
expect(act.color).toBe('#ffdc4a');
|
|
109
|
+
expect(task.color).toBe('#659df2');
|
|
110
|
+
expect(story.color).toBe('#ffffff');
|
|
111
|
+
expect(swimlane.color).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
it('MVP swimlane appears before Release 2', () => {
|
|
114
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
115
|
+
const swimlanes = items.filter(i => i.kind === 'swimlane');
|
|
116
|
+
const mvpIdx = swimlanes.findIndex(s => s.title === 'MVP');
|
|
117
|
+
const r2Idx = swimlanes.findIndex(s => s.title === 'Release 2');
|
|
118
|
+
expect(mvpIdx).toBeLessThan(r2Idx);
|
|
119
|
+
});
|
|
120
|
+
it('stories are placed below their corresponding task column', () => {
|
|
121
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
122
|
+
// Story A belongs to Task 1.1 → same column x
|
|
123
|
+
const task11 = items.find(i => i.kind === 'task' && i.title === 'Task 1.1');
|
|
124
|
+
const storyA = items.find(i => i.kind === 'story' && i.title === 'Story A');
|
|
125
|
+
expect(storyA.x).toBe(task11.x);
|
|
126
|
+
// Story C belongs to Task 2.1 → same column x
|
|
127
|
+
const task21 = items.find(i => i.kind === 'task' && i.title === 'Task 2.1');
|
|
128
|
+
const storyC = items.find(i => i.kind === 'story' && i.title === 'Story C');
|
|
129
|
+
expect(storyC.x).toBe(task21.x);
|
|
130
|
+
});
|
|
131
|
+
it('MVP swimlane Y is less than Release 2 swimlane Y (canvas coordinates)', () => {
|
|
132
|
+
const { items } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
133
|
+
const mvp = items.find(i => i.kind === 'swimlane' && i.title === 'MVP');
|
|
134
|
+
const r2 = items.find(i => i.kind === 'swimlane' && i.title === 'Release 2');
|
|
135
|
+
expect(mvp.y).toBeLessThan(r2.y);
|
|
136
|
+
});
|
|
137
|
+
it('first activity Y is correct (frameTop + PADDING + TITLE_H + CARD_H/2)', () => {
|
|
138
|
+
const { items, frameHeight } = (0, miro_1.computeLayout)(SIMPLE_MAP);
|
|
139
|
+
const act1 = items.find(i => i.kind === 'activity' && i.title === 'Actividad 1');
|
|
140
|
+
const expectedY = -frameHeight / 2 + 40 + 50 + 50; // PADDING(40) + TITLE_H(50) + CARD_H/2(50)
|
|
141
|
+
expect(act1.y).toBe(expectedY);
|
|
142
|
+
});
|
|
143
|
+
it('handles empty activities gracefully', () => {
|
|
144
|
+
const emptyMap = { project: 'Empty', goal: '', activities: [] };
|
|
145
|
+
const { frameWidth, frameHeight, items } = (0, miro_1.computeLayout)(emptyMap);
|
|
146
|
+
expect(frameWidth).toBeGreaterThanOrEqual(0);
|
|
147
|
+
expect(frameHeight).toBeGreaterThanOrEqual(0);
|
|
148
|
+
expect(items).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// cli/tests/core/profile-pins.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t -c tag.gpgSign=false ${cmd}`, { cwd, stdio: 'pipe' });
|
|
12
|
+
function makeTaggedRepo(base, name, versions) {
|
|
13
|
+
const dir = path_1.default.join(base, name);
|
|
14
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
15
|
+
GIT(dir, 'init -q -b main');
|
|
16
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), 'init');
|
|
17
|
+
GIT(dir, 'add -A');
|
|
18
|
+
GIT(dir, 'commit -qm init');
|
|
19
|
+
for (const v of versions) {
|
|
20
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), v);
|
|
21
|
+
GIT(dir, 'add -A');
|
|
22
|
+
GIT(dir, `commit -qm ${v}`);
|
|
23
|
+
GIT(dir, `tag v${v}`);
|
|
24
|
+
}
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
function addRelease(source, version) {
|
|
28
|
+
fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), version);
|
|
29
|
+
GIT(source, 'add -A');
|
|
30
|
+
GIT(source, `commit -qm ${version}`);
|
|
31
|
+
GIT(source, `tag v${version}`);
|
|
32
|
+
}
|
|
33
|
+
describe('verifyProjectPins (gate de awm sync)', () => {
|
|
34
|
+
let tmpHome;
|
|
35
|
+
let tmpWork;
|
|
36
|
+
let originalHome;
|
|
37
|
+
let originalAwmHome;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pins-home-'));
|
|
40
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pins-work-'));
|
|
41
|
+
originalHome = process.env.HOME;
|
|
42
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
43
|
+
process.env.HOME = tmpHome;
|
|
44
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
45
|
+
jest.resetModules();
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
49
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
50
|
+
if (originalHome === undefined)
|
|
51
|
+
delete process.env.HOME;
|
|
52
|
+
else
|
|
53
|
+
process.env.HOME = originalHome;
|
|
54
|
+
if (originalAwmHome === undefined)
|
|
55
|
+
delete process.env.AWM_HOME;
|
|
56
|
+
else
|
|
57
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
58
|
+
});
|
|
59
|
+
it('match: máquina en la versión pineada → sin failures', async () => {
|
|
60
|
+
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
61
|
+
const { writeRegistriesConfig, syncRegistries } = require('../../src/core/registries');
|
|
62
|
+
writeRegistriesConfig([{ name: 'baseline', remote: source }]);
|
|
63
|
+
await syncRegistries(); // queda en v1.0.0
|
|
64
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
65
|
+
expect(await verifyProjectPins({ baseline: '1.0.0' })).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
it('mismatch: la máquina avanzó más allá del pin → failure con actual y required', async () => {
|
|
68
|
+
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
69
|
+
const { writeRegistriesConfig, syncRegistries } = require('../../src/core/registries');
|
|
70
|
+
writeRegistriesConfig([{ name: 'baseline', remote: source }]);
|
|
71
|
+
await syncRegistries();
|
|
72
|
+
addRelease(source, '1.1.0');
|
|
73
|
+
await syncRegistries(); // máquina avanza a v1.1.0
|
|
74
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
75
|
+
expect(await verifyProjectPins({ baseline: '1.0.0' })).toEqual([
|
|
76
|
+
{ name: 'baseline', required: '1.0.0', actual: '1.1.0', reason: 'mismatch' },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
it('registry pineado no configurado en la máquina → missing-registry', async () => {
|
|
80
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
81
|
+
expect(await verifyProjectPins({ equipo: '2.0.0' })).toEqual([
|
|
82
|
+
{ name: 'equipo', required: '2.0.0', actual: null, reason: 'missing-registry' },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
it('máquina siguiendo HEAD (sin tag) con pin declarado → mismatch con actual null', async () => {
|
|
86
|
+
const source = makeTaggedRepo(tmpWork, 'src', []);
|
|
87
|
+
const { writeRegistriesConfig, syncRegistries } = require('../../src/core/registries');
|
|
88
|
+
writeRegistriesConfig([{ name: 'baseline', remote: source }]);
|
|
89
|
+
await syncRegistries(); // head-fallback
|
|
90
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
91
|
+
expect(await verifyProjectPins({ baseline: '1.0.0' })).toEqual([
|
|
92
|
+
{ name: 'baseline', required: '1.0.0', actual: null, reason: 'mismatch' },
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
it('pin gate corre aunque extensions esté vacío (B1 regression)', async () => {
|
|
96
|
+
// Un profile {extensions: [], registries: {baseline: '1.0.0'}} con la máquina en v1.1.0
|
|
97
|
+
// DEBE producir mismatch — el early-exit de extensions no puede bypassear el gate.
|
|
98
|
+
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
|
|
99
|
+
const { writeRegistriesConfig, syncRegistries } = require('../../src/core/registries');
|
|
100
|
+
writeRegistriesConfig([{ name: 'baseline', remote: source }]);
|
|
101
|
+
await syncRegistries(); // máquina en v1.1.0
|
|
102
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
103
|
+
const failures = await verifyProjectPins({ baseline: '1.0.0' });
|
|
104
|
+
expect(failures).toHaveLength(1);
|
|
105
|
+
expect(failures[0]).toMatchObject({ name: 'baseline', required: '1.0.0', reason: 'mismatch' });
|
|
106
|
+
});
|
|
107
|
+
it('CRITERIO ROADMAP end-to-end: pineado no recibe main hasta bump; rollback funciona', async () => {
|
|
108
|
+
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
109
|
+
const { writeRegistriesConfig, syncRegistries } = require('../../src/core/registries');
|
|
110
|
+
const { savePreferences, getPreferences } = require('../../src/utils/config');
|
|
111
|
+
const { verifyProjectPins } = require('../../src/core/profile-pins');
|
|
112
|
+
const versionFile = path_1.default.join(tmpHome, '.awm/registries/baseline/VERSION');
|
|
113
|
+
// proyecto pineado a 1.0.0, máquina en 1.0.0 → ok
|
|
114
|
+
writeRegistriesConfig([{ name: 'baseline', remote: source }]);
|
|
115
|
+
await syncRegistries();
|
|
116
|
+
expect(await verifyProjectPins({ baseline: '1.0.0' })).toEqual([]);
|
|
117
|
+
// el remote avanza (release 1.1.0) y la máquina updatea → el proyecto pineado FALLA (no recibe el cambio en silencio)
|
|
118
|
+
addRelease(source, '1.1.0');
|
|
119
|
+
await syncRegistries();
|
|
120
|
+
expect(fs_1.default.readFileSync(versionFile, 'utf-8')).toBe('1.1.0');
|
|
121
|
+
expect((await verifyProjectPins({ baseline: '1.0.0' }))[0]?.reason).toBe('mismatch');
|
|
122
|
+
// bump explícito del profile → pasa
|
|
123
|
+
expect(await verifyProjectPins({ baseline: '1.1.0' })).toEqual([]);
|
|
124
|
+
// rollback: pin de máquina a 1.0.0 → contenido vuelve y el proyecto pineado a 1.0.0 pasa
|
|
125
|
+
const prefs = getPreferences();
|
|
126
|
+
savePreferences({ ...prefs, pins: { baseline: '1.0.0' } });
|
|
127
|
+
await syncRegistries();
|
|
128
|
+
expect(fs_1.default.readFileSync(versionFile, 'utf-8')).toBe('1.0.0');
|
|
129
|
+
expect(await verifyProjectPins({ baseline: '1.0.0' })).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// cli/tests/core/profile-registries.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
describe('profile.registries (pin de proyecto)', () => {
|
|
11
|
+
let tmpProj;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpProj = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prof-'));
|
|
14
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpProj, '.awm'), { recursive: true });
|
|
15
|
+
jest.resetModules();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs_1.default.rmSync(tmpProj, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
const writeRaw = (obj) => fs_1.default.writeFileSync(path_1.default.join(tmpProj, '.awm/profile.json'), JSON.stringify(obj));
|
|
21
|
+
it('lee registries válido y normaliza el prefijo v', () => {
|
|
22
|
+
writeRaw({ extensions: [], registries: { base: 'v1.2.0', equipo: '0.3.0' } });
|
|
23
|
+
const { readProfile } = require('../../src/core/profile');
|
|
24
|
+
expect(readProfile(tmpProj).registries).toEqual({ base: '1.2.0', equipo: '0.3.0' });
|
|
25
|
+
});
|
|
26
|
+
it('profile sin registries → campo ausente (sin verificación)', () => {
|
|
27
|
+
writeRaw({ extensions: ['x'] });
|
|
28
|
+
const { readProfile } = require('../../src/core/profile');
|
|
29
|
+
const p = readProfile(tmpProj);
|
|
30
|
+
expect(p.extensions).toEqual(['x']);
|
|
31
|
+
expect(p.registries).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
it('registries no-objeto → error explícito con path', () => {
|
|
34
|
+
writeRaw({ extensions: [], registries: ['base'] });
|
|
35
|
+
const { readProfile } = require('../../src/core/profile');
|
|
36
|
+
expect(() => readProfile(tmpProj)).toThrow(/profile.*registries/s);
|
|
37
|
+
});
|
|
38
|
+
it('versión malformada → error explícito que nombra la clave', () => {
|
|
39
|
+
writeRaw({ extensions: [], registries: { base: 'latest' } });
|
|
40
|
+
const { readProfile } = require('../../src/core/profile');
|
|
41
|
+
expect(() => readProfile(tmpProj)).toThrow(/base.*latest/s);
|
|
42
|
+
});
|
|
43
|
+
it('writeProfile + readProfile round-trip preserva registries', () => {
|
|
44
|
+
const { readProfile, writeProfile } = require('../../src/core/profile');
|
|
45
|
+
writeProfile(tmpProj, { extensions: ['a'], registries: { base: '1.0.0' } });
|
|
46
|
+
expect(readProfile(tmpProj)).toEqual({ extensions: ['a'], registries: { base: '1.0.0' } });
|
|
47
|
+
});
|
|
48
|
+
it.each([[''], ['.'], ['..'], ['a/b'], ['a\\b']])('clave "%s" en registries → error (path traversal guard)', (key) => {
|
|
49
|
+
writeRaw({ extensions: [], registries: { [key]: '1.0.0' } });
|
|
50
|
+
const { readProfile } = require('../../src/core/profile');
|
|
51
|
+
expect(() => readProfile(tmpProj)).toThrow(/not a valid registry name/);
|
|
52
|
+
});
|
|
53
|
+
it('addExtension preserva registries existente', () => {
|
|
54
|
+
const { readProfile, writeProfile, addExtension } = require('../../src/core/profile');
|
|
55
|
+
writeProfile(tmpProj, { extensions: [], registries: { base: '1.0.0' } });
|
|
56
|
+
addExtension(tmpProj, 'nuevo');
|
|
57
|
+
expect(readProfile(tmpProj)).toEqual({ extensions: ['nuevo'], registries: { base: '1.0.0' } });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const profile_1 = require("../../src/core/profile");
|
|
10
|
+
function tmpRoot() {
|
|
11
|
+
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-profile-'));
|
|
12
|
+
}
|
|
13
|
+
describe('findProjectRoot', () => {
|
|
14
|
+
it('finds the root via a .git marker walking up from a subdir', () => {
|
|
15
|
+
const root = tmpRoot();
|
|
16
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git'));
|
|
17
|
+
const sub = path_1.default.join(root, 'a', 'b');
|
|
18
|
+
fs_1.default.mkdirSync(sub, { recursive: true });
|
|
19
|
+
// realpathSync normalizes /private symlink prefixes on macOS tmp dirs.
|
|
20
|
+
expect((0, profile_1.findProjectRoot)(sub)).toBe(fs_1.default.realpathSync(root));
|
|
21
|
+
});
|
|
22
|
+
it('finds the root via package.json', () => {
|
|
23
|
+
const root = tmpRoot();
|
|
24
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
25
|
+
expect((0, profile_1.findProjectRoot)(root)).toBe(fs_1.default.realpathSync(root));
|
|
26
|
+
});
|
|
27
|
+
it('finds the root via .awm/profile.json', () => {
|
|
28
|
+
const root = tmpRoot();
|
|
29
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
|
|
30
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'profile.json'), '{"extensions":[]}');
|
|
31
|
+
expect((0, profile_1.findProjectRoot)(root)).toBe(fs_1.default.realpathSync(root));
|
|
32
|
+
});
|
|
33
|
+
it('returns null when no marker is found up to the filesystem root', () => {
|
|
34
|
+
const root = tmpRoot(); // bare tmp dir, no markers
|
|
35
|
+
expect((0, profile_1.findProjectRoot)(root)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('readProfile / writeProfile / addExtension', () => {
|
|
39
|
+
it('returns an empty profile when none exists', () => {
|
|
40
|
+
const root = tmpRoot();
|
|
41
|
+
expect((0, profile_1.readProfile)(root)).toEqual({ extensions: [] });
|
|
42
|
+
});
|
|
43
|
+
it('round-trips a written profile', () => {
|
|
44
|
+
const root = tmpRoot();
|
|
45
|
+
(0, profile_1.writeProfile)(root, { extensions: ['frontend'] });
|
|
46
|
+
expect((0, profile_1.readProfile)(root)).toEqual({ extensions: ['frontend'] });
|
|
47
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, '.awm', 'profile.json'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('addExtension appends and dedupes', () => {
|
|
50
|
+
const root = tmpRoot();
|
|
51
|
+
(0, profile_1.addExtension)(root, 'frontend');
|
|
52
|
+
(0, profile_1.addExtension)(root, 'frontend');
|
|
53
|
+
(0, profile_1.addExtension)(root, 'docs');
|
|
54
|
+
expect((0, profile_1.readProfile)(root).extensions).toEqual(['frontend', 'docs']);
|
|
55
|
+
});
|
|
56
|
+
it('tolerates a malformed extensions field', () => {
|
|
57
|
+
const root = tmpRoot();
|
|
58
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
|
|
59
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'profile.json'), '{"extensions":"oops"}');
|
|
60
|
+
expect((0, profile_1.readProfile)(root)).toEqual({ extensions: [] });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('ensureProfile', () => {
|
|
64
|
+
it('creates an empty profile when none exists and reports it created one', () => {
|
|
65
|
+
const root = tmpRoot();
|
|
66
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, '.awm', 'profile.json'))).toBe(false);
|
|
67
|
+
expect((0, profile_1.ensureProfile)(root)).toBe(true);
|
|
68
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, '.awm', 'profile.json'))).toBe(true);
|
|
69
|
+
expect((0, profile_1.readProfile)(root)).toEqual({ extensions: [] });
|
|
70
|
+
});
|
|
71
|
+
it('is idempotent and preserves existing extensions', () => {
|
|
72
|
+
const root = tmpRoot();
|
|
73
|
+
(0, profile_1.addExtension)(root, 'frontend');
|
|
74
|
+
expect((0, profile_1.ensureProfile)(root)).toBe(false);
|
|
75
|
+
expect((0, profile_1.readProfile)(root).extensions).toEqual(['frontend']);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('ensureSkillsGitignored', () => {
|
|
79
|
+
it('appends the pattern when .gitignore is absent', () => {
|
|
80
|
+
const root = tmpRoot();
|
|
81
|
+
(0, profile_1.ensureSkillsGitignored)(root, ['claude-code']);
|
|
82
|
+
const gi = fs_1.default.readFileSync(path_1.default.join(root, '.gitignore'), 'utf-8');
|
|
83
|
+
expect(gi.split(/\r?\n/)).toContain('.claude/skills/');
|
|
84
|
+
});
|
|
85
|
+
it('is idempotent and preserves existing entries', () => {
|
|
86
|
+
const root = tmpRoot();
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.gitignore'), 'node_modules\n');
|
|
88
|
+
(0, profile_1.ensureSkillsGitignored)(root, ['claude-code']);
|
|
89
|
+
(0, profile_1.ensureSkillsGitignored)(root, ['claude-code']);
|
|
90
|
+
const lines = fs_1.default.readFileSync(path_1.default.join(root, '.gitignore'), 'utf-8').split(/\r?\n/);
|
|
91
|
+
expect(lines).toContain('node_modules');
|
|
92
|
+
expect(lines.filter((l) => l.trim() === '.claude/skills/').length).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
it('does not duplicate when the unslashed variant already exists', () => {
|
|
95
|
+
const root = tmpRoot();
|
|
96
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.gitignore'), '.claude/skills\n');
|
|
97
|
+
(0, profile_1.ensureSkillsGitignored)(root, ['claude-code']);
|
|
98
|
+
const lines = fs_1.default.readFileSync(path_1.default.join(root, '.gitignore'), 'utf-8').split(/\r?\n/);
|
|
99
|
+
expect(lines.filter((l) => l.trim().startsWith('.claude/skills')).length).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('shouldRecordExtension', () => {
|
|
103
|
+
it('records only project-scope bundles installed locally', () => {
|
|
104
|
+
expect((0, profile_1.shouldRecordExtension)('project', 'local')).toBe(true);
|
|
105
|
+
expect((0, profile_1.shouldRecordExtension)('project', 'global')).toBe(false);
|
|
106
|
+
expect((0, profile_1.shouldRecordExtension)('baseline', 'global')).toBe(false);
|
|
107
|
+
expect((0, profile_1.shouldRecordExtension)('baseline', 'local')).toBe(false);
|
|
108
|
+
expect((0, profile_1.shouldRecordExtension)('ambient', 'global')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// cli/tests/core/registries-capability.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
describe('capabilityRoot', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let tmpWork;
|
|
13
|
+
let originalHome;
|
|
14
|
+
let originalAwmHome;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-capability-home-'));
|
|
17
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-capability-work-'));
|
|
18
|
+
originalHome = process.env.HOME;
|
|
19
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
20
|
+
process.env.HOME = tmpHome;
|
|
21
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
22
|
+
jest.resetModules();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
26
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
27
|
+
if (originalHome === undefined)
|
|
28
|
+
delete process.env.HOME;
|
|
29
|
+
else
|
|
30
|
+
process.env.HOME = originalHome;
|
|
31
|
+
if (originalAwmHome === undefined)
|
|
32
|
+
delete process.env.AWM_HOME;
|
|
33
|
+
else
|
|
34
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
35
|
+
});
|
|
36
|
+
it('devuelve el primer root configurado que tiene el dir pedido', () => {
|
|
37
|
+
const m = require('../../src/core/registries');
|
|
38
|
+
// dos registries en disco: 'a' sin hooks, 'b' con hooks
|
|
39
|
+
const aRoot = path_1.default.join(tmpHome, '.awm/registries/a');
|
|
40
|
+
const bRoot = path_1.default.join(tmpHome, '.awm/registries/b');
|
|
41
|
+
fs_1.default.mkdirSync(path_1.default.join(aRoot, 'skills'), { recursive: true });
|
|
42
|
+
fs_1.default.mkdirSync(path_1.default.join(bRoot, 'hooks'), { recursive: true });
|
|
43
|
+
m.writeRegistriesConfig([{ name: 'a', remote: 'x' }, { name: 'b', remote: 'y' }]);
|
|
44
|
+
expect(m.capabilityRoot('hooks')).toBe(bRoot);
|
|
45
|
+
expect(m.capabilityRoot('skills')).toBe(aRoot);
|
|
46
|
+
});
|
|
47
|
+
it('ningún root tiene el dir → null', () => {
|
|
48
|
+
const m = require('../../src/core/registries');
|
|
49
|
+
m.writeRegistriesConfig([]);
|
|
50
|
+
expect(m.capabilityRoot('hooks')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|