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,127 @@
|
|
|
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/context/orchestrator.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const orchestrator_1 = require("../../../src/core/context/orchestrator");
|
|
11
|
+
jest.mock('../../../src/commands/hooks/install', () => ({ installHook: jest.fn() }));
|
|
12
|
+
jest.mock('../../../src/commands/hooks/uninstall', () => ({ uninstallHook: jest.fn() }));
|
|
13
|
+
jest.mock('../../../src/commands/hooks/status', () => ({
|
|
14
|
+
computeHookStatus: jest.fn().mockReturnValue({ overall: 'NOT_INSTALLED' }),
|
|
15
|
+
}));
|
|
16
|
+
const install_1 = require("../../../src/commands/hooks/install");
|
|
17
|
+
const uninstall_1 = require("../../../src/commands/hooks/uninstall");
|
|
18
|
+
const status_1 = require("../../../src/commands/hooks/status");
|
|
19
|
+
function tmpRegistry() {
|
|
20
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-orch-'));
|
|
21
|
+
const dir = path_1.default.join(root, 'skills/using-awm');
|
|
22
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), '---\nversion: "1.0.0"\n---\nBODY');
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
describe('InjectionOrchestrator (claude-code dispatch via HookMergeStrategy)', () => {
|
|
27
|
+
const ccOverride = {
|
|
28
|
+
label: 'Claude Code', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
29
|
+
injection: { type: 'cc-settings-merge' },
|
|
30
|
+
hooks: { type: 'cc-settings-merge', settingsPath: '', scriptsDir: '', matcher: '', eventName: '' },
|
|
31
|
+
};
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('installContext delegates to installHook', () => {
|
|
36
|
+
const reg = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cc-reg-'));
|
|
37
|
+
const dir = path_1.default.join(reg, 'skills/using-awm');
|
|
38
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
39
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), '---\nversion: "1.0.0"\n---\nBODY');
|
|
40
|
+
const ctxPath = path_1.default.join(reg, 'awm-context.md');
|
|
41
|
+
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: ccOverride, contextPathOverride: ctxPath });
|
|
42
|
+
orch.installContext({ agent: 'claude-code', scope: 'global', registryRoot: reg, installMethod: 'symlink', profileExtensions: [] });
|
|
43
|
+
expect(install_1.installHook).toHaveBeenCalledWith(expect.objectContaining({ agent: 'claude-code' }));
|
|
44
|
+
});
|
|
45
|
+
it('uninstallContext delegates to uninstallHook', () => {
|
|
46
|
+
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: ccOverride });
|
|
47
|
+
orch.uninstallContext({ agent: 'claude-code', scope: 'global', registryRoot: '/any', installMethod: 'symlink', profileExtensions: [] });
|
|
48
|
+
expect(uninstall_1.uninstallHook).toHaveBeenCalledWith(expect.objectContaining({ agent: 'claude-code' }));
|
|
49
|
+
});
|
|
50
|
+
it('contextStatus delegates to computeHookStatus and returns absent when NOT_INSTALLED', () => {
|
|
51
|
+
status_1.computeHookStatus.mockReturnValue({ overall: 'NOT_INSTALLED' });
|
|
52
|
+
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: ccOverride });
|
|
53
|
+
const state = orch.contextStatus({ agent: 'claude-code', scope: 'global', registryRoot: '/any', installMethod: 'symlink', profileExtensions: [] });
|
|
54
|
+
expect(status_1.computeHookStatus).toHaveBeenCalledWith('claude-code');
|
|
55
|
+
expect(state).toBe('absent');
|
|
56
|
+
});
|
|
57
|
+
it('contextStatus returns injected when hook reports HEALTHY', () => {
|
|
58
|
+
status_1.computeHookStatus.mockReturnValue({ overall: 'HEALTHY' });
|
|
59
|
+
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: ccOverride });
|
|
60
|
+
const state = orch.contextStatus({ agent: 'claude-code', scope: 'global', registryRoot: '/any', installMethod: 'symlink', profileExtensions: [] });
|
|
61
|
+
expect(state).toBe('injected');
|
|
62
|
+
});
|
|
63
|
+
it('throws when providerOverride has no injection (does not fall through to real agent config)', () => {
|
|
64
|
+
const noInjection = { label: 'Test', skill: { global: '', local: '' }, workflow: null, agent: null };
|
|
65
|
+
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: noInjection });
|
|
66
|
+
expect(() => orch.installContext({ agent: 'claude-code', scope: 'global', registryRoot: '/any', installMethod: 'symlink', profileExtensions: [] }))
|
|
67
|
+
.toThrow('no injection mechanism');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('InjectionOrchestrator (opencode, real strategy)', () => {
|
|
71
|
+
let configPath;
|
|
72
|
+
let absPath;
|
|
73
|
+
let orch;
|
|
74
|
+
let registryRoot;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-oc-'));
|
|
77
|
+
configPath = path_1.default.join(dir, 'opencode.json');
|
|
78
|
+
absPath = path_1.default.join(dir, 'awm-context.md');
|
|
79
|
+
registryRoot = tmpRegistry();
|
|
80
|
+
orch = new orchestrator_1.InjectionOrchestrator({
|
|
81
|
+
providerOverride: {
|
|
82
|
+
label: 'OpenCode', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
83
|
+
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
84
|
+
},
|
|
85
|
+
contextPathOverride: absPath,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it('installContext materializes content and injects the sentinel; status reports injected', () => {
|
|
89
|
+
orch.installContext({ agent: 'opencode', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] });
|
|
90
|
+
expect(fs_1.default.existsSync(absPath)).toBe(true);
|
|
91
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
92
|
+
expect(cfg.instructions).toContain(absPath);
|
|
93
|
+
expect(orch.contextStatus({ agent: 'opencode', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] })).toBe('injected');
|
|
94
|
+
});
|
|
95
|
+
it('uninstallContext removes the sentinel; status reports absent', () => {
|
|
96
|
+
const args = { agent: 'opencode', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] };
|
|
97
|
+
orch.installContext(args);
|
|
98
|
+
orch.uninstallContext(args);
|
|
99
|
+
expect(orch.contextStatus(args)).toBe('absent');
|
|
100
|
+
});
|
|
101
|
+
it('throws when the agent has no injection mechanism', () => {
|
|
102
|
+
const bare = new orchestrator_1.InjectionOrchestrator();
|
|
103
|
+
expect(() => bare.installContext({ agent: 'antigravity', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] }))
|
|
104
|
+
.toThrow('no injection mechanism');
|
|
105
|
+
});
|
|
106
|
+
it('contextStatus returns stale when the materialized file drifts without re-materializing it', () => {
|
|
107
|
+
const args = { agent: 'opencode', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] };
|
|
108
|
+
orch.installContext(args);
|
|
109
|
+
// Drift the materialized file content after install
|
|
110
|
+
fs_1.default.writeFileSync(absPath, 'DRIFTED CONTENT');
|
|
111
|
+
// contextStatus must detect stale without correcting the file
|
|
112
|
+
expect(orch.contextStatus(args)).toBe('stale');
|
|
113
|
+
// Confirm the drifted content is still on disk (not silently corrected)
|
|
114
|
+
expect(fs_1.default.readFileSync(absPath, 'utf-8')).toBe('DRIFTED CONTENT');
|
|
115
|
+
});
|
|
116
|
+
it('uninstallContext succeeds even when the registry does not exist', () => {
|
|
117
|
+
const args = { agent: 'opencode', scope: 'global', registryRoot, installMethod: 'symlink', profileExtensions: [] };
|
|
118
|
+
orch.installContext(args);
|
|
119
|
+
// Remove the registry to simulate a degraded state
|
|
120
|
+
fs_1.default.rmSync(registryRoot, { recursive: true, force: true });
|
|
121
|
+
// uninstallContext must not throw 'using-awm skill not found'
|
|
122
|
+
expect(() => orch.uninstallContext(args)).not.toThrow();
|
|
123
|
+
// After removal the sentinel must be gone from opencode.json
|
|
124
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
125
|
+
expect((cfg.instructions ?? []).includes(absPath)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
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 project_constitution_inject_1 = require("../../../src/core/context/project-constitution-inject");
|
|
10
|
+
function tmpProject(withConstitution) {
|
|
11
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-const-'));
|
|
12
|
+
if (withConstitution)
|
|
13
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'CONSTITUTION.md'), '# rules\n');
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
describe('injectProjectConstitution (#6)', () => {
|
|
17
|
+
it('writes project-local opencode.json with a relative CONSTITUTION.md instruction', () => {
|
|
18
|
+
const root = tmpProject(true);
|
|
19
|
+
try {
|
|
20
|
+
expect((0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode')).toBe('injected');
|
|
21
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'opencode.json'), 'utf-8'));
|
|
22
|
+
expect(cfg.instructions).toEqual(['CONSTITUTION.md']);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
it('is idempotent (no duplicate entry on a second run)', () => {
|
|
29
|
+
const root = tmpProject(true);
|
|
30
|
+
try {
|
|
31
|
+
(0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode');
|
|
32
|
+
expect((0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode')).toBe('already');
|
|
33
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'opencode.json'), 'utf-8'));
|
|
34
|
+
expect(cfg.instructions).toEqual(['CONSTITUTION.md']);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it('preserves existing instructions and appends', () => {
|
|
41
|
+
const root = tmpProject(true);
|
|
42
|
+
try {
|
|
43
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'opencode.json'), JSON.stringify({ $schema: 'x', instructions: ['./AGENTS.md'] }, null, 2));
|
|
44
|
+
expect((0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode')).toBe('injected');
|
|
45
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'opencode.json'), 'utf-8'));
|
|
46
|
+
expect(cfg.instructions).toEqual(['./AGENTS.md', 'CONSTITUTION.md']);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('returns no-constitution and writes nothing when CONSTITUTION.md is absent', () => {
|
|
53
|
+
const root = tmpProject(false);
|
|
54
|
+
try {
|
|
55
|
+
expect((0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode')).toBe('no-constitution');
|
|
56
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, 'opencode.json'))).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it('returns not-applicable for Claude (hook delivers it) and writes nothing', () => {
|
|
63
|
+
const root = tmpProject(true);
|
|
64
|
+
try {
|
|
65
|
+
expect((0, project_constitution_inject_1.injectProjectConstitution)(root, 'claude-code')).toBe('not-applicable');
|
|
66
|
+
expect(fs_1.default.existsSync(path_1.default.join(root, 'opencode.json'))).toBe(false);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it('throws a clear error when instructions is a non-array', () => {
|
|
73
|
+
const root = tmpProject(true);
|
|
74
|
+
try {
|
|
75
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'opencode.json'), JSON.stringify({ instructions: 'oops' }));
|
|
76
|
+
expect(() => (0, project_constitution_inject_1.injectProjectConstitution)(root, 'opencode')).toThrow(/must be an array/);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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/context/provider.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const provider_1 = require("../../../src/core/context/provider");
|
|
11
|
+
function tmpRegistry(skillBody) {
|
|
12
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-reg-'));
|
|
13
|
+
const dir = path_1.default.join(root, 'skills/using-awm');
|
|
14
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
15
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), skillBody);
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
describe('sha256', () => {
|
|
19
|
+
it('is deterministic and hex-encoded', () => {
|
|
20
|
+
expect((0, provider_1.sha256)('hello')).toBe((0, provider_1.sha256)('hello'));
|
|
21
|
+
expect((0, provider_1.sha256)('hello')).toMatch(/^[0-9a-f]{64}$/);
|
|
22
|
+
expect((0, provider_1.sha256)('hello')).not.toBe((0, provider_1.sha256)('world'));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('buildContext', () => {
|
|
26
|
+
it('embeds the using-awm body, version from frontmatter, and active extensions', () => {
|
|
27
|
+
const reg = tmpRegistry('---\nname: using-awm\nversion: "2.1.0"\n---\nBODY-MARKER');
|
|
28
|
+
const ctx = (0, provider_1.buildContext)({ registryRoot: reg, profileExtensions: ['frontend', 'docs'] });
|
|
29
|
+
expect(ctx.markdown).toContain('BODY-MARKER');
|
|
30
|
+
expect(ctx.markdown).toContain('frontend, docs');
|
|
31
|
+
expect(ctx.sourceVersion).toBe('2.1.0');
|
|
32
|
+
expect(ctx.contentHash).toBe((0, provider_1.sha256)(ctx.markdown));
|
|
33
|
+
});
|
|
34
|
+
it('falls back to version 0.0.0 when frontmatter has no version', () => {
|
|
35
|
+
const reg = tmpRegistry('---\nname: using-awm\n---\nBODY');
|
|
36
|
+
expect((0, provider_1.buildContext)({ registryRoot: reg, profileExtensions: [] }).sourceVersion).toBe('0.0.0');
|
|
37
|
+
});
|
|
38
|
+
it('throws an actionable error when the using-awm skill is missing', () => {
|
|
39
|
+
const reg = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-'));
|
|
40
|
+
expect(() => (0, provider_1.buildContext)({ registryRoot: reg, profileExtensions: [] })).toThrow('using-awm skill not found');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
// NOTE: the 'generic robustness invariant' test that validated specific prose in the
|
|
44
|
+
// using-awm SKILL.md has been removed — content now lives in awm-baseline-registry
|
|
45
|
+
// (an external repo), not in this monorepo. Content-level tests belong there.
|
|
@@ -0,0 +1,106 @@
|
|
|
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/context/regenerate.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
describe('regenerateGlobalContext', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let originalHome;
|
|
13
|
+
let originalAwmHome;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regen-'));
|
|
16
|
+
originalHome = process.env.HOME;
|
|
17
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
18
|
+
process.env.HOME = tmpHome;
|
|
19
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
20
|
+
jest.resetModules();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
24
|
+
if (originalHome === undefined)
|
|
25
|
+
delete process.env.HOME;
|
|
26
|
+
else
|
|
27
|
+
process.env.HOME = originalHome;
|
|
28
|
+
if (originalAwmHome === undefined)
|
|
29
|
+
delete process.env.AWM_HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
32
|
+
});
|
|
33
|
+
// Crea un registry falso con el skill canónico using-awm que buildContext lee.
|
|
34
|
+
// Escribe registries.json apuntando al content root y crea el skill en la raíz del content root.
|
|
35
|
+
function seedRegistry(body = '---\nname: using-awm\nversion: "1.0.0"\n---\nUSING-AWM-BODY') {
|
|
36
|
+
const awmHome = path_1.default.join(tmpHome, '.awm');
|
|
37
|
+
const contentRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
38
|
+
const dir = path_1.default.join(contentRoot, 'skills', 'using-awm');
|
|
39
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), body);
|
|
41
|
+
// Write registries.json so capabilityRoot('skills') resolves to contentRoot
|
|
42
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2) + '\n');
|
|
43
|
+
}
|
|
44
|
+
// Escribe un opencode.json con instructions[] = entries.
|
|
45
|
+
function seedOpencode(entries) {
|
|
46
|
+
const ocDir = path_1.default.join(tmpHome, '.config', 'opencode');
|
|
47
|
+
fs_1.default.mkdirSync(ocDir, { recursive: true });
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(ocDir, 'opencode.json'), JSON.stringify({ $schema: 'https://opencode.ai/config.json', instructions: entries }, null, 2));
|
|
49
|
+
}
|
|
50
|
+
function contextPath() {
|
|
51
|
+
return path_1.default.join(tmpHome, '.awm', 'context', 'awm-context.md');
|
|
52
|
+
}
|
|
53
|
+
it('returns empty when no config-instructions agent has a config file', () => {
|
|
54
|
+
seedRegistry();
|
|
55
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
56
|
+
expect(regenerateGlobalContext()).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it('skips an agent whose config exists but has no AWM sentinel', () => {
|
|
59
|
+
seedRegistry();
|
|
60
|
+
seedOpencode(['docs/rules.md']); // sin el sentinel
|
|
61
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
62
|
+
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
63
|
+
expect(fs_1.default.existsSync(contextPath())).toBe(false); // no se crea el archivo
|
|
64
|
+
});
|
|
65
|
+
it('refreshes a stale agent (sentinel present, materialized file absent) and recreates the file', () => {
|
|
66
|
+
seedRegistry();
|
|
67
|
+
seedOpencode([contextPath()]); // sentinel presente, pero el archivo no existe → stale
|
|
68
|
+
expect(fs_1.default.existsSync(contextPath())).toBe(false);
|
|
69
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
70
|
+
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'refreshed' }]);
|
|
71
|
+
expect(fs_1.default.existsSync(contextPath())).toBe(true);
|
|
72
|
+
expect(fs_1.default.readFileSync(contextPath(), 'utf-8')).toContain('USING-AWM-BODY');
|
|
73
|
+
});
|
|
74
|
+
it('reports fresh (no rewrite) when the agent is already injected', () => {
|
|
75
|
+
seedRegistry();
|
|
76
|
+
seedOpencode([contextPath()]);
|
|
77
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
78
|
+
regenerateGlobalContext(); // 1ª pasada: stale → refreshed, crea el archivo
|
|
79
|
+
const mtime1 = fs_1.default.statSync(contextPath()).mtimeMs;
|
|
80
|
+
const second = regenerateGlobalContext(); // 2ª pasada: ya injected → fresh
|
|
81
|
+
expect(second).toEqual([{ agent: 'opencode', action: 'fresh' }]);
|
|
82
|
+
expect(fs_1.default.statSync(contextPath()).mtimeMs).toBe(mtime1); // archivo intacto
|
|
83
|
+
});
|
|
84
|
+
it('does not throw when the registry has no using-awm skill (stale but unregenerable → skipped)', () => {
|
|
85
|
+
// registry con skills/ pero sin using-awm dentro; opencode stale
|
|
86
|
+
const awmHome = path_1.default.join(tmpHome, '.awm');
|
|
87
|
+
const contentRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
88
|
+
// Create skills/ dir (so capabilityRoot resolves) but omit using-awm/
|
|
89
|
+
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills'), { recursive: true });
|
|
90
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2) + '\n');
|
|
91
|
+
seedOpencode([contextPath()]);
|
|
92
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
93
|
+
expect(() => regenerateGlobalContext()).not.toThrow();
|
|
94
|
+
expect(regenerateGlobalContext()).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
95
|
+
});
|
|
96
|
+
it('skips agent when contextStatus throws an unexpected error', () => {
|
|
97
|
+
seedRegistry();
|
|
98
|
+
seedOpencode([contextPath()]);
|
|
99
|
+
const { regenerateGlobalContext } = require('../../../src/core/context/regenerate');
|
|
100
|
+
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
101
|
+
const brokenOrch = new InjectionOrchestrator();
|
|
102
|
+
brokenOrch.contextStatus = () => { throw new Error('unexpected orchestrator error'); };
|
|
103
|
+
expect(regenerateGlobalContext(brokenOrch)).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
104
|
+
expect(fs_1.default.existsSync(contextPath())).toBe(false); // installContext never called
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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/context/strategies/config-instructions.test.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const config_instructions_1 = require("../../../../src/core/context/strategies/config-instructions");
|
|
11
|
+
const provider_1 = require("../../../../src/core/context/provider");
|
|
12
|
+
function setup(opencodeJson) {
|
|
13
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-oc-'));
|
|
14
|
+
const configPath = path_1.default.join(dir, 'opencode.json');
|
|
15
|
+
if (opencodeJson)
|
|
16
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(opencodeJson, null, 2));
|
|
17
|
+
const absPath = path_1.default.join(dir, '.awm/context/awm-context.md');
|
|
18
|
+
fs_1.default.mkdirSync(path_1.default.dirname(absPath), { recursive: true });
|
|
19
|
+
fs_1.default.writeFileSync(absPath, 'CTX');
|
|
20
|
+
const provider = {
|
|
21
|
+
label: 'OpenCode', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
22
|
+
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
23
|
+
};
|
|
24
|
+
const input = {
|
|
25
|
+
ref: { absPath, scope: 'global', contentHash: (0, provider_1.sha256)('CTX') },
|
|
26
|
+
registryRoot: '/reg', installMethod: 'symlink', agent: 'opencode', scope: 'global',
|
|
27
|
+
};
|
|
28
|
+
return { configPath, absPath, provider, input };
|
|
29
|
+
}
|
|
30
|
+
const strat = new config_instructions_1.ConfigInstructionsStrategy();
|
|
31
|
+
describe('ConfigInstructionsStrategy.inject', () => {
|
|
32
|
+
it('creates opencode.json with the sentinel when it does not exist', () => {
|
|
33
|
+
const { configPath, absPath, provider, input } = setup();
|
|
34
|
+
strat.inject(input, provider);
|
|
35
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
36
|
+
expect(cfg.instructions).toContain(absPath);
|
|
37
|
+
expect(cfg.$schema).toBe('https://opencode.ai/config.json');
|
|
38
|
+
});
|
|
39
|
+
it('preserves user instructions and is idempotent (no duplicate)', () => {
|
|
40
|
+
const { configPath, absPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
|
|
41
|
+
strat.inject(input, provider);
|
|
42
|
+
strat.inject(input, provider);
|
|
43
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
44
|
+
expect(cfg.instructions).toContain('docs/rules.md');
|
|
45
|
+
expect(cfg.instructions.filter((e) => e === absPath)).toHaveLength(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('ConfigInstructionsStrategy.remove', () => {
|
|
49
|
+
it('removes only the sentinel, preserving user entries', () => {
|
|
50
|
+
const { configPath, absPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
|
|
51
|
+
strat.inject(input, provider);
|
|
52
|
+
strat.remove(input, provider);
|
|
53
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
54
|
+
expect(cfg.instructions).toEqual(['docs/rules.md']);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('ConfigInstructionsStrategy.status', () => {
|
|
58
|
+
it('absent when no config / no entry', () => {
|
|
59
|
+
const { provider, input } = setup();
|
|
60
|
+
expect(strat.status(input, provider)).toBe('absent');
|
|
61
|
+
});
|
|
62
|
+
it('injected when entry present and materialized hash matches', () => {
|
|
63
|
+
const { provider, input } = setup();
|
|
64
|
+
strat.inject(input, provider);
|
|
65
|
+
expect(strat.status(input, provider)).toBe('injected');
|
|
66
|
+
});
|
|
67
|
+
it('stale when entry present but materialized file content drifted', () => {
|
|
68
|
+
const { absPath, provider, input } = setup();
|
|
69
|
+
strat.inject(input, provider);
|
|
70
|
+
fs_1.default.writeFileSync(absPath, 'DRIFTED');
|
|
71
|
+
expect(strat.status(input, provider)).toBe('stale');
|
|
72
|
+
});
|
|
73
|
+
it('stale when sentinel entry is present but the materialized file no longer exists', () => {
|
|
74
|
+
const { absPath, provider, input } = setup();
|
|
75
|
+
strat.inject(input, provider);
|
|
76
|
+
fs_1.default.rmSync(absPath);
|
|
77
|
+
expect(strat.status(input, provider)).toBe('stale');
|
|
78
|
+
});
|
|
79
|
+
it('throws actionable error on malformed opencode.json instead of clobbering', () => {
|
|
80
|
+
const { configPath, provider, input } = setup();
|
|
81
|
+
fs_1.default.writeFileSync(configPath, '{ not json');
|
|
82
|
+
expect(() => strat.inject(input, provider)).toThrow('not valid JSON');
|
|
83
|
+
});
|
|
84
|
+
it('throws when instructions is a non-array value instead of silently overwriting it', () => {
|
|
85
|
+
const { provider, input } = setup({ instructions: 'docs/rules.md' });
|
|
86
|
+
expect(() => strat.inject(input, provider)).toThrow("'instructions' field must be an array");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
// cli/tests/core/context/strategies/hook-merge.test.ts
|
|
37
|
+
const hook_merge_1 = require("../../../../src/core/context/strategies/hook-merge");
|
|
38
|
+
const install = __importStar(require("../../../../src/commands/hooks/install"));
|
|
39
|
+
const uninstall = __importStar(require("../../../../src/commands/hooks/uninstall"));
|
|
40
|
+
const status = __importStar(require("../../../../src/commands/hooks/status"));
|
|
41
|
+
function input() {
|
|
42
|
+
return {
|
|
43
|
+
ref: { absPath: '/tmp/awm-context.md', scope: 'global', contentHash: 'h' },
|
|
44
|
+
registryRoot: '/reg', installMethod: 'symlink', agent: 'claude-code', scope: 'global',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const provider = {};
|
|
48
|
+
const strat = new hook_merge_1.HookMergeStrategy();
|
|
49
|
+
describe('HookMergeStrategy', () => {
|
|
50
|
+
it('inject delegates to installHook with agent/registryRoot/installMethod', () => {
|
|
51
|
+
const spy = jest.spyOn(install, 'installHook').mockReturnValue({});
|
|
52
|
+
strat.inject(input(), provider);
|
|
53
|
+
expect(spy).toHaveBeenCalledWith({ agent: 'claude-code', registryRoot: '/reg', installMethod: 'symlink' });
|
|
54
|
+
spy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
it('remove delegates to uninstallHook', () => {
|
|
57
|
+
const spy = jest.spyOn(uninstall, 'uninstallHook').mockReturnValue({});
|
|
58
|
+
strat.remove(input(), provider);
|
|
59
|
+
expect(spy).toHaveBeenCalledWith({ agent: 'claude-code' });
|
|
60
|
+
spy.mockRestore();
|
|
61
|
+
});
|
|
62
|
+
it.each([
|
|
63
|
+
['HEALTHY', 'injected'],
|
|
64
|
+
['DEGRADED', 'stale'],
|
|
65
|
+
['NOT_INSTALLED', 'absent'],
|
|
66
|
+
])('status maps hook overall %s → %s', (overall, expected) => {
|
|
67
|
+
const spy = jest.spyOn(status, 'computeHookStatus').mockReturnValue({ overall });
|
|
68
|
+
expect(strat.status(input(), provider)).toBe(expected);
|
|
69
|
+
spy.mockRestore();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
describe('context contracts', () => {
|
|
4
|
+
it('compiles AwmContext / MaterializedRef / InjectionInput with the expected shape', () => {
|
|
5
|
+
const ctx = { markdown: '# AWM', sourceVersion: '1.0.0', contentHash: 'abc' };
|
|
6
|
+
const ref = { absPath: '/tmp/awm-context.md', scope: 'global', contentHash: 'abc' };
|
|
7
|
+
const input = {
|
|
8
|
+
ref, registryRoot: '/reg', installMethod: 'symlink', agent: 'opencode', scope: 'global',
|
|
9
|
+
};
|
|
10
|
+
const states = ['injected', 'absent', 'stale'];
|
|
11
|
+
expect(ctx.contentHash).toBe(ref.contentHash);
|
|
12
|
+
expect(input.agent).toBe('opencode');
|
|
13
|
+
expect(states).toHaveLength(3);
|
|
14
|
+
});
|
|
15
|
+
});
|