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,185 @@
|
|
|
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 codex_agents_1 = require("../../../../src/core/context/strategies/codex-agents");
|
|
10
|
+
const provider_1 = require("../../../../src/core/context/provider");
|
|
11
|
+
describe('CodexAgentsStrategy', () => {
|
|
12
|
+
let tmpHome;
|
|
13
|
+
let tmpWork;
|
|
14
|
+
let originalHome;
|
|
15
|
+
let originalAwmHome;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-codex-home-'));
|
|
18
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-codex-work-'));
|
|
19
|
+
originalHome = process.env.HOME;
|
|
20
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
21
|
+
process.env.HOME = tmpHome;
|
|
22
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (originalHome === undefined)
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
else
|
|
28
|
+
process.env.HOME = originalHome;
|
|
29
|
+
if (originalAwmHome === undefined)
|
|
30
|
+
delete process.env.AWM_HOME;
|
|
31
|
+
else
|
|
32
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
33
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
34
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
35
|
+
jest.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
it('injects global AWM bootstrap without changing user rules', () => {
|
|
38
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.codex'), { recursive: true });
|
|
39
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
40
|
+
fs_1.default.writeFileSync(file, '# Personal\n\nDo not delete.\n');
|
|
41
|
+
const result = new codex_agents_1.CodexAgentsStrategy().injectGlobal({
|
|
42
|
+
markdown: '# AWM\n\nUse `development-process`.',
|
|
43
|
+
});
|
|
44
|
+
expect(result).toBe('injected');
|
|
45
|
+
const written = fs_1.default.readFileSync(file, 'utf8');
|
|
46
|
+
expect(written).toContain('# Personal\n\nDo not delete.\n');
|
|
47
|
+
expect(written).toContain('Use `development-process`.');
|
|
48
|
+
});
|
|
49
|
+
it('creates the global file and returns unchanged on an idempotent repeat', () => {
|
|
50
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
51
|
+
const context = { markdown: '# AWM\n\nBootstrap.' };
|
|
52
|
+
expect(strategy.injectGlobal(context)).toBe('injected');
|
|
53
|
+
expect(strategy.injectGlobal(context)).toBe('unchanged');
|
|
54
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.codex/AGENTS.md'))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('uses HOME at call time', () => {
|
|
57
|
+
const firstHome = tmpHome;
|
|
58
|
+
const secondHome = path_1.default.join(tmpWork, 'second-home');
|
|
59
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
60
|
+
strategy.injectGlobal({ markdown: 'first' });
|
|
61
|
+
process.env.HOME = secondHome;
|
|
62
|
+
strategy.injectGlobal({ markdown: 'second' });
|
|
63
|
+
expect(fs_1.default.readFileSync(path_1.default.join(firstHome, '.codex/AGENTS.md'), 'utf8')).toContain('first');
|
|
64
|
+
expect(fs_1.default.readFileSync(path_1.default.join(secondHome, '.codex/AGENTS.md'), 'utf8')).toContain('second');
|
|
65
|
+
});
|
|
66
|
+
it('updates only its global managed block', () => {
|
|
67
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
68
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
69
|
+
fs_1.default.writeFileSync(file, 'before\n');
|
|
70
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
71
|
+
strategy.injectGlobal({ markdown: 'old' });
|
|
72
|
+
fs_1.default.appendFileSync(file, 'after\n');
|
|
73
|
+
strategy.injectGlobal({ markdown: 'new' });
|
|
74
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('before\n\n<!-- AWM:START -->\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\nnew\n<!-- AWM:END -->\nafter\n');
|
|
75
|
+
});
|
|
76
|
+
it('fails on ambiguous global markers without changing the file', () => {
|
|
77
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
78
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
79
|
+
const ambiguous = '<!-- AWM:START -->\nuser';
|
|
80
|
+
fs_1.default.writeFileSync(file, ambiguous);
|
|
81
|
+
expect(() => new codex_agents_1.CodexAgentsStrategy().injectGlobal({ markdown: 'new' })).toThrow('unmatched');
|
|
82
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe(ambiguous);
|
|
83
|
+
});
|
|
84
|
+
it('rejects inline marker examples without changing the global file', () => {
|
|
85
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
86
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
87
|
+
const ambiguous = '`<!-- AWM:START -->` example and `<!-- AWM:END -->` example';
|
|
88
|
+
fs_1.default.writeFileSync(file, ambiguous);
|
|
89
|
+
expect(() => new codex_agents_1.CodexAgentsStrategy().injectGlobal({ markdown: 'new' })).toThrow('standalone');
|
|
90
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe(ambiguous);
|
|
91
|
+
});
|
|
92
|
+
it.each([
|
|
93
|
+
'<!-- AWM:BOUNDARY prefix=9 suffix=1 -->',
|
|
94
|
+
'<!-- AWM:BOUNDARY prefix=1 suffix=2 -->',
|
|
95
|
+
'<!-- AWM:BOUNDARY prefix=1 suffix=1 -->\n<!-- AWM:BOUNDARY prefix=0 suffix=1 -->',
|
|
96
|
+
'owned\n<!-- AWM:BOUNDARY prefix=1 suffix=1 -->',
|
|
97
|
+
'<!-- AWM:BOUNDARY prefix=1',
|
|
98
|
+
'inline <!-- AWM:BOUNDARY prefix=1 suffix=1 -->',
|
|
99
|
+
])('rejects ambiguous boundary metadata without changing the file: %j', (metadata) => {
|
|
100
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
101
|
+
const materialized = path_1.default.join(tmpWork, 'awm-context.md');
|
|
102
|
+
const markdown = '# AWM\n\nExpected.';
|
|
103
|
+
const original = `<!-- AWM:START -->\n${metadata}\nowned\n<!-- AWM:END -->`;
|
|
104
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
105
|
+
fs_1.default.writeFileSync(file, original);
|
|
106
|
+
fs_1.default.writeFileSync(materialized, markdown);
|
|
107
|
+
const provider = codexProvider(file);
|
|
108
|
+
const input = injectionInput(materialized, markdown);
|
|
109
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
110
|
+
expect(() => strategy.inject(input, provider)).toThrow('malformed AWM boundary metadata');
|
|
111
|
+
expect(() => strategy.remove(input, provider)).toThrow('malformed AWM boundary metadata');
|
|
112
|
+
expect(() => strategy.status(input, provider)).toThrow('malformed AWM boundary metadata');
|
|
113
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe(original);
|
|
114
|
+
});
|
|
115
|
+
it('injects project constitution guidance without owning the whole AGENTS.md', () => {
|
|
116
|
+
const project = path_1.default.join(tmpWork, 'repo');
|
|
117
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
118
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'CONSTITUTION.md'), '# Rules\n');
|
|
119
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'AGENTS.md'), '# Repo-owned rules\n');
|
|
120
|
+
const result = new codex_agents_1.CodexAgentsStrategy().injectProject(project);
|
|
121
|
+
expect(result).toBe('injected');
|
|
122
|
+
const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
|
|
123
|
+
expect(written).toContain('# Repo-owned rules');
|
|
124
|
+
expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
|
|
125
|
+
});
|
|
126
|
+
it('creates project guidance even before CONSTITUTION.md exists and is idempotent', () => {
|
|
127
|
+
const project = path_1.default.join(tmpWork, 'repo');
|
|
128
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
129
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
130
|
+
expect(strategy.injectProject(project)).toBe('injected');
|
|
131
|
+
expect(strategy.injectProject(project)).toBe('unchanged');
|
|
132
|
+
expect(fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8'))
|
|
133
|
+
.toContain('when that file exists');
|
|
134
|
+
});
|
|
135
|
+
it('implements global status and remove while preserving user bytes', () => {
|
|
136
|
+
const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');
|
|
137
|
+
const materialized = path_1.default.join(tmpWork, 'awm-context.md');
|
|
138
|
+
const markdown = '# AWM\n\nExpected.';
|
|
139
|
+
fs_1.default.writeFileSync(materialized, markdown);
|
|
140
|
+
const provider = codexProvider(file);
|
|
141
|
+
const input = injectionInput(materialized, markdown);
|
|
142
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
143
|
+
expect(strategy.status(input, provider)).toBe('absent');
|
|
144
|
+
expect(strategy.inject(input, provider)).toBe('injected');
|
|
145
|
+
expect(strategy.status(input, provider)).toBe('injected');
|
|
146
|
+
const staleInput = {
|
|
147
|
+
...input,
|
|
148
|
+
ref: { ...input.ref, contentHash: (0, provider_1.sha256)('# AWM\n\nUpdated.') },
|
|
149
|
+
};
|
|
150
|
+
expect(strategy.status(staleInput, provider)).toBe('stale');
|
|
151
|
+
fs_1.default.appendFileSync(file, 'user suffix\n');
|
|
152
|
+
strategy.remove(input, provider);
|
|
153
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe('user suffix\n');
|
|
154
|
+
expect(strategy.status(input, provider)).toBe('absent');
|
|
155
|
+
});
|
|
156
|
+
it('validates public inputs and never writes outside the configured roots', () => {
|
|
157
|
+
const strategy = new codex_agents_1.CodexAgentsStrategy();
|
|
158
|
+
expect(() => strategy.injectGlobal({ markdown: '' })).toThrow('markdown');
|
|
159
|
+
expect(() => strategy.injectGlobal(null)).toThrow('context');
|
|
160
|
+
expect(() => strategy.injectProject('')).toThrow('projectRoot');
|
|
161
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
162
|
+
strategy.injectGlobal({ markdown: 'safe' });
|
|
163
|
+
strategy.injectProject(path_1.default.join(tmpWork, 'safe-project'));
|
|
164
|
+
const opened = open.mock.calls.map((call) => String(call[0]));
|
|
165
|
+
expect(opened.every((file) => file.startsWith(tmpHome) || file.startsWith(tmpWork))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
function codexProvider(globalPath) {
|
|
169
|
+
return {
|
|
170
|
+
label: 'Codex',
|
|
171
|
+
skill: { global: '', local: '', renderer: 'link' },
|
|
172
|
+
workflow: null,
|
|
173
|
+
agent: null,
|
|
174
|
+
injection: { type: 'managed-agents-md', globalPath, localFile: 'AGENTS.md' },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function injectionInput(absPath, markdown) {
|
|
178
|
+
return {
|
|
179
|
+
ref: { absPath, scope: 'global', contentHash: (0, provider_1.sha256)(markdown) },
|
|
180
|
+
registryRoot: '/registry',
|
|
181
|
+
installMethod: 'copy',
|
|
182
|
+
agent: 'codex',
|
|
183
|
+
scope: 'global',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -18,7 +18,7 @@ function setup(opencodeJson) {
|
|
|
18
18
|
fs_1.default.mkdirSync(path_1.default.dirname(absPath), { recursive: true });
|
|
19
19
|
fs_1.default.writeFileSync(absPath, 'CTX');
|
|
20
20
|
const provider = {
|
|
21
|
-
label: 'OpenCode', skill: { global: '', local: '' }, workflow: null, agent: null,
|
|
21
|
+
label: 'OpenCode', skill: { global: '', local: '', renderer: 'link' }, workflow: null, agent: null,
|
|
22
22
|
injection: { type: 'config-instructions', configPath, field: 'instructions' },
|
|
23
23
|
};
|
|
24
24
|
const input = {
|
|
@@ -184,6 +184,25 @@ describe('machineChecks — global skill integrity', () => {
|
|
|
184
184
|
expect(row?.remedy).toEqual({ kind: 'command', value: 'awm init' });
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
|
+
describe('computeProviderOverall (Task 9)', () => {
|
|
188
|
+
const CHECK_IDS = ['binary.version', 'skills.global', 'agents.native', 'hook.trust', 'context.global'];
|
|
189
|
+
function providerWith(states) {
|
|
190
|
+
return {
|
|
191
|
+
id: 'codex',
|
|
192
|
+
label: 'Codex',
|
|
193
|
+
checks: states.map((state, i) => ({ id: CHECK_IDS[i % CHECK_IDS.length], state })),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
it('healthy when every check is in an ok/pending/warn state', () => {
|
|
197
|
+
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'healthy', 'shared', 'pending-trust', 'stale'])])).toBe('healthy');
|
|
198
|
+
});
|
|
199
|
+
it('degrades on a single broken/missing/absent/unsupported/conflict check', () => {
|
|
200
|
+
expect((0, checks_1.computeProviderOverall)([providerWith(['supported', 'absent'])])).toBe('degraded');
|
|
201
|
+
});
|
|
202
|
+
it('healthy for an empty provider list', () => {
|
|
203
|
+
expect((0, checks_1.computeProviderOverall)([])).toBe('healthy');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
187
206
|
describe('machine.context.<agent> checks', () => {
|
|
188
207
|
function machineWith(contextInjection) {
|
|
189
208
|
return { ...healthyMachine(), contextInjection };
|
|
@@ -75,6 +75,34 @@ describe('gatherContext', () => {
|
|
|
75
75
|
expect(ctx.machine.devCore.present).toBe(true);
|
|
76
76
|
expect(ctx.machine.devCore.brokenLinks).toContain('brainstorming');
|
|
77
77
|
});
|
|
78
|
+
// Regression coverage for the second BLOCKER-adjacent bug found while
|
|
79
|
+
// building the real Codex+OpenCode coexistence E2E test: OpenCode and
|
|
80
|
+
// Codex share ~/.agents/skills, but agent-type artifacts are per-agent
|
|
81
|
+
// (never shared, R12/R13). Before this fix, devCorePresent only looked
|
|
82
|
+
// at the skill link, so once OpenCode's own init had already linked the
|
|
83
|
+
// shared skill, Codex's gatherContext wrongly reported devCore as fully
|
|
84
|
+
// present/no-broken-links even though Codex's own native .toml had never
|
|
85
|
+
// been rendered — causing stepDevCore to skip forever.
|
|
86
|
+
it('machine: devCore surfaces a missing per-agent artifact even when the shared skill link is already present', () => {
|
|
87
|
+
const bundleWithAgent = () => ({
|
|
88
|
+
name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
|
|
89
|
+
dependsOn: [], skills: [{ name: 'development-process', onSignal: false }],
|
|
90
|
+
workflows: [], agents: ['development-process'],
|
|
91
|
+
});
|
|
92
|
+
// Simulate OpenCode having already linked the shared ~/.agents/skills dir.
|
|
93
|
+
const sharedSkillsDir = path_1.default.join(tmpHome, '.agents', 'skills');
|
|
94
|
+
fs_1.default.mkdirSync(sharedSkillsDir, { recursive: true });
|
|
95
|
+
const target = path_1.default.join(tmpHome, 'targets', 'development-process');
|
|
96
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
97
|
+
fs_1.default.symlinkSync(target, path_1.default.join(sharedSkillsDir, 'development-process'), 'dir');
|
|
98
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
99
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [bundleWithAgent()], agent: 'codex' });
|
|
100
|
+
// The skill itself is linked (shared with OpenCode)...
|
|
101
|
+
expect(ctx.machine.devCore.present).toBe(true);
|
|
102
|
+
// ...but Codex's own native agent artifact was never installed, so it
|
|
103
|
+
// must surface as a broken/missing link — NOT a clean, skippable state.
|
|
104
|
+
expect(ctx.machine.devCore.brokenLinks).toContain('development-process.toml');
|
|
105
|
+
});
|
|
78
106
|
it('machine: ambient wanted read from ~/.awm/config.json, installed reflects links', () => {
|
|
79
107
|
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
80
108
|
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm', 'config.json'), JSON.stringify({ ambient: ['personal-notion'] }));
|
|
@@ -144,6 +172,52 @@ describe('gatherContext', () => {
|
|
|
144
172
|
expect(ctx.project.context).toEqual({ present: true, file: 'CLAUDE.md' });
|
|
145
173
|
});
|
|
146
174
|
});
|
|
175
|
+
describe('gatherContext — providers matrix (Task 9)', () => {
|
|
176
|
+
let tmpHome;
|
|
177
|
+
let originalHome;
|
|
178
|
+
let originalAwmHome;
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-providers-'));
|
|
181
|
+
originalHome = process.env.HOME;
|
|
182
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
183
|
+
process.env.HOME = tmpHome;
|
|
184
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
185
|
+
jest.resetModules();
|
|
186
|
+
});
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
189
|
+
if (originalHome === undefined)
|
|
190
|
+
delete process.env.HOME;
|
|
191
|
+
else
|
|
192
|
+
process.env.HOME = originalHome;
|
|
193
|
+
if (originalAwmHome === undefined)
|
|
194
|
+
delete process.env.AWM_HOME;
|
|
195
|
+
else
|
|
196
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
197
|
+
});
|
|
198
|
+
function healthySharedSkills() {
|
|
199
|
+
return { valid: ['development-process'], repairable: [], dead: [] };
|
|
200
|
+
}
|
|
201
|
+
it('reports shared skills for both owners without scanning twice', () => {
|
|
202
|
+
// OpenCode and Codex both read/write ~/.agents/skills (providers/index.ts) —
|
|
203
|
+
// the physical directory must be scanned once and attributed to both owners.
|
|
204
|
+
const scan = jest.fn(() => healthySharedSkills());
|
|
205
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
206
|
+
const report = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills: scan });
|
|
207
|
+
expect(scan).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(report.providers.every((provider) => provider.checks.some((check) => check.state === 'shared'))).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
it('marks skills.global healthy (not shared) for a single unshared provider', () => {
|
|
211
|
+
const scan = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
212
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
213
|
+
const report = gatherContext({ cwd: tmpHome, bundles: [], agents: ['claude-code'], scanSkills: scan });
|
|
214
|
+
expect(scan).toHaveBeenCalledTimes(1);
|
|
215
|
+
const claude = report.providers.find((p) => p.id === 'claude-code');
|
|
216
|
+
const skillsCheck = claude.checks.find((c) => c.id === 'skills.global');
|
|
217
|
+
expect(skillsCheck.state).not.toBe('shared');
|
|
218
|
+
expect(skillsCheck.owners).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
147
221
|
describe('gatherMachine — agnostic skill health (#4)', () => {
|
|
148
222
|
it('classifies the target agent skills dir, not always Claude', () => {
|
|
149
223
|
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// cli/tests/core/diagnostics/provider-checks.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Code-quality review follow-up on commit 8f02a00. Two real, reproduced defects:
|
|
9
|
+
//
|
|
10
|
+
// 1. CRITICAL — agentsNativeCheck/hookTrustCheck/contextGlobalCheck returned
|
|
11
|
+
// `state: 'unsupported'` to mean "doesn't structurally apply to this
|
|
12
|
+
// provider" (Antigravity has no agent/hooks/injection config, OpenCode has
|
|
13
|
+
// no hooks, Claude Code's context.global is covered by its hook). But
|
|
14
|
+
// 'unsupported' is ALSO what binaryVersionCheck uses for a genuine
|
|
15
|
+
// version-incompatibility failure, and both doctor's glyph logic and
|
|
16
|
+
// checks.ts's DEGRADING_PROVIDER_STATES treat it as a real failure. A
|
|
17
|
+
// fully-healthy claude-code-only (or opencode-only, or antigravity-only)
|
|
18
|
+
// `awm doctor` run could therefore never report `overall: 'healthy'`. Fixed
|
|
19
|
+
// by having those three functions return `null` (omitted entirely from
|
|
20
|
+
// `checks[]`) instead of a fabricated 'unsupported' row — this file proves
|
|
21
|
+
// the fix with realistic, fully-initialized single-provider fixtures, not
|
|
22
|
+
// just synthetic state-literal inputs.
|
|
23
|
+
//
|
|
24
|
+
// 2. IMPORTANT — skillsGlobalCheck set `state: 'shared'` unconditionally once
|
|
25
|
+
// a skills directory had more than one owner, even when it had broken/dead
|
|
26
|
+
// symlinks — masking real breakage behind a green checkmark and never
|
|
27
|
+
// degrading `overall`. Fixed by checking broken status BEFORE falling back
|
|
28
|
+
// to 'shared'.
|
|
29
|
+
const fs_1 = __importDefault(require("fs"));
|
|
30
|
+
const os_1 = __importDefault(require("os"));
|
|
31
|
+
const path_1 = __importDefault(require("path"));
|
|
32
|
+
describe('gatherProviderChecks — realistic fully-healthy single-provider fixtures', () => {
|
|
33
|
+
let tmpHome;
|
|
34
|
+
let tmpWork;
|
|
35
|
+
let originalHome;
|
|
36
|
+
let originalAwmHome;
|
|
37
|
+
let writeSpy;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-home-'));
|
|
40
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-work-'));
|
|
41
|
+
originalHome = process.env.HOME;
|
|
42
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
43
|
+
process.env.HOME = tmpHome;
|
|
44
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
45
|
+
jest.resetModules();
|
|
46
|
+
writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
writeSpy.mockRestore();
|
|
50
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
51
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
52
|
+
if (originalHome === undefined)
|
|
53
|
+
delete process.env.HOME;
|
|
54
|
+
else
|
|
55
|
+
process.env.HOME = originalHome;
|
|
56
|
+
if (originalAwmHome === undefined)
|
|
57
|
+
delete process.env.AWM_HOME;
|
|
58
|
+
else
|
|
59
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
60
|
+
});
|
|
61
|
+
/** Seeds a registry content root with a baseline bundle carrying a skill
|
|
62
|
+
* and (optionally) a canonical agent artifact. Mirrors the fixture in
|
|
63
|
+
* tests/integration/codex-provider-isolated.test.ts. */
|
|
64
|
+
function seedRegistry(root, opts) {
|
|
65
|
+
if (opts.withHooks) {
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'hooks'), { recursive: true });
|
|
67
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/run-hook.cmd'), '#!/bin/sh\nexec sh "$1"\n', { mode: 0o755 });
|
|
69
|
+
}
|
|
70
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
|
|
72
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills/development-process'), { recursive: true });
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\n---\nBody.');
|
|
74
|
+
const agents = [];
|
|
75
|
+
if (opts.withAgent) {
|
|
76
|
+
agents.push('development-process');
|
|
77
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'agents'), { recursive: true });
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'agents/development-process.md'), '---\nname: development-process\ndescription: Orchestrates the dev lifecycle.\n---\nBody.');
|
|
79
|
+
}
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
|
|
81
|
+
version: 1,
|
|
82
|
+
bundles: [{ name: 'dev-core', source: 'bundles/dev-core', version: '1.0.0', scope: 'baseline', visibility: 'public' }],
|
|
83
|
+
}));
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'bundles/dev-core'), { recursive: true });
|
|
85
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'bundles/dev-core/bundle.json'), JSON.stringify({
|
|
86
|
+
name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
|
|
87
|
+
dependsOn: [], skills: ['development-process'], workflows: [], agents,
|
|
88
|
+
}));
|
|
89
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
|
|
90
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
|
|
91
|
+
}
|
|
92
|
+
it('claude-code-only, fully initialized via a real `awm init`, reports overall: healthy', async () => {
|
|
93
|
+
seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: true, withHooks: true });
|
|
94
|
+
const { runInit } = require('../../../src/commands/init');
|
|
95
|
+
const code = await runInit({ cwd: tmpWork, yes: true });
|
|
96
|
+
expect(code).toBeLessThanOrEqual(1);
|
|
97
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
98
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
99
|
+
const ctx = gatherContext({ cwd: tmpWork, agents: ['claude-code'] });
|
|
100
|
+
expect(computeProviderOverall(ctx.providers)).toBe('healthy');
|
|
101
|
+
const claude = ctx.providers[0];
|
|
102
|
+
// Claude's context.global is deliberately omitted (covered by hook.trust),
|
|
103
|
+
// not rendered as a fabricated 'unsupported' row.
|
|
104
|
+
expect(claude.checks.map((c) => c.id)).not.toContain('context.global');
|
|
105
|
+
expect(claude.checks.every((c) => !['unsupported', 'missing', 'broken', 'absent', 'conflict'].includes(c.state))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it('opencode-only, fully installed via the real install/injection pipeline, reports overall: healthy', () => {
|
|
108
|
+
seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: true, withHooks: false });
|
|
109
|
+
// OpenCode has no hook config — routed directly through the same real
|
|
110
|
+
// installBundle/InjectionOrchestrator functions `awm init` itself calls,
|
|
111
|
+
// rather than through runInit's step orchestration (a separate,
|
|
112
|
+
// pre-existing gap — stepHook unconditionally attempts a hook install
|
|
113
|
+
// for every agent, including ones with no hook mechanism at all — is out
|
|
114
|
+
// of scope for this fix and reported separately).
|
|
115
|
+
const { installBundle } = require('../../../src/core/bundle-install');
|
|
116
|
+
const { discoverAllBundles } = require('../../../src/core/bundles');
|
|
117
|
+
const { contentRoots } = require('../../../src/core/registries');
|
|
118
|
+
const { InjectionOrchestrator } = require('../../../src/core/context/orchestrator');
|
|
119
|
+
const bundles = discoverAllBundles();
|
|
120
|
+
const contentDir = contentRoots()[0];
|
|
121
|
+
installBundle({ bundleName: 'dev-core', bundles, agents: ['opencode'], method: 'symlink', projectRoot: tmpWork, contentDir });
|
|
122
|
+
new InjectionOrchestrator().installContext({
|
|
123
|
+
agent: 'opencode', scope: 'global', registryRoot: contentDir, installMethod: 'symlink', profileExtensions: [],
|
|
124
|
+
});
|
|
125
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
126
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
127
|
+
const ctx = gatherContext({ cwd: tmpWork, agents: ['opencode'] });
|
|
128
|
+
expect(computeProviderOverall(ctx.providers)).toBe('healthy');
|
|
129
|
+
const opencode = ctx.providers[0];
|
|
130
|
+
// OpenCode has no hooks — omitted, not a fabricated 'unsupported' row.
|
|
131
|
+
expect(opencode.checks.map((c) => c.id)).not.toContain('hook.trust');
|
|
132
|
+
});
|
|
133
|
+
it('antigravity-only, fully installed via the real install pipeline, reports overall: healthy', () => {
|
|
134
|
+
seedRegistry(path_1.default.join(tmpHome, '.awm/registries/baseline'), { withAgent: false, withHooks: false });
|
|
135
|
+
const { installBundle } = require('../../../src/core/bundle-install');
|
|
136
|
+
const { discoverAllBundles } = require('../../../src/core/bundles');
|
|
137
|
+
const { contentRoots } = require('../../../src/core/registries');
|
|
138
|
+
const bundles = discoverAllBundles();
|
|
139
|
+
installBundle({ bundleName: 'dev-core', bundles, agents: ['antigravity'], method: 'symlink', projectRoot: tmpWork, contentDir: contentRoots()[0] });
|
|
140
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
141
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
142
|
+
const ctx = gatherContext({ cwd: tmpWork, agents: ['antigravity'] });
|
|
143
|
+
expect(computeProviderOverall(ctx.providers)).toBe('healthy');
|
|
144
|
+
const antigravity = ctx.providers[0];
|
|
145
|
+
// Antigravity has no agent/hooks/injection config at all — all three
|
|
146
|
+
// checks omitted, not fabricated 'unsupported' rows. Only binary.version
|
|
147
|
+
// and skills.global structurally apply.
|
|
148
|
+
expect(antigravity.checks.map((c) => c.id).sort())
|
|
149
|
+
.toEqual(['binary.version', 'skills.global']);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('gatherProviderChecks — shared skills.global does not mask broken links', () => {
|
|
153
|
+
let tmpHome;
|
|
154
|
+
let originalHome;
|
|
155
|
+
let originalAwmHome;
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-shared-'));
|
|
158
|
+
originalHome = process.env.HOME;
|
|
159
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
160
|
+
process.env.HOME = tmpHome;
|
|
161
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
162
|
+
jest.resetModules();
|
|
163
|
+
});
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
166
|
+
if (originalHome === undefined)
|
|
167
|
+
delete process.env.HOME;
|
|
168
|
+
else
|
|
169
|
+
process.env.HOME = originalHome;
|
|
170
|
+
if (originalAwmHome === undefined)
|
|
171
|
+
delete process.env.AWM_HOME;
|
|
172
|
+
else
|
|
173
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
174
|
+
});
|
|
175
|
+
it('reports a degraded (not shared) skills.global state when the shared dir has broken links', () => {
|
|
176
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
177
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
178
|
+
// OpenCode and Codex share ~/.agents/skills (providers/index.ts). Stub the
|
|
179
|
+
// scan to report 2 broken/dead links on that shared directory.
|
|
180
|
+
const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: ['stale-skill'], dead: ['ghost-skill'] }));
|
|
181
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
|
|
182
|
+
expect(scanSkills).toHaveBeenCalledTimes(1);
|
|
183
|
+
for (const provider of ctx.providers) {
|
|
184
|
+
const skillsCheck = provider.checks.find((c) => c.id === 'skills.global');
|
|
185
|
+
expect(skillsCheck.state).toBe('broken'); // NOT 'shared' — must not mask the breakage
|
|
186
|
+
expect(skillsCheck.owners).toEqual(['opencode', 'codex']); // still reports the shared ownership
|
|
187
|
+
expect(skillsCheck.detail).toContain('2 broken links');
|
|
188
|
+
expect(skillsCheck.remediationCode).toBe('repair-global-skills');
|
|
189
|
+
}
|
|
190
|
+
expect(computeProviderOverall(ctx.providers)).toBe('degraded');
|
|
191
|
+
});
|
|
192
|
+
it('still reports shared (healthy, non-degrading) when the shared dir has no broken links', () => {
|
|
193
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
194
|
+
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
195
|
+
const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: [], dead: [] }));
|
|
196
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
|
|
197
|
+
for (const provider of ctx.providers) {
|
|
198
|
+
const skillsCheck = provider.checks.find((c) => c.id === 'skills.global');
|
|
199
|
+
expect(skillsCheck.state).toBe('shared');
|
|
200
|
+
}
|
|
201
|
+
// Other checks (binary.version missing codex, hook.trust absent, etc.) may
|
|
202
|
+
// still degrade `overall` on a bare HOME — this test only pins that a
|
|
203
|
+
// HEALTHY shared skills dir itself reports 'shared', not a false positive.
|
|
204
|
+
const skillsStates = ctx.providers.map((p) => p.checks.find((c) => c.id === 'skills.global').state);
|
|
205
|
+
expect(skillsStates).toEqual(['shared', 'shared']);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('gatherProviderChecks — agents.native reports broken on a malformed Codex .toml', () => {
|
|
209
|
+
let tmpHome;
|
|
210
|
+
let originalHome;
|
|
211
|
+
let originalAwmHome;
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-checks-broken-agent-'));
|
|
214
|
+
originalHome = process.env.HOME;
|
|
215
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
216
|
+
process.env.HOME = tmpHome;
|
|
217
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
218
|
+
jest.resetModules();
|
|
219
|
+
});
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
222
|
+
if (originalHome === undefined)
|
|
223
|
+
delete process.env.HOME;
|
|
224
|
+
else
|
|
225
|
+
process.env.HOME = originalHome;
|
|
226
|
+
if (originalAwmHome === undefined)
|
|
227
|
+
delete process.env.AWM_HOME;
|
|
228
|
+
else
|
|
229
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
230
|
+
});
|
|
231
|
+
it('reports state: broken and remediationCode: reinstall-native-agents for a hand-edited/truncated .toml', () => {
|
|
232
|
+
// Codex's agent renderer is 'codex-agent-toml' (providers/index.ts) — its global
|
|
233
|
+
// dir is ~/.codex/agents. tomlAgentsHealthy (provider-checks.ts) treats a .toml as
|
|
234
|
+
// broken when it can't be read, or when it's missing the `developer_instructions = `
|
|
235
|
+
// key that renderCodexAgent (Task 4) always emits — i.e. hand-edited/truncated.
|
|
236
|
+
const agentsDir = path_1.default.join(tmpHome, '.codex/agents');
|
|
237
|
+
fs_1.default.mkdirSync(agentsDir, { recursive: true });
|
|
238
|
+
fs_1.default.writeFileSync(path_1.default.join(agentsDir, 'development-process.toml'), 'name = "development-process"\n# missing the developer_instructions key entirely\n');
|
|
239
|
+
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
240
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [] }));
|
|
241
|
+
const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['codex'], scanSkills });
|
|
242
|
+
const codex = ctx.providers.find((p) => p.id === 'codex');
|
|
243
|
+
const agentsNative = codex.checks.find((c) => c.id === 'agents.native');
|
|
244
|
+
expect(agentsNative).toMatchObject({
|
|
245
|
+
id: 'agents.native',
|
|
246
|
+
state: 'broken',
|
|
247
|
+
target: agentsDir,
|
|
248
|
+
detail: '1 malformed .toml',
|
|
249
|
+
remediationCode: 'reinstall-native-agents',
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -41,4 +41,24 @@ describe('Executor Engine', () => {
|
|
|
41
41
|
const dest = path_1.default.join(targetDir, 'does-not-exist');
|
|
42
42
|
expect(() => (0, executor_1.removeArtifact)(dest)).toThrow('Artifact not found');
|
|
43
43
|
});
|
|
44
|
+
it('never removes the live target before staging succeeds', () => {
|
|
45
|
+
const missingSource = path_1.default.join(sourceDir, 'does-not-exist-source');
|
|
46
|
+
const target = path_1.default.join(targetDir, 'target');
|
|
47
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(target, 'sentinel'), 'before');
|
|
49
|
+
expect(() => (0, executor_1.installArtifact)(missingSource, target, 'copy')).toThrow('Source path does not exist');
|
|
50
|
+
expect(fs_1.default.readFileSync(path_1.default.join(target, 'sentinel'), 'utf8')).toBe('before'); // verifies R17
|
|
51
|
+
});
|
|
52
|
+
it('stageArtifact stages next to the target without touching it, replaceArtifact swaps it in', () => {
|
|
53
|
+
const target = path_1.default.join(targetDir, 'staged-target');
|
|
54
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
55
|
+
fs_1.default.writeFileSync(path_1.default.join(target, 'sentinel'), 'before');
|
|
56
|
+
const staged = (0, executor_1.stageArtifact)(sourceDir, target, 'copy');
|
|
57
|
+
expect(fs_1.default.readFileSync(path_1.default.join(target, 'sentinel'), 'utf8')).toBe('before'); // untouched pre-replace
|
|
58
|
+
expect(fs_1.default.existsSync(path_1.default.join(staged, 'test.txt'))).toBe(true);
|
|
59
|
+
(0, executor_1.replaceArtifact)(staged, target);
|
|
60
|
+
expect(fs_1.default.existsSync(path_1.default.join(target, 'sentinel'))).toBe(false);
|
|
61
|
+
expect(fs_1.default.existsSync(path_1.default.join(target, 'test.txt'))).toBe(true);
|
|
62
|
+
expect(fs_1.default.existsSync(staged)).toBe(false);
|
|
63
|
+
});
|
|
44
64
|
});
|
|
@@ -91,9 +91,10 @@ describe('runExport (engine end-to-end)', () => {
|
|
|
91
91
|
const originalCwd = process.cwd();
|
|
92
92
|
process.chdir(cwdTmp);
|
|
93
93
|
try {
|
|
94
|
+
const actualCwd = process.cwd();
|
|
94
95
|
const summary = (0, export_1.runExport)({ name: 'mermaid', roots: [root], zip: okZip }); // sin `out`
|
|
95
|
-
expect(summary.outDir).toBe(path_1.default.join(
|
|
96
|
-
expect(fs_1.default.existsSync(path_1.default.join(
|
|
96
|
+
expect(summary.outDir).toBe(path_1.default.join(actualCwd, 'awm-export', 'claude-ai'));
|
|
97
|
+
expect(fs_1.default.existsSync(path_1.default.join(actualCwd, 'awm-export/claude-ai/mermaid/SKILL.md'))).toBe(true);
|
|
97
98
|
}
|
|
98
99
|
finally {
|
|
99
100
|
process.chdir(originalCwd);
|