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,257 @@
|
|
|
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
|
+
// tests/core/install-transaction.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 install_transaction_1 = require("../../src/core/install-transaction");
|
|
11
|
+
const artifact_state_1 = require("../../src/core/artifact-state");
|
|
12
|
+
// Per CLAUDE.md: no test may touch the real ~/.awm. Every test here backs up
|
|
13
|
+
// or transacts through awmHome(), so HOME/AWM_HOME must point at an isolated
|
|
14
|
+
// tmpdir (pattern from tests/commands/hooks/install.test.ts).
|
|
15
|
+
let tmpHome;
|
|
16
|
+
let tmpWork;
|
|
17
|
+
let originalHome;
|
|
18
|
+
let originalAwmHome;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-txn-home-'));
|
|
21
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-txn-work-'));
|
|
22
|
+
originalHome = process.env.HOME;
|
|
23
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
29
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
30
|
+
if (originalHome === undefined)
|
|
31
|
+
delete process.env.HOME;
|
|
32
|
+
else
|
|
33
|
+
process.env.HOME = originalHome;
|
|
34
|
+
if (originalAwmHome === undefined)
|
|
35
|
+
delete process.env.AWM_HOME;
|
|
36
|
+
else
|
|
37
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
38
|
+
});
|
|
39
|
+
function makeOp(name, overrides = {}) {
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
type: 'skill',
|
|
43
|
+
scope: 'local',
|
|
44
|
+
targetPath: path_1.default.join(tmpWork, name),
|
|
45
|
+
sourcePath: path_1.default.join(tmpWork, `${name}-source`),
|
|
46
|
+
renderer: 'link',
|
|
47
|
+
owners: ['claude-code'],
|
|
48
|
+
method: 'copy',
|
|
49
|
+
output: 'link',
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function planWithTwoTargets() {
|
|
54
|
+
const a = makeOp('a');
|
|
55
|
+
const b = makeOp('b');
|
|
56
|
+
return { operations: [a, b], records: [], reports: [] };
|
|
57
|
+
}
|
|
58
|
+
function planWithThreeTargets() {
|
|
59
|
+
const a = makeOp('a');
|
|
60
|
+
const b = makeOp('b');
|
|
61
|
+
const c = makeOp('c');
|
|
62
|
+
return { operations: [a, b, c], records: [], reports: [] };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds a plan whose second operation ("b") is guaranteed to be replaced
|
|
66
|
+
* (real filesystem write) before verification runs, letting the test force a
|
|
67
|
+
* verification failure via a `verify` override while still exercising the
|
|
68
|
+
* REAL default backup/stage/replace/rollback pipeline underneath.
|
|
69
|
+
*/
|
|
70
|
+
function planThatFailsSecondVerification(original) {
|
|
71
|
+
const sourceA = path_1.default.join(tmpWork, 'source-a');
|
|
72
|
+
fs_1.default.mkdirSync(sourceA, { recursive: true });
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(sourceA, 'file.txt'), 'new-a');
|
|
74
|
+
const sourceB = path_1.default.join(tmpWork, 'source-b');
|
|
75
|
+
fs_1.default.mkdirSync(sourceB, { recursive: true });
|
|
76
|
+
fs_1.default.writeFileSync(path_1.default.join(sourceB, 'file.txt'), 'new-b');
|
|
77
|
+
const opA = makeOp('a', { targetPath: original, sourcePath: sourceA });
|
|
78
|
+
const opB = makeOp('b', { targetPath: path_1.default.join(tmpWork, 'target-b'), sourcePath: sourceB });
|
|
79
|
+
return { operations: [opA, opB], records: [], reports: [] };
|
|
80
|
+
}
|
|
81
|
+
describe('applyInstallPlan', () => {
|
|
82
|
+
it('validates and backs up every target before the first replacement', () => {
|
|
83
|
+
const calls = [];
|
|
84
|
+
(0, install_transaction_1.applyInstallPlan)(planWithTwoTargets(), {
|
|
85
|
+
validate: (op) => calls.push(`validate:${op.name}`),
|
|
86
|
+
backup: (op) => { calls.push(`backup:${op.name}`); return null; },
|
|
87
|
+
stage: (op) => { calls.push(`stage:${op.name}`); return `/staged/${op.name}`; },
|
|
88
|
+
replace: (op) => calls.push(`replace:${op.name}`),
|
|
89
|
+
verify: (op) => calls.push(`verify:${op.name}`),
|
|
90
|
+
rollback: (op) => calls.push(`rollback:${op.name}`),
|
|
91
|
+
});
|
|
92
|
+
expect(calls.indexOf('validate:a')).toBeLessThan(calls.indexOf('backup:a'));
|
|
93
|
+
expect(calls.indexOf('backup:b')).toBeLessThan(calls.indexOf('replace:a')); // verifies R24.1
|
|
94
|
+
});
|
|
95
|
+
it('restores already replaced targets when verification fails', () => {
|
|
96
|
+
const original = path_1.default.join(tmpWork, 'target-a');
|
|
97
|
+
fs_1.default.mkdirSync(original, { recursive: true });
|
|
98
|
+
fs_1.default.writeFileSync(path_1.default.join(original, 'sentinel'), 'before');
|
|
99
|
+
const plan = planThatFailsSecondVerification(original);
|
|
100
|
+
const deps = {
|
|
101
|
+
...(0, install_transaction_1.defaultTransactionDeps)(),
|
|
102
|
+
verify: (op) => {
|
|
103
|
+
if (op.name === 'b')
|
|
104
|
+
throw new Error('verification failed: forced for test');
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
expect(() => (0, install_transaction_1.applyInstallPlan)(plan, deps)).toThrow('verification failed');
|
|
108
|
+
expect(fs_1.default.readFileSync(path_1.default.join(original, 'sentinel'), 'utf8')).toBe('before'); // verifies R25
|
|
109
|
+
});
|
|
110
|
+
it('returns a transactionId and the list of modified files on success', () => {
|
|
111
|
+
const sourceA = path_1.default.join(tmpWork, 'source-a');
|
|
112
|
+
fs_1.default.mkdirSync(sourceA, { recursive: true });
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(sourceA, 'file.txt'), 'content-a');
|
|
114
|
+
const targetA = path_1.default.join(tmpWork, 'target-a');
|
|
115
|
+
const plan = {
|
|
116
|
+
operations: [makeOp('a', { targetPath: targetA, sourcePath: sourceA })],
|
|
117
|
+
records: [],
|
|
118
|
+
reports: [{ owner: 'claude-code', targetPath: targetA, action: 'install' }],
|
|
119
|
+
};
|
|
120
|
+
const summary = (0, install_transaction_1.applyInstallPlan)(plan);
|
|
121
|
+
expect(summary.transactionId).toBeTruthy();
|
|
122
|
+
expect(summary.modifiedFiles).toEqual([targetA]);
|
|
123
|
+
expect(fs_1.default.existsSync(path_1.default.join(targetA, 'file.txt'))).toBe(true);
|
|
124
|
+
const backupDir = path_1.default.join(process.env.AWM_HOME, 'backups', summary.transactionId);
|
|
125
|
+
expect(fs_1.default.existsSync(path_1.default.join(backupDir, 'manifest.json'))).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
it('is a safe no-op for an empty plan', () => {
|
|
128
|
+
const summary = (0, install_transaction_1.applyInstallPlan)({ operations: [], records: [], reports: [] });
|
|
129
|
+
expect(summary.installed).toEqual([]);
|
|
130
|
+
expect(summary.modifiedFiles).toEqual([]);
|
|
131
|
+
expect(summary.transactionId).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
it('keeps rolling back remaining targets, and still surfaces the original error, when a MIDDLE rollback fails', () => {
|
|
134
|
+
// 3 ops (a, b, c) all replaced; verification fails on 'c', so rollback
|
|
135
|
+
// runs in reverse order: c, b, a. Making 'b' — the middle op in that
|
|
136
|
+
// reversed order, with an op both before and after it in the loop —
|
|
137
|
+
// the one that throws is what actually proves the per-op try/catch
|
|
138
|
+
// isolates failures: 'a' still runs AFTER 'b' throws, not just
|
|
139
|
+
// trivially "nothing left to run" as a last-in-order failure would.
|
|
140
|
+
const calls = [];
|
|
141
|
+
const plan = planWithThreeTargets();
|
|
142
|
+
expect(() => (0, install_transaction_1.applyInstallPlan)(plan, {
|
|
143
|
+
validate: () => { },
|
|
144
|
+
backup: () => null,
|
|
145
|
+
stage: (op) => `/staged/${op.name}`,
|
|
146
|
+
replace: (op) => calls.push(`replace:${op.name}`),
|
|
147
|
+
verify: (op) => { if (op.name === 'c')
|
|
148
|
+
throw new Error('verification failed: forced'); },
|
|
149
|
+
rollback: (op) => {
|
|
150
|
+
calls.push(`rollback:${op.name}`);
|
|
151
|
+
if (op.name === 'b')
|
|
152
|
+
throw new Error('rollback failed for b (simulated)');
|
|
153
|
+
},
|
|
154
|
+
})).toThrow('verification failed: forced');
|
|
155
|
+
// All three replaced ops got a rollback attempt, in reverse order —
|
|
156
|
+
// including 'a', which runs strictly after 'b' throws, proving the
|
|
157
|
+
// loop doesn't stop or skip ahead when a non-terminal rollback fails.
|
|
158
|
+
expect(calls).toEqual(['replace:a', 'replace:b', 'replace:c', 'rollback:c', 'rollback:b', 'rollback:a']);
|
|
159
|
+
});
|
|
160
|
+
describe('artifact-state persistence across multiple calls (BLOCKER regression)', () => {
|
|
161
|
+
// A real `awm init` run makes several separate applyInstallPlan calls
|
|
162
|
+
// in sequence — one per bundle (dev-core, each ambient bundle, each
|
|
163
|
+
// synced project extension). Each call's plan.records only covers the
|
|
164
|
+
// artifacts THAT call touched. Persisting must be additive: an
|
|
165
|
+
// earlier call's ownership records must survive a later call, not be
|
|
166
|
+
// silently discarded by a wholesale overwrite of state/artifacts.json.
|
|
167
|
+
function sourceFor(name) {
|
|
168
|
+
const src = path_1.default.join(tmpWork, `${name}-source`);
|
|
169
|
+
fs_1.default.mkdirSync(src, { recursive: true });
|
|
170
|
+
fs_1.default.writeFileSync(path_1.default.join(src, 'file.txt'), name);
|
|
171
|
+
return src;
|
|
172
|
+
}
|
|
173
|
+
function planFor(name, targetPath) {
|
|
174
|
+
const op = makeOp(name, { targetPath, sourcePath: sourceFor(name) });
|
|
175
|
+
return { operations: [op], records: [op], reports: [{ owner: 'claude-code', targetPath, action: 'install' }] };
|
|
176
|
+
}
|
|
177
|
+
it('preserves an earlier applyInstallPlan call’s records after a later call for a DIFFERENT target', () => {
|
|
178
|
+
const targetA = path_1.default.join(tmpWork, 'target-a');
|
|
179
|
+
const targetB = path_1.default.join(tmpWork, 'target-b');
|
|
180
|
+
(0, install_transaction_1.applyInstallPlan)(planFor('a', targetA));
|
|
181
|
+
(0, install_transaction_1.applyInstallPlan)(planFor('b', targetB));
|
|
182
|
+
const state = (0, artifact_state_1.readArtifactState)();
|
|
183
|
+
expect(state.map((r) => r.targetPath).sort()).toEqual([targetA, targetB].sort());
|
|
184
|
+
});
|
|
185
|
+
it('replaces (does not duplicate) the record for a targetPath touched by two separate applyInstallPlan calls', () => {
|
|
186
|
+
const targetA = path_1.default.join(tmpWork, 'target-a');
|
|
187
|
+
(0, install_transaction_1.applyInstallPlan)(planFor('a', targetA));
|
|
188
|
+
const secondOp = makeOp('a', { targetPath: targetA, sourcePath: sourceFor('a2'), owners: ['claude-code', 'codex'] });
|
|
189
|
+
(0, install_transaction_1.applyInstallPlan)({
|
|
190
|
+
operations: [secondOp],
|
|
191
|
+
records: [secondOp],
|
|
192
|
+
reports: [{ owner: 'codex', targetPath: targetA, action: 'retain' }],
|
|
193
|
+
});
|
|
194
|
+
const state = (0, artifact_state_1.readArtifactState)();
|
|
195
|
+
expect(state).toHaveLength(1);
|
|
196
|
+
expect(state[0].targetPath).toBe(targetA);
|
|
197
|
+
expect(state[0].owners).toEqual(['claude-code', 'codex']);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe('beginBackupSession / restoreBackup', () => {
|
|
202
|
+
it('backs up existing targets before mutation and restores them on rollback', () => {
|
|
203
|
+
const fileA = path_1.default.join(tmpWork, 'a.json');
|
|
204
|
+
const fileB = path_1.default.join(tmpWork, 'b.json');
|
|
205
|
+
fs_1.default.writeFileSync(fileA, 'A-before');
|
|
206
|
+
fs_1.default.writeFileSync(fileB, 'B-before');
|
|
207
|
+
const session = (0, install_transaction_1.beginBackupSession)([fileA, fileB]);
|
|
208
|
+
fs_1.default.writeFileSync(fileA, 'A-after');
|
|
209
|
+
fs_1.default.writeFileSync(fileB, 'B-after');
|
|
210
|
+
session.rollback();
|
|
211
|
+
expect(fs_1.default.readFileSync(fileA, 'utf8')).toBe('A-before');
|
|
212
|
+
expect(fs_1.default.readFileSync(fileB, 'utf8')).toBe('B-before');
|
|
213
|
+
});
|
|
214
|
+
it('removes a target that did not exist before the session, on rollback', () => {
|
|
215
|
+
const newFile = path_1.default.join(tmpWork, 'created.json');
|
|
216
|
+
const session = (0, install_transaction_1.beginBackupSession)([newFile]);
|
|
217
|
+
fs_1.default.writeFileSync(newFile, 'created content');
|
|
218
|
+
session.rollback();
|
|
219
|
+
expect(fs_1.default.existsSync(newFile)).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
it('commit marks the manifest committed without restoring anything', () => {
|
|
222
|
+
const fileA = path_1.default.join(tmpWork, 'c.json');
|
|
223
|
+
fs_1.default.writeFileSync(fileA, 'C-before');
|
|
224
|
+
const session = (0, install_transaction_1.beginBackupSession)([fileA]);
|
|
225
|
+
fs_1.default.writeFileSync(fileA, 'C-after');
|
|
226
|
+
session.commit();
|
|
227
|
+
expect(fs_1.default.readFileSync(fileA, 'utf8')).toBe('C-after');
|
|
228
|
+
const manifestPath = path_1.default.join(process.env.AWM_HOME, 'backups', session.transactionId, 'manifest.json');
|
|
229
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
230
|
+
expect(manifest.committed).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
it('refuses to back up a filesystem root', () => {
|
|
233
|
+
expect(() => (0, install_transaction_1.beginBackupSession)(['/'])).toThrow('refusing to back up a filesystem root');
|
|
234
|
+
});
|
|
235
|
+
it('creates the backup directory and manifest with restrictive permissions, no content leaked', () => {
|
|
236
|
+
const fileA = path_1.default.join(tmpWork, 'perm.json');
|
|
237
|
+
fs_1.default.writeFileSync(fileA, 'secret-content');
|
|
238
|
+
const session = (0, install_transaction_1.beginBackupSession)([fileA]);
|
|
239
|
+
const backupDir = path_1.default.join(process.env.AWM_HOME, 'backups', session.transactionId);
|
|
240
|
+
const dirMode = fs_1.default.statSync(backupDir).mode & 0o777;
|
|
241
|
+
const manifestPath = path_1.default.join(backupDir, 'manifest.json');
|
|
242
|
+
const manifestMode = fs_1.default.statSync(manifestPath).mode & 0o777;
|
|
243
|
+
expect(dirMode).toBe(0o700);
|
|
244
|
+
expect(manifestMode).toBe(0o600);
|
|
245
|
+
const manifestRaw = fs_1.default.readFileSync(manifestPath, 'utf8');
|
|
246
|
+
expect(manifestRaw).not.toContain('secret-content');
|
|
247
|
+
});
|
|
248
|
+
it('restoreBackup restores exactly the manifest targets', () => {
|
|
249
|
+
const fileA = path_1.default.join(tmpWork, 'd.json');
|
|
250
|
+
fs_1.default.writeFileSync(fileA, 'D-before');
|
|
251
|
+
const session = (0, install_transaction_1.beginBackupSession)([fileA]);
|
|
252
|
+
fs_1.default.writeFileSync(fileA, 'D-after');
|
|
253
|
+
const result = (0, install_transaction_1.restoreBackup)(session.transactionId);
|
|
254
|
+
expect(result.restored).toEqual([fileA]);
|
|
255
|
+
expect(fs_1.default.readFileSync(fileA, 'utf8')).toBe('D-before');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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 provider_artifacts_1 = require("../../src/core/provider-artifacts");
|
|
10
|
+
describe('preflightLinkArtifactPairs', () => {
|
|
11
|
+
it('rejects a mixed Codex selection before any installer call', () => {
|
|
12
|
+
const install = jest.fn();
|
|
13
|
+
expect(() => {
|
|
14
|
+
const pairs = [
|
|
15
|
+
{ agent: 'codex', artifact: { type: 'skill', name: 'linked-skill' } },
|
|
16
|
+
{ agent: 'codex', artifact: { type: 'agent', name: 'toml-agent' } },
|
|
17
|
+
];
|
|
18
|
+
(0, provider_artifacts_1.preflightLinkArtifactPairs)(pairs);
|
|
19
|
+
for (const item of pairs)
|
|
20
|
+
install(item);
|
|
21
|
+
}).toThrow("Renderer 'codex-agent-toml' for codex agent artifacts is not implemented yet");
|
|
22
|
+
expect(install).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('scanLegacyArtifacts', () => {
|
|
26
|
+
const originalHome = process.env.HOME;
|
|
27
|
+
const originalAwmHome = process.env.AWM_HOME;
|
|
28
|
+
let tmpHome;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-artifacts-'));
|
|
31
|
+
process.env.HOME = tmpHome;
|
|
32
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (originalHome === undefined)
|
|
36
|
+
delete process.env.HOME;
|
|
37
|
+
else
|
|
38
|
+
process.env.HOME = originalHome;
|
|
39
|
+
if (originalAwmHome === undefined)
|
|
40
|
+
delete process.env.AWM_HOME;
|
|
41
|
+
else
|
|
42
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
43
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
it('never lists Codex TOML agents for legacy removal', () => {
|
|
46
|
+
const skillPath = path_1.default.join(tmpHome, '.agents/skills/linked-skill');
|
|
47
|
+
const agentPath = path_1.default.join(tmpHome, '.codex/agents/protected.toml');
|
|
48
|
+
fs_1.default.mkdirSync(skillPath, { recursive: true });
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(agentPath), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(agentPath, 'name = "protected"\n');
|
|
51
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['codex'], 'global');
|
|
52
|
+
const legacyRemove = jest.fn((target) => fs_1.default.rmSync(target, {
|
|
53
|
+
recursive: true,
|
|
54
|
+
force: true,
|
|
55
|
+
}));
|
|
56
|
+
for (const artifact of listed) {
|
|
57
|
+
for (const fullPath of artifact.fullPaths)
|
|
58
|
+
legacyRemove(fullPath);
|
|
59
|
+
}
|
|
60
|
+
expect(listed.map((artifact) => artifact.name)).toEqual(['linked-skill']);
|
|
61
|
+
expect(legacyRemove).not.toHaveBeenCalledWith(agentPath);
|
|
62
|
+
expect(fs_1.default.existsSync(agentPath)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const provider_version_1 = require("../../src/core/provider-version");
|
|
4
|
+
describe('assertProviderSupported', () => {
|
|
5
|
+
it('accepts the current stable Codex line', () => {
|
|
6
|
+
const exec = jest.fn(() => Buffer.from('codex-cli 0.145.0\n'));
|
|
7
|
+
expect((0, provider_version_1.assertProviderSupported)('codex', exec)).toEqual({
|
|
8
|
+
provider: 'codex',
|
|
9
|
+
version: '0.145.0',
|
|
10
|
+
});
|
|
11
|
+
expect(exec).toHaveBeenCalledWith('codex', ['--version'], expect.any(Object));
|
|
12
|
+
});
|
|
13
|
+
it.each(['0.145.1', '0.146.0', '1.0.0'])('accepts stable Codex version %s above the minimum', (version) => {
|
|
14
|
+
expect((0, provider_version_1.assertProviderSupported)('codex', () => Buffer.from(`codex-cli ${version}\n`))).toEqual({ provider: 'codex', version });
|
|
15
|
+
});
|
|
16
|
+
it.each([
|
|
17
|
+
[Buffer.from('codex-cli 0.144.9\n'), 'requires Codex >= 0.145.0'],
|
|
18
|
+
[Buffer.from('unknown\n'), 'could not parse Codex version'],
|
|
19
|
+
[Buffer.from('codex-cli 0.145.0-beta.1\n'), 'could not parse Codex version'],
|
|
20
|
+
[Buffer.from('codex-cli v0.145.0\n'), 'could not parse Codex version'],
|
|
21
|
+
[Buffer.from('codex-cli 0.145\n'), 'could not parse Codex version'],
|
|
22
|
+
[Buffer.from('unexpected-tool 9.9.9\n'), 'could not parse Codex version'],
|
|
23
|
+
[Buffer.from('notice: codex-cli 0.145.0\n'), 'could not parse Codex version'],
|
|
24
|
+
[Buffer.from('codex-cli stable 0.145.0\n'), 'could not parse Codex version'],
|
|
25
|
+
[Buffer.from('codex-cli 0.145.0 trailing\n'), 'could not parse Codex version'],
|
|
26
|
+
])('rejects unsupported output without mutation', (output, message) => {
|
|
27
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('codex', () => output)).toThrow(message);
|
|
28
|
+
});
|
|
29
|
+
it('returns a null version for providers without a version gate', () => {
|
|
30
|
+
const exec = jest.fn();
|
|
31
|
+
expect((0, provider_version_1.assertProviderSupported)('claude-code', exec)).toEqual({
|
|
32
|
+
provider: 'claude-code',
|
|
33
|
+
version: null,
|
|
34
|
+
});
|
|
35
|
+
expect(exec).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it('reports a missing Codex binary distinctly', () => {
|
|
38
|
+
const missing = Object.assign(new Error('spawnSync codex ENOENT'), { code: 'ENOENT' });
|
|
39
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('codex', () => { throw missing; }))
|
|
40
|
+
.toThrow('Codex is not installed or not available on PATH');
|
|
41
|
+
});
|
|
42
|
+
it('reports other Codex probe failures distinctly', () => {
|
|
43
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('codex', () => {
|
|
44
|
+
throw new Error('permission denied');
|
|
45
|
+
})).toThrow('Codex version probe failed: permission denied');
|
|
46
|
+
});
|
|
47
|
+
it('fails loudly for invalid runtime provider input', () => {
|
|
48
|
+
const exec = jest.fn();
|
|
49
|
+
expect(() => (0, provider_version_1.assertProviderSupported)('invalid', exec))
|
|
50
|
+
.toThrow('Unknown agent target');
|
|
51
|
+
expect(exec).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
// tests/core/reconciliation.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Exercises the REAL planReconciliation (no mocks) end-to-end: seeds a real
|
|
9
|
+
// registry content root with a baseline bundle, an ambient bundle, and a
|
|
10
|
+
// project-scope bundle, then verifies the computed plan only reconciles the
|
|
11
|
+
// machine-scope (baseline + ambient) artifacts — and, applied via the real
|
|
12
|
+
// applyInstallPlan, actually fixes both a MISSING target and a STALE target.
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
function writeBundle(contentRoot, name, scope, skills, dependsOn = []) {
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'bundles', name), { recursive: true });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'bundles', name, 'bundle.json'), JSON.stringify({
|
|
19
|
+
name, version: '1.0.0', scope, dependsOn,
|
|
20
|
+
skills: skills.map((s) => ({ name: s })), workflows: [], agents: [],
|
|
21
|
+
}));
|
|
22
|
+
for (const skill of skills) {
|
|
23
|
+
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', skill), { recursive: true });
|
|
24
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', skill, 'SKILL.md'), `# ${skill}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function addToCatalog(contentRoot, name, scope) {
|
|
28
|
+
const catalogFile = path_1.default.join(contentRoot, 'catalog.json');
|
|
29
|
+
const catalog = JSON.parse(fs_1.default.readFileSync(catalogFile, 'utf-8'));
|
|
30
|
+
catalog.bundles.push({ name, source: `./bundles/${name}`, version: '1.0.0', scope });
|
|
31
|
+
fs_1.default.writeFileSync(catalogFile, JSON.stringify(catalog));
|
|
32
|
+
}
|
|
33
|
+
describe('planReconciliation (real, unmocked)', () => {
|
|
34
|
+
let tmpHome;
|
|
35
|
+
let contentRoot;
|
|
36
|
+
let originalHome;
|
|
37
|
+
let originalAwmHome;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-reconcile-'));
|
|
40
|
+
originalHome = process.env.HOME;
|
|
41
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
42
|
+
process.env.HOME = tmpHome;
|
|
43
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
44
|
+
jest.resetModules();
|
|
45
|
+
contentRoot = path_1.default.join(tmpHome, 'registry');
|
|
46
|
+
fs_1.default.mkdirSync(contentRoot, { recursive: true });
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
|
|
48
|
+
version: 1,
|
|
49
|
+
bundles: [
|
|
50
|
+
{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' },
|
|
51
|
+
{ name: 'ambient-x', source: './bundles/ambient-x', version: '1.0.0', scope: 'ambient' },
|
|
52
|
+
{ name: 'proj-y', source: './bundles/proj-y', version: '1.0.0', scope: 'project' },
|
|
53
|
+
],
|
|
54
|
+
}));
|
|
55
|
+
writeBundle(contentRoot, 'dev', 'baseline', ['brainstorming']);
|
|
56
|
+
writeBundle(contentRoot, 'ambient-x', 'ambient', ['ambient-skill']);
|
|
57
|
+
writeBundle(contentRoot, 'proj-y', 'project', ['proj-skill']);
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
61
|
+
if (originalHome === undefined)
|
|
62
|
+
delete process.env.HOME;
|
|
63
|
+
else
|
|
64
|
+
process.env.HOME = originalHome;
|
|
65
|
+
if (originalAwmHome === undefined)
|
|
66
|
+
delete process.env.AWM_HOME;
|
|
67
|
+
else
|
|
68
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
69
|
+
});
|
|
70
|
+
it('plans only the machine-scope (baseline + ambient) artifacts, excluding project-scope bundles', () => {
|
|
71
|
+
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
72
|
+
const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
73
|
+
const names = plan.operations.map((op) => op.name).sort();
|
|
74
|
+
expect(names).toEqual(['ambient-skill', 'brainstorming']); // proj-skill excluded (R — project bundles are `awm sync`'s job, not `awm update`'s)
|
|
75
|
+
const claudeSkillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
76
|
+
const targets = plan.operations.map((op) => op.targetPath).sort();
|
|
77
|
+
expect(targets).toEqual([
|
|
78
|
+
path_1.default.join(claudeSkillsDir, 'ambient-skill'),
|
|
79
|
+
path_1.default.join(claudeSkillsDir, 'brainstorming'),
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
it('reconciles drift end-to-end when applied: installs a MISSING target and replaces a STALE one', () => {
|
|
83
|
+
const claudeSkillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
84
|
+
fs_1.default.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
85
|
+
// Drift scenario 1 — stale: 'brainstorming' exists but as a plain file
|
|
86
|
+
// with the wrong content (as if it were copied once and the registry
|
|
87
|
+
// has since diverged), not a symlink to the registry source.
|
|
88
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeSkillsDir, 'brainstorming'), 'stale, wrong content');
|
|
89
|
+
// Drift scenario 2 — missing: 'ambient-skill' was never installed at all.
|
|
90
|
+
expect(fs_1.default.existsSync(path_1.default.join(claudeSkillsDir, 'ambient-skill'))).toBe(false);
|
|
91
|
+
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
92
|
+
const { applyInstallPlan } = require('../../src/core/install-transaction');
|
|
93
|
+
const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
94
|
+
const summary = applyInstallPlan(plan);
|
|
95
|
+
expect(summary.installed.sort()).toEqual([
|
|
96
|
+
'ambient-skill → claude-code',
|
|
97
|
+
'brainstorming → claude-code',
|
|
98
|
+
]);
|
|
99
|
+
// Stale target is now a real symlink to the registry source, not the old file.
|
|
100
|
+
const brainstormingStat = fs_1.default.lstatSync(path_1.default.join(claudeSkillsDir, 'brainstorming'));
|
|
101
|
+
expect(brainstormingStat.isSymbolicLink()).toBe(true);
|
|
102
|
+
expect(fs_1.default.realpathSync(path_1.default.join(claudeSkillsDir, 'brainstorming')))
|
|
103
|
+
.toBe(fs_1.default.realpathSync(path_1.default.join(contentRoot, 'skills', 'brainstorming')));
|
|
104
|
+
// Missing target now exists as a symlink too.
|
|
105
|
+
const ambientStat = fs_1.default.lstatSync(path_1.default.join(claudeSkillsDir, 'ambient-skill'));
|
|
106
|
+
expect(ambientStat.isSymbolicLink()).toBe(true);
|
|
107
|
+
expect(fs_1.default.realpathSync(path_1.default.join(claudeSkillsDir, 'ambient-skill')))
|
|
108
|
+
.toBe(fs_1.default.realpathSync(path_1.default.join(contentRoot, 'skills', 'ambient-skill')));
|
|
109
|
+
});
|
|
110
|
+
it('scopes to the selected agent target only', () => {
|
|
111
|
+
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
112
|
+
const plan = planReconciliation({ targets: ['opencode'], roots: [contentRoot] });
|
|
113
|
+
const owners = new Set(plan.operations.flatMap((op) => op.owners));
|
|
114
|
+
expect(owners).toEqual(new Set(['opencode']));
|
|
115
|
+
});
|
|
116
|
+
// NOTE on the two tests below: `resolveBundleClosure` (bundles.ts) already
|
|
117
|
+
// treats a `dependsOn` entry naming a bundle that isn't in the catalog as
|
|
118
|
+
// silently absent from the closure — it does NOT throw (verified directly:
|
|
119
|
+
// a baseline bundle with `dependsOn: ['does-not-exist']` resolves its own
|
|
120
|
+
// artifacts fine; the ghost name is just dropped). So a plain dangling
|
|
121
|
+
// `dependsOn` string never reaches `planReconciliation`'s
|
|
122
|
+
// `try { expandBundleArtifacts(...) } catch { continue; }` guard at all —
|
|
123
|
+
// the first test below confirms that directly. To actually force the catch
|
|
124
|
+
// branch (and check it skips only the one broken bundle, not the whole
|
|
125
|
+
// batch, and doesn't silently eat some other bug), the second test injects
|
|
126
|
+
// a bundle whose `dependsOn` field is malformed in a way that genuinely
|
|
127
|
+
// breaks closure resolution (`dependsOn` is not an array, so
|
|
128
|
+
// `resolveBundleClosure`'s `for (const dep of b.dependsOn)` throws) —
|
|
129
|
+
// mirroring the kind of real, unresolvable-closure error the catch is
|
|
130
|
+
// meant to guard against.
|
|
131
|
+
it('tolerates a plain dangling dependsOn reference without needing the catch — the bundle installs normally', () => {
|
|
132
|
+
writeBundle(contentRoot, 'dangling-dep', 'baseline', ['dangling-skill'], ['does-not-exist']);
|
|
133
|
+
addToCatalog(contentRoot, 'dangling-dep', 'baseline');
|
|
134
|
+
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
135
|
+
const plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
136
|
+
const names = plan.operations.map((op) => op.name).sort();
|
|
137
|
+
// 'dangling-skill' installs fine even though its bundle's dependsOn
|
|
138
|
+
// points nowhere — resolveBundleClosure just drops the missing dep.
|
|
139
|
+
expect(names).toEqual(['ambient-skill', 'brainstorming', 'dangling-skill']);
|
|
140
|
+
});
|
|
141
|
+
it('skips a bundle whose closure genuinely fails to resolve, without aborting reconciliation for the rest', () => {
|
|
142
|
+
// Malformed dependsOn (not an array) makes resolveBundleClosure throw
|
|
143
|
+
// when it tries to iterate it — a real "unresolvable closure" error,
|
|
144
|
+
// unlike a plain dangling name reference (see test above).
|
|
145
|
+
writeBundle(contentRoot, 'broken', 'baseline', ['broken-skill'], 5);
|
|
146
|
+
addToCatalog(contentRoot, 'broken', 'baseline');
|
|
147
|
+
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
148
|
+
let plan;
|
|
149
|
+
expect(() => {
|
|
150
|
+
plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
151
|
+
}).not.toThrow();
|
|
152
|
+
// The healthy bundles (baseline 'dev' + ambient 'ambient-x') still
|
|
153
|
+
// reconcile correctly.
|
|
154
|
+
const names = plan.operations.map((op) => op.name).sort();
|
|
155
|
+
expect(names).toEqual(['ambient-skill', 'brainstorming']);
|
|
156
|
+
// The broken bundle is cleanly absent — no partial/broken entry for
|
|
157
|
+
// 'broken-skill' or any operation touching it.
|
|
158
|
+
expect(names).not.toContain('broken-skill');
|
|
159
|
+
expect(plan.operations.some((op) => op.sourcePath?.includes('broken'))).toBe(false);
|
|
160
|
+
// Applying the plan end-to-end still works (no dangling reference to
|
|
161
|
+
// the broken bundle leaked into the plan).
|
|
162
|
+
const { applyInstallPlan } = require('../../src/core/install-transaction');
|
|
163
|
+
expect(() => applyInstallPlan(plan)).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -93,7 +93,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
93
93
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
94
94
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
95
95
|
fs_1.default.mkdirSync(awmDir, { recursive: true });
|
|
96
|
-
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '9.9.9' } }));
|
|
96
|
+
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '9.9.9' } }));
|
|
97
97
|
const results = await m.syncRegistries();
|
|
98
98
|
expect(results[0].action).toBe('error');
|
|
99
99
|
// El dir no debe quedar en disco tras el fallo
|
|
@@ -110,7 +110,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
110
110
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
111
111
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
112
112
|
fs_1.default.mkdirSync(awmDir, { recursive: true });
|
|
113
|
-
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
114
114
|
const results = await m.syncRegistries();
|
|
115
115
|
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
|
|
116
116
|
});
|
|
@@ -174,7 +174,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
174
174
|
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0' }]);
|
|
175
175
|
// establece pin a la versión anterior
|
|
176
176
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
177
|
-
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
177
|
+
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
178
178
|
// segunda sync con pin: retrocede a v1.0.0
|
|
179
179
|
const r2 = await m.syncRegistries();
|
|
180
180
|
expect(r2).toEqual([{ name: 'personal', action: 'pulled', version: 'v1.0.0' }]);
|
|
@@ -188,7 +188,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
188
188
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
189
189
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
190
190
|
fs_1.default.mkdirSync(awmDir, { recursive: true });
|
|
191
|
-
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
|
|
191
|
+
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
|
|
192
192
|
// primera sync en canal dev: queda en HEAD (no en el tag)
|
|
193
193
|
const r1 = await m.syncRegistries();
|
|
194
194
|
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const canonical_agent_1 = require("../../../src/core/renderers/canonical-agent");
|
|
4
|
+
// Real canonical agent content from the awm-baseline-registry (registries/baseline/agents/development-process.md),
|
|
5
|
+
// which the installer materializes at ~/.awm/registries/baseline/agents/development-process.md.
|
|
6
|
+
const canonicalWithModePrimary = `---
|
|
7
|
+
name: development-process
|
|
8
|
+
description: Use as agent profile to orchestrate the development lifecycle - invokes the development-process skill which contains the full orchestration logic
|
|
9
|
+
mode: primary
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Development Process Orchestrator
|
|
13
|
+
|
|
14
|
+
You are a development orchestrator. You do NOT write code directly.
|
|
15
|
+
|
|
16
|
+
## On Every Conversation Start
|
|
17
|
+
|
|
18
|
+
1. **Invoke the \`development-process\` skill.** This skill contains the complete orchestration logic: state detection, lifecycle phases, decision rules, and the full catalog of available skills.
|
|
19
|
+
2. Follow the skill's instructions exactly - it will guide you through identifying project state, recommending the next phase, and delegating to the correct skill.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
- NEVER start writing code without first invoking \`development-process\`
|
|
24
|
+
- NEVER duplicate orchestration logic here - the skill is the single source of truth
|
|
25
|
+
- NEVER invoke a downstream skill without user approval
|
|
26
|
+
`;
|
|
27
|
+
it('parses name, description, and instructions from valid frontmatter', () => {
|
|
28
|
+
const source = `---
|
|
29
|
+
name: my-agent
|
|
30
|
+
description: Does a thing
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Body text here.
|
|
34
|
+
`;
|
|
35
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
|
|
36
|
+
name: 'my-agent',
|
|
37
|
+
description: 'Does a thing',
|
|
38
|
+
instructions: 'Body text here.',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('ignores provider-only mode while retaining canonical instructions', () => {
|
|
42
|
+
const parsed = (0, canonical_agent_1.parseCanonicalAgent)(canonicalWithModePrimary);
|
|
43
|
+
expect(parsed).toEqual({
|
|
44
|
+
name: 'development-process',
|
|
45
|
+
description: 'Use as agent profile to orchestrate the development lifecycle - invokes the development-process skill which contains the full orchestration logic',
|
|
46
|
+
instructions: expect.stringContaining('You do NOT write code directly.'),
|
|
47
|
+
}); // verifies R8, R9
|
|
48
|
+
});
|
|
49
|
+
it.each([
|
|
50
|
+
['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
|
|
51
|
+
['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
|
|
52
|
+
['---\nname: ok\ndescription: x\n---\n', 'non-empty instruction body'],
|
|
53
|
+
['name: ok', 'frontmatter'],
|
|
54
|
+
['---\nname: ok\nnot a field\n---\nbody', 'invalid canonical agent frontmatter line'],
|
|
55
|
+
['---\nname: ok\ndescription: first\ndescription: second\n---\nbody', 'duplicate canonical agent frontmatter key: description'],
|
|
56
|
+
])('rejects invalid canonical agent sources', (source, message) => {
|
|
57
|
+
expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow(message); // verifies R17
|
|
58
|
+
});
|