@kuznai/inception-engine 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/src/core/deploy.js +65 -1
- package/dist/src/core/ownership.d.ts +1 -0
- package/dist/src/core/ownership.js +34 -4
- package/dist/src/core/revert.js +81 -17
- package/dist/test/helpers/path.d.ts +2 -2
- package/dist/test/helpers/path.js +3 -3
- package/dist/test/os/posix/deploy.test.js +395 -0
- package/dist/test/os/posix/revert.test.js +188 -0
- package/dist/test/os/windows/deploy.test.d.ts +1 -0
- package/dist/test/os/windows/deploy.test.js +238 -0
- package/dist/test/unit/adapters.test.js +139 -2
- package/dist/test/unit/capabilities.test.js +44 -1
- package/dist/test/unit/cli.test.js +162 -0
- package/dist/test/unit/deploy.test.js +62 -647
- package/dist/test/unit/formatters.test.d.ts +1 -0
- package/dist/test/unit/formatters.test.js +74 -0
- package/dist/test/unit/ownership.test.js +61 -4
- package/dist/test/unit/preflight.test.js +59 -0
- package/dist/test/unit/resolve.test.js +67 -1
- package/dist/test/unit/revert.test.js +16 -182
- package/package.json +1 -1
- package/dist/test/os/windows/agentRules-integration.test.js +0 -245
- package/dist/test/os/windows/revert-integration.test.js +0 -72
- /package/dist/test/os/{windows/agentRules-integration.test.d.ts → posix/deploy.test.d.ts} +0 -0
- /package/dist/test/os/{windows/revert-integration.test.d.ts → posix/revert.test.d.ts} +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { executeDeploy, planDeploy } from "../../../src/core/deploy.js";
|
|
6
|
+
import { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
|
|
7
|
+
import { UserError } from "../../../src/errors.js";
|
|
8
|
+
import { exists, makeTmpDir } from "../../helpers/fs.js";
|
|
9
|
+
import { createSkillSource, testSkillManifest, } from "../../helpers/skill-dir.js";
|
|
10
|
+
describe("executeDeploy (Windows)", {
|
|
11
|
+
skip: process.platform !== "win32",
|
|
12
|
+
}, () => {
|
|
13
|
+
it("creates copies", async () => {
|
|
14
|
+
const sourceDir = await makeTmpDir();
|
|
15
|
+
const home = await makeTmpDir();
|
|
16
|
+
try {
|
|
17
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
18
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
19
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
20
|
+
assert.equal(succeeded, 1);
|
|
21
|
+
assert.equal(failed.length, 0);
|
|
22
|
+
assert.ok(await exists(actions[0]?.target));
|
|
23
|
+
assert.ok(await exists(path.join(actions[0]?.target, "SKILL.md")));
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
27
|
+
await rm(home, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
it("registers deployment in registry on copy deploy", async () => {
|
|
31
|
+
const sourceDir = await makeTmpDir();
|
|
32
|
+
const home = await makeTmpDir();
|
|
33
|
+
try {
|
|
34
|
+
const skillSource = await createSkillSource(sourceDir, "skills/test-skill");
|
|
35
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
36
|
+
await executeDeploy(actions, false, false, home);
|
|
37
|
+
const entry = await lookupDeployment(home, actions[0]?.target);
|
|
38
|
+
assert.ok(entry);
|
|
39
|
+
if (entry.kind !== "skill-dir") {
|
|
40
|
+
assert.fail("Expected skill-dir");
|
|
41
|
+
}
|
|
42
|
+
assert.equal(entry.skill, "test-skill");
|
|
43
|
+
assert.equal(entry.agent, "claude-code");
|
|
44
|
+
assert.equal(entry.source, skillSource);
|
|
45
|
+
assert.equal(entry.method, "copy");
|
|
46
|
+
assert.ok(!(await exists(path.join(skillSource, ".inception-totem"))), "no .inception-totem should be written to source directory");
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
50
|
+
await rm(home, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it("does not create copies in dry-run mode", async () => {
|
|
54
|
+
const sourceDir = await makeTmpDir();
|
|
55
|
+
const home = await makeTmpDir();
|
|
56
|
+
try {
|
|
57
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
58
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
59
|
+
const { succeeded, failed } = await executeDeploy(actions, true, false, home);
|
|
60
|
+
assert.equal(succeeded, 1);
|
|
61
|
+
assert.equal(failed.length, 0);
|
|
62
|
+
assert.ok(!(await exists(actions[0]?.target)));
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
66
|
+
await rm(home, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("overwrites an existing copy with a matching registry entry", async () => {
|
|
70
|
+
const sourceDir = await makeTmpDir();
|
|
71
|
+
const home = await makeTmpDir();
|
|
72
|
+
try {
|
|
73
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
74
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
75
|
+
await executeDeploy(actions, false, false, home);
|
|
76
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
77
|
+
assert.equal(succeeded, 1);
|
|
78
|
+
assert.equal(failed.length, 0);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
82
|
+
await rm(home, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
it("reports missing source during planning", async () => {
|
|
86
|
+
const home = await makeTmpDir();
|
|
87
|
+
try {
|
|
88
|
+
await assert.rejects(planDeploy(testSkillManifest, "/nonexistent/source", ["claude-code"], home), (err) => {
|
|
89
|
+
assert.ok(err instanceof UserError);
|
|
90
|
+
assert.equal(err.code, "DEPLOY_FAILED");
|
|
91
|
+
assert.match(err.message, /source not found/);
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await rm(home, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it("refuses to overwrite an unmanaged target", async () => {
|
|
100
|
+
const sourceDir = await makeTmpDir();
|
|
101
|
+
const home = await makeTmpDir();
|
|
102
|
+
try {
|
|
103
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
104
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
105
|
+
const target = actions[0]?.target;
|
|
106
|
+
await mkdir(target, { recursive: true });
|
|
107
|
+
await writeFile(path.join(target, "something.txt"), "user content");
|
|
108
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
109
|
+
assert.equal(succeeded, 0);
|
|
110
|
+
assert.equal(failed.length, 1);
|
|
111
|
+
assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
|
|
112
|
+
assert.ok(await exists(path.join(target, "something.txt")));
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
116
|
+
await rm(home, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
it("refuses to overwrite a target with a mismatched registry entry", async () => {
|
|
120
|
+
const sourceDir = await makeTmpDir();
|
|
121
|
+
const home = await makeTmpDir();
|
|
122
|
+
try {
|
|
123
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
124
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
125
|
+
const target = actions[0]?.target;
|
|
126
|
+
await mkdir(target, { recursive: true });
|
|
127
|
+
await writeFile(path.join(target, "SKILL.md"), "other content");
|
|
128
|
+
await registerDeployment(home, target, {
|
|
129
|
+
kind: "skill-dir",
|
|
130
|
+
source: "/completely/different/source",
|
|
131
|
+
skill: "different-skill",
|
|
132
|
+
agent: "codex",
|
|
133
|
+
method: "copy",
|
|
134
|
+
});
|
|
135
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
136
|
+
assert.equal(succeeded, 0);
|
|
137
|
+
assert.equal(failed.length, 1);
|
|
138
|
+
assert.ok(failed[0]?.error.includes("not managed by inception-engine"));
|
|
139
|
+
assert.ok(await exists(path.join(target, "SKILL.md")));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
143
|
+
await rm(home, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
it("removes the backup after a successful redeploy", async () => {
|
|
147
|
+
const sourceDir = await makeTmpDir();
|
|
148
|
+
const home = await makeTmpDir();
|
|
149
|
+
try {
|
|
150
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
151
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
152
|
+
const target = actions[0]?.target;
|
|
153
|
+
const backupPath = `${target}.inception-backup`;
|
|
154
|
+
await executeDeploy(actions, false, false, home);
|
|
155
|
+
await executeDeploy(actions, false, false, home);
|
|
156
|
+
assert.ok(!(await exists(backupPath)));
|
|
157
|
+
assert.ok(await exists(target));
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
161
|
+
await rm(home, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe("atomic redeploy behavior (Windows)", {
|
|
166
|
+
skip: process.platform !== "win32",
|
|
167
|
+
}, () => {
|
|
168
|
+
it("cleans up stale .inception-backup from a previous failed attempt", async () => {
|
|
169
|
+
const sourceDir = await makeTmpDir();
|
|
170
|
+
const home = await makeTmpDir();
|
|
171
|
+
try {
|
|
172
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
173
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
174
|
+
const target = actions[0]?.target;
|
|
175
|
+
const backupPath = `${target}.inception-backup`;
|
|
176
|
+
await executeDeploy(actions, false, false, home);
|
|
177
|
+
await mkdir(backupPath, { recursive: true });
|
|
178
|
+
await writeFile(path.join(backupPath, "stale.txt"), "leftover");
|
|
179
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
180
|
+
assert.equal(succeeded, 1);
|
|
181
|
+
assert.equal(failed.length, 0);
|
|
182
|
+
assert.ok(!(await exists(backupPath)));
|
|
183
|
+
assert.ok(await exists(target));
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
187
|
+
await rm(home, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
it("does not create a backup on first deploy", async () => {
|
|
191
|
+
const sourceDir = await makeTmpDir();
|
|
192
|
+
const home = await makeTmpDir();
|
|
193
|
+
try {
|
|
194
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
195
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
196
|
+
const target = actions[0]?.target;
|
|
197
|
+
const backupPath = `${target}.inception-backup`;
|
|
198
|
+
const { succeeded } = await executeDeploy(actions, false, false, home);
|
|
199
|
+
assert.equal(succeeded, 1);
|
|
200
|
+
assert.ok(await exists(target));
|
|
201
|
+
assert.ok(!(await exists(backupPath)));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
205
|
+
await rm(home, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
it("restores the backup when deploy fails because the registry is not writable", async () => {
|
|
209
|
+
const sourceDir = await makeTmpDir();
|
|
210
|
+
const home = await makeTmpDir();
|
|
211
|
+
try {
|
|
212
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
213
|
+
const { actions } = await planDeploy(testSkillManifest, sourceDir, ["claude-code"], home);
|
|
214
|
+
const target = actions[0]?.target;
|
|
215
|
+
const backupPath = `${target}.inception-backup`;
|
|
216
|
+
const { succeeded: firstSucceeded } = await executeDeploy(actions, false, false, home);
|
|
217
|
+
assert.equal(firstSucceeded, 1);
|
|
218
|
+
const registryFile = path.join(home, ".inception-engine", "registry.json");
|
|
219
|
+
await chmod(registryFile, 0o444);
|
|
220
|
+
const { succeeded, failed } = await executeDeploy(actions, false, false, home);
|
|
221
|
+
assert.equal(succeeded, 0);
|
|
222
|
+
assert.equal(failed.length, 1);
|
|
223
|
+
assert.ok(!(await exists(backupPath)));
|
|
224
|
+
assert.ok(await exists(target));
|
|
225
|
+
assert.ok(await exists(path.join(target, "SKILL.md")));
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
try {
|
|
229
|
+
await chmod(path.join(home, ".inception-engine", "registry.json"), 0o666);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// best effort
|
|
233
|
+
}
|
|
234
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
235
|
+
await rm(home, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -2,9 +2,9 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { realpath, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
|
-
import { compileMcpServerActions } from "../../src/core/adapters/mcp.js";
|
|
6
5
|
import { compileHookActions } from "../../src/core/adapters/hooks.js";
|
|
7
|
-
import {
|
|
6
|
+
import { compileMcpServerActions } from "../../src/core/adapters/mcp.js";
|
|
7
|
+
import { compilePermissionsActions, compilePermissionsReverts, } from "../../src/core/adapters/permissions.js";
|
|
8
8
|
import { compileAgentRuleActions, compileAgentRuleReverts, } from "../../src/core/adapters/rules.js";
|
|
9
9
|
import { makeTmpDir } from "../helpers/fs.js";
|
|
10
10
|
import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
|
|
@@ -831,6 +831,34 @@ describe("compilePermissionsActions", () => {
|
|
|
831
831
|
assert.ok(agents.includes("codex"));
|
|
832
832
|
});
|
|
833
833
|
});
|
|
834
|
+
describe("compilePermissionsReverts", () => {
|
|
835
|
+
it("returns config and toml revert actions for supported agents", () => {
|
|
836
|
+
const actions = compilePermissionsReverts({
|
|
837
|
+
name: "multi",
|
|
838
|
+
agents: ["claude-code", "codex", "opencode"],
|
|
839
|
+
config: {},
|
|
840
|
+
}, null, "/home/test");
|
|
841
|
+
assert.equal(actions.length, 3);
|
|
842
|
+
assert.deepEqual(actions.map((action) => [action.agent, action.kind]), [
|
|
843
|
+
["claude-code", "config-patch"],
|
|
844
|
+
["codex", "toml-patch"],
|
|
845
|
+
["opencode", "config-patch"],
|
|
846
|
+
]);
|
|
847
|
+
assertPathEndsWith(actions[0]?.target ?? "", ".claude/settings.json", `expected claude-code target under .claude/settings.json, got: ${actions[0]?.target}`);
|
|
848
|
+
assertPathEndsWith(actions[1]?.target ?? "", ".codex/config.toml", `expected codex target under .codex/config.toml, got: ${actions[1]?.target}`);
|
|
849
|
+
assertPathEndsWith(actions[2]?.target ?? "", "opencode/opencode.json", `expected opencode target under opencode/opencode.json, got: ${actions[2]?.target}`);
|
|
850
|
+
});
|
|
851
|
+
it("skips unsupported agents and respects agentFilter", () => {
|
|
852
|
+
const actions = compilePermissionsReverts({
|
|
853
|
+
name: "filtered",
|
|
854
|
+
agents: ["claude-code", "gemini-cli", "codex"],
|
|
855
|
+
config: {},
|
|
856
|
+
}, ["codex"], "/home/test");
|
|
857
|
+
assert.equal(actions.length, 1);
|
|
858
|
+
assert.equal(actions[0]?.agent, "codex");
|
|
859
|
+
assert.equal(actions[0]?.kind, "toml-patch");
|
|
860
|
+
});
|
|
861
|
+
});
|
|
834
862
|
describe("compileHookActions", () => {
|
|
835
863
|
const validClaudeHookConfig = {
|
|
836
864
|
hooks: {
|
|
@@ -1015,6 +1043,68 @@ describe("compileAgentDefinitionActions", () => {
|
|
|
1015
1043
|
await rm(dir, { recursive: true });
|
|
1016
1044
|
}
|
|
1017
1045
|
});
|
|
1046
|
+
it("emits a warning and skips repo-scoped definitions when repo is missing", async () => {
|
|
1047
|
+
const compile = await getAdapter();
|
|
1048
|
+
const dir = await makeTmpDir();
|
|
1049
|
+
try {
|
|
1050
|
+
await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
|
|
1051
|
+
const realRoot = await realpath(dir);
|
|
1052
|
+
const { actions, warnings } = await compile({
|
|
1053
|
+
name: "my-agent",
|
|
1054
|
+
agents: ["claude-code"],
|
|
1055
|
+
path: "my-agent.md",
|
|
1056
|
+
scope: "repo",
|
|
1057
|
+
}, dir, dir, realRoot, ["claude-code"], "/home/test");
|
|
1058
|
+
assert.equal(actions.length, 0);
|
|
1059
|
+
assert.equal(warnings.length, 1);
|
|
1060
|
+
assert.equal(warnings[0]?.kind, "confidence");
|
|
1061
|
+
assert.match(warnings[0]?.message ?? "", /scope "repo" requires a repository path/);
|
|
1062
|
+
}
|
|
1063
|
+
finally {
|
|
1064
|
+
await rm(dir, { recursive: true });
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
it("uses the repo path as a workspace fallback for workspace-scoped definitions", async () => {
|
|
1068
|
+
const compile = await getAdapter();
|
|
1069
|
+
const dir = await makeTmpDir();
|
|
1070
|
+
try {
|
|
1071
|
+
await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
|
|
1072
|
+
const realRoot = await realpath(dir);
|
|
1073
|
+
const { actions, warnings } = await compile({
|
|
1074
|
+
name: "my-agent",
|
|
1075
|
+
agents: ["gemini-cli"],
|
|
1076
|
+
path: "my-agent.md",
|
|
1077
|
+
scope: "workspace",
|
|
1078
|
+
}, dir, dir, realRoot, ["gemini-cli"], "/home/test", "/repo/test");
|
|
1079
|
+
assert.equal(warnings.length, 0);
|
|
1080
|
+
assert.equal(actions.length, 1);
|
|
1081
|
+
assert.equal(normalizeSlashes(actions[0]?.target ?? ""), "/repo/test/.gemini/agents/my-agent.md");
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
await rm(dir, { recursive: true });
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
it("emits a warning and skips workspace-scoped definitions when repo and workspace are missing", async () => {
|
|
1088
|
+
const compile = await getAdapter();
|
|
1089
|
+
const dir = await makeTmpDir();
|
|
1090
|
+
try {
|
|
1091
|
+
await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
|
|
1092
|
+
const realRoot = await realpath(dir);
|
|
1093
|
+
const { actions, warnings } = await compile({
|
|
1094
|
+
name: "my-agent",
|
|
1095
|
+
agents: ["gemini-cli"],
|
|
1096
|
+
path: "my-agent.md",
|
|
1097
|
+
scope: "workspace",
|
|
1098
|
+
}, dir, dir, realRoot, ["gemini-cli"], "/home/test");
|
|
1099
|
+
assert.equal(actions.length, 0);
|
|
1100
|
+
assert.equal(warnings.length, 1);
|
|
1101
|
+
assert.equal(warnings[0]?.kind, "confidence");
|
|
1102
|
+
assert.match(warnings[0]?.message ?? "", /scope "workspace" requires a workspace or repository path/);
|
|
1103
|
+
}
|
|
1104
|
+
finally {
|
|
1105
|
+
await rm(dir, { recursive: true });
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1018
1108
|
it("returns a file-write action for opencode with correct target", async () => {
|
|
1019
1109
|
const compile = await getAdapter();
|
|
1020
1110
|
const dir = await makeTmpDir();
|
|
@@ -1292,3 +1382,50 @@ describe("compileAgentDefinitionActions", () => {
|
|
|
1292
1382
|
}
|
|
1293
1383
|
});
|
|
1294
1384
|
});
|
|
1385
|
+
describe("compileAgentDefinitionReverts", () => {
|
|
1386
|
+
async function getAdapter() {
|
|
1387
|
+
const { compileAgentDefinitionReverts } = await import("../../src/core/adapters/agent-definitions.js");
|
|
1388
|
+
return compileAgentDefinitionReverts;
|
|
1389
|
+
}
|
|
1390
|
+
it("returns revert actions for supported markdown and toml targets", async () => {
|
|
1391
|
+
const compile = await getAdapter();
|
|
1392
|
+
const actions = compile({
|
|
1393
|
+
name: "my-agent",
|
|
1394
|
+
agents: ["claude-code", "gemini-cli", "github-copilot"],
|
|
1395
|
+
path: "my-agent.md",
|
|
1396
|
+
scope: "repo",
|
|
1397
|
+
}, null, "/home/test", "/repo/test");
|
|
1398
|
+
assert.equal(actions.length, 3);
|
|
1399
|
+
assert.deepEqual(actions.map((action) => action.agent), ["claude-code", "gemini-cli", "github-copilot"]);
|
|
1400
|
+
assert.equal(normalizeSlashes(actions[0]?.target ?? ""), "/repo/test/.claude/agents/my-agent.md");
|
|
1401
|
+
assert.equal(normalizeSlashes(actions[1]?.target ?? ""), "/repo/test/.gemini/agents/my-agent.md");
|
|
1402
|
+
assert.equal(normalizeSlashes(actions[2]?.target ?? ""), "/repo/test/.github/copilot/agents/my-agent.md");
|
|
1403
|
+
});
|
|
1404
|
+
it("returns a global toml revert for gemini-cli", async () => {
|
|
1405
|
+
const compile = await getAdapter();
|
|
1406
|
+
const actions = compile({
|
|
1407
|
+
name: "my-agent",
|
|
1408
|
+
agents: ["gemini-cli"],
|
|
1409
|
+
path: "my-agent.toml",
|
|
1410
|
+
scope: "global",
|
|
1411
|
+
}, null, "/home/test");
|
|
1412
|
+
assert.equal(actions.length, 1);
|
|
1413
|
+
assert.equal(actions[0]?.agent, "gemini-cli");
|
|
1414
|
+
assert.equal(normalizeSlashes(actions[0]?.target ?? ""), "/home/test/.gemini/agents/my-agent.toml");
|
|
1415
|
+
});
|
|
1416
|
+
it("skips unsupported targets and missing workspace or repo requirements", async () => {
|
|
1417
|
+
const compile = await getAdapter();
|
|
1418
|
+
assert.deepEqual(compile({
|
|
1419
|
+
name: "my-agent",
|
|
1420
|
+
agents: ["codex"],
|
|
1421
|
+
path: "my-agent.md",
|
|
1422
|
+
scope: "repo",
|
|
1423
|
+
}, null, "/home/test", "/repo/test"), []);
|
|
1424
|
+
assert.deepEqual(compile({
|
|
1425
|
+
name: "my-agent",
|
|
1426
|
+
agents: ["gemini-cli"],
|
|
1427
|
+
path: "my-agent.md",
|
|
1428
|
+
scope: "workspace",
|
|
1429
|
+
}, null, "/home/test"), []);
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { describeCapabilityConfidence, planCapabilityForDeploy, shouldInitIncludeAgent, } from "../../src/core/capabilities.js";
|
|
3
|
+
import { describeCapabilityConfidence, planCapabilityForDeploy, resolveCapabilitySurface, shouldInitIncludeAgent, } from "../../src/core/capabilities.js";
|
|
4
4
|
describe("capabilities planner", () => {
|
|
5
5
|
it("treats antigravity agentRules as deployable but not init-default", () => {
|
|
6
6
|
const plan = planCapabilityForDeploy({
|
|
@@ -68,4 +68,47 @@ describe("capabilities planner", () => {
|
|
|
68
68
|
assert.equal(confidence.confidence, "documented");
|
|
69
69
|
assert.equal(confidence.message, null);
|
|
70
70
|
});
|
|
71
|
+
it("treats antigravity agentRules as shared-via gemini-cli but directly deployable when the primary is absent", () => {
|
|
72
|
+
const plan = planCapabilityForDeploy({
|
|
73
|
+
agentId: "antigravity",
|
|
74
|
+
capability: "agentRules",
|
|
75
|
+
entryName: "gemini-compatible-rules",
|
|
76
|
+
targetAgentIds: ["antigravity"],
|
|
77
|
+
scope: "repo",
|
|
78
|
+
});
|
|
79
|
+
assert.equal(plan.outcome, "action");
|
|
80
|
+
const confidence = describeCapabilityConfidence("antigravity", "agentRules", "repo");
|
|
81
|
+
assert.match(confidence.message ?? "", /shared through "gemini-cli"/);
|
|
82
|
+
assert.doesNotMatch(confidence.message ?? "", /requires the primary target to deploy/);
|
|
83
|
+
});
|
|
84
|
+
it("marks gemini-cli hooks as planned and excludes them from init defaults", () => {
|
|
85
|
+
const surface = resolveCapabilitySurface("gemini-cli", "hooks");
|
|
86
|
+
assert.equal(surface.supportStatus, "planned");
|
|
87
|
+
assert.equal(surface.plannedSurface, "settings.json hooks field");
|
|
88
|
+
const plan = planCapabilityForDeploy({
|
|
89
|
+
agentId: "gemini-cli",
|
|
90
|
+
capability: "hooks",
|
|
91
|
+
entryName: "future-hooks",
|
|
92
|
+
targetAgentIds: ["gemini-cli"],
|
|
93
|
+
});
|
|
94
|
+
assert.equal(plan.outcome, "warn");
|
|
95
|
+
if (plan.outcome !== "warn")
|
|
96
|
+
return;
|
|
97
|
+
assert.match(plan.warning.message, /planned via settings\.json hooks field/);
|
|
98
|
+
assert.equal(shouldInitIncludeAgent("gemini-cli", "hooks"), false);
|
|
99
|
+
});
|
|
100
|
+
it("reports unsupported confidence for codex hooks and opencode workspace rules", () => {
|
|
101
|
+
const codexHooks = describeCapabilityConfidence("codex", "hooks");
|
|
102
|
+
assert.match(codexHooks.message ?? "", /hooks surface is unsupported/);
|
|
103
|
+
const workspaceRules = describeCapabilityConfidence("opencode", "agentRules", "workspace");
|
|
104
|
+
assert.match(workspaceRules.message ?? "", /workspace-local instruction surface distinct from repo-local AGENTS\.md/);
|
|
105
|
+
});
|
|
106
|
+
it("resolves executionConfigs support records for supported and unsupported agents", () => {
|
|
107
|
+
const gemini = resolveCapabilitySurface("gemini-cli", "executionConfigs");
|
|
108
|
+
assert.equal(gemini.supportStatus, "supported");
|
|
109
|
+
assert.equal(gemini.schemaLabel, "Settings execution config");
|
|
110
|
+
const claude = resolveCapabilitySurface("claude-code", "executionConfigs");
|
|
111
|
+
assert.equal(claude.supportStatus, "unsupported");
|
|
112
|
+
assert.equal(claude.schemaLabel, "an unsupported surface");
|
|
113
|
+
});
|
|
71
114
|
});
|
|
@@ -244,6 +244,21 @@ describe("init command", () => {
|
|
|
244
244
|
await rm(dir, { recursive: true });
|
|
245
245
|
}
|
|
246
246
|
});
|
|
247
|
+
it("skips skill directories whose folder name is not a valid skill name", async () => {
|
|
248
|
+
const dir = await makeTmpDir();
|
|
249
|
+
try {
|
|
250
|
+
await mkdir(path.join(dir, "-bad-skill"), { recursive: true });
|
|
251
|
+
await writeFile(path.join(dir, "-bad-skill", "SKILL.md"), "---\nname: bad\ndescription: test\n---\n");
|
|
252
|
+
const { stdout, code } = await run(["init", dir]);
|
|
253
|
+
assert.equal(code, 0);
|
|
254
|
+
assert.ok(stdout.includes("not a valid skill name"), `expected invalid-skill warning, got: ${stdout}`);
|
|
255
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
256
|
+
assert.deepEqual(manifest.skills, []);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await rm(dir, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
247
262
|
it("generated manifest always includes all five section keys", async () => {
|
|
248
263
|
const dir = await makeTmpDir();
|
|
249
264
|
try {
|
|
@@ -348,6 +363,20 @@ describe("init command", () => {
|
|
|
348
363
|
await rm(dir, { recursive: true });
|
|
349
364
|
}
|
|
350
365
|
});
|
|
366
|
+
it("skips agentRules files whose basename cannot derive a valid name", async () => {
|
|
367
|
+
const dir = await makeTmpDir();
|
|
368
|
+
try {
|
|
369
|
+
await writeFile(path.join(dir, "-.md"), "# invalid rules\n");
|
|
370
|
+
const { code, stdout } = await run(["init", dir]);
|
|
371
|
+
assert.equal(code, 0);
|
|
372
|
+
assert.ok(stdout.includes("could not derive a valid agentRules name"), `expected invalid agentRules warning, got: ${stdout}`);
|
|
373
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
374
|
+
assert.deepEqual(manifest.agentRules, []);
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await rm(dir, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
351
380
|
it("agentRules name collision with skill gets -rules suffix", async () => {
|
|
352
381
|
const dir = await makeTmpDir();
|
|
353
382
|
try {
|
|
@@ -831,4 +860,137 @@ describe("init command", () => {
|
|
|
831
860
|
await rm(dir, { recursive: true });
|
|
832
861
|
}
|
|
833
862
|
});
|
|
863
|
+
it("skips invalid agentDefinition file names and renames colliding discovered definitions", async () => {
|
|
864
|
+
const dir = await makeTmpDir();
|
|
865
|
+
try {
|
|
866
|
+
await mkdir(path.join(dir, "helper"), { recursive: true });
|
|
867
|
+
await writeFile(path.join(dir, "helper", "SKILL.md"), "---\nname: helper\ndescription: helper skill\n---\n");
|
|
868
|
+
await mkdir(path.join(dir, ".claude", "agents"), { recursive: true });
|
|
869
|
+
await writeFile(path.join(dir, ".claude", "agents", "helper.md"), "---\nname: helper\ndescription: helper agent\n---\n# Helper\n");
|
|
870
|
+
await writeFile(path.join(dir, ".claude", "agents", "-.md"), "---\nname: invalid\ndescription: invalid agent\n---\n# Invalid\n");
|
|
871
|
+
const { code, stdout } = await run(["init", dir]);
|
|
872
|
+
assert.equal(code, 0);
|
|
873
|
+
assert.ok(stdout.includes('agentDefinitions name "helper" collides'), `expected collision warning, got: ${stdout}`);
|
|
874
|
+
assert.ok(stdout.includes("could not derive a valid agentDefinitions name"), `expected invalid agentDefinitions warning, got: ${stdout}`);
|
|
875
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
876
|
+
assert.deepEqual(manifest.agentDefinitions.map((entry) => entry.name), ["helper-agent"]);
|
|
877
|
+
assert.ok(normalizeSlashes(manifest.agentDefinitions[0]?.path ?? "").includes(".claude/agents/helper.md"));
|
|
878
|
+
}
|
|
879
|
+
finally {
|
|
880
|
+
await rm(dir, { recursive: true });
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
it("agent-definitions-manifest.json takes precedence over discovered definitions", async () => {
|
|
884
|
+
const dir = await makeTmpDir();
|
|
885
|
+
try {
|
|
886
|
+
await mkdir(path.join(dir, ".claude", "agents"), { recursive: true });
|
|
887
|
+
await writeFile(path.join(dir, ".claude", "agents", "discovered.md"), "---\nname: discovered\ndescription: discovered agent\n---\n# Discovered\n");
|
|
888
|
+
await writeFile(path.join(dir, "agent-definitions-manifest.json"), JSON.stringify([
|
|
889
|
+
{
|
|
890
|
+
name: "sidecar-agent",
|
|
891
|
+
path: "agents/sidecar-agent.md",
|
|
892
|
+
agents: ["opencode"],
|
|
893
|
+
scope: "repo",
|
|
894
|
+
},
|
|
895
|
+
]));
|
|
896
|
+
const { code } = await run(["init", dir]);
|
|
897
|
+
assert.equal(code, 0);
|
|
898
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
899
|
+
assert.deepEqual(manifest.agentDefinitions, [
|
|
900
|
+
{
|
|
901
|
+
name: "sidecar-agent",
|
|
902
|
+
path: "agents/sidecar-agent.md",
|
|
903
|
+
agents: ["opencode"],
|
|
904
|
+
scope: "repo",
|
|
905
|
+
},
|
|
906
|
+
]);
|
|
907
|
+
}
|
|
908
|
+
finally {
|
|
909
|
+
await rm(dir, { recursive: true });
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
it("invalid JSON in agent-definitions-manifest.json produces discovered definitions and a warning", async () => {
|
|
913
|
+
const dir = await makeTmpDir();
|
|
914
|
+
try {
|
|
915
|
+
await mkdir(path.join(dir, ".claude", "agents"), { recursive: true });
|
|
916
|
+
await writeFile(path.join(dir, ".claude", "agents", "discovered.md"), "---\nname: discovered\ndescription: discovered agent\n---\n# Discovered\n");
|
|
917
|
+
await writeFile(path.join(dir, "agent-definitions-manifest.json"), "{not json}");
|
|
918
|
+
const { code, stdout } = await run(["init", dir]);
|
|
919
|
+
assert.equal(code, 0);
|
|
920
|
+
assert.ok(stdout.includes("agent-definitions-manifest.json"), "stdout should mention agent-definitions-manifest.json warning");
|
|
921
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
922
|
+
assert.equal(manifest.agentDefinitions.length, 1);
|
|
923
|
+
assert.equal(manifest.agentDefinitions[0]?.name, "discovered");
|
|
924
|
+
}
|
|
925
|
+
finally {
|
|
926
|
+
await rm(dir, { recursive: true });
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
it("non-array agent-definitions-manifest.json produces discovered definitions and a warning", async () => {
|
|
930
|
+
const dir = await makeTmpDir();
|
|
931
|
+
try {
|
|
932
|
+
await mkdir(path.join(dir, ".claude", "agents"), { recursive: true });
|
|
933
|
+
await writeFile(path.join(dir, ".claude", "agents", "discovered.md"), "---\nname: discovered\ndescription: discovered agent\n---\n# Discovered\n");
|
|
934
|
+
await writeFile(path.join(dir, "agent-definitions-manifest.json"), JSON.stringify({ name: "not-an-array" }));
|
|
935
|
+
const { code, stdout } = await run(["init", dir]);
|
|
936
|
+
assert.equal(code, 0);
|
|
937
|
+
assert.ok(stdout.includes("agent-definitions-manifest.json"), "stdout should mention agent-definitions-manifest.json warning");
|
|
938
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
939
|
+
assert.equal(manifest.agentDefinitions.length, 1);
|
|
940
|
+
assert.equal(manifest.agentDefinitions[0]?.name, "discovered");
|
|
941
|
+
}
|
|
942
|
+
finally {
|
|
943
|
+
await rm(dir, { recursive: true });
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
it("agent-definitions-manifest.json with mixed valid and invalid entries keeps only valid sidecar entries", async () => {
|
|
947
|
+
const dir = await makeTmpDir();
|
|
948
|
+
try {
|
|
949
|
+
await writeFile(path.join(dir, "agent-definitions-manifest.json"), JSON.stringify([
|
|
950
|
+
{
|
|
951
|
+
name: "valid-agent",
|
|
952
|
+
path: "agents/valid-agent.md",
|
|
953
|
+
agents: ["claude-code"],
|
|
954
|
+
scope: "repo",
|
|
955
|
+
},
|
|
956
|
+
{ name: "invalid-agent" },
|
|
957
|
+
]));
|
|
958
|
+
const { code, stdout } = await run(["init", dir]);
|
|
959
|
+
assert.equal(code, 0);
|
|
960
|
+
assert.ok(stdout.includes("agent-definitions-manifest.json"), "stdout should mention invalid agent-definitions-manifest.json entry");
|
|
961
|
+
const manifest = JSON.parse((await import("node:fs/promises").then((m) => m.readFile(path.join(dir, "inception.json"), "utf-8"))).toString());
|
|
962
|
+
assert.equal(manifest.agentDefinitions.length, 1);
|
|
963
|
+
assert.equal(manifest.agentDefinitions[0]?.name, "valid-agent");
|
|
964
|
+
assert.deepEqual(manifest.agentDefinitions[0]?.agents, ["claude-code"]);
|
|
965
|
+
}
|
|
966
|
+
finally {
|
|
967
|
+
await rm(dir, { recursive: true });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
it("verbose init output includes detailed discovered sections", async () => {
|
|
971
|
+
const dir = await makeTmpDir();
|
|
972
|
+
try {
|
|
973
|
+
await mkdir(path.join(dir, "alpha"), { recursive: true });
|
|
974
|
+
await writeFile(path.join(dir, "alpha", "SKILL.md"), "---\nname: alpha\ndescription: test\n---\n");
|
|
975
|
+
await writeFile(path.join(dir, "CLAUDE.md"), "# rules\n");
|
|
976
|
+
await mkdir(path.join(dir, ".claude", "agents"), { recursive: true });
|
|
977
|
+
await writeFile(path.join(dir, ".claude", "agents", "reviewer.md"), "---\nname: reviewer\ndescription: test\n---\n# Agent\n");
|
|
978
|
+
await writeFile(path.join(dir, "mcp-servers.json"), JSON.stringify([
|
|
979
|
+
{
|
|
980
|
+
name: "my-server",
|
|
981
|
+
agents: ["claude-code"],
|
|
982
|
+
config: { command: "npx" },
|
|
983
|
+
},
|
|
984
|
+
]));
|
|
985
|
+
const { code, stdout } = await run(["init", dir, "--verbose"]);
|
|
986
|
+
assert.equal(code, 0);
|
|
987
|
+
assert.ok(stdout.includes("alpha → alpha"), stdout);
|
|
988
|
+
assert.ok(stdout.includes("agentRules:"), stdout);
|
|
989
|
+
assert.ok(stdout.includes("agentDefinitions:"), stdout);
|
|
990
|
+
assert.ok(stdout.includes("mcpServers:"), stdout);
|
|
991
|
+
}
|
|
992
|
+
finally {
|
|
993
|
+
await rm(dir, { recursive: true });
|
|
994
|
+
}
|
|
995
|
+
});
|
|
834
996
|
});
|