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
|
@@ -1,25 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
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"));
|
|
3
9
|
const providers_1 = require("../../src/providers");
|
|
4
10
|
describe('Hook configuration in providers', () => {
|
|
11
|
+
const originalHome = process.env.HOME;
|
|
12
|
+
const originalAwmHome = process.env.AWM_HOME;
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let tmpWork;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-hook-home-'));
|
|
17
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-hook-work-'));
|
|
18
|
+
process.env.HOME = tmpHome;
|
|
19
|
+
process.env.AWM_HOME = path_1.default.join(tmpWork, 'awm');
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (originalHome === undefined)
|
|
23
|
+
delete process.env.HOME;
|
|
24
|
+
else
|
|
25
|
+
process.env.HOME = originalHome;
|
|
26
|
+
if (originalAwmHome === undefined)
|
|
27
|
+
delete process.env.AWM_HOME;
|
|
28
|
+
else
|
|
29
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
30
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
31
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
32
|
+
});
|
|
5
33
|
it('claude-code provider defines a HookConfig', () => {
|
|
6
|
-
const cc = providers_1.
|
|
34
|
+
const cc = (0, providers_1.providerFor)('claude-code');
|
|
7
35
|
expect(cc.hooks).toBeDefined();
|
|
8
36
|
expect(cc.hooks?.type).toBe('cc-settings-merge');
|
|
9
37
|
expect(cc.hooks?.eventName).toBe('SessionStart');
|
|
10
38
|
expect(cc.hooks?.matcher).toBe('startup|clear|compact');
|
|
11
39
|
});
|
|
12
|
-
it('
|
|
13
|
-
const cc = providers_1.
|
|
14
|
-
expect(cc.hooks?.settingsPath).
|
|
40
|
+
it('preserves the legacy Claude Code AWM_HOME hooks directory', () => {
|
|
41
|
+
const cc = (0, providers_1.providerFor)('claude-code');
|
|
42
|
+
expect(cc.hooks?.settingsPath).toBe(path_1.default.join(tmpHome, '.claude/settings.json'));
|
|
43
|
+
expect(cc.hooks?.scriptsDir).toBe(path_1.default.join(process.env.AWM_HOME, 'hooks'));
|
|
15
44
|
});
|
|
16
|
-
it('
|
|
17
|
-
|
|
18
|
-
expect(
|
|
19
|
-
});
|
|
20
|
-
it('antigravity and opencode have no hooks (single-harness scope)', () => {
|
|
21
|
-
expect(providers_1.PROVIDERS['antigravity'].hooks).toBeUndefined();
|
|
22
|
-
expect(providers_1.PROVIDERS['opencode'].hooks).toBeUndefined();
|
|
45
|
+
it('antigravity and opencode have no hooks', () => {
|
|
46
|
+
expect((0, providers_1.providerFor)('antigravity').hooks).toBeUndefined();
|
|
47
|
+
expect((0, providers_1.providerFor)('opencode').hooks).toBeUndefined();
|
|
23
48
|
});
|
|
24
49
|
it('getHookConfig returns config for supported target', () => {
|
|
25
50
|
const config = (0, providers_1.getHookConfig)('claude-code');
|
|
@@ -27,19 +52,12 @@ describe('Hook configuration in providers', () => {
|
|
|
27
52
|
expect(config?.type).toBe('cc-settings-merge');
|
|
28
53
|
});
|
|
29
54
|
it('getHookConfig returns undefined for unsupported target', () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
jest.resetModules();
|
|
38
|
-
const { PROVIDERS: P } = require('../../src/providers');
|
|
39
|
-
expect(P['claude-code'].hooks.scriptsDir).toBe('/tmp/awm-test/hooks');
|
|
40
|
-
if (originalEnv === undefined)
|
|
41
|
-
delete process.env.AWM_HOME;
|
|
42
|
-
else
|
|
43
|
-
process.env.AWM_HOME = originalEnv;
|
|
55
|
+
expect((0, providers_1.getHookConfig)('antigravity')).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
it('respects AWM_HOME changes at call time', () => {
|
|
58
|
+
const nextAwmHome = path_1.default.join(tmpWork, 'next-awm');
|
|
59
|
+
process.env.AWM_HOME = nextAwmHome;
|
|
60
|
+
expect((0, providers_1.getHookConfig)('claude-code')?.scriptsDir)
|
|
61
|
+
.toBe(path_1.default.join(nextAwmHome, 'hooks'));
|
|
44
62
|
});
|
|
45
63
|
});
|
|
@@ -3,56 +3,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
|
|
7
|
-
const providers_1 = require("../../src/providers");
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
7
|
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const providers_1 = require("../../src/providers");
|
|
9
10
|
describe('Providers Routing', () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const originalHome = process.env.HOME;
|
|
12
|
+
const originalAwmHome = process.env.AWM_HOME;
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let tmpWork;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-home-'));
|
|
17
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-provider-work-'));
|
|
18
|
+
process.env.HOME = tmpHome;
|
|
19
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm-test');
|
|
14
20
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (originalHome === undefined)
|
|
23
|
+
delete process.env.HOME;
|
|
24
|
+
else
|
|
25
|
+
process.env.HOME = originalHome;
|
|
26
|
+
if (originalAwmHome === undefined)
|
|
27
|
+
delete process.env.AWM_HOME;
|
|
28
|
+
else
|
|
29
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
30
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
31
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
18
32
|
});
|
|
19
|
-
it('
|
|
20
|
-
const
|
|
21
|
-
|
|
33
|
+
it('resolves Codex paths from the current HOME at call time', () => {
|
|
34
|
+
const first = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'awm-home-a-'));
|
|
35
|
+
const second = fs_1.default.mkdtempSync(path_1.default.join(tmpWork, 'awm-home-b-'));
|
|
36
|
+
process.env.HOME = first;
|
|
37
|
+
expect((0, providers_1.getTargetPath)('skill', 'codex', 'global'))
|
|
38
|
+
.toBe(path_1.default.join(first, '.agents/skills'));
|
|
39
|
+
process.env.HOME = second;
|
|
40
|
+
expect((0, providers_1.getTargetPath)('agent', 'codex', 'global'))
|
|
41
|
+
.toBe(path_1.default.join(second, '.codex/agents'));
|
|
22
42
|
});
|
|
23
|
-
it('
|
|
24
|
-
|
|
43
|
+
it('returns a fresh provider graph using current HOME and AWM_HOME', () => {
|
|
44
|
+
const first = (0, providers_1.providers)();
|
|
45
|
+
const nextHome = path_1.default.join(tmpWork, 'next-home');
|
|
46
|
+
const nextAwmHome = path_1.default.join(tmpWork, 'next-awm');
|
|
47
|
+
process.env.HOME = nextHome;
|
|
48
|
+
process.env.AWM_HOME = nextAwmHome;
|
|
49
|
+
const second = (0, providers_1.providers)();
|
|
50
|
+
expect(second).not.toBe(first);
|
|
51
|
+
expect(second.codex.skill.global).toBe(path_1.default.join(nextHome, '.agents/skills'));
|
|
52
|
+
expect(second.codex.hooks?.scriptsDir).toBe(path_1.default.join(nextAwmHome, 'hooks/codex'));
|
|
53
|
+
expect(first.codex.skill.global).toBe(path_1.default.join(tmpHome, '.agents/skills'));
|
|
25
54
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
it('declares the complete Codex capability metadata', () => {
|
|
56
|
+
expect((0, providers_1.providerFor)('codex')).toEqual({
|
|
57
|
+
label: 'Codex',
|
|
58
|
+
minimumVersion: '0.145.0',
|
|
59
|
+
versionCommand: { command: 'codex', args: ['--version'] },
|
|
60
|
+
skill: {
|
|
61
|
+
global: path_1.default.join(tmpHome, '.agents/skills'),
|
|
62
|
+
local: '.agents/skills',
|
|
63
|
+
renderer: 'link',
|
|
64
|
+
},
|
|
65
|
+
workflow: null,
|
|
66
|
+
agent: {
|
|
67
|
+
global: path_1.default.join(tmpHome, '.codex/agents'),
|
|
68
|
+
local: '.codex/agents',
|
|
69
|
+
renderer: 'codex-agent-toml',
|
|
70
|
+
},
|
|
71
|
+
hooks: {
|
|
72
|
+
type: 'codex-hooks-json',
|
|
73
|
+
settingsPath: path_1.default.join(tmpHome, '.codex/hooks.json'),
|
|
74
|
+
scriptsDir: path_1.default.join(process.env.AWM_HOME, 'hooks/codex'),
|
|
75
|
+
matcher: 'startup|resume|clear|compact',
|
|
76
|
+
eventName: 'SessionStart',
|
|
77
|
+
},
|
|
78
|
+
injection: {
|
|
79
|
+
type: 'managed-agents-md',
|
|
80
|
+
globalPath: path_1.default.join(tmpHome, '.codex/AGENTS.md'),
|
|
81
|
+
localFile: 'AGENTS.md',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
30
84
|
});
|
|
31
|
-
it('
|
|
32
|
-
const
|
|
33
|
-
expect(
|
|
85
|
+
it('keeps Claude Code, OpenCode, and Antigravity contracts unchanged', () => {
|
|
86
|
+
const graph = (0, providers_1.providers)();
|
|
87
|
+
expect(graph.antigravity).toEqual({
|
|
88
|
+
label: 'Antigravity',
|
|
89
|
+
skill: {
|
|
90
|
+
global: path_1.default.join(tmpHome, '.gemini/antigravity/skills'),
|
|
91
|
+
local: '.agent/skills',
|
|
92
|
+
renderer: 'link',
|
|
93
|
+
},
|
|
94
|
+
workflow: {
|
|
95
|
+
global: path_1.default.join(tmpHome, '.gemini/antigravity/global_workflows'),
|
|
96
|
+
local: '.agent/workflows',
|
|
97
|
+
renderer: 'link',
|
|
98
|
+
},
|
|
99
|
+
agent: null,
|
|
100
|
+
});
|
|
101
|
+
expect(graph.opencode).toEqual({
|
|
102
|
+
label: 'OpenCode',
|
|
103
|
+
skill: {
|
|
104
|
+
global: path_1.default.join(tmpHome, '.agents/skills'),
|
|
105
|
+
local: '.agents/skills',
|
|
106
|
+
renderer: 'link',
|
|
107
|
+
},
|
|
108
|
+
workflow: null,
|
|
109
|
+
agent: {
|
|
110
|
+
global: path_1.default.join(tmpHome, '.config/opencode/agents'),
|
|
111
|
+
local: '.agents/profiles',
|
|
112
|
+
renderer: 'link',
|
|
113
|
+
},
|
|
114
|
+
injection: {
|
|
115
|
+
type: 'config-instructions',
|
|
116
|
+
configPath: path_1.default.join(tmpHome, '.config/opencode/opencode.json'),
|
|
117
|
+
field: 'instructions',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
expect(graph['claude-code']).toEqual({
|
|
121
|
+
label: 'Claude Code',
|
|
122
|
+
skill: {
|
|
123
|
+
global: path_1.default.join(tmpHome, '.claude/skills'),
|
|
124
|
+
local: '.claude/skills',
|
|
125
|
+
renderer: 'link',
|
|
126
|
+
},
|
|
127
|
+
workflow: null,
|
|
128
|
+
agent: {
|
|
129
|
+
global: path_1.default.join(tmpHome, '.claude/agents'),
|
|
130
|
+
local: '.claude/agents',
|
|
131
|
+
renderer: 'link',
|
|
132
|
+
},
|
|
133
|
+
hooks: {
|
|
134
|
+
type: 'cc-settings-merge',
|
|
135
|
+
settingsPath: path_1.default.join(tmpHome, '.claude/settings.json'),
|
|
136
|
+
scriptsDir: path_1.default.join(process.env.AWM_HOME, 'hooks'),
|
|
137
|
+
matcher: 'startup|clear|compact',
|
|
138
|
+
eventName: 'SessionStart',
|
|
139
|
+
},
|
|
140
|
+
injection: { type: 'cc-settings-merge' },
|
|
141
|
+
});
|
|
34
142
|
});
|
|
35
|
-
it('
|
|
36
|
-
|
|
37
|
-
|
|
143
|
+
it('keeps Claude Code and OpenCode destinations unchanged', () => {
|
|
144
|
+
expect((0, providers_1.getTargetPath)('skill', 'claude-code', 'global'))
|
|
145
|
+
.toBe(path_1.default.join(process.env.HOME, '.claude/skills'));
|
|
146
|
+
expect((0, providers_1.getTargetPath)('skill', 'opencode', 'global'))
|
|
147
|
+
.toBe(path_1.default.join(process.env.HOME, '.agents/skills'));
|
|
38
148
|
});
|
|
39
|
-
it('
|
|
40
|
-
|
|
41
|
-
expect(
|
|
149
|
+
it('uses AGENT_TARGETS as the single iterable target catalog', () => {
|
|
150
|
+
expect(providers_1.AGENT_TARGETS).toEqual(['antigravity', 'opencode', 'claude-code', 'codex']);
|
|
151
|
+
expect(Object.keys((0, providers_1.providers)())).toEqual([...providers_1.AGENT_TARGETS]);
|
|
42
152
|
});
|
|
43
|
-
it('throws on
|
|
153
|
+
it('throws on unsupported artifacts', () => {
|
|
154
|
+
expect(() => (0, providers_1.getTargetPath)('workflow', 'opencode', 'global')).toThrow('not supported');
|
|
44
155
|
expect(() => (0, providers_1.getTargetPath)('workflow', 'claude-code', 'global')).toThrow('not supported');
|
|
156
|
+
expect(() => (0, providers_1.getTargetPath)('agent', 'antigravity', 'local')).toThrow('not supported');
|
|
45
157
|
});
|
|
46
|
-
it('
|
|
47
|
-
expect(() => (0, providers_1.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
it('exports PROVIDERS with all three targets', () => {
|
|
51
|
-
expect(Object.keys(providers_1.PROVIDERS)).toEqual(expect.arrayContaining(['antigravity', 'opencode', 'claude-code']));
|
|
52
|
-
});
|
|
53
|
-
it('marks unsupported artifact types as null', () => {
|
|
54
|
-
expect(providers_1.PROVIDERS['antigravity'].agent).toBeNull();
|
|
55
|
-
expect(providers_1.PROVIDERS['opencode'].workflow).toBeNull();
|
|
56
|
-
expect(providers_1.PROVIDERS['claude-code'].workflow).toBeNull();
|
|
158
|
+
it('fails loudly for invalid runtime provider input', () => {
|
|
159
|
+
expect(() => (0, providers_1.providerFor)('unknown-agent')).toThrow('Unknown agent target');
|
|
160
|
+
expect(() => (0, providers_1.getTargetPath)('skill', 'unknown-agent', 'global'))
|
|
161
|
+
.toThrow('Unknown agent target');
|
|
57
162
|
});
|
|
58
163
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Structural regression guard for the recurring class of bug found across
|
|
3
|
+
// multiple review rounds in this repo's history (harness-retro,
|
|
4
|
+
// 2026-07-25 — codex-cli-provider-plan): the Codex TOML renderer's
|
|
5
|
+
// character-escaping was fixed piecemeal, one control character at a time
|
|
6
|
+
// (quote runs, then DEL, then the rest of the C0 range), because each fix
|
|
7
|
+
// only tested the specific character it addressed rather than the full
|
|
8
|
+
// class. This test exercises the ENTIRE character class the renderer must
|
|
9
|
+
// escape in one pass, so a future partial regression (e.g. someone "fixing"
|
|
10
|
+
// tomlMultiline for a new case and narrowing the character class by
|
|
11
|
+
// accident) fails immediately instead of waiting for another review round
|
|
12
|
+
// to notice one more missed code point.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately does not import the renderer's private helpers (tomlString/
|
|
15
|
+
// tomlMultiline aren't exported) — it drives the same public entry point
|
|
16
|
+
// (`renderCodexAgent`) real callers use, and inspects the actual TOML text.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const codex_agent_1 = require("../../src/core/renderers/codex-agent");
|
|
19
|
+
// Every C0 control character other than tab (0x09) and newline (0x0A), plus
|
|
20
|
+
// DEL (0x7F) — the full set TOML requires escaping in both single-line and
|
|
21
|
+
// multi-line basic strings.
|
|
22
|
+
const REQUIRES_ESCAPE = [
|
|
23
|
+
...Array.from({ length: 0x09 }, (_, i) => i), // 0x00-0x08
|
|
24
|
+
...Array.from({ length: 0x1f - 0x0b + 1 }, (_, i) => 0x0b + i), // 0x0B-0x1F
|
|
25
|
+
0x7f,
|
|
26
|
+
];
|
|
27
|
+
function canonicalAgentWith(instructions, description = 'd') {
|
|
28
|
+
return `---\nname: test-agent\ndescription: ${description}\n---\n\n${instructions}\n`;
|
|
29
|
+
}
|
|
30
|
+
// A run of embedded quotes long enough to exercise the original critical bug
|
|
31
|
+
// (a naive `/"""/g` replace mis-escaped runs whose length was >3 and not a
|
|
32
|
+
// multiple of 3 — e.g. 4, 5, 7, 8 consecutive quotes).
|
|
33
|
+
const QUOTE_RUN_LENGTHS = [1, 3, 4, 5, 7, 8];
|
|
34
|
+
describe('codex-agent renderer — escaping completeness (structural)', () => {
|
|
35
|
+
it('escapes every C0-minus-tab/newline control character and DEL as \\uXXXX in developer_instructions', () => {
|
|
36
|
+
const body = REQUIRES_ESCAPE.map((code) => `x${String.fromCharCode(code)}y`).join('|');
|
|
37
|
+
const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
|
|
38
|
+
for (const code of REQUIRES_ESCAPE) {
|
|
39
|
+
const raw = String.fromCharCode(code);
|
|
40
|
+
expect(toml).not.toContain(raw); // no literal control byte survives
|
|
41
|
+
const escape = `\\u${code.toString(16).padStart(4, '0')}`;
|
|
42
|
+
expect(toml).toContain(escape);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('never leaves a run of 3+ unescaped double quotes for any embedded quote-run length', () => {
|
|
46
|
+
for (const length of QUOTE_RUN_LENGTHS) {
|
|
47
|
+
const body = `before ${'"'.repeat(length)} after`;
|
|
48
|
+
const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
|
|
49
|
+
// Strip the legitimate opening/closing """ delimiters, then confirm
|
|
50
|
+
// no 3-consecutive-unescaped-quote run remains inside the body —
|
|
51
|
+
// that would prematurely close a TOML multi-line basic string.
|
|
52
|
+
const startMarker = 'developer_instructions = """\n';
|
|
53
|
+
const start = toml.indexOf(startMarker) + startMarker.length;
|
|
54
|
+
const end = toml.lastIndexOf('\n"""');
|
|
55
|
+
const rendered = toml.slice(start, end);
|
|
56
|
+
expect(rendered).not.toMatch(/(?<!\\)"""/);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
it('tab and newline stay raw (legitimately unescaped in a TOML multi-line basic string)', () => {
|
|
60
|
+
const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith('line one\twith tab\nline two'));
|
|
61
|
+
expect(toml).toContain('line one\twith tab\nline two');
|
|
62
|
+
});
|
|
63
|
+
it('produces output parseable as a TOML multi-line basic string boundary (no stray raw quote runs at all)', () => {
|
|
64
|
+
// Combine control chars + quotes + backslashes in one body — the
|
|
65
|
+
// realistic worst case, not just each hazard in isolation.
|
|
66
|
+
const body = `a\\b"""c${String.fromCharCode(0x00)}d${String.fromCharCode(0x1f)}e${String.fromCharCode(0x7f)}f`;
|
|
67
|
+
const toml = (0, codex_agent_1.renderCodexAgent)(canonicalAgentWith(body));
|
|
68
|
+
const startMarker = 'developer_instructions = """\n';
|
|
69
|
+
const start = toml.indexOf(startMarker) + startMarker.length;
|
|
70
|
+
const end = toml.lastIndexOf('\n"""');
|
|
71
|
+
const rendered = toml.slice(start, end);
|
|
72
|
+
expect(rendered).not.toMatch(/(?<!\\)"""/);
|
|
73
|
+
expect(rendered).not.toContain(String.fromCharCode(0x00));
|
|
74
|
+
expect(rendered).not.toContain(String.fromCharCode(0x1f));
|
|
75
|
+
expect(rendered).not.toContain(String.fromCharCode(0x7f));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const provider_preflight_1 = require("../../src/ui/provider-preflight");
|
|
4
|
+
describe.each(['--all', 'regular'])('%s add preflight boundary', (flow) => {
|
|
5
|
+
it('reports the exact error and stops before install or preference writes', () => {
|
|
6
|
+
const error = jest.fn();
|
|
7
|
+
const exit = jest.fn();
|
|
8
|
+
const installArtifact = jest.fn();
|
|
9
|
+
const savePreferences = jest.fn();
|
|
10
|
+
const pairs = [
|
|
11
|
+
{ agent: 'codex', artifact: { type: 'skill' } },
|
|
12
|
+
{ agent: 'codex', artifact: { type: 'agent' } },
|
|
13
|
+
];
|
|
14
|
+
const proceed = (0, provider_preflight_1.preflightLinkArtifactsForCli)(pairs, { error, exit });
|
|
15
|
+
if (proceed) {
|
|
16
|
+
installArtifact(flow);
|
|
17
|
+
savePreferences(flow);
|
|
18
|
+
}
|
|
19
|
+
expect(proceed).toBe(false);
|
|
20
|
+
expect(error).toHaveBeenCalledWith("Renderer 'codex-agent-toml' for codex agent artifacts is not implemented yet");
|
|
21
|
+
expect(exit).toHaveBeenCalledWith(1);
|
|
22
|
+
expect(installArtifact).not.toHaveBeenCalled();
|
|
23
|
+
expect(savePreferences).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -3,34 +3,194 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
// tests/utils/config.test.ts
|
|
7
|
-
const config_1 = require("../../src/utils/config");
|
|
8
6
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
7
|
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
11
9
|
describe('Preferences Manager', () => {
|
|
12
|
-
let
|
|
10
|
+
let tmpHome;
|
|
11
|
+
let tmpWork;
|
|
12
|
+
let originalHome;
|
|
13
|
+
let originalAwmHome;
|
|
13
14
|
beforeEach(() => {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prefs-home-'));
|
|
16
|
+
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-prefs-work-'));
|
|
17
|
+
originalHome = process.env.HOME;
|
|
18
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
19
|
+
process.env.HOME = tmpHome;
|
|
20
|
+
process.env.AWM_HOME = tmpHome;
|
|
21
|
+
jest.resetModules();
|
|
16
22
|
});
|
|
17
23
|
afterEach(() => {
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
if (originalHome === undefined)
|
|
25
|
+
delete process.env.HOME;
|
|
26
|
+
else
|
|
27
|
+
process.env.HOME = originalHome;
|
|
28
|
+
if (originalAwmHome === undefined)
|
|
29
|
+
delete process.env.AWM_HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
32
|
+
jest.resetModules();
|
|
33
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
34
|
+
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
20
35
|
});
|
|
21
|
-
it('creates default preferences if none exist
|
|
22
|
-
const
|
|
23
|
-
expect(
|
|
36
|
+
it('creates default preferences if none exist', () => {
|
|
37
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
38
|
+
expect(getPreferences()).toEqual({
|
|
39
|
+
defaultAgent: 'claude-code',
|
|
40
|
+
enabledAgents: ['claude-code'],
|
|
41
|
+
installMethod: 'symlink',
|
|
42
|
+
defaultScope: 'local',
|
|
43
|
+
});
|
|
24
44
|
});
|
|
25
45
|
it('preferencesExist reflects whether the file is on disk', () => {
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
|
|
46
|
+
const { getPreferences, preferencesExist } = require('../../src/utils/config');
|
|
47
|
+
expect(preferencesExist()).toBe(false);
|
|
48
|
+
getPreferences();
|
|
49
|
+
expect(preferencesExist()).toBe(true);
|
|
29
50
|
});
|
|
30
51
|
it('saves and loads preferences correctly', () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const { getPreferences, savePreferences } = require('../../src/utils/config');
|
|
53
|
+
savePreferences({
|
|
54
|
+
defaultAgent: 'opencode',
|
|
55
|
+
enabledAgents: ['opencode'],
|
|
56
|
+
installMethod: 'copy',
|
|
57
|
+
defaultScope: 'local',
|
|
58
|
+
});
|
|
59
|
+
expect(getPreferences()).toEqual({
|
|
60
|
+
defaultAgent: 'opencode',
|
|
61
|
+
enabledAgents: ['opencode'],
|
|
62
|
+
installMethod: 'copy',
|
|
63
|
+
defaultScope: 'local',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('migrates defaultAgent without losing optional preferences', () => {
|
|
67
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
|
|
68
|
+
defaultAgent: 'claude-code',
|
|
69
|
+
installMethod: 'symlink',
|
|
70
|
+
defaultScope: 'local',
|
|
71
|
+
baseRemote: 'https://example.test/baseline.git',
|
|
72
|
+
channel: 'dev',
|
|
73
|
+
pins: { baseline: '1.4.0' },
|
|
74
|
+
futurePreference: { mode: 'preview' },
|
|
75
|
+
}));
|
|
76
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
77
|
+
const prefs = getPreferences();
|
|
78
|
+
expect(prefs.enabledAgents).toEqual(['claude-code']);
|
|
79
|
+
expect(prefs.baseRemote).toBe('https://example.test/baseline.git');
|
|
80
|
+
expect(prefs.channel).toBe('dev');
|
|
81
|
+
expect(prefs.pins).toEqual({ baseline: '1.4.0' });
|
|
82
|
+
expect(prefs.futurePreference)
|
|
83
|
+
.toEqual({ mode: 'preview' });
|
|
84
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, 'preferences.json'), 'utf8')))
|
|
85
|
+
.toEqual(prefs);
|
|
86
|
+
});
|
|
87
|
+
it('does not overwrite malformed preferences', () => {
|
|
88
|
+
const file = path_1.default.join(tmpHome, 'preferences.json');
|
|
89
|
+
fs_1.default.writeFileSync(file, '{"defaultAgent":');
|
|
90
|
+
const before = fs_1.default.readFileSync(file, 'utf8');
|
|
91
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
92
|
+
expect(() => getPreferences()).toThrow('preferences.json is not valid JSON');
|
|
93
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe(before);
|
|
94
|
+
});
|
|
95
|
+
it('deduplicates enabled agents and requires the default to remain enabled', () => {
|
|
96
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
|
|
97
|
+
defaultAgent: 'claude-code',
|
|
98
|
+
enabledAgents: ['claude-code', 'codex', 'codex'],
|
|
99
|
+
installMethod: 'symlink',
|
|
100
|
+
defaultScope: 'local',
|
|
101
|
+
}));
|
|
102
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
103
|
+
expect(getPreferences().enabledAgents).toEqual(['claude-code', 'codex']);
|
|
104
|
+
});
|
|
105
|
+
it('rejects preferences whose default agent is disabled', () => {
|
|
106
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify({
|
|
107
|
+
defaultAgent: 'claude-code',
|
|
108
|
+
enabledAgents: ['codex'],
|
|
109
|
+
installMethod: 'symlink',
|
|
110
|
+
defaultScope: 'local',
|
|
111
|
+
}));
|
|
112
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
113
|
+
expect(() => getPreferences())
|
|
114
|
+
.toThrow('preferences.json defaultAgent must be included in enabledAgents');
|
|
115
|
+
});
|
|
116
|
+
it.each([
|
|
117
|
+
['a non-object value', [], 'preferences.json must contain a JSON object'],
|
|
118
|
+
['an invalid defaultAgent', {
|
|
119
|
+
defaultAgent: 'unknown',
|
|
120
|
+
installMethod: 'symlink',
|
|
121
|
+
defaultScope: 'local',
|
|
122
|
+
}, 'preferences.json has an invalid defaultAgent'],
|
|
123
|
+
['an invalid installMethod', {
|
|
124
|
+
defaultAgent: 'claude-code',
|
|
125
|
+
installMethod: 'hardlink',
|
|
126
|
+
defaultScope: 'local',
|
|
127
|
+
}, 'preferences.json has an invalid installMethod'],
|
|
128
|
+
['an invalid defaultScope', {
|
|
129
|
+
defaultAgent: 'claude-code',
|
|
130
|
+
installMethod: 'symlink',
|
|
131
|
+
defaultScope: 'workspace',
|
|
132
|
+
}, 'preferences.json has an invalid defaultScope'],
|
|
133
|
+
['an invalid enabledAgents field', {
|
|
134
|
+
defaultAgent: 'claude-code',
|
|
135
|
+
enabledAgents: ['claude-code', 'unknown'],
|
|
136
|
+
installMethod: 'symlink',
|
|
137
|
+
defaultScope: 'local',
|
|
138
|
+
}, 'preferences.json has an invalid enabledAgents'],
|
|
139
|
+
])('rejects %s', (_name, value, message) => {
|
|
140
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpHome, 'preferences.json'), JSON.stringify(value));
|
|
141
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
142
|
+
expect(() => getPreferences()).toThrow(message);
|
|
143
|
+
});
|
|
144
|
+
it.each([
|
|
145
|
+
['an empty baseRemote', { baseRemote: '' }, 'preferences.json has an invalid baseRemote'],
|
|
146
|
+
['a non-string baseRemote', { baseRemote: 42 }, 'preferences.json has an invalid baseRemote'],
|
|
147
|
+
['an invalid channel', { channel: 'preview' }, 'preferences.json has an invalid channel'],
|
|
148
|
+
['an array pins value', { pins: [] }, 'preferences.json has invalid pins'],
|
|
149
|
+
['a null pins value', { pins: null }, 'preferences.json has invalid pins'],
|
|
150
|
+
['an empty pin name', { pins: { '': '1.2.3' } }, 'preferences.json has invalid pins'],
|
|
151
|
+
['an empty pin value', { pins: { baseline: '' } }, 'preferences.json has invalid pins'],
|
|
152
|
+
['a non-string pin value', { pins: { baseline: 123 } }, 'preferences.json has invalid pins'],
|
|
153
|
+
])('rejects %s without overwriting the file', (_name, invalidField, message) => {
|
|
154
|
+
const file = path_1.default.join(tmpHome, 'preferences.json');
|
|
155
|
+
const before = JSON.stringify({
|
|
156
|
+
defaultAgent: 'claude-code',
|
|
157
|
+
enabledAgents: ['claude-code'],
|
|
158
|
+
installMethod: 'symlink',
|
|
159
|
+
defaultScope: 'local',
|
|
160
|
+
...invalidField,
|
|
161
|
+
});
|
|
162
|
+
fs_1.default.writeFileSync(file, before);
|
|
163
|
+
const { getPreferences } = require('../../src/utils/config');
|
|
164
|
+
expect(() => getPreferences()).toThrow(message);
|
|
165
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).toBe(before);
|
|
166
|
+
});
|
|
167
|
+
it('rejects incomplete updates without dropping enabled agents', () => {
|
|
168
|
+
const config = require('../../src/utils/config');
|
|
169
|
+
config.savePreferences({
|
|
170
|
+
defaultAgent: 'claude-code',
|
|
171
|
+
enabledAgents: ['claude-code', 'codex'],
|
|
172
|
+
installMethod: 'symlink',
|
|
173
|
+
defaultScope: 'local',
|
|
174
|
+
});
|
|
175
|
+
const incomplete = {
|
|
176
|
+
defaultAgent: 'opencode',
|
|
177
|
+
installMethod: 'copy',
|
|
178
|
+
defaultScope: 'global',
|
|
179
|
+
};
|
|
180
|
+
expect(() => {
|
|
181
|
+
// @ts-expect-error enabledAgents is required for every preference update
|
|
182
|
+
config.savePreferences(incomplete);
|
|
183
|
+
}).toThrow('preferences.json has an invalid enabledAgents');
|
|
184
|
+
expect(config.getPreferences().enabledAgents).toEqual(['claude-code', 'codex']);
|
|
185
|
+
});
|
|
186
|
+
it('validates preferences before saving them', () => {
|
|
187
|
+
const { savePreferences } = require('../../src/utils/config');
|
|
188
|
+
expect(() => savePreferences({
|
|
189
|
+
defaultAgent: 'codex',
|
|
190
|
+
enabledAgents: ['claude-code'],
|
|
191
|
+
installMethod: 'symlink',
|
|
192
|
+
defaultScope: 'local',
|
|
193
|
+
})).toThrow('preferences.json defaultAgent must be included in enabledAgents');
|
|
194
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, 'preferences.json'))).toBe(false);
|
|
35
195
|
});
|
|
36
196
|
});
|