agentic-workflow-manager 3.0.1 → 3.2.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/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/export.js +48 -0
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/discovery.js +9 -3
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/export/index.js +51 -0
- package/dist/src/core/export/pack.js +58 -0
- package/dist/src/core/export/resolve.js +77 -0
- package/dist/src/core/export/transform.js +57 -0
- package/dist/src/core/export/types.js +2 -0
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +164 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +99 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/export.test.js +64 -0
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +104 -0
- package/dist/tests/core/export/pack.test.js +122 -0
- package/dist/tests/core/export/resolve.test.js +78 -0
- package/dist/tests/core/export/transform.test.js +74 -0
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +129 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
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 crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const atomic_file_1 = require("../../src/core/atomic-file");
|
|
11
|
+
describe('writeFileAtomic', () => {
|
|
12
|
+
let dir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-'));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.restoreAllMocks();
|
|
18
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
it('writes through an operation-scoped temporary file and renames it', () => {
|
|
21
|
+
const file = path_1.default.join(dir, 'nested', 'AGENTS.md');
|
|
22
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
23
|
+
const rename = jest.spyOn(fs_1.default, 'renameSync');
|
|
24
|
+
(0, atomic_file_1.writeFileAtomic)(file, 'content', 0o600);
|
|
25
|
+
const temporary = String(open.mock.calls[0][0]);
|
|
26
|
+
expect(path_1.default.dirname(temporary)).toBe(path_1.default.dirname(file));
|
|
27
|
+
expect(path_1.default.basename(temporary)).toMatch(/^\.AGENTS\.md\.\d+\.[0-9a-f]{32}\.tmp$/);
|
|
28
|
+
expect(open.mock.calls[0][1]).toBe('wx');
|
|
29
|
+
expect(rename).toHaveBeenCalledWith(temporary, file);
|
|
30
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('content');
|
|
31
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(0o600);
|
|
32
|
+
expect(fs_1.default.existsSync(temporary)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('cleans only its temporary file when rename fails', () => {
|
|
35
|
+
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
36
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
37
|
+
const unrelated = path_1.default.join(dir, '.unrelated.tmp');
|
|
38
|
+
fs_1.default.writeFileSync(unrelated, 'keep');
|
|
39
|
+
jest.spyOn(fs_1.default, 'renameSync').mockImplementation(() => {
|
|
40
|
+
throw new Error('rename failed');
|
|
41
|
+
});
|
|
42
|
+
expect(() => (0, atomic_file_1.writeFileAtomic)(file, 'content')).toThrow('rename failed');
|
|
43
|
+
const temp = String(open.mock.calls[0][0]);
|
|
44
|
+
expect(fs_1.default.existsSync(temp)).toBe(false);
|
|
45
|
+
expect(fs_1.default.readFileSync(unrelated, 'utf8')).toBe('keep');
|
|
46
|
+
expect(fs_1.default.existsSync(file)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it('preserves the permission bits and content of an existing regular target on failure', () => {
|
|
49
|
+
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
50
|
+
fs_1.default.writeFileSync(file, 'original', { mode: 0o600 });
|
|
51
|
+
jest.spyOn(fs_1.default, 'renameSync').mockImplementation(() => {
|
|
52
|
+
throw new Error('rename failed');
|
|
53
|
+
});
|
|
54
|
+
expect(() => (0, atomic_file_1.writeFileAtomic)(file, 'replacement', 0o644)).toThrow('rename failed');
|
|
55
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('original');
|
|
56
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(0o600);
|
|
57
|
+
});
|
|
58
|
+
it('preserves existing target permissions after replacement', () => {
|
|
59
|
+
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
60
|
+
fs_1.default.writeFileSync(file, 'original', { mode: 0o600 });
|
|
61
|
+
(0, atomic_file_1.writeFileAtomic)(file, 'replacement');
|
|
62
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('replacement');
|
|
63
|
+
expect(fs_1.default.statSync(file).mode & 0o777).toBe(0o600);
|
|
64
|
+
});
|
|
65
|
+
it('rejects a target symlink without severing it or changing its victim', () => {
|
|
66
|
+
const victim = path_1.default.join(dir, 'victim');
|
|
67
|
+
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
68
|
+
fs_1.default.writeFileSync(victim, 'victim');
|
|
69
|
+
fs_1.default.symlinkSync(victim, file);
|
|
70
|
+
expect(() => (0, atomic_file_1.writeFileAtomic)(file, 'replacement')).toThrow('refusing to replace symlink');
|
|
71
|
+
expect(fs_1.default.lstatSync(file).isSymbolicLink()).toBe(true);
|
|
72
|
+
expect(fs_1.default.readFileSync(victim, 'utf8')).toBe('victim');
|
|
73
|
+
});
|
|
74
|
+
it('cannot follow or remove a pre-created staging symlink', () => {
|
|
75
|
+
const file = path_1.default.join(dir, 'AGENTS.md');
|
|
76
|
+
const victim = path_1.default.join(dir, 'victim');
|
|
77
|
+
const random = Buffer.alloc(16, 0xab);
|
|
78
|
+
const temporary = path_1.default.join(dir, `.AGENTS.md.${process.pid}.${random.toString('hex')}.tmp`);
|
|
79
|
+
fs_1.default.writeFileSync(victim, 'victim');
|
|
80
|
+
fs_1.default.symlinkSync(victim, temporary);
|
|
81
|
+
jest.spyOn(crypto_1.default, 'randomBytes').mockImplementation((() => random));
|
|
82
|
+
expect(() => (0, atomic_file_1.writeFileAtomic)(file, 'replacement')).toThrow();
|
|
83
|
+
expect(fs_1.default.lstatSync(temporary).isSymbolicLink()).toBe(true);
|
|
84
|
+
expect(fs_1.default.readFileSync(victim, 'utf8')).toBe('victim');
|
|
85
|
+
expect(fs_1.default.existsSync(file)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it.each([
|
|
88
|
+
['', 'content', 0o644, 'file'],
|
|
89
|
+
['file', null, 0o644, 'content'],
|
|
90
|
+
['file', 'content', -1, 'mode'],
|
|
91
|
+
['file', 'content', 1.5, 'mode'],
|
|
92
|
+
['file', 'content', 0o10000, 'mode'],
|
|
93
|
+
])('rejects invalid runtime inputs', (file, content, mode, message) => {
|
|
94
|
+
expect(() => (0, atomic_file_1.writeFileAtomic)(file, content, mode)).toThrow(message);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,4 +1,37 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -9,6 +42,61 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
42
|
const bundles_1 = require("../../src/core/bundles");
|
|
10
43
|
const bundle_install_1 = require("../../src/core/bundle-install");
|
|
11
44
|
const profile_1 = require("../../src/core/profile");
|
|
45
|
+
const executor_1 = require("../../src/core/executor");
|
|
46
|
+
const executor = __importStar(require("../../src/core/executor"));
|
|
47
|
+
/**
|
|
48
|
+
* `installBundle` is a thin façade — it expands the bundle closure into
|
|
49
|
+
* artifact intents, runs them through `planInstall` (dedup + ownership +
|
|
50
|
+
* shared-skill-group gating), and hands the resulting plan to
|
|
51
|
+
* `applyInstallPlan` (install-transaction.ts) to actually touch the
|
|
52
|
+
* filesystem. Most tests below use this simpler test double instead — it
|
|
53
|
+
* skips per-artifact on a missing source (unlike the real, transactional
|
|
54
|
+
* applyInstallPlan, which aborts the whole plan) and doesn't render
|
|
55
|
+
* `codex-agent-toml` targets, which keeps the fixtures here from needing
|
|
56
|
+
* valid canonical-agent frontmatter for every scenario. Tests that
|
|
57
|
+
* specifically exercise the REAL default applyInstallPlan pass no
|
|
58
|
+
* `applyPlan` override at all — see below.
|
|
59
|
+
*/
|
|
60
|
+
function testApplyPlan(plan) {
|
|
61
|
+
const installed = [];
|
|
62
|
+
const skipped = [];
|
|
63
|
+
const modifiedFiles = [];
|
|
64
|
+
for (const op of plan.operations) {
|
|
65
|
+
if (!fs_1.default.existsSync(op.sourcePath)) {
|
|
66
|
+
skipped.push(`${op.name} (source missing: ${op.sourcePath})`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
(0, executor_1.installArtifact)(op.sourcePath, op.targetPath, op.method);
|
|
70
|
+
installed.push(`${op.name} → ${op.owners.join(',')} (${op.scope})`);
|
|
71
|
+
modifiedFiles.push(op.targetPath);
|
|
72
|
+
}
|
|
73
|
+
return { installed, skipped, transactionId: 'test-fixture', modifiedFiles };
|
|
74
|
+
}
|
|
75
|
+
// Per CLAUDE.md: no test may touch the real ~/.awm. installBundle now calls
|
|
76
|
+
// getPreferences() internally (to resolve enabledAgents for the planner), so
|
|
77
|
+
// every test in this file needs HOME/AWM_HOME pointed at an isolated tmpdir,
|
|
78
|
+
// following the pattern in tests/commands/hooks/install.test.ts.
|
|
79
|
+
let tmpHome;
|
|
80
|
+
let originalHome;
|
|
81
|
+
let originalAwmHome;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-binstall-home-'));
|
|
84
|
+
originalHome = process.env.HOME;
|
|
85
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
86
|
+
process.env.HOME = tmpHome;
|
|
87
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
91
|
+
if (originalHome === undefined)
|
|
92
|
+
delete process.env.HOME;
|
|
93
|
+
else
|
|
94
|
+
process.env.HOME = originalHome;
|
|
95
|
+
if (originalAwmHome === undefined)
|
|
96
|
+
delete process.env.AWM_HOME;
|
|
97
|
+
else
|
|
98
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
99
|
+
});
|
|
12
100
|
/**
|
|
13
101
|
* Builds a fixture with:
|
|
14
102
|
* - content registry: catalog + two project bundles (base, ext dependsOn base),
|
|
@@ -32,7 +120,7 @@ function makeFixture() {
|
|
|
32
120
|
fs_1.default.mkdirSync(path_1.default.join(content, 'workflows'), { recursive: true });
|
|
33
121
|
fs_1.default.writeFileSync(path_1.default.join(content, 'workflows', 'wf-ext.md'), '# wf');
|
|
34
122
|
fs_1.default.mkdirSync(path_1.default.join(content, 'agents'), { recursive: true });
|
|
35
|
-
fs_1.default.writeFileSync(path_1.default.join(content, 'agents', 'ag-ext.md'), '
|
|
123
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'agents', 'ag-ext.md'), '---\nname: ag-ext\ndescription: Test agent for bundle-install fixtures\n---\nDo the thing.\n');
|
|
36
124
|
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
37
125
|
version: 1,
|
|
38
126
|
bundles: [
|
|
@@ -55,7 +143,7 @@ describe('installBundle', () => {
|
|
|
55
143
|
const { content, projectRoot, bundles } = makeFixture();
|
|
56
144
|
const result = (0, bundle_install_1.installBundle)({
|
|
57
145
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
58
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
146
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
59
147
|
});
|
|
60
148
|
const skillsDir = path_1.default.join(projectRoot, '.claude', 'skills');
|
|
61
149
|
expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 's-base'))).toBe(true); // from dep `base`
|
|
@@ -63,21 +151,29 @@ describe('installBundle', () => {
|
|
|
63
151
|
expect(fs_1.default.lstatSync(path_1.default.join(skillsDir, 's-ext')).isSymbolicLink()).toBe(true);
|
|
64
152
|
expect(result.installed.some((l) => l.includes('s-base'))).toBe(true);
|
|
65
153
|
});
|
|
66
|
-
it('installs supported artifact types
|
|
154
|
+
it('installs supported artifact types (claude-code agents)', () => {
|
|
67
155
|
const { content, projectRoot, bundles } = makeFixture();
|
|
68
156
|
const result = (0, bundle_install_1.installBundle)({
|
|
69
157
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
70
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
158
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
71
159
|
});
|
|
72
|
-
// claude-code has no workflow dir → wf-ext is skipped; agents are supported.
|
|
73
160
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.claude', 'agents', 'ag-ext.md'))).toBe(true);
|
|
74
|
-
|
|
161
|
+
// ACCEPTED GAP (Task 5, confirmed unchanged by Task 6): claude-code has
|
|
162
|
+
// no `workflow` provider config, so planInstall (install-planner.ts)
|
|
163
|
+
// silently drops wf-ext for this agent before a plan/operations ever
|
|
164
|
+
// exist for it — there is no "unsupported for this agent" signal for
|
|
165
|
+
// applyInstallPlan to surface as `skipped`, by design (install-planner.ts
|
|
166
|
+
// mirrors the legacy installBundle skip semantics for this exact case).
|
|
167
|
+
// Fixing this would mean adding a skip-reason channel to InstallPlan
|
|
168
|
+
// itself, which is install-planner.ts's contract (Task 5), not Task 6's.
|
|
169
|
+
expect(result.skipped.some((l) => l.includes('wf-ext'))).toBe(false);
|
|
170
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.claude', 'workflows', 'wf-ext.md'))).toBe(false);
|
|
75
171
|
});
|
|
76
172
|
it('is idempotent: a second run leaves valid symlinks and does not throw', () => {
|
|
77
173
|
const { content, projectRoot, bundles } = makeFixture();
|
|
78
174
|
const opts = {
|
|
79
175
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
80
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
176
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
81
177
|
};
|
|
82
178
|
(0, bundle_install_1.installBundle)(opts);
|
|
83
179
|
expect(() => (0, bundle_install_1.installBundle)(opts)).not.toThrow();
|
|
@@ -88,18 +184,113 @@ describe('installBundle', () => {
|
|
|
88
184
|
fs_1.default.rmSync(path_1.default.join(content, 'skills', 's-base'), { recursive: true, force: true });
|
|
89
185
|
const result = (0, bundle_install_1.installBundle)({
|
|
90
186
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
91
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
187
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
92
188
|
});
|
|
93
189
|
expect(result.skipped.some((l) => l.includes('s-base'))).toBe(true);
|
|
94
190
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.claude', 'skills', 's-ext'))).toBe(true);
|
|
95
191
|
});
|
|
192
|
+
it('materializes a Codex agent as rendered TOML via the real default applyInstallPlan (no applyPlan override)', () => {
|
|
193
|
+
const { content, projectRoot, bundles } = makeFixture();
|
|
194
|
+
const ext = bundles.find((bundle) => bundle.name === 'ext');
|
|
195
|
+
const agentOnly = [{
|
|
196
|
+
...ext,
|
|
197
|
+
dependsOn: [],
|
|
198
|
+
skills: [],
|
|
199
|
+
workflows: [],
|
|
200
|
+
}];
|
|
201
|
+
const result = (0, bundle_install_1.installBundle)({
|
|
202
|
+
bundleName: 'ext',
|
|
203
|
+
bundles: agentOnly,
|
|
204
|
+
agents: ['codex'],
|
|
205
|
+
method: 'symlink',
|
|
206
|
+
projectRoot,
|
|
207
|
+
contentDir: content,
|
|
208
|
+
});
|
|
209
|
+
const tomlPath = path_1.default.join(projectRoot, '.codex/agents/ag-ext.toml');
|
|
210
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.codex/agents/ag-ext.md'))).toBe(false);
|
|
211
|
+
expect(fs_1.default.existsSync(tomlPath)).toBe(true);
|
|
212
|
+
expect(fs_1.default.readFileSync(tomlPath, 'utf8')).toContain('name = "ag-ext"');
|
|
213
|
+
expect(result.transactionId).toBeTruthy();
|
|
214
|
+
expect(result.modifiedFiles).toContain(tomlPath);
|
|
215
|
+
});
|
|
216
|
+
it('keeps Codex skill installs on the legacy link renderer', () => {
|
|
217
|
+
const { content, projectRoot, bundles } = makeFixture();
|
|
218
|
+
const base = bundles.find((bundle) => bundle.name === 'base');
|
|
219
|
+
const result = (0, bundle_install_1.installBundle)({
|
|
220
|
+
bundleName: 'base',
|
|
221
|
+
bundles: [base],
|
|
222
|
+
agents: ['codex'],
|
|
223
|
+
method: 'symlink',
|
|
224
|
+
projectRoot,
|
|
225
|
+
contentDir: content,
|
|
226
|
+
applyPlan: testApplyPlan,
|
|
227
|
+
});
|
|
228
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-base'))).toBe(true);
|
|
229
|
+
// KNOWN FORMAT CHANGE (Task 6): the old `[bundleName]` suffix is gone —
|
|
230
|
+
// ArtifactIntent (and therefore the plan) no longer carries bundle
|
|
231
|
+
// provenance, only artifact name/type/owners. See this task's report.
|
|
232
|
+
expect(result.installed).toContain('s-base → codex (local)');
|
|
233
|
+
});
|
|
234
|
+
it('applies real filesystem changes for mixed Codex bundles when no applyPlan is supplied', () => {
|
|
235
|
+
const { content, projectRoot, bundles } = makeFixture();
|
|
236
|
+
const result = (0, bundle_install_1.addBundle)({
|
|
237
|
+
bundleName: 'ext',
|
|
238
|
+
bundles,
|
|
239
|
+
agents: ['codex'],
|
|
240
|
+
method: 'symlink',
|
|
241
|
+
projectRoot,
|
|
242
|
+
contentDir: content,
|
|
243
|
+
});
|
|
244
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-base'))).toBe(true);
|
|
245
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-ext'))).toBe(true);
|
|
246
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.codex/agents/ag-ext.toml'))).toBe(true);
|
|
247
|
+
expect(result.recordedExtension).toBe('ext');
|
|
248
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm/profile.json'))).toBe(true);
|
|
249
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.gitignore'))).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
it('installs a skill shared by two agents (OpenCode + Codex) with exactly one replaceArtifact call, but the summary contains both providers', () => {
|
|
252
|
+
const { content, projectRoot, bundles } = makeFixture();
|
|
253
|
+
const base = bundles.find((bundle) => bundle.name === 'base');
|
|
254
|
+
const replaceSpy = jest.spyOn(executor, 'replaceArtifact');
|
|
255
|
+
const result = (0, bundle_install_1.installBundle)({
|
|
256
|
+
bundleName: 'base',
|
|
257
|
+
bundles: [base],
|
|
258
|
+
agents: ['opencode', 'codex'],
|
|
259
|
+
method: 'symlink',
|
|
260
|
+
projectRoot,
|
|
261
|
+
contentDir: content,
|
|
262
|
+
});
|
|
263
|
+
const skillTarget = path_1.default.join(projectRoot, '.agents/skills/s-base');
|
|
264
|
+
expect(fs_1.default.existsSync(skillTarget)).toBe(true);
|
|
265
|
+
// One physical write for the shared target...
|
|
266
|
+
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
|
267
|
+
replaceSpy.mockRestore();
|
|
268
|
+
// ...but the real installBundle/applyInstallPlan result must report
|
|
269
|
+
// BOTH providers as owning the artifact, not just the one whose
|
|
270
|
+
// selection triggered the write (install-planner.ts tags the other as
|
|
271
|
+
// 'retain', which applyInstallPlan must still surface).
|
|
272
|
+
const installedForSkill = result.installed.filter((line) => line.startsWith('s-base → '));
|
|
273
|
+
expect(installedForSkill).toEqual(expect.arrayContaining(['s-base → opencode', 's-base → codex']));
|
|
274
|
+
expect(result.modifiedFiles).toContain(skillTarget);
|
|
275
|
+
});
|
|
96
276
|
});
|
|
97
277
|
describe('addBundle', () => {
|
|
278
|
+
// KNOWN RED (Task 6): addBundle's `recordedExtension` detection greps
|
|
279
|
+
// `summary.installed` lines for a `[bundleName]` suffix to tell "this
|
|
280
|
+
// bundle's own artifacts installed" apart from "only a dependency
|
|
281
|
+
// installed". ArtifactIntent (Task 5's contract, see install-planner.ts)
|
|
282
|
+
// does not carry bundle provenance, so that suffix can no longer be
|
|
283
|
+
// produced anywhere in the pipeline — recordedExtension is always null
|
|
284
|
+
// now. Left unfixed deliberately: fixing it means either adding bundle
|
|
285
|
+
// provenance to ArtifactIntent/ManagedArtifactRecord or having addBundle
|
|
286
|
+
// compare against the named bundle's own artifact list directly, both of
|
|
287
|
+
// which are Task 6-shaped decisions (it owns applyInstallPlan's real
|
|
288
|
+
// return contract). Flagged prominently in this task's final report.
|
|
98
289
|
it('records a project bundle installed locally as an extension + gitignores symlinks', () => {
|
|
99
290
|
const { content, projectRoot, bundles } = makeFixture();
|
|
100
291
|
const result = (0, bundle_install_1.addBundle)({
|
|
101
292
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
102
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
293
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
103
294
|
});
|
|
104
295
|
expect(result.recordedExtension).toBe('ext');
|
|
105
296
|
expect((0, profile_1.readProfile)(projectRoot).extensions).toEqual(['ext']);
|
|
@@ -110,7 +301,7 @@ describe('addBundle', () => {
|
|
|
110
301
|
const { content, projectRoot, bundles } = makeFixture();
|
|
111
302
|
(0, bundle_install_1.addBundle)({
|
|
112
303
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
113
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
304
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
114
305
|
});
|
|
115
306
|
expect((0, profile_1.readProfile)(projectRoot).extensions).toEqual(['ext']); // not ['base','ext']
|
|
116
307
|
});
|
|
@@ -118,7 +309,7 @@ describe('addBundle', () => {
|
|
|
118
309
|
const { content, projectRoot, bundles } = makeFixture();
|
|
119
310
|
const opts = {
|
|
120
311
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
121
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
312
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
122
313
|
};
|
|
123
314
|
(0, bundle_install_1.addBundle)(opts);
|
|
124
315
|
(0, bundle_install_1.addBundle)(opts);
|
|
@@ -131,7 +322,7 @@ describe('addBundle', () => {
|
|
|
131
322
|
fs_1.default.rmSync(path_1.default.join(content, 'agents', 'ag-ext.md'), { force: true });
|
|
132
323
|
const result = (0, bundle_install_1.addBundle)({
|
|
133
324
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
134
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
325
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
135
326
|
});
|
|
136
327
|
expect(result.recordedExtension).toBeNull();
|
|
137
328
|
expect((0, profile_1.readProfile)(projectRoot).extensions).toEqual([]);
|
|
@@ -143,7 +334,7 @@ describe('addBundle', () => {
|
|
|
143
334
|
fs_1.default.rmSync(path_1.default.join(content, 'agents', 'ag-ext.md'), { force: true });
|
|
144
335
|
const result = (0, bundle_install_1.addBundle)({
|
|
145
336
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
146
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
337
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
147
338
|
});
|
|
148
339
|
expect(result.installed.some((l) => l.includes('s-base'))).toBe(true); // dep installed
|
|
149
340
|
expect(result.recordedExtension).toBeNull(); // ext not recorded
|
|
@@ -153,7 +344,7 @@ describe('addBundle', () => {
|
|
|
153
344
|
const { content, projectRoot, bundles } = makeFixture();
|
|
154
345
|
const result = (0, bundle_install_1.addBundle)({
|
|
155
346
|
bundleName: 'ext', bundles, agents: ['claude-code'],
|
|
156
|
-
method: 'symlink', projectRoot, contentDir: content,
|
|
347
|
+
method: 'symlink', projectRoot, contentDir: content, applyPlan: testApplyPlan,
|
|
157
348
|
scopeOverride: 'local',
|
|
158
349
|
});
|
|
159
350
|
expect(result.recordedExtension).toBe('ext');
|
|
@@ -167,7 +358,7 @@ describe('syncProfile', () => {
|
|
|
167
358
|
(0, profile_1.writeProfile)(projectRoot, { extensions: ['ext'] });
|
|
168
359
|
const result = (0, bundle_install_1.syncProfile)({
|
|
169
360
|
projectRoot, bundles, agents: ['claude-code'],
|
|
170
|
-
method: 'symlink', contentDir: content,
|
|
361
|
+
method: 'symlink', contentDir: content, applyPlan: testApplyPlan,
|
|
171
362
|
});
|
|
172
363
|
expect(result.extensions).toEqual(['ext']);
|
|
173
364
|
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.claude', 'skills', 's-ext'))).toBe(true);
|
|
@@ -178,7 +369,7 @@ describe('syncProfile', () => {
|
|
|
178
369
|
(0, profile_1.writeProfile)(projectRoot, { extensions: [] });
|
|
179
370
|
const result = (0, bundle_install_1.syncProfile)({
|
|
180
371
|
projectRoot, bundles, agents: ['claude-code'],
|
|
181
|
-
method: 'symlink', contentDir: content,
|
|
372
|
+
method: 'symlink', contentDir: content, applyPlan: testApplyPlan,
|
|
182
373
|
});
|
|
183
374
|
expect(result.extensions).toEqual([]);
|
|
184
375
|
expect(result.installed).toEqual([]);
|
|
@@ -189,7 +380,7 @@ describe('syncProfile', () => {
|
|
|
189
380
|
(0, profile_1.writeProfile)(projectRoot, { extensions: ['stale-bundle', 'ext'] });
|
|
190
381
|
const result = (0, bundle_install_1.syncProfile)({
|
|
191
382
|
projectRoot, bundles, agents: ['claude-code'],
|
|
192
|
-
method: 'symlink', contentDir: content,
|
|
383
|
+
method: 'symlink', contentDir: content, applyPlan: testApplyPlan,
|
|
193
384
|
});
|
|
194
385
|
expect(result.extensions).toEqual(['stale-bundle', 'ext']);
|
|
195
386
|
expect(result.skipped.some((l) => l.includes('stale-bundle'))).toBe(true);
|
|
@@ -68,6 +68,18 @@ describe('resolveBundleSkills', () => {
|
|
|
68
68
|
expect((0, bundles_1.resolveBundleSkills)('dev', bundles).sort()).toEqual(['architecture-advisor', 'brainstorming']);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
describe('resolveBundleAgents', () => {
|
|
72
|
+
it('follows dependsOn transitively and dedupes, mirroring resolveBundleSkills', () => {
|
|
73
|
+
const content = makeFixture();
|
|
74
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
75
|
+
expect((0, bundles_1.resolveBundleAgents)('frontend', bundles)).toEqual(['development-process']); // via dep on 'dev'
|
|
76
|
+
});
|
|
77
|
+
it('returns own agents when no deps', () => {
|
|
78
|
+
const content = makeFixture();
|
|
79
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
80
|
+
expect((0, bundles_1.resolveBundleAgents)('dev', bundles)).toEqual(['development-process']);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
71
83
|
describe('defaultScopeForBundle', () => {
|
|
72
84
|
it('maps baseline and ambient to global, project to local', () => {
|
|
73
85
|
expect((0, bundles_1.defaultScopeForBundle)('baseline')).toBe('global');
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const managed_block_1 = require("../../../src/core/context/managed-block");
|
|
4
|
+
const body = 'Use AWM through the development-process skill.';
|
|
5
|
+
const malformedBoundaries = [
|
|
6
|
+
`${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=9 suffix=1 -->\nowned\n${managed_block_1.AWM_END}`,
|
|
7
|
+
`${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=1 suffix=2 -->\nowned\n${managed_block_1.AWM_END}`,
|
|
8
|
+
`${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\n<!-- AWM:BOUNDARY prefix=0 suffix=1 -->\nowned\n${managed_block_1.AWM_END}`,
|
|
9
|
+
`${managed_block_1.AWM_START}\nowned\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\n${managed_block_1.AWM_END}`,
|
|
10
|
+
`${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=1\nowned\n${managed_block_1.AWM_END}`,
|
|
11
|
+
`${managed_block_1.AWM_START}\ninline <!-- AWM:BOUNDARY prefix=1 suffix=1 -->\nowned\n${managed_block_1.AWM_END}`,
|
|
12
|
+
`<!-- AWM:BOUNDARY prefix=0 suffix=1 -->\n${managed_block_1.AWM_START}\nowned\n${managed_block_1.AWM_END}`,
|
|
13
|
+
];
|
|
14
|
+
describe('mergeManagedBlock', () => {
|
|
15
|
+
it('appends exactly one block and preserves user content byte-for-byte', () => {
|
|
16
|
+
const original = '# My rules\n\nKeep this.\n';
|
|
17
|
+
const merged = (0, managed_block_1.mergeManagedBlock)(original, body);
|
|
18
|
+
expect(merged).toContain(original);
|
|
19
|
+
expect(merged.match(/<!-- AWM:START -->/g)).toHaveLength(1);
|
|
20
|
+
expect((0, managed_block_1.mergeManagedBlock)(merged, body)).toBe(merged);
|
|
21
|
+
});
|
|
22
|
+
it.each([
|
|
23
|
+
['', `${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=0 suffix=1 -->\n${body}\n${managed_block_1.AWM_END}\n`],
|
|
24
|
+
['user', `user\n\n${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=2 suffix=1 -->\n${body}\n${managed_block_1.AWM_END}\n`],
|
|
25
|
+
['user\n', `user\n\n${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\n${body}\n${managed_block_1.AWM_END}\n`],
|
|
26
|
+
['user\n\n', `user\n\n${managed_block_1.AWM_START}\n<!-- AWM:BOUNDARY prefix=0 suffix=1 -->\n${body}\n${managed_block_1.AWM_END}\n`],
|
|
27
|
+
])('uses a stable separator for original %j', (original, expected) => {
|
|
28
|
+
expect((0, managed_block_1.mergeManagedBlock)(original, body)).toBe(expected);
|
|
29
|
+
});
|
|
30
|
+
it('replaces a valid block while preserving every byte before and after it', () => {
|
|
31
|
+
const before = 'prefix\r\n\r\n';
|
|
32
|
+
const after = '\r\nsuffix\n';
|
|
33
|
+
const original = `${before}${managed_block_1.AWM_START}\nold\n${managed_block_1.AWM_END}${after}`;
|
|
34
|
+
expect((0, managed_block_1.mergeManagedBlock)(original, 'new\n')).toBe(`${before}${managed_block_1.AWM_START}\nnew\n${managed_block_1.AWM_END}${after}`);
|
|
35
|
+
});
|
|
36
|
+
it('normalizes only trailing newlines in the managed body and is idempotent', () => {
|
|
37
|
+
const once = (0, managed_block_1.mergeManagedBlock)('', `${body}\n\n`);
|
|
38
|
+
expect((0, managed_block_1.mergeManagedBlock)(once, `${body}\n`)).toBe(once);
|
|
39
|
+
});
|
|
40
|
+
it.each([
|
|
41
|
+
[`${managed_block_1.AWM_START}\nbody\n`, 'unmatched'],
|
|
42
|
+
[`${managed_block_1.AWM_END}\n`, 'unmatched'],
|
|
43
|
+
[`${managed_block_1.AWM_START}\na\n${managed_block_1.AWM_END}\n${managed_block_1.AWM_START}\nb\n${managed_block_1.AWM_END}`, 'duplicate'],
|
|
44
|
+
[`${managed_block_1.AWM_START}\n${managed_block_1.AWM_START}\n${managed_block_1.AWM_END}\n${managed_block_1.AWM_END}`, 'nested'],
|
|
45
|
+
[`${managed_block_1.AWM_END}\nbody\n${managed_block_1.AWM_START}`, 'reversed'],
|
|
46
|
+
['<!-- AWM:STARTED -->\nuser', 'malformed'],
|
|
47
|
+
['<!-- prefix AWM:END -->\nuser', 'malformed'],
|
|
48
|
+
['<!-- AWM:START', 'incomplete'],
|
|
49
|
+
['<!-- AWM:END', 'incomplete'],
|
|
50
|
+
[`inline ${managed_block_1.AWM_START}\nbody\n${managed_block_1.AWM_END}`, 'standalone'],
|
|
51
|
+
[`\`${managed_block_1.AWM_START}\` and \`${managed_block_1.AWM_END}\``, 'standalone'],
|
|
52
|
+
[`\`\`\`md\n${managed_block_1.AWM_START}\nbody\n${managed_block_1.AWM_END}\n\`\`\``, 'standalone'],
|
|
53
|
+
])('rejects ambiguous markers in %j', (input, message) => {
|
|
54
|
+
expect(() => (0, managed_block_1.mergeManagedBlock)(input, body)).toThrow(message);
|
|
55
|
+
});
|
|
56
|
+
it.each([
|
|
57
|
+
[null, body, 'original'],
|
|
58
|
+
[undefined, body, 'original'],
|
|
59
|
+
[42, body, 'original'],
|
|
60
|
+
['', null, 'body'],
|
|
61
|
+
['', {}, 'body'],
|
|
62
|
+
])('rejects invalid runtime input', (original, managedBody, message) => {
|
|
63
|
+
expect(() => (0, managed_block_1.mergeManagedBlock)(original, managedBody)).toThrow(message);
|
|
64
|
+
});
|
|
65
|
+
it.each(malformedBoundaries)('rejects ambiguous boundary metadata in %j', (original) => {
|
|
66
|
+
expect(() => (0, managed_block_1.mergeManagedBlock)(original, body)).toThrow('malformed AWM boundary metadata');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('removeManagedBlock', () => {
|
|
70
|
+
it.each([
|
|
71
|
+
'',
|
|
72
|
+
'user',
|
|
73
|
+
'user\n',
|
|
74
|
+
'user\n\n',
|
|
75
|
+
'# Rules\n\nKeep every byte.\n',
|
|
76
|
+
])('restores original %j byte-for-byte after merge', (original) => {
|
|
77
|
+
expect((0, managed_block_1.removeManagedBlock)((0, managed_block_1.mergeManagedBlock)(original, body))).toBe(original);
|
|
78
|
+
});
|
|
79
|
+
it('removes only the valid block and preserves every surrounding byte', () => {
|
|
80
|
+
const original = `before\n\n${managed_block_1.AWM_START}\nowned\n${managed_block_1.AWM_END}\n\nafter`;
|
|
81
|
+
expect((0, managed_block_1.removeManagedBlock)(original)).toBe('before\n\n\n\nafter');
|
|
82
|
+
});
|
|
83
|
+
it('is unchanged when no block exists and rejects ambiguous markers', () => {
|
|
84
|
+
expect((0, managed_block_1.removeManagedBlock)('user only\n')).toBe('user only\n');
|
|
85
|
+
expect(() => (0, managed_block_1.removeManagedBlock)(`${managed_block_1.AWM_START}\nmissing end`)).toThrow('unmatched');
|
|
86
|
+
});
|
|
87
|
+
it.each(malformedBoundaries)('rejects ambiguous boundary metadata before removal in %j', (original) => {
|
|
88
|
+
expect(() => (0, managed_block_1.removeManagedBlock)(original)).toThrow('malformed AWM boundary metadata');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -25,7 +25,7 @@ function tmpRegistry() {
|
|
|
25
25
|
}
|
|
26
26
|
describe('InjectionOrchestrator (claude-code dispatch via HookMergeStrategy)', () => {
|
|
27
27
|
const ccOverride = {
|
|
28
|
-
label: 'Claude Code', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
28
|
+
label: 'Claude Code', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
29
29
|
injection: { type: 'cc-settings-merge' },
|
|
30
30
|
hooks: { type: 'cc-settings-merge', settingsPath: '', scriptsDir: '', matcher: '', eventName: '' },
|
|
31
31
|
};
|
|
@@ -61,7 +61,12 @@ describe('InjectionOrchestrator (claude-code dispatch via HookMergeStrategy)', (
|
|
|
61
61
|
expect(state).toBe('injected');
|
|
62
62
|
});
|
|
63
63
|
it('throws when providerOverride has no injection (does not fall through to real agent config)', () => {
|
|
64
|
-
const noInjection = {
|
|
64
|
+
const noInjection = {
|
|
65
|
+
label: 'Test',
|
|
66
|
+
skill: { global: '', local: '', renderer: 'link' },
|
|
67
|
+
workflow: null,
|
|
68
|
+
agent: null,
|
|
69
|
+
};
|
|
65
70
|
const orch = new orchestrator_1.InjectionOrchestrator({ providerOverride: noInjection });
|
|
66
71
|
expect(() => orch.installContext({ agent: 'claude-code', scope: 'global', registryRoot: '/any', installMethod: 'symlink', profileExtensions: [] }))
|
|
67
72
|
.toThrow('no injection mechanism');
|
|
@@ -79,7 +84,7 @@ describe('InjectionOrchestrator (opencode, real strategy)', () => {
|
|
|
79
84
|
registryRoot = tmpRegistry();
|
|
80
85
|
orch = new orchestrator_1.InjectionOrchestrator({
|
|
81
86
|
providerOverride: {
|
|
82
|
-
label: 'OpenCode', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
87
|
+
label: 'OpenCode', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
83
88
|
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
84
89
|
},
|
|
85
90
|
contextPathOverride: absPath,
|
|
@@ -125,3 +130,43 @@ describe('InjectionOrchestrator (opencode, real strategy)', () => {
|
|
|
125
130
|
expect((cfg.instructions ?? []).includes(absPath)).toBe(false);
|
|
126
131
|
});
|
|
127
132
|
});
|
|
133
|
+
describe('InjectionOrchestrator (codex, managed AGENTS.md strategy)', () => {
|
|
134
|
+
let roots;
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
roots = [];
|
|
137
|
+
jest.clearAllMocks();
|
|
138
|
+
});
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
for (const root of roots)
|
|
141
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
it('owns only the managed block and never dispatches to Claude hooks', () => {
|
|
144
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-codex-orch-'));
|
|
145
|
+
const registryRoot = tmpRegistry();
|
|
146
|
+
roots.push(dir, registryRoot);
|
|
147
|
+
const agentsPath = path_1.default.join(dir, 'AGENTS.md');
|
|
148
|
+
const contextPath = path_1.default.join(dir, 'awm-context.md');
|
|
149
|
+
fs_1.default.writeFileSync(agentsPath, '# User rules\n');
|
|
150
|
+
const orch = new orchestrator_1.InjectionOrchestrator({
|
|
151
|
+
providerOverride: {
|
|
152
|
+
label: 'Codex', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
153
|
+
injection: { type: 'managed-agents-md', globalPath: agentsPath, localFile: 'AGENTS.md' },
|
|
154
|
+
},
|
|
155
|
+
contextPathOverride: contextPath,
|
|
156
|
+
});
|
|
157
|
+
const op = {
|
|
158
|
+
agent: 'codex',
|
|
159
|
+
scope: 'global',
|
|
160
|
+
registryRoot,
|
|
161
|
+
installMethod: 'copy',
|
|
162
|
+
profileExtensions: [],
|
|
163
|
+
};
|
|
164
|
+
orch.installContext(op);
|
|
165
|
+
expect(fs_1.default.readFileSync(agentsPath, 'utf8')).toContain('# User rules\n');
|
|
166
|
+
expect(fs_1.default.readFileSync(agentsPath, 'utf8')).toContain('BODY');
|
|
167
|
+
expect(install_1.installHook).not.toHaveBeenCalled();
|
|
168
|
+
expect(orch.contextStatus(op)).toBe('injected');
|
|
169
|
+
orch.uninstallContext(op);
|
|
170
|
+
expect(fs_1.default.readFileSync(agentsPath, 'utf8')).toBe('# User rules\n');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -100,7 +100,7 @@ describe('regenerateGlobalContext', () => {
|
|
|
100
100
|
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
101
101
|
const brokenOrch = new InjectionOrchestrator();
|
|
102
102
|
brokenOrch.contextStatus = () => { throw new Error('unexpected orchestrator error'); };
|
|
103
|
-
expect(regenerateGlobalContext(brokenOrch)).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
103
|
+
expect(regenerateGlobalContext(undefined, brokenOrch)).toEqual([{ agent: 'opencode', action: 'skipped' }]);
|
|
104
104
|
expect(fs_1.default.existsSync(contextPath())).toBe(false); // installContext never called
|
|
105
105
|
});
|
|
106
106
|
});
|