agentic-workflow-manager 3.1.0 → 3.2.1
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/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/executor.js +30 -9
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +179 -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 +97 -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/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 +3 -2
- 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 +177 -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 +235 -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,235 @@
|
|
|
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
|
+
// Direct, focused tests for planInitMutationTargets (mutation-targets.ts).
|
|
7
|
+
//
|
|
8
|
+
// Previously this module was only exercised indirectly via
|
|
9
|
+
// tests/commands/init.test.ts's broader init-flow tests, which happen to hit
|
|
10
|
+
// the hooks.settingsPath branch but not the ambient/baseline/project-extension
|
|
11
|
+
// branches, nor the Finding-1 fix (global skills directory) directly.
|
|
12
|
+
//
|
|
13
|
+
// registries.ts computes AWM_HOME at module-require time (not call time), so
|
|
14
|
+
// every test here sets HOME/AWM_HOME BEFORE requiring
|
|
15
|
+
// core/init/mutation-targets.ts, and calls jest.resetModules() first — the
|
|
16
|
+
// same pattern used throughout tests/commands/hooks/install.test.ts and
|
|
17
|
+
// tests/core/skill-integrity.test.ts. Per CLAUDE.md, no test may touch the
|
|
18
|
+
// real ~/.awm.
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
function bundle(name, scope, skills) {
|
|
23
|
+
return {
|
|
24
|
+
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
25
|
+
dependsOn: [], skills: skills.map((s) => ({ name: s, onSignal: false })),
|
|
26
|
+
workflows: [], agents: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let tmpHome;
|
|
30
|
+
let originalHome;
|
|
31
|
+
let originalAwmHome;
|
|
32
|
+
let extraDirs;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-'));
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
37
|
+
process.env.HOME = tmpHome;
|
|
38
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
39
|
+
jest.resetModules();
|
|
40
|
+
extraDirs = [];
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
|
+
for (const dir of extraDirs)
|
|
45
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
if (originalHome === undefined)
|
|
47
|
+
delete process.env.HOME;
|
|
48
|
+
else
|
|
49
|
+
process.env.HOME = originalHome;
|
|
50
|
+
if (originalAwmHome === undefined)
|
|
51
|
+
delete process.env.AWM_HOME;
|
|
52
|
+
else
|
|
53
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Loads a fresh copy of the module under test (and providerFor, which needs
|
|
57
|
+
* the same env-var-driven paths) AFTER env vars are set for this test.
|
|
58
|
+
*/
|
|
59
|
+
function load() {
|
|
60
|
+
const mutationTargets = require('../../../src/core/init/mutation-targets');
|
|
61
|
+
const providers = require('../../../src/providers');
|
|
62
|
+
return {
|
|
63
|
+
planInitMutationTargets: mutationTargets.planInitMutationTargets,
|
|
64
|
+
providerFor: providers.providerFor,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** A cwd guaranteed to have no project-root marker (.git/package.json/.awm) in its ancestry. */
|
|
68
|
+
function bareCwd() {
|
|
69
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-cwd-'));
|
|
70
|
+
extraDirs.push(dir);
|
|
71
|
+
return dir;
|
|
72
|
+
}
|
|
73
|
+
describe('planInitMutationTargets', () => {
|
|
74
|
+
it('always includes preferences.json and the artifact-state file', () => {
|
|
75
|
+
const { planInitMutationTargets } = load();
|
|
76
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
77
|
+
expect(targets).toContain(path_1.default.join(tmpHome, '.awm', 'preferences.json'));
|
|
78
|
+
expect(targets).toContain(path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'));
|
|
79
|
+
});
|
|
80
|
+
describe('hooks', () => {
|
|
81
|
+
it('includes hooks.settingsPath and hooks.scriptsDir for an agent with a hook config (claude-code)', () => {
|
|
82
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
83
|
+
const provider = providerFor('claude-code');
|
|
84
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
85
|
+
expect(provider.hooks).toBeDefined();
|
|
86
|
+
expect(targets).toContain(provider.hooks.settingsPath);
|
|
87
|
+
expect(targets).toContain(provider.hooks.scriptsDir);
|
|
88
|
+
});
|
|
89
|
+
it('includes hooks.json + scriptsDir for codex (its own hook config shape)', () => {
|
|
90
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
91
|
+
const provider = providerFor('codex');
|
|
92
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'codex', bundles: [] });
|
|
93
|
+
expect(targets).toContain(provider.hooks.settingsPath);
|
|
94
|
+
expect(targets).toContain(provider.hooks.scriptsDir);
|
|
95
|
+
});
|
|
96
|
+
it('includes no hook paths for an agent with no hook config (antigravity)', () => {
|
|
97
|
+
const { planInitMutationTargets } = load();
|
|
98
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'antigravity', bundles: [] });
|
|
99
|
+
// antigravity has neither hooks nor injection — its only entries are
|
|
100
|
+
// the two always-present machine paths.
|
|
101
|
+
expect(targets).toEqual([
|
|
102
|
+
path_1.default.join(tmpHome, '.awm', 'preferences.json'),
|
|
103
|
+
path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'),
|
|
104
|
+
path_1.default.join(tmpHome, '.gemini', 'antigravity', 'skills'),
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('injection', () => {
|
|
109
|
+
it('includes the config-instructions path for opencode', () => {
|
|
110
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
111
|
+
const provider = providerFor('opencode');
|
|
112
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'opencode', bundles: [] });
|
|
113
|
+
expect(provider.injection?.type).toBe('config-instructions');
|
|
114
|
+
expect(targets).toContain(provider.injection.configPath);
|
|
115
|
+
});
|
|
116
|
+
it('includes the managed-agents-md global path for codex', () => {
|
|
117
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
118
|
+
const provider = providerFor('codex');
|
|
119
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'codex', bundles: [] });
|
|
120
|
+
expect(provider.injection?.type).toBe('managed-agents-md');
|
|
121
|
+
expect(targets).toContain(provider.injection.globalPath);
|
|
122
|
+
});
|
|
123
|
+
it('adds no separate injection path for claude-code (cc-settings-merge is covered by the hook)', () => {
|
|
124
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
125
|
+
const provider = providerFor('claude-code');
|
|
126
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
127
|
+
expect(provider.injection?.type).toBe('cc-settings-merge');
|
|
128
|
+
// Every target should be explainable by preferences/state/hooks/skills-dir —
|
|
129
|
+
// none of them should be a bespoke injection config file.
|
|
130
|
+
expect(targets).toEqual([
|
|
131
|
+
path_1.default.join(tmpHome, '.awm', 'preferences.json'),
|
|
132
|
+
path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'),
|
|
133
|
+
provider.hooks.settingsPath,
|
|
134
|
+
provider.hooks.scriptsDir,
|
|
135
|
+
provider.skill.global,
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('machine-level bundle targets (baseline + ambient)', () => {
|
|
140
|
+
it('includes physical skill targets for baseline and ambient bundles, but not a project-scope bundle', () => {
|
|
141
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
142
|
+
const bundles = [
|
|
143
|
+
bundle('dev-core', 'baseline', ['using-awm']),
|
|
144
|
+
bundle('ambient-pack', 'ambient', ['ambient-skill']),
|
|
145
|
+
bundle('project-only', 'project', ['project-skill']),
|
|
146
|
+
];
|
|
147
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
148
|
+
const skillsDir = providerFor('claude-code').skill.global;
|
|
149
|
+
expect(targets).toContain(path_1.default.join(skillsDir, 'using-awm'));
|
|
150
|
+
expect(targets).toContain(path_1.default.join(skillsDir, 'ambient-skill'));
|
|
151
|
+
// project-scope bundles are only enumerated via .awm/profile.json
|
|
152
|
+
// extensions below, never from the machine-level baseline/ambient pass.
|
|
153
|
+
expect(targets).not.toContain(path_1.default.join(skillsDir, 'project-skill'));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('project-level targets', () => {
|
|
157
|
+
function makeProjectRoot() {
|
|
158
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-project-'));
|
|
159
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git')); // project-root marker for findProjectRoot
|
|
160
|
+
extraDirs.push(root);
|
|
161
|
+
return root;
|
|
162
|
+
}
|
|
163
|
+
it('includes profile.json, sensors.json, and extension bundle targets when a project root is found', () => {
|
|
164
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
165
|
+
const projectRoot = makeProjectRoot();
|
|
166
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
167
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['frontend'] }));
|
|
168
|
+
const bundles = [bundle('frontend', 'project', ['frontend-skill'])];
|
|
169
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'claude-code', bundles });
|
|
170
|
+
expect(targets).toContain(path_1.default.join(projectRoot, '.awm', 'profile.json'));
|
|
171
|
+
expect(targets).toContain(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
|
|
172
|
+
// frontend is a project-scope bundle -> installs locally under the project root
|
|
173
|
+
const localSkillsDir = path_1.default.join(projectRoot, providerFor('claude-code').skill.local);
|
|
174
|
+
expect(targets).toContain(path_1.default.join(localSkillsDir, 'frontend-skill'));
|
|
175
|
+
});
|
|
176
|
+
it('includes the project injection file for config-instructions agents (opencode)', () => {
|
|
177
|
+
const { planInitMutationTargets } = load();
|
|
178
|
+
const projectRoot = makeProjectRoot();
|
|
179
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'opencode', bundles: [] });
|
|
180
|
+
expect(targets).toContain(path_1.default.join(projectRoot, 'opencode.json'));
|
|
181
|
+
});
|
|
182
|
+
it('includes the project injection file (AGENTS.md) for managed-agents-md agents (codex)', () => {
|
|
183
|
+
const { planInitMutationTargets } = load();
|
|
184
|
+
const projectRoot = makeProjectRoot();
|
|
185
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'codex', bundles: [] });
|
|
186
|
+
expect(targets).toContain(path_1.default.join(projectRoot, 'AGENTS.md'));
|
|
187
|
+
});
|
|
188
|
+
it('omits every project-level target when no project root is found', () => {
|
|
189
|
+
const { planInitMutationTargets } = load();
|
|
190
|
+
const cwd = bareCwd(); // no .git/package.json/.awm ancestor
|
|
191
|
+
const targets = planInitMutationTargets({ cwd, agent: 'claude-code', bundles: [] });
|
|
192
|
+
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'profile.json')))).toBe(false);
|
|
193
|
+
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'sensors.json')))).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
it('ignores an extension named in the profile that no longer resolves to a known bundle', () => {
|
|
196
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
197
|
+
const projectRoot = makeProjectRoot();
|
|
198
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['stale-bundle'] }));
|
|
200
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'claude-code', bundles: [] });
|
|
201
|
+
const localSkillsDir = path_1.default.join(projectRoot, providerFor('claude-code').skill.local);
|
|
202
|
+
// No target should be produced under the local skills dir for a bundle
|
|
203
|
+
// that isn't in the (empty) known-bundles list passed in.
|
|
204
|
+
expect(targets.some((t) => t.startsWith(localSkillsDir))).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('Finding 1 fix — global skills directory itself', () => {
|
|
208
|
+
it('includes providerFor(agent).skill.global for every agent, independent of the bundle catalog', () => {
|
|
209
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
210
|
+
for (const agent of ['claude-code', 'codex', 'opencode', 'antigravity']) {
|
|
211
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent, bundles: [] });
|
|
212
|
+
expect(targets).toContain(providerFor(agent).skill.global);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('the skills-directory target covers orphaned entries repairGlobalSkills would mutate, not just bundle-derived subpaths', () => {
|
|
216
|
+
// Regression guard for the logic gap: before the fix, only
|
|
217
|
+
// bundle-DERIVED skill paths (e.g. .../skills/using-awm) were
|
|
218
|
+
// enumerated. An orphaned symlink like .../skills/some-old-skill
|
|
219
|
+
// (belonging to no currently-known bundle — the exact condition
|
|
220
|
+
// that makes repairGlobalSkills classify it as repairable/dead)
|
|
221
|
+
// would never appear in the bundle-driven enumeration. The parent
|
|
222
|
+
// directory itself must be present so beginBackupSession snapshots
|
|
223
|
+
// the whole tree.
|
|
224
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
225
|
+
const bundles = [bundle('dev-core', 'baseline', ['using-awm'])];
|
|
226
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
227
|
+
const skillsDir = providerFor('claude-code').skill.global;
|
|
228
|
+
expect(targets).toContain(skillsDir);
|
|
229
|
+
// Sanity: the orphan path itself is a child of the now-covered dir,
|
|
230
|
+
// even though it is never separately enumerated.
|
|
231
|
+
const orphanPath = path_1.default.join(skillsDir, 'some-old-orphaned-skill');
|
|
232
|
+
expect(orphanPath.startsWith(skillsDir)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -74,7 +74,7 @@ describe('runInitSteps — orchestrator', () => {
|
|
|
74
74
|
const contentDir = roots[0] ?? '';
|
|
75
75
|
const ctx = gatherContext({ cwd, bundles });
|
|
76
76
|
return {
|
|
77
|
-
cwd, ctx, bundles, agent: 'claude-code', installMethod: 'symlink',
|
|
77
|
+
cwd, ctx, bundles, agent: 'claude-code', enabledAgents: ['claude-code'], installMethod: 'symlink',
|
|
78
78
|
registryRoot: contentRoot, contentDir, sensorPacksRoot: contentRoot,
|
|
79
79
|
confirmExtensions: async (p) => p,
|
|
80
80
|
// syncCache es no-op: el cache ya está sembrado en disco
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
describe('gatherProviderFacts / assertClaudeBaselinePreserved', () => {
|
|
10
|
+
let tmpHome;
|
|
11
|
+
let originalHome;
|
|
12
|
+
let originalAwmHome;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-facts-'));
|
|
15
|
+
originalHome = process.env.HOME;
|
|
16
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
17
|
+
process.env.HOME = tmpHome;
|
|
18
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
19
|
+
jest.resetModules();
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
23
|
+
if (originalHome === undefined)
|
|
24
|
+
delete process.env.HOME;
|
|
25
|
+
else
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
if (originalAwmHome === undefined)
|
|
28
|
+
delete process.env.AWM_HOME;
|
|
29
|
+
else
|
|
30
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
31
|
+
});
|
|
32
|
+
it('two snapshots of untouched state are identical (no false positive)', () => {
|
|
33
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
34
|
+
const before = gatherProviderFacts('claude-code');
|
|
35
|
+
const after = gatherProviderFacts('claude-code');
|
|
36
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).not.toThrow();
|
|
37
|
+
expect(before.hash).toBe(after.hash);
|
|
38
|
+
});
|
|
39
|
+
it('detects a top-level file change under a managed directory', () => {
|
|
40
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
41
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
42
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
43
|
+
fs_1.default.writeFileSync(path_1.default.join(skillsDir, 'SKILL.md'), 'original');
|
|
44
|
+
const before = gatherProviderFacts('claude-code');
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(skillsDir, 'SKILL.md'), 'mutated');
|
|
46
|
+
const after = gatherProviderFacts('claude-code');
|
|
47
|
+
expect(before.hash).not.toBe(after.hash);
|
|
48
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).toThrow(/must never happen \(R19\)/);
|
|
49
|
+
});
|
|
50
|
+
it('detects a content-only change two levels deep under a managed directory', () => {
|
|
51
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
52
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
53
|
+
// .claude/skills/some-skill/nested/deep.md — two directory levels below skills/
|
|
54
|
+
const nested = path_1.default.join(skillsDir, 'some-skill', 'nested');
|
|
55
|
+
fs_1.default.mkdirSync(nested, { recursive: true });
|
|
56
|
+
const deepFile = path_1.default.join(nested, 'deep.md');
|
|
57
|
+
fs_1.default.writeFileSync(deepFile, 'original deep content');
|
|
58
|
+
const before = gatherProviderFacts('claude-code');
|
|
59
|
+
// Content-only mutation — no rename, no size-preserving trick needed;
|
|
60
|
+
// this alone would have been invisible to a top-level-only stat digest.
|
|
61
|
+
fs_1.default.writeFileSync(deepFile, 'mutated deep content');
|
|
62
|
+
const after = gatherProviderFacts('claude-code');
|
|
63
|
+
expect(before.hash).not.toBe(after.hash);
|
|
64
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).toThrow(/must never happen \(R19\)/);
|
|
65
|
+
});
|
|
66
|
+
it('detects an added file three levels deep without touching any existing file', () => {
|
|
67
|
+
const { gatherProviderFacts } = require('../../../src/core/init/provider-facts');
|
|
68
|
+
const skillsDir = path_1.default.join(tmpHome, '.claude', 'skills');
|
|
69
|
+
const nested = path_1.default.join(skillsDir, 'pkg', 'a', 'b');
|
|
70
|
+
fs_1.default.mkdirSync(nested, { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(nested, 'existing.md'), 'unchanged');
|
|
72
|
+
const before = gatherProviderFacts('claude-code');
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(nested, 'new-file.md'), 'new');
|
|
74
|
+
const after = gatherProviderFacts('claude-code');
|
|
75
|
+
expect(before.hash).not.toBe(after.hash);
|
|
76
|
+
});
|
|
77
|
+
it('throws a distinct message when comparing facts for mismatched agents', () => {
|
|
78
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
79
|
+
const claude = gatherProviderFacts('claude-code');
|
|
80
|
+
const codex = gatherProviderFacts('codex');
|
|
81
|
+
expect(() => assertClaudeBaselinePreserved(claude, codex)).toThrow('mismatched agents');
|
|
82
|
+
});
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
// Nested-scriptsDir exclusion (Task 9 fix) — Codex's hook scriptsDir
|
|
85
|
+
// (~/.awm/hooks/codex) is physically nested inside Claude's own hook
|
|
86
|
+
// scriptsDir (~/.awm/hooks — providers/index.ts). A naive recursive hash
|
|
87
|
+
// of Claude's scriptsDir would pick up every Codex hook write as a
|
|
88
|
+
// "Claude baseline changed" false positive. These two tests pin that
|
|
89
|
+
// exclusion directly, rather than relying only on the E2E's indirect
|
|
90
|
+
// before/after tree-snapshot check.
|
|
91
|
+
// -----------------------------------------------------------------------
|
|
92
|
+
it('does NOT flag a write inside Codex\'s nested scriptsDir as a Claude baseline change', () => {
|
|
93
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
94
|
+
// Seed Claude's own scriptsDir first (mirrors a real prior Claude hook install).
|
|
95
|
+
const claudeScripts = path_1.default.join(tmpHome, '.awm', 'hooks');
|
|
96
|
+
fs_1.default.mkdirSync(claudeScripts, { recursive: true });
|
|
97
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'session-start'), '#!/bin/sh\n');
|
|
98
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'run-hook.cmd'), '#!/bin/sh\n');
|
|
99
|
+
const before = gatherProviderFacts('claude-code');
|
|
100
|
+
// Codex's own scriptsDir is nested one level inside Claude's — simulate a
|
|
101
|
+
// real Codex hook install writing there for the first time.
|
|
102
|
+
const codexScripts = path_1.default.join(tmpHome, '.awm', 'hooks', 'codex');
|
|
103
|
+
fs_1.default.mkdirSync(codexScripts, { recursive: true });
|
|
104
|
+
fs_1.default.writeFileSync(path_1.default.join(codexScripts, 'session-start'), '#!/bin/sh\necho codex\n');
|
|
105
|
+
const afterAdd = gatherProviderFacts('claude-code');
|
|
106
|
+
expect(afterAdd.hash).toBe(before.hash);
|
|
107
|
+
expect(() => assertClaudeBaselinePreserved(before, afterAdd)).not.toThrow();
|
|
108
|
+
// Modifying the Codex-owned nested file's content must also stay invisible.
|
|
109
|
+
fs_1.default.writeFileSync(path_1.default.join(codexScripts, 'session-start'), '#!/bin/sh\necho codex v2\n');
|
|
110
|
+
const afterModify = gatherProviderFacts('claude-code');
|
|
111
|
+
expect(afterModify.hash).toBe(before.hash);
|
|
112
|
+
expect(() => assertClaudeBaselinePreserved(before, afterModify)).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
// Codex-only bootstrap: Claude's scriptsDir has NEVER existed.
|
|
116
|
+
//
|
|
117
|
+
// Every nested-exclusion test above seeds ~/.awm/hooks first, so they only
|
|
118
|
+
// ever exercise the present→present transition. On a fresh Codex-only
|
|
119
|
+
// machine (a cloud box, the actual rollout target) that directory does not
|
|
120
|
+
// exist, and installing the Codex hook creates it as a side effect of
|
|
121
|
+
// `mkdirSync(dirname, {recursive:true})` in syncExecutable — flipping
|
|
122
|
+
// Claude's own scriptsDir from absent to present-but-empty. That is not a
|
|
123
|
+
// change to anything Claude owns, but it moved the hash and aborted init.
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
it('does NOT flag Codex creating its nested scriptsDir when Claude\'s has never existed', () => {
|
|
126
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
127
|
+
const claudeScripts = path_1.default.join(tmpHome, '.awm', 'hooks');
|
|
128
|
+
expect(fs_1.default.existsSync(claudeScripts)).toBe(false);
|
|
129
|
+
const before = gatherProviderFacts('claude-code');
|
|
130
|
+
// Exactly what syncExecutable does for the Codex hook: one recursive
|
|
131
|
+
// mkdir that necessarily materializes Claude's hooks/ as the parent.
|
|
132
|
+
const codexScripts = path_1.default.join(claudeScripts, 'codex');
|
|
133
|
+
fs_1.default.mkdirSync(codexScripts, { recursive: true });
|
|
134
|
+
fs_1.default.writeFileSync(path_1.default.join(codexScripts, 'session-start'), '#!/bin/sh\necho codex\n');
|
|
135
|
+
const after = gatherProviderFacts('claude-code');
|
|
136
|
+
expect(after.hash).toBe(before.hash);
|
|
137
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
it('still flags real Claude content appearing where the directory did not exist', () => {
|
|
140
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
141
|
+
const before = gatherProviderFacts('claude-code');
|
|
142
|
+
// The guard must not be relaxed into blindness: an absent directory
|
|
143
|
+
// gaining Claude-owned content is still a genuine baseline change.
|
|
144
|
+
const claudeScripts = path_1.default.join(tmpHome, '.awm', 'hooks');
|
|
145
|
+
fs_1.default.mkdirSync(claudeScripts, { recursive: true });
|
|
146
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'session-start'), '#!/bin/sh\n');
|
|
147
|
+
const after = gatherProviderFacts('claude-code');
|
|
148
|
+
expect(after.hash).not.toBe(before.hash);
|
|
149
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).toThrow(/must never happen \(R19\)/);
|
|
150
|
+
});
|
|
151
|
+
it('still flags Claude content being emptied out of an existing directory', () => {
|
|
152
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
153
|
+
const claudeScripts = path_1.default.join(tmpHome, '.awm', 'hooks');
|
|
154
|
+
fs_1.default.mkdirSync(claudeScripts, { recursive: true });
|
|
155
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'session-start'), '#!/bin/sh\n');
|
|
156
|
+
const before = gatherProviderFacts('claude-code');
|
|
157
|
+
fs_1.default.rmSync(path_1.default.join(claudeScripts, 'session-start'));
|
|
158
|
+
const after = gatherProviderFacts('claude-code');
|
|
159
|
+
expect(after.hash).not.toBe(before.hash);
|
|
160
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).toThrow(/must never happen \(R19\)/);
|
|
161
|
+
});
|
|
162
|
+
it('still flags a genuinely Claude-owned change alongside an untouched Codex nested dir', () => {
|
|
163
|
+
const { gatherProviderFacts, assertClaudeBaselinePreserved } = require('../../../src/core/init/provider-facts');
|
|
164
|
+
const claudeScripts = path_1.default.join(tmpHome, '.awm', 'hooks');
|
|
165
|
+
const codexScripts = path_1.default.join(claudeScripts, 'codex');
|
|
166
|
+
fs_1.default.mkdirSync(codexScripts, { recursive: true });
|
|
167
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'session-start'), '#!/bin/sh\n');
|
|
168
|
+
fs_1.default.writeFileSync(path_1.default.join(codexScripts, 'session-start'), '#!/bin/sh\necho codex\n');
|
|
169
|
+
const before = gatherProviderFacts('claude-code');
|
|
170
|
+
// A genuinely Claude-owned file (a sibling of the codex/ subdir, not inside it)
|
|
171
|
+
// changes — the exclusion must not swallow this.
|
|
172
|
+
fs_1.default.writeFileSync(path_1.default.join(claudeScripts, 'session-start'), '#!/bin/sh\necho mutated claude script\n');
|
|
173
|
+
const after = gatherProviderFacts('claude-code');
|
|
174
|
+
expect(after.hash).not.toBe(before.hash);
|
|
175
|
+
expect(() => assertClaudeBaselinePreserved(before, after)).toThrow(/must never happen \(R19\)/);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -55,7 +55,7 @@ function spies() {
|
|
|
55
55
|
function deps(ctx, actions, over = {}) {
|
|
56
56
|
return {
|
|
57
57
|
cwd: '/repo', ctx, bundles: [bundle('dev', 'baseline', ['brainstorming'])],
|
|
58
|
-
agent: 'claude-code', installMethod: 'symlink',
|
|
58
|
+
agent: 'claude-code', enabledAgents: ['claude-code'], installMethod: 'symlink',
|
|
59
59
|
registryRoot: '/cache', contentDir: '/cache/registry', sensorPacksRoot: '/cache/registry',
|
|
60
60
|
confirmExtensions: async (p) => p, actions, ...over,
|
|
61
61
|
};
|
|
@@ -111,6 +111,22 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
|
|
|
111
111
|
expect((0, steps_1.stepHook)(deps({ machine: m, project: null }, a)).action).toBe('applied');
|
|
112
112
|
expect(a.installHook).toHaveBeenCalled();
|
|
113
113
|
});
|
|
114
|
+
// Regression: a real (unstubbed) `awm init --agent opencode` used to throw
|
|
115
|
+
// "hooks not supported for agent target: opencode" from installHook,
|
|
116
|
+
// because stepHook never checked whether the target agent has a hook
|
|
117
|
+
// mechanism at all before calling installHook (providers/index.ts:
|
|
118
|
+
// OpenCode/Antigravity have no `hooks` config). Found while building the
|
|
119
|
+
// real Codex+OpenCode coexistence E2E test — mirrors provider-checks.ts's
|
|
120
|
+
// `hookTrustCheck`, which already treats a missing `hooks` config as "not
|
|
121
|
+
// applicable" rather than a failure.
|
|
122
|
+
it('hook skips (not calls installHook) for an agent with no hook mechanism', () => {
|
|
123
|
+
const a = spies();
|
|
124
|
+
const m = machine();
|
|
125
|
+
m.hook = { present: false };
|
|
126
|
+
const r = (0, steps_1.stepHook)(deps({ machine: m, project: null }, a, { agent: 'opencode', enabledAgents: ['opencode'] }));
|
|
127
|
+
expect(r.action).toBe('skipped');
|
|
128
|
+
expect(a.installHook).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
114
130
|
it('devCore installs baseline when links broken', () => {
|
|
115
131
|
const a = spies();
|
|
116
132
|
const m = machine();
|
|
@@ -137,6 +153,56 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
|
|
|
137
153
|
const a = spies();
|
|
138
154
|
expect((0, steps_1.stepAmbient)(deps({ machine: machine(), project: null }, a)).action).toBe('skipped');
|
|
139
155
|
});
|
|
156
|
+
// Regression coverage for the BLOCKER found in post-implementation QA:
|
|
157
|
+
// stepDevCore/stepAmbient used to pass a `[d.agent]` singleton as
|
|
158
|
+
// `agents`, which install-planner.ts's `assertCompleteSharedGroup` (R14)
|
|
159
|
+
// refuses whenever a co-owner sharing the same physical skill directory
|
|
160
|
+
// (OpenCode and Codex both resolve to ~/.agents/skills) is independently
|
|
161
|
+
// enabled — making `awm init --agent codex` structurally fail once
|
|
162
|
+
// OpenCode was already enabled, and vice versa.
|
|
163
|
+
it('devCore includes a co-enabled agent sharing the same skill target (codex + opencode)', () => {
|
|
164
|
+
const a = spies();
|
|
165
|
+
const m = machine();
|
|
166
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
167
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
|
|
168
|
+
agent: 'codex', enabledAgents: ['claude-code', 'opencode', 'codex'],
|
|
169
|
+
}));
|
|
170
|
+
expect(r.action).toBe('applied');
|
|
171
|
+
expect(a.installBundle).toHaveBeenCalledWith(expect.objectContaining({ agents: expect.arrayContaining(['codex', 'opencode']) }));
|
|
172
|
+
const call = a.installBundle.mock.calls[0][0];
|
|
173
|
+
expect(call.agents).toHaveLength(2); // claude-code is NOT included — it doesn't share the skill target
|
|
174
|
+
});
|
|
175
|
+
it('devCore does not add a co-owner that is not currently enabled', () => {
|
|
176
|
+
const a = spies();
|
|
177
|
+
const m = machine();
|
|
178
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
179
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
|
|
180
|
+
agent: 'codex', enabledAgents: ['claude-code', 'codex'],
|
|
181
|
+
}));
|
|
182
|
+
expect(r.action).toBe('applied');
|
|
183
|
+
expect(a.installBundle).toHaveBeenCalledWith(expect.objectContaining({ agents: ['codex'] }));
|
|
184
|
+
});
|
|
185
|
+
it('devCore never adds a co-owner for claude-code (its skill dir is never shared)', () => {
|
|
186
|
+
const a = spies();
|
|
187
|
+
const m = machine();
|
|
188
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
189
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
|
|
190
|
+
agent: 'claude-code', enabledAgents: ['claude-code', 'opencode', 'codex'],
|
|
191
|
+
}));
|
|
192
|
+
expect(r.action).toBe('applied');
|
|
193
|
+
expect(a.installBundle).toHaveBeenCalledWith(expect.objectContaining({ agents: ['claude-code'] }));
|
|
194
|
+
});
|
|
195
|
+
it('ambient includes a co-enabled agent sharing the same skill target (codex + opencode)', () => {
|
|
196
|
+
const a = spies();
|
|
197
|
+
const m = machine();
|
|
198
|
+
m.ambient = { wanted: ['docs'], installed: [] };
|
|
199
|
+
const r = (0, steps_1.stepAmbient)(deps({ machine: m, project: null }, a, {
|
|
200
|
+
agent: 'opencode', enabledAgents: ['claude-code', 'opencode', 'codex'],
|
|
201
|
+
}));
|
|
202
|
+
expect(r.action).toBe('applied');
|
|
203
|
+
const call = a.installBundle.mock.calls[0][0];
|
|
204
|
+
expect(call.agents.sort()).toEqual(['codex', 'opencode']);
|
|
205
|
+
});
|
|
140
206
|
});
|
|
141
207
|
describe('stepProfile', () => {
|
|
142
208
|
it('adds confirmed extension when detector finds a match', async () => {
|
|
@@ -237,6 +303,31 @@ describe('stepActivation', () => {
|
|
|
237
303
|
(0, steps_1.stepActivation)(deps(ctx, a, { agent: 'opencode' }));
|
|
238
304
|
expect(a.gatherProject).toHaveBeenCalledWith(expect.any(String), expect.any(Array), 'opencode');
|
|
239
305
|
});
|
|
306
|
+
// Same class of bug as stepDevCore/stepAmbient's shared-group regression
|
|
307
|
+
// above, in the LOCAL-scope (project) code path: OpenCode and Codex share
|
|
308
|
+
// both their global AND local skill directories, so a project extension
|
|
309
|
+
// with a skill artifact hits the identical R14 refusal whenever
|
|
310
|
+
// syncProfile is called with a `[d.agent]` singleton and a co-owner is
|
|
311
|
+
// independently enabled.
|
|
312
|
+
it('activation includes a co-enabled agent sharing the same LOCAL skill target (codex + opencode)', () => {
|
|
313
|
+
const a = spies();
|
|
314
|
+
a.gatherProject = jest.fn((_cwd, _bundles) => project({ activeBundles: { expected: ['x'], linked: [], broken: [] } }));
|
|
315
|
+
const r = (0, steps_1.stepActivation)(deps({ machine: machine(), project: project() }, a, {
|
|
316
|
+
agent: 'codex', enabledAgents: ['claude-code', 'opencode', 'codex'],
|
|
317
|
+
}));
|
|
318
|
+
expect(r.action).toBe('applied');
|
|
319
|
+
const call = a.syncProfile.mock.calls[0][0];
|
|
320
|
+
expect(call.agents.sort()).toEqual(['codex', 'opencode']);
|
|
321
|
+
});
|
|
322
|
+
it('activation does not add a co-owner that is not currently enabled', () => {
|
|
323
|
+
const a = spies();
|
|
324
|
+
a.gatherProject = jest.fn((_cwd, _bundles) => project({ activeBundles: { expected: ['x'], linked: [], broken: [] } }));
|
|
325
|
+
const r = (0, steps_1.stepActivation)(deps({ machine: machine(), project: project() }, a, {
|
|
326
|
+
agent: 'codex', enabledAgents: ['claude-code', 'codex'],
|
|
327
|
+
}));
|
|
328
|
+
expect(r.action).toBe('applied');
|
|
329
|
+
expect(a.syncProfile).toHaveBeenCalledWith(expect.objectContaining({ agents: ['codex'] }));
|
|
330
|
+
});
|
|
240
331
|
});
|
|
241
332
|
describe('stepSensors', () => {
|
|
242
333
|
it('skips when sensors present', () => {
|
|
@@ -296,7 +387,7 @@ describe('stepGlobalSkillsRepair', () => {
|
|
|
296
387
|
m.globalSkills = { valid: [], repairable: ['b'], dead: [] };
|
|
297
388
|
const r = (0, steps_1.stepGlobalSkillsRepair)(deps({ machine: m, project: null }, a, { agent: 'opencode' }));
|
|
298
389
|
expect(r.action).toBe('applied');
|
|
299
|
-
expect(a.repairGlobalSkills).toHaveBeenCalledWith(providers_1.
|
|
390
|
+
expect(a.repairGlobalSkills).toHaveBeenCalledWith((0, providers_1.providerFor)('opencode').skill.global, expect.any(Array));
|
|
300
391
|
});
|
|
301
392
|
});
|
|
302
393
|
describe('stepConstitutionInjection (#6)', () => {
|
|
@@ -318,6 +409,12 @@ describe('stepConstitutionInjection (#6)', () => {
|
|
|
318
409
|
expect(r.action).toBe('skipped');
|
|
319
410
|
expect(a.injectProjectConstitution).not.toHaveBeenCalled();
|
|
320
411
|
});
|
|
412
|
+
it('installs Codex project guidance before CONSTITUTION.md exists', () => {
|
|
413
|
+
const a = spies();
|
|
414
|
+
const r = (0, steps_1.stepConstitutionInjection)(deps({ machine: machine(), project: project({ constitution: { present: false } }) }, a, { agent: 'codex' }));
|
|
415
|
+
expect(r.action).toBe('applied');
|
|
416
|
+
expect(a.injectProjectConstitution).toHaveBeenCalledWith({ projectRoot: '/repo', agent: 'codex' });
|
|
417
|
+
});
|
|
321
418
|
it('maps already → skipped when CONSTITUTION.md was already in opencode.json', () => {
|
|
322
419
|
const a = spies();
|
|
323
420
|
a.injectProjectConstitution.mockReturnValue('already');
|
|
@@ -360,4 +457,11 @@ describe('stepContextInjection', () => {
|
|
|
360
457
|
expect(r.action).toBe('applied');
|
|
361
458
|
expect(a.installContext).toHaveBeenCalled();
|
|
362
459
|
});
|
|
460
|
+
it('installs the Codex global managed block when absent', () => {
|
|
461
|
+
const a = spies();
|
|
462
|
+
a.contextStatus.mockReturnValue('absent');
|
|
463
|
+
const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'codex' }));
|
|
464
|
+
expect(r.action).toBe('applied');
|
|
465
|
+
expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'codex', scope: 'global' }));
|
|
466
|
+
});
|
|
363
467
|
});
|