@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
import { formatDryRunPlan } from "../../src/formatters.js";
|
|
5
|
+
function stripAnsi(value) {
|
|
6
|
+
return stripVTControlCharacters(value);
|
|
7
|
+
}
|
|
8
|
+
describe("formatDryRunPlan", () => {
|
|
9
|
+
it("returns an empty string when there are no planned changes", () => {
|
|
10
|
+
assert.equal(formatDryRunPlan([]), "");
|
|
11
|
+
});
|
|
12
|
+
it("groups changes by agent, sorts groups, and renders supported detail fields", () => {
|
|
13
|
+
const plan = [
|
|
14
|
+
{
|
|
15
|
+
agent: "opencode",
|
|
16
|
+
kind: "config-patch",
|
|
17
|
+
skill: "beta",
|
|
18
|
+
target: "/targets/opencode.json",
|
|
19
|
+
verb: "patch-config",
|
|
20
|
+
patch: { enabled: true },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
agent: "claude-code",
|
|
24
|
+
kind: "file-write",
|
|
25
|
+
skill: "alpha",
|
|
26
|
+
source: "/source/alpha.md",
|
|
27
|
+
target: "/targets/alpha.md",
|
|
28
|
+
verb: "write-file",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
agent: "claude-code",
|
|
32
|
+
kind: "config-patch",
|
|
33
|
+
skill: "gamma",
|
|
34
|
+
target: "/targets/gamma.json",
|
|
35
|
+
verb: "unapply-patch",
|
|
36
|
+
patch: { enabled: null },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
agent: "claude-code",
|
|
40
|
+
kind: "toml-patch",
|
|
41
|
+
skill: "delta",
|
|
42
|
+
target: "/targets/delta.toml",
|
|
43
|
+
verb: "patch-toml",
|
|
44
|
+
patch: { approval_policy: "suggest" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
agent: "claude-code",
|
|
48
|
+
kind: "frontmatter-emit",
|
|
49
|
+
skill: "epsilon",
|
|
50
|
+
target: "/targets/epsilon.md",
|
|
51
|
+
verb: "emit-frontmatter",
|
|
52
|
+
frontmatter: { tools: ["github"] },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
agent: "claude-code",
|
|
56
|
+
kind: "file-write",
|
|
57
|
+
skill: "zeta",
|
|
58
|
+
target: "/targets/zeta.md",
|
|
59
|
+
verb: "remove",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const output = stripAnsi(formatDryRunPlan(plan));
|
|
63
|
+
assert.match(output, /^claude-code\n(?:[\s\S]*?)\nopencode\n/m, `expected claude-code group before opencode, got:\n${output}`);
|
|
64
|
+
assert.match(output, /write-file alpha/);
|
|
65
|
+
assert.match(output, /source: \/source\/alpha\.md/);
|
|
66
|
+
assert.match(output, /target: \/targets\/alpha\.md/);
|
|
67
|
+
assert.match(output, /undo:\s+\{"enabled":null\}/);
|
|
68
|
+
assert.match(output, /patch:\s+\{"approval_policy":"suggest"\}/);
|
|
69
|
+
assert.match(output, /frontmatter: \{"tools":\["github"\]\}/);
|
|
70
|
+
assert.match(output, /remove zeta/);
|
|
71
|
+
assert.doesNotMatch(output, /source: \/targets\/zeta\.md/);
|
|
72
|
+
assert.match(output, /patch-config beta/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
|
-
import { defaultRegistryPersistence, lookupDeployment, registerDeployment, registryPath, unregisterDeployment, verifyDeployment, } from "../../src/core/ownership.js";
|
|
5
|
+
import { defaultRegistryPersistence, lookupDeployment, registerDeployment, registryDirPath, registryPath, unregisterDeployment, verifyDeployment, } from "../../src/core/ownership.js";
|
|
6
6
|
import { exists, makeTmpDir } from "../helpers/fs.js";
|
|
7
7
|
describe("registerDeployment", () => {
|
|
8
8
|
it("creates registry file and adds entry", async () => {
|
|
@@ -56,7 +56,7 @@ describe("registerDeployment", () => {
|
|
|
56
56
|
await rm(home, { recursive: true });
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
|
-
it("sets registry file permissions to
|
|
59
|
+
it("sets registry file permissions to 0o600 on POSIX", {
|
|
60
60
|
skip: process.platform === "win32",
|
|
61
61
|
}, async () => {
|
|
62
62
|
const home = await makeTmpDir();
|
|
@@ -70,7 +70,27 @@ describe("registerDeployment", () => {
|
|
|
70
70
|
});
|
|
71
71
|
const { stat } = await import("node:fs/promises");
|
|
72
72
|
const statResult = await stat(registryPath(home));
|
|
73
|
-
assert.equal(statResult.mode & 0o777,
|
|
73
|
+
assert.equal(statResult.mode & 0o777, 0o600);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await rm(home, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it("sets registry directory permissions to 0o700 on POSIX", {
|
|
80
|
+
skip: process.platform === "win32",
|
|
81
|
+
}, async () => {
|
|
82
|
+
const home = await makeTmpDir();
|
|
83
|
+
try {
|
|
84
|
+
await registerDeployment(home, "/fake/target", {
|
|
85
|
+
kind: "skill-dir",
|
|
86
|
+
source: "/fake/source",
|
|
87
|
+
skill: "my-skill",
|
|
88
|
+
agent: "claude-code",
|
|
89
|
+
method: "symlink",
|
|
90
|
+
});
|
|
91
|
+
const { stat } = await import("node:fs/promises");
|
|
92
|
+
const statResult = await stat(registryDirPath(home));
|
|
93
|
+
assert.equal(statResult.mode & 0o777, 0o700);
|
|
74
94
|
}
|
|
75
95
|
finally {
|
|
76
96
|
await rm(home, { recursive: true });
|
|
@@ -121,6 +141,26 @@ describe("registerDeployment", () => {
|
|
|
121
141
|
await rm(home, { recursive: true, force: true });
|
|
122
142
|
}
|
|
123
143
|
});
|
|
144
|
+
it("refuses to write the registry through a symlinked state directory", {
|
|
145
|
+
skip: process.platform === "win32",
|
|
146
|
+
}, async () => {
|
|
147
|
+
const home = await makeTmpDir();
|
|
148
|
+
const elsewhere = await makeTmpDir();
|
|
149
|
+
try {
|
|
150
|
+
await symlink(elsewhere, registryDirPath(home), "dir");
|
|
151
|
+
await assert.rejects(registerDeployment(home, "/fake/target", {
|
|
152
|
+
kind: "skill-dir",
|
|
153
|
+
source: "/fake/source",
|
|
154
|
+
skill: "my-skill",
|
|
155
|
+
agent: "claude-code",
|
|
156
|
+
method: "copy",
|
|
157
|
+
}), /Refusing to use registry directory symlink/);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await rm(home, { recursive: true, force: true });
|
|
161
|
+
await rm(elsewhere, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
124
164
|
});
|
|
125
165
|
describe("unregisterDeployment", () => {
|
|
126
166
|
it("removes the entry for the given target", async () => {
|
|
@@ -219,6 +259,23 @@ describe("lookupDeployment", () => {
|
|
|
219
259
|
await rm(home, { recursive: true });
|
|
220
260
|
}
|
|
221
261
|
});
|
|
262
|
+
it("returns null when the registry file path is a symlink", {
|
|
263
|
+
skip: process.platform === "win32",
|
|
264
|
+
}, async () => {
|
|
265
|
+
const home = await makeTmpDir();
|
|
266
|
+
const elsewhere = await makeTmpDir();
|
|
267
|
+
try {
|
|
268
|
+
await mkdir(registryDirPath(home), { recursive: true });
|
|
269
|
+
await writeFile(path.join(elsewhere, "registry.json"), "{}");
|
|
270
|
+
await symlink(path.join(elsewhere, "registry.json"), registryPath(home), "file");
|
|
271
|
+
const entry = await lookupDeployment(home, "/anything");
|
|
272
|
+
assert.equal(entry, null);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await rm(home, { recursive: true, force: true });
|
|
276
|
+
await rm(elsewhere, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
222
279
|
});
|
|
223
280
|
describe("verifyDeployment", () => {
|
|
224
281
|
it("returns entry when all fields match", async () => {
|
|
@@ -124,6 +124,65 @@ describe("runPreflight", () => {
|
|
|
124
124
|
const implementationOnlyWarning = warnings.find((w) => w.kind === "config-authority" && /implementation-only/.test(w.message));
|
|
125
125
|
assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
|
|
126
126
|
});
|
|
127
|
+
it("emits shared-surface config-authority guidance when github-copilot agentRules ride through claude-code", async () => {
|
|
128
|
+
const manifest = {
|
|
129
|
+
...emptyManifest,
|
|
130
|
+
agentRules: [
|
|
131
|
+
{
|
|
132
|
+
name: "shared-rules",
|
|
133
|
+
path: "CLAUDE.md",
|
|
134
|
+
agents: ["claude-code", "github-copilot"],
|
|
135
|
+
scope: "repo",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
140
|
+
"claude-code",
|
|
141
|
+
"github-copilot",
|
|
142
|
+
]);
|
|
143
|
+
const warning = warnings.find((w) => w.kind === "config-authority" &&
|
|
144
|
+
w.message.includes('shared through "claude-code"'));
|
|
145
|
+
assert.ok(warning, `expected shared-surface warning, got: ${JSON.stringify(warnings)}`);
|
|
146
|
+
assert.match(warning.message, /requires the primary target to deploy/);
|
|
147
|
+
});
|
|
148
|
+
it("emits shared-surface config-authority guidance for antigravity repo rules", async () => {
|
|
149
|
+
const manifest = {
|
|
150
|
+
...emptyManifest,
|
|
151
|
+
agentRules: [
|
|
152
|
+
{
|
|
153
|
+
name: "gemini-rules",
|
|
154
|
+
path: "GEMINI.md",
|
|
155
|
+
agents: ["antigravity"],
|
|
156
|
+
scope: "repo",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
161
|
+
"antigravity",
|
|
162
|
+
]);
|
|
163
|
+
const warning = warnings.find((w) => w.kind === "config-authority" &&
|
|
164
|
+
w.message.includes('shared through "gemini-cli"'));
|
|
165
|
+
assert.ok(warning, `expected antigravity shared-via warning, got: ${JSON.stringify(warnings)}`);
|
|
166
|
+
assert.doesNotMatch(warning.message, /requires the primary target to deploy/);
|
|
167
|
+
});
|
|
168
|
+
it("emits provisional config-authority warning for supported gemini executionConfigs", async () => {
|
|
169
|
+
const manifest = {
|
|
170
|
+
...emptyManifest,
|
|
171
|
+
executionConfigs: [
|
|
172
|
+
{
|
|
173
|
+
name: "safe-mode",
|
|
174
|
+
agents: ["gemini-cli"],
|
|
175
|
+
config: { sandbox: "workspace-write" },
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
180
|
+
"gemini-cli",
|
|
181
|
+
]);
|
|
182
|
+
const capabilityWarning = warnings.find((w) => w.kind === "config-authority" && /execution-config/.test(w.message));
|
|
183
|
+
assert.ok(capabilityWarning, `expected executionConfig warning, got: ${JSON.stringify(warnings)}`);
|
|
184
|
+
assert.match(capabilityWarning.message, /provisional/);
|
|
185
|
+
});
|
|
127
186
|
});
|
|
128
187
|
describe("github-copilot devcontainer support", () => {
|
|
129
188
|
it("emits no capability warning for devcontainer MCP when Copilot is detected and devcontainer scope is targeted", async () => {
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
5
|
import { AGENT_REGISTRY } from "../../src/config/agents.js";
|
|
6
6
|
import { resolveAgentDetectPath, resolveAgentSkillPath, resolveHome, } from "../../src/core/resolve.js";
|
|
7
|
-
import { resolveTargetTemplate } from "../../src/core/runtime-paths.js";
|
|
7
|
+
import { resolveRuntimePaths, resolveTargetTemplate, } from "../../src/core/runtime-paths.js";
|
|
8
8
|
import { UserError } from "../../src/errors.js";
|
|
9
9
|
const posixJoin = path.posix.join;
|
|
10
10
|
function getAgent(id) {
|
|
@@ -113,7 +113,73 @@ describe("resolveTargetTemplate", () => {
|
|
|
113
113
|
const result = resolveTargetTemplate("{home}/.claude/settings.json", home);
|
|
114
114
|
assert.equal(result, `${home}/.claude/settings.json`);
|
|
115
115
|
});
|
|
116
|
+
it("resolves appdata, local_appdata, xdg_config, repo, and workspace roots", () => {
|
|
117
|
+
const savedAppData = process.env.APPDATA;
|
|
118
|
+
const savedLocalAppData = process.env.LOCALAPPDATA;
|
|
119
|
+
const savedXdg = process.env.XDG_CONFIG_HOME;
|
|
120
|
+
try {
|
|
121
|
+
process.env.APPDATA = "/env/appdata";
|
|
122
|
+
process.env.LOCALAPPDATA = "/env/local";
|
|
123
|
+
process.env.XDG_CONFIG_HOME = "/env/xdg";
|
|
124
|
+
assert.equal(resolveTargetTemplate("{appdata}/opencode/opencode.json", "/home/user"), "/env/appdata/opencode/opencode.json");
|
|
125
|
+
assert.equal(resolveTargetTemplate("{local_appdata}/Temp/config.json", "/home/user"), "/env/local/Temp/config.json");
|
|
126
|
+
assert.equal(resolveTargetTemplate("{xdg_config}/opencode/config.json", "/home/user"), "/env/xdg/opencode/config.json");
|
|
127
|
+
assert.equal(resolveTargetTemplate("{repo}/.claude/mcp.json", "/home/user", "/repo"), "/repo/.claude/mcp.json");
|
|
128
|
+
assert.equal(resolveTargetTemplate("{workspace}/CLAUDE.md", "/home/user", "/repo"), "/repo/CLAUDE.md");
|
|
129
|
+
assert.equal(resolveTargetTemplate("{workspace}/CLAUDE.md", "/home/user", "/repo", "/workspace"), "/workspace/CLAUDE.md");
|
|
130
|
+
assert.equal(resolveTargetTemplate("{home}", "/home/user"), "/home/user");
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (savedAppData === undefined)
|
|
134
|
+
delete process.env.APPDATA;
|
|
135
|
+
else
|
|
136
|
+
process.env.APPDATA = savedAppData;
|
|
137
|
+
if (savedLocalAppData === undefined)
|
|
138
|
+
delete process.env.LOCALAPPDATA;
|
|
139
|
+
else
|
|
140
|
+
process.env.LOCALAPPDATA = savedLocalAppData;
|
|
141
|
+
if (savedXdg === undefined)
|
|
142
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
143
|
+
else
|
|
144
|
+
process.env.XDG_CONFIG_HOME = savedXdg;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
it("throws for invalid templates and missing repo or workspace roots", () => {
|
|
148
|
+
assert.throws(() => resolveTargetTemplate("relative/path.txt", "/home/user"), /Invalid target template/);
|
|
149
|
+
assert.throws(() => resolveTargetTemplate("{repo}/x", "/home/user"), /no repo directory was provided/);
|
|
150
|
+
assert.throws(() => resolveTargetTemplate("{workspace}/x", "/home/user"), /no workspace directory was provided/);
|
|
151
|
+
});
|
|
116
152
|
it("throws when the template escapes the placeholder root", () => {
|
|
117
153
|
assert.throws(() => resolveTargetTemplate("{home}/../.ssh/config", "/home/user"), /outside its placeholder root/);
|
|
118
154
|
});
|
|
119
155
|
});
|
|
156
|
+
describe("resolveRuntimePaths", () => {
|
|
157
|
+
it("uses absolute env vars and ignores relative overrides", () => {
|
|
158
|
+
const savedAppData = process.env.APPDATA;
|
|
159
|
+
const savedLocalAppData = process.env.LOCALAPPDATA;
|
|
160
|
+
const savedXdg = process.env.XDG_CONFIG_HOME;
|
|
161
|
+
try {
|
|
162
|
+
process.env.APPDATA = "/custom/appdata";
|
|
163
|
+
process.env.LOCALAPPDATA = "relative/local";
|
|
164
|
+
process.env.XDG_CONFIG_HOME = "relative/xdg";
|
|
165
|
+
const paths = resolveRuntimePaths("/home/user");
|
|
166
|
+
assert.equal(paths.appdata, "/custom/appdata");
|
|
167
|
+
assert.equal(paths.localAppdata, "/home/user/AppData/Local");
|
|
168
|
+
assert.equal(paths.xdgConfig, "/home/user/.config");
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (savedAppData === undefined)
|
|
172
|
+
delete process.env.APPDATA;
|
|
173
|
+
else
|
|
174
|
+
process.env.APPDATA = savedAppData;
|
|
175
|
+
if (savedLocalAppData === undefined)
|
|
176
|
+
delete process.env.LOCALAPPDATA;
|
|
177
|
+
else
|
|
178
|
+
process.env.LOCALAPPDATA = savedLocalAppData;
|
|
179
|
+
if (savedXdg === undefined)
|
|
180
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
181
|
+
else
|
|
182
|
+
process.env.XDG_CONFIG_HOME = savedXdg;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { chmod, mkdir, readFile, rm,
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
5
|
import { lookupDeployment, registerDeployment, } from "../../src/core/ownership.js";
|
|
@@ -153,192 +153,28 @@ describe("planRevertAll", () => {
|
|
|
153
153
|
action.skill === "gemini-safety"), "expected planRevertAll to include Gemini execution config revert action");
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
|
-
describe("executeRevert
|
|
157
|
-
it("
|
|
156
|
+
describe("executeRevert reserved state protections", () => {
|
|
157
|
+
it("refuses to modify inception-engine state paths from manifest-managed actions", async () => {
|
|
158
158
|
const home = await makeTmpDir();
|
|
159
|
-
const sourceDir = await makeTmpDir();
|
|
160
159
|
try {
|
|
161
|
-
const
|
|
162
|
-
const target = actions[0]?.target;
|
|
163
|
-
// Create symlink and register in registry
|
|
160
|
+
const target = path.join(home, ".inception-engine", "registry.json");
|
|
164
161
|
await mkdir(path.dirname(target), { recursive: true });
|
|
165
|
-
await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
175
|
-
assert.equal(succeeded, 1);
|
|
176
|
-
assert.equal(skipped, 0);
|
|
177
|
-
assert.ok(!(await exists(target)));
|
|
178
|
-
// Registry entry should be removed
|
|
179
|
-
const entry = await lookupDeployment(home, target);
|
|
180
|
-
assert.equal(entry, null);
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
await rm(home, { recursive: true, force: true });
|
|
184
|
-
await rm(sourceDir, { recursive: true });
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
it("removes a copied directory registered in the deployment registry", async () => {
|
|
188
|
-
const home = await makeTmpDir();
|
|
189
|
-
try {
|
|
190
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
191
|
-
const target = actions[0]?.target;
|
|
192
|
-
await mkdir(target, { recursive: true });
|
|
193
|
-
await writeFile(path.join(target, "SKILL.md"), "test");
|
|
194
|
-
await registerDeployment(home, target, {
|
|
195
|
-
kind: "skill-dir",
|
|
196
|
-
source: "/original/source",
|
|
197
|
-
skill: "test-skill",
|
|
198
|
-
agent: "claude-code",
|
|
199
|
-
method: "copy",
|
|
200
|
-
});
|
|
201
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
202
|
-
assert.equal(succeeded, 1);
|
|
203
|
-
assert.equal(skipped, 0);
|
|
204
|
-
assert.ok(!(await exists(target)));
|
|
205
|
-
// Registry entry should be removed
|
|
206
|
-
const entry = await lookupDeployment(home, target);
|
|
207
|
-
assert.equal(entry, null);
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
await rm(home, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
it("skips missing targets", async () => {
|
|
214
|
-
const home = await makeTmpDir();
|
|
215
|
-
try {
|
|
216
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
217
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
218
|
-
assert.equal(succeeded, 0);
|
|
219
|
-
assert.equal(skipped, 1);
|
|
220
|
-
}
|
|
221
|
-
finally {
|
|
222
|
-
await rm(home, { recursive: true });
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
it("records a failure when removal is blocked by permissions", async () => {
|
|
226
|
-
const home = await makeTmpDir();
|
|
227
|
-
const sourceDir = await makeTmpDir();
|
|
228
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
229
|
-
const target = actions[0]?.target ?? "";
|
|
230
|
-
const targetParent = path.dirname(target);
|
|
231
|
-
try {
|
|
232
|
-
// Create the target directory and register it
|
|
233
|
-
await mkdir(target, { recursive: true });
|
|
234
|
-
await writeFile(path.join(target, "SKILL.md"), "test");
|
|
235
|
-
await registerDeployment(home, target, {
|
|
236
|
-
kind: "skill-dir",
|
|
237
|
-
source: sourceDir,
|
|
238
|
-
skill: "test-skill",
|
|
239
|
-
agent: "claude-code",
|
|
240
|
-
method: "copy",
|
|
241
|
-
});
|
|
242
|
-
// Make the parent directory non-writable so rm() fails
|
|
243
|
-
await chmod(targetParent, 0o555);
|
|
244
|
-
const { succeeded, skipped, failed } = await executeRevert(actions, false, false, home);
|
|
162
|
+
await writeFile(target, "{}");
|
|
163
|
+
const { failed, succeeded } = await executeRevert([
|
|
164
|
+
{
|
|
165
|
+
kind: "file-write",
|
|
166
|
+
skill: "state-target",
|
|
167
|
+
agent: "claude-code",
|
|
168
|
+
target,
|
|
169
|
+
},
|
|
170
|
+
], false, false, home);
|
|
245
171
|
assert.equal(succeeded, 0);
|
|
246
|
-
assert.equal(skipped, 0);
|
|
247
172
|
assert.equal(failed.length, 1);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await chmod(targetParent, 0o755);
|
|
251
|
-
await rm(home, { recursive: true, force: true });
|
|
252
|
-
await rm(sourceDir, { recursive: true, force: true });
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
it("does not remove in dry-run mode", async () => {
|
|
256
|
-
const home = await makeTmpDir();
|
|
257
|
-
const sourceDir = await makeTmpDir();
|
|
258
|
-
try {
|
|
259
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
260
|
-
const target = actions[0]?.target;
|
|
261
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
262
|
-
await symlink(sourceDir, target, "dir");
|
|
263
|
-
await registerDeployment(home, target, {
|
|
264
|
-
kind: "skill-dir",
|
|
265
|
-
source: sourceDir,
|
|
266
|
-
skill: "test-skill",
|
|
267
|
-
agent: "claude-code",
|
|
268
|
-
method: "symlink",
|
|
269
|
-
});
|
|
270
|
-
const { succeeded } = await executeRevert(actions, true, false, home);
|
|
271
|
-
assert.equal(succeeded, 1);
|
|
272
|
-
assert.ok(await exists(target), "symlink should still exist after dry-run");
|
|
273
|
-
// Registry entry should still exist after dry-run
|
|
274
|
-
const entry = await lookupDeployment(home, target);
|
|
275
|
-
assert.ok(entry, "registry entry should still exist after dry-run");
|
|
276
|
-
}
|
|
277
|
-
finally {
|
|
278
|
-
await rm(home, { recursive: true, force: true });
|
|
279
|
-
await rm(sourceDir, { recursive: true });
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
it("skips target that exists but is not in registry", async () => {
|
|
283
|
-
const home = await makeTmpDir();
|
|
284
|
-
const sourceDir = await makeTmpDir();
|
|
285
|
-
try {
|
|
286
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
287
|
-
const target = actions[0]?.target;
|
|
288
|
-
// Create symlink but do NOT register — should be treated as unmanaged
|
|
289
|
-
await writeFile(path.join(sourceDir, "SKILL.md"), "---");
|
|
290
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
291
|
-
await symlink(sourceDir, target, "dir");
|
|
292
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
293
|
-
assert.equal(succeeded, 0);
|
|
294
|
-
assert.equal(skipped, 1);
|
|
295
|
-
assert.ok(await exists(target), "symlink should still exist — not managed");
|
|
296
|
-
}
|
|
297
|
-
finally {
|
|
298
|
-
await rm(home, { recursive: true, force: true });
|
|
299
|
-
await rm(sourceDir, { recursive: true });
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
it("skips directory that exists but is not in registry", async () => {
|
|
303
|
-
const home = await makeTmpDir();
|
|
304
|
-
try {
|
|
305
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
306
|
-
const target = actions[0]?.target;
|
|
307
|
-
await mkdir(target, { recursive: true });
|
|
308
|
-
await writeFile(path.join(target, "SKILL.md"), "user content");
|
|
309
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
310
|
-
assert.equal(succeeded, 0);
|
|
311
|
-
assert.equal(skipped, 1);
|
|
312
|
-
assert.ok(await exists(target), "directory should still exist — not in registry");
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
await rm(home, { recursive: true, force: true });
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
it("skips target whose registry entry has mismatched skill or agent", async () => {
|
|
319
|
-
const home = await makeTmpDir();
|
|
320
|
-
const sourceDir = await makeTmpDir();
|
|
321
|
-
try {
|
|
322
|
-
const actions = planRevert(testManifest, ["claude-code"], home);
|
|
323
|
-
const target = actions[0]?.target;
|
|
324
|
-
// Create symlink and register under a different skill/agent
|
|
325
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
326
|
-
await symlink(sourceDir, target, "dir");
|
|
327
|
-
await registerDeployment(home, target, {
|
|
328
|
-
kind: "skill-dir",
|
|
329
|
-
source: sourceDir,
|
|
330
|
-
skill: "different-skill",
|
|
331
|
-
agent: "codex",
|
|
332
|
-
method: "symlink",
|
|
333
|
-
});
|
|
334
|
-
const { succeeded, skipped } = await executeRevert(actions, false, false, home);
|
|
335
|
-
assert.equal(succeeded, 0);
|
|
336
|
-
assert.equal(skipped, 1);
|
|
337
|
-
assert.ok(await exists(target), "symlink should still exist — registry entry does not match");
|
|
173
|
+
assert.match(failed[0]?.error ?? "", /Refusing to modify inception-engine state directory/);
|
|
174
|
+
assert.equal(await readFile(target, "utf-8"), "{}");
|
|
338
175
|
}
|
|
339
176
|
finally {
|
|
340
177
|
await rm(home, { recursive: true, force: true });
|
|
341
|
-
await rm(sourceDir, { recursive: true });
|
|
342
178
|
}
|
|
343
179
|
});
|
|
344
180
|
});
|
|
@@ -905,9 +741,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
|
|
|
905
741
|
assert.ok(agents.includes("codex"));
|
|
906
742
|
});
|
|
907
743
|
});
|
|
908
|
-
describe("executeRevert — mcpServer and agentRule integration", {
|
|
909
|
-
skip: process.platform === "win32",
|
|
910
|
-
}, () => {
|
|
744
|
+
describe("executeRevert — mcpServer and agentRule integration", () => {
|
|
911
745
|
it("reverts a deployed mcpServer config-patch and unregisters it", async () => {
|
|
912
746
|
const home = await makeTmpDir();
|
|
913
747
|
try {
|