@dexto/tools-plan 1.5.8 → 1.6.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/errors.cjs +126 -0
- package/dist/errors.js +99 -64
- package/dist/index.cjs +36 -0
- package/dist/index.d.cts +224 -0
- package/dist/index.d.ts +1 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -39
- package/dist/plan-service-getter.cjs +16 -0
- package/dist/plan-service-getter.d.ts +4 -0
- package/dist/plan-service-getter.d.ts.map +1 -0
- package/dist/plan-service-getter.js +0 -0
- package/dist/plan-service.cjs +247 -0
- package/dist/plan-service.d.ts +2 -2
- package/dist/plan-service.d.ts.map +1 -1
- package/dist/plan-service.js +201 -215
- package/dist/plan-service.test.cjs +227 -0
- package/dist/plan-service.test.js +200 -216
- package/dist/tool-factory-config.cjs +38 -0
- package/dist/tool-factory-config.d.ts +32 -0
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +13 -0
- package/dist/tool-factory.cjs +71 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +40 -0
- package/dist/tool-factory.test.cjs +96 -0
- package/dist/tool-factory.test.d.ts +7 -0
- package/dist/tool-factory.test.d.ts.map +1 -0
- package/dist/tool-factory.test.js +95 -0
- package/dist/tools/plan-create-tool.cjs +102 -0
- package/dist/tools/plan-create-tool.d.ts +15 -3
- package/dist/tools/plan-create-tool.d.ts.map +1 -1
- package/dist/tools/plan-create-tool.js +77 -71
- package/dist/tools/plan-create-tool.test.cjs +174 -0
- package/dist/tools/plan-create-tool.test.js +142 -109
- package/dist/tools/plan-read-tool.cjs +65 -0
- package/dist/tools/plan-read-tool.d.ts +6 -3
- package/dist/tools/plan-read-tool.d.ts.map +1 -1
- package/dist/tools/plan-read-tool.js +39 -38
- package/dist/tools/plan-read-tool.test.cjs +109 -0
- package/dist/tools/plan-read-tool.test.js +78 -75
- package/dist/tools/plan-review-tool.cjs +98 -0
- package/dist/tools/plan-review-tool.d.ts +14 -5
- package/dist/tools/plan-review-tool.d.ts.map +1 -1
- package/dist/tools/plan-review-tool.js +73 -83
- package/dist/tools/plan-update-tool.cjs +92 -0
- package/dist/tools/plan-update-tool.d.ts +12 -3
- package/dist/tools/plan-update-tool.d.ts.map +1 -1
- package/dist/tools/plan-update-tool.js +65 -69
- package/dist/tools/plan-update-tool.test.cjs +203 -0
- package/dist/tools/plan-update-tool.test.js +171 -142
- package/dist/types.cjs +44 -0
- package/dist/types.js +17 -24
- package/package.json +8 -8
- package/.dexto-plugin/plugin.json +0 -7
- package/dist/tool-provider.d.ts +0 -44
- package/dist/tool-provider.d.ts.map +0 -1
- package/dist/tool-provider.js +0 -81
- package/dist/tool-provider.test.d.ts +0 -7
- package/dist/tool-provider.test.d.ts.map +0 -1
- package/dist/tool-provider.test.js +0 -185
- package/skills/plan/SKILL.md +0 -102
|
@@ -1,72 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { createPatch } from 'diff';
|
|
9
|
-
import { PlanError } from '../errors.js';
|
|
10
|
-
const PlanUpdateInputSchema = z
|
|
11
|
-
.object({
|
|
12
|
-
content: z.string().describe('Updated plan content in markdown format'),
|
|
13
|
-
})
|
|
14
|
-
.strict();
|
|
15
|
-
/**
|
|
16
|
-
* Generate diff preview for plan update
|
|
17
|
-
*/
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createPatch } from "diff";
|
|
3
|
+
import { createLocalToolCallHeader, defineTool } from "@dexto/core";
|
|
4
|
+
import { PlanError } from "../errors.js";
|
|
5
|
+
const PlanUpdateInputSchema = z.object({
|
|
6
|
+
content: z.string().describe("Updated plan content in markdown format")
|
|
7
|
+
}).strict();
|
|
18
8
|
function generateDiffPreview(filePath, originalContent, newContent) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
9
|
+
const unified = createPatch(filePath, originalContent, newContent, "before", "after", {
|
|
10
|
+
context: 3
|
|
11
|
+
});
|
|
12
|
+
const additions = (unified.match(/^\+[^+]/gm) || []).length;
|
|
13
|
+
const deletions = (unified.match(/^-[^-]/gm) || []).length;
|
|
14
|
+
return {
|
|
15
|
+
type: "diff",
|
|
16
|
+
title: "Update Plan",
|
|
17
|
+
unified,
|
|
18
|
+
filename: filePath,
|
|
19
|
+
additions,
|
|
20
|
+
deletions
|
|
21
|
+
};
|
|
31
22
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
23
|
+
function createPlanUpdateTool(getPlanService) {
|
|
24
|
+
return defineTool({
|
|
25
|
+
id: "plan_update",
|
|
26
|
+
description: "Update the existing implementation plan for this session. Shows a diff preview for approval before saving. The plan must already exist (use plan_create first).",
|
|
27
|
+
inputSchema: PlanUpdateInputSchema,
|
|
28
|
+
presentation: {
|
|
29
|
+
describeHeader: () => createLocalToolCallHeader({
|
|
30
|
+
title: "Update Plan"
|
|
31
|
+
}),
|
|
32
|
+
/**
|
|
33
|
+
* Generate diff preview for approval UI
|
|
34
|
+
*/
|
|
35
|
+
preview: async (input, context) => {
|
|
36
|
+
const resolvedPlanService = await getPlanService(context);
|
|
37
|
+
const { content: newContent } = input;
|
|
38
|
+
if (!context.sessionId) {
|
|
39
|
+
throw PlanError.sessionIdRequired();
|
|
40
|
+
}
|
|
41
|
+
const existing = await resolvedPlanService.read(context.sessionId);
|
|
42
|
+
if (!existing) {
|
|
43
|
+
throw PlanError.planNotFound(context.sessionId);
|
|
44
|
+
}
|
|
45
|
+
const planPath = resolvedPlanService.getPlanPath(context.sessionId);
|
|
46
|
+
return generateDiffPreview(planPath, existing.content, newContent);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async execute(input, context) {
|
|
50
|
+
const resolvedPlanService = await getPlanService(context);
|
|
51
|
+
const { content } = input;
|
|
52
|
+
if (!context.sessionId) {
|
|
53
|
+
throw PlanError.sessionIdRequired();
|
|
54
|
+
}
|
|
55
|
+
const result = await resolvedPlanService.update(context.sessionId, content);
|
|
56
|
+
const planPath = resolvedPlanService.getPlanPath(context.sessionId);
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
path: planPath,
|
|
60
|
+
status: result.meta.status,
|
|
61
|
+
_display: generateDiffPreview(planPath, result.oldContent, result.newContent)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
72
65
|
}
|
|
66
|
+
export {
|
|
67
|
+
createPlanUpdateTool
|
|
68
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_vitest = require("vitest");
|
|
25
|
+
var path = __toESM(require("node:path"), 1);
|
|
26
|
+
var fs = __toESM(require("node:fs/promises"), 1);
|
|
27
|
+
var os = __toESM(require("node:os"), 1);
|
|
28
|
+
var import_plan_update_tool = require("./plan-update-tool.js");
|
|
29
|
+
var import_plan_service = require("../plan-service.js");
|
|
30
|
+
var import_errors = require("../errors.js");
|
|
31
|
+
var import_core = require("@dexto/core");
|
|
32
|
+
const createMockLogger = () => {
|
|
33
|
+
const logger = {
|
|
34
|
+
debug: import_vitest.vi.fn(),
|
|
35
|
+
silly: import_vitest.vi.fn(),
|
|
36
|
+
info: import_vitest.vi.fn(),
|
|
37
|
+
warn: import_vitest.vi.fn(),
|
|
38
|
+
error: import_vitest.vi.fn(),
|
|
39
|
+
trackException: import_vitest.vi.fn(),
|
|
40
|
+
createChild: import_vitest.vi.fn(() => logger),
|
|
41
|
+
createFileOnlyChild: import_vitest.vi.fn(() => logger),
|
|
42
|
+
setLevel: import_vitest.vi.fn(),
|
|
43
|
+
getLevel: import_vitest.vi.fn(() => "debug"),
|
|
44
|
+
getLogFilePath: import_vitest.vi.fn(() => null),
|
|
45
|
+
destroy: import_vitest.vi.fn(async () => void 0)
|
|
46
|
+
};
|
|
47
|
+
return logger;
|
|
48
|
+
};
|
|
49
|
+
function createToolContext(logger, overrides = {}) {
|
|
50
|
+
return { logger, ...overrides };
|
|
51
|
+
}
|
|
52
|
+
(0, import_vitest.describe)("plan_update tool", () => {
|
|
53
|
+
let logger;
|
|
54
|
+
let tempDir;
|
|
55
|
+
let planService;
|
|
56
|
+
(0, import_vitest.beforeEach)(async () => {
|
|
57
|
+
logger = createMockLogger();
|
|
58
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-plan-update-test-"));
|
|
59
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
60
|
+
planService = new import_plan_service.PlanService({ basePath: tempDir }, logger);
|
|
61
|
+
import_vitest.vi.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
(0, import_vitest.afterEach)(async () => {
|
|
64
|
+
try {
|
|
65
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
(0, import_vitest.describe)("generatePreview", () => {
|
|
70
|
+
(0, import_vitest.it)("should return DiffDisplayData with unified diff", async () => {
|
|
71
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
72
|
+
const sessionId = "test-session";
|
|
73
|
+
const originalContent = "# Plan\n\n## Steps\n1. First step";
|
|
74
|
+
const newContent = "# Plan\n\n## Steps\n1. First step\n2. Second step";
|
|
75
|
+
const previewFn = tool.presentation?.preview;
|
|
76
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
77
|
+
await planService.create(sessionId, originalContent);
|
|
78
|
+
const preview = await previewFn(
|
|
79
|
+
{ content: newContent },
|
|
80
|
+
createToolContext(logger, { sessionId })
|
|
81
|
+
);
|
|
82
|
+
(0, import_vitest.expect)(preview.type).toBe("diff");
|
|
83
|
+
(0, import_vitest.expect)(preview.title).toBe("Update Plan");
|
|
84
|
+
(0, import_vitest.expect)(preview.filename).toContain(sessionId);
|
|
85
|
+
(0, import_vitest.expect)(preview.filename).toMatch(/plan\.md$/);
|
|
86
|
+
(0, import_vitest.expect)(preview.unified).toContain("-1. First step");
|
|
87
|
+
(0, import_vitest.expect)(preview.unified).toContain("+1. First step");
|
|
88
|
+
(0, import_vitest.expect)(preview.unified).toContain("+2. Second step");
|
|
89
|
+
(0, import_vitest.expect)(preview.additions).toBeGreaterThan(0);
|
|
90
|
+
});
|
|
91
|
+
(0, import_vitest.it)("should throw error when plan does not exist", async () => {
|
|
92
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
93
|
+
const sessionId = "test-session";
|
|
94
|
+
const previewFn = tool.presentation?.preview;
|
|
95
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
96
|
+
try {
|
|
97
|
+
await previewFn(
|
|
98
|
+
{ content: "# New Content" },
|
|
99
|
+
createToolContext(logger, { sessionId })
|
|
100
|
+
);
|
|
101
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
|
|
104
|
+
(0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
(0, import_vitest.it)("should throw error when sessionId is missing", async () => {
|
|
108
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
109
|
+
const previewFn = tool.presentation?.preview;
|
|
110
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
111
|
+
try {
|
|
112
|
+
await previewFn({ content: "# Content" }, createToolContext(logger));
|
|
113
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
114
|
+
} catch (error) {
|
|
115
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
|
|
116
|
+
(0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.SESSION_ID_REQUIRED);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
(0, import_vitest.it)("should show deletions in diff", async () => {
|
|
120
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
121
|
+
const sessionId = "test-session";
|
|
122
|
+
const originalContent = "# Plan\n\nLine to remove\nKeep this";
|
|
123
|
+
const newContent = "# Plan\n\nKeep this";
|
|
124
|
+
const previewFn = tool.presentation?.preview;
|
|
125
|
+
(0, import_vitest.expect)(previewFn).toBeDefined();
|
|
126
|
+
await planService.create(sessionId, originalContent);
|
|
127
|
+
const preview = await previewFn(
|
|
128
|
+
{ content: newContent },
|
|
129
|
+
createToolContext(logger, { sessionId })
|
|
130
|
+
);
|
|
131
|
+
(0, import_vitest.expect)(preview.deletions).toBeGreaterThan(0);
|
|
132
|
+
(0, import_vitest.expect)(preview.unified).toContain("-Line to remove");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
(0, import_vitest.describe)("execute", () => {
|
|
136
|
+
(0, import_vitest.it)("should update plan content and return success", async () => {
|
|
137
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
138
|
+
const sessionId = "test-session";
|
|
139
|
+
const originalContent = "# Original Plan";
|
|
140
|
+
const newContent = "# Updated Plan";
|
|
141
|
+
await planService.create(sessionId, originalContent);
|
|
142
|
+
const result = await tool.execute(
|
|
143
|
+
{ content: newContent },
|
|
144
|
+
createToolContext(logger, { sessionId })
|
|
145
|
+
);
|
|
146
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
147
|
+
(0, import_vitest.expect)(result.path).toContain(sessionId);
|
|
148
|
+
(0, import_vitest.expect)(result.path).toMatch(/plan\.md$/);
|
|
149
|
+
const plan = await planService.read(sessionId);
|
|
150
|
+
(0, import_vitest.expect)(plan.content).toBe(newContent);
|
|
151
|
+
});
|
|
152
|
+
(0, import_vitest.it)("should include _display data with diff", async () => {
|
|
153
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
154
|
+
const sessionId = "test-session";
|
|
155
|
+
await planService.create(sessionId, "# Original");
|
|
156
|
+
const result = await tool.execute(
|
|
157
|
+
{ content: "# Updated" },
|
|
158
|
+
createToolContext(logger, { sessionId })
|
|
159
|
+
);
|
|
160
|
+
(0, import_vitest.expect)(result._display).toBeDefined();
|
|
161
|
+
(0, import_vitest.expect)(result._display.type).toBe("diff");
|
|
162
|
+
(0, import_vitest.expect)(result._display.title).toBe("Update Plan");
|
|
163
|
+
(0, import_vitest.expect)(result._display.unified).toContain("-# Original");
|
|
164
|
+
(0, import_vitest.expect)(result._display.unified).toContain("+# Updated");
|
|
165
|
+
});
|
|
166
|
+
(0, import_vitest.it)("should throw error when plan does not exist", async () => {
|
|
167
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
168
|
+
const sessionId = "non-existent";
|
|
169
|
+
try {
|
|
170
|
+
await tool.execute(
|
|
171
|
+
{ content: "# Content" },
|
|
172
|
+
createToolContext(logger, { sessionId })
|
|
173
|
+
);
|
|
174
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
|
|
177
|
+
(0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
(0, import_vitest.it)("should throw error when sessionId is missing", async () => {
|
|
181
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
182
|
+
try {
|
|
183
|
+
await tool.execute({ content: "# Content" }, createToolContext(logger));
|
|
184
|
+
import_vitest.expect.fail("Should have thrown an error");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
|
|
187
|
+
(0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.SESSION_ID_REQUIRED);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
(0, import_vitest.it)("should preserve plan status after update", async () => {
|
|
191
|
+
const tool = (0, import_plan_update_tool.createPlanUpdateTool)(async () => planService);
|
|
192
|
+
const sessionId = "test-session";
|
|
193
|
+
await planService.create(sessionId, "# Plan");
|
|
194
|
+
await planService.updateMeta(sessionId, { status: "approved" });
|
|
195
|
+
await tool.execute(
|
|
196
|
+
{ content: "# Updated Plan" },
|
|
197
|
+
createToolContext(logger, { sessionId })
|
|
198
|
+
);
|
|
199
|
+
const plan = await planService.read(sessionId);
|
|
200
|
+
(0, import_vitest.expect)(plan.meta.status).toBe("approved");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -1,151 +1,180 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { PlanService } from '../plan-service.js';
|
|
12
|
-
import { PlanErrorCode } from '../errors.js';
|
|
13
|
-
import { DextoRuntimeError } from '@dexto/core';
|
|
14
|
-
// Create mock logger
|
|
15
|
-
const createMockLogger = () => ({
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { createPlanUpdateTool } from "./plan-update-tool.js";
|
|
6
|
+
import { PlanService } from "../plan-service.js";
|
|
7
|
+
import { PlanErrorCode } from "../errors.js";
|
|
8
|
+
import { DextoRuntimeError } from "@dexto/core";
|
|
9
|
+
const createMockLogger = () => {
|
|
10
|
+
const logger = {
|
|
16
11
|
debug: vi.fn(),
|
|
12
|
+
silly: vi.fn(),
|
|
17
13
|
info: vi.fn(),
|
|
18
14
|
warn: vi.fn(),
|
|
19
15
|
error: vi.fn(),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
trackException: vi.fn(),
|
|
17
|
+
createChild: vi.fn(() => logger),
|
|
18
|
+
createFileOnlyChild: vi.fn(() => logger),
|
|
19
|
+
setLevel: vi.fn(),
|
|
20
|
+
getLevel: vi.fn(() => "debug"),
|
|
21
|
+
getLogFilePath: vi.fn(() => null),
|
|
22
|
+
destroy: vi.fn(async () => void 0)
|
|
23
|
+
};
|
|
24
|
+
return logger;
|
|
25
|
+
};
|
|
26
|
+
function createToolContext(logger, overrides = {}) {
|
|
27
|
+
return { logger, ...overrides };
|
|
28
|
+
}
|
|
29
|
+
describe("plan_update tool", () => {
|
|
30
|
+
let logger;
|
|
31
|
+
let tempDir;
|
|
32
|
+
let planService;
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
logger = createMockLogger();
|
|
35
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-plan-update-test-"));
|
|
36
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
37
|
+
planService = new PlanService({ basePath: tempDir }, logger);
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
try {
|
|
42
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
describe("generatePreview", () => {
|
|
47
|
+
it("should return DiffDisplayData with unified diff", async () => {
|
|
48
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
49
|
+
const sessionId = "test-session";
|
|
50
|
+
const originalContent = "# Plan\n\n## Steps\n1. First step";
|
|
51
|
+
const newContent = "# Plan\n\n## Steps\n1. First step\n2. Second step";
|
|
52
|
+
const previewFn = tool.presentation?.preview;
|
|
53
|
+
expect(previewFn).toBeDefined();
|
|
54
|
+
await planService.create(sessionId, originalContent);
|
|
55
|
+
const preview = await previewFn(
|
|
56
|
+
{ content: newContent },
|
|
57
|
+
createToolContext(logger, { sessionId })
|
|
58
|
+
);
|
|
59
|
+
expect(preview.type).toBe("diff");
|
|
60
|
+
expect(preview.title).toBe("Update Plan");
|
|
61
|
+
expect(preview.filename).toContain(sessionId);
|
|
62
|
+
expect(preview.filename).toMatch(/plan\.md$/);
|
|
63
|
+
expect(preview.unified).toContain("-1. First step");
|
|
64
|
+
expect(preview.unified).toContain("+1. First step");
|
|
65
|
+
expect(preview.unified).toContain("+2. Second step");
|
|
66
|
+
expect(preview.additions).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
it("should throw error when plan does not exist", async () => {
|
|
69
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
70
|
+
const sessionId = "test-session";
|
|
71
|
+
const previewFn = tool.presentation?.preview;
|
|
72
|
+
expect(previewFn).toBeDefined();
|
|
73
|
+
try {
|
|
74
|
+
await previewFn(
|
|
75
|
+
{ content: "# New Content" },
|
|
76
|
+
createToolContext(logger, { sessionId })
|
|
77
|
+
);
|
|
78
|
+
expect.fail("Should have thrown an error");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
81
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
it("should throw error when sessionId is missing", async () => {
|
|
85
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
86
|
+
const previewFn = tool.presentation?.preview;
|
|
87
|
+
expect(previewFn).toBeDefined();
|
|
88
|
+
try {
|
|
89
|
+
await previewFn({ content: "# Content" }, createToolContext(logger));
|
|
90
|
+
expect.fail("Should have thrown an error");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
93
|
+
expect(error.code).toBe(PlanErrorCode.SESSION_ID_REQUIRED);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it("should show deletions in diff", async () => {
|
|
97
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
98
|
+
const sessionId = "test-session";
|
|
99
|
+
const originalContent = "# Plan\n\nLine to remove\nKeep this";
|
|
100
|
+
const newContent = "# Plan\n\nKeep this";
|
|
101
|
+
const previewFn = tool.presentation?.preview;
|
|
102
|
+
expect(previewFn).toBeDefined();
|
|
103
|
+
await planService.create(sessionId, originalContent);
|
|
104
|
+
const preview = await previewFn(
|
|
105
|
+
{ content: newContent },
|
|
106
|
+
createToolContext(logger, { sessionId })
|
|
107
|
+
);
|
|
108
|
+
expect(preview.deletions).toBeGreaterThan(0);
|
|
109
|
+
expect(preview.unified).toContain("-Line to remove");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("execute", () => {
|
|
113
|
+
it("should update plan content and return success", async () => {
|
|
114
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
115
|
+
const sessionId = "test-session";
|
|
116
|
+
const originalContent = "# Original Plan";
|
|
117
|
+
const newContent = "# Updated Plan";
|
|
118
|
+
await planService.create(sessionId, originalContent);
|
|
119
|
+
const result = await tool.execute(
|
|
120
|
+
{ content: newContent },
|
|
121
|
+
createToolContext(logger, { sessionId })
|
|
122
|
+
);
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.path).toContain(sessionId);
|
|
125
|
+
expect(result.path).toMatch(/plan\.md$/);
|
|
126
|
+
const plan = await planService.read(sessionId);
|
|
127
|
+
expect(plan.content).toBe(newContent);
|
|
128
|
+
});
|
|
129
|
+
it("should include _display data with diff", async () => {
|
|
130
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
131
|
+
const sessionId = "test-session";
|
|
132
|
+
await planService.create(sessionId, "# Original");
|
|
133
|
+
const result = await tool.execute(
|
|
134
|
+
{ content: "# Updated" },
|
|
135
|
+
createToolContext(logger, { sessionId })
|
|
136
|
+
);
|
|
137
|
+
expect(result._display).toBeDefined();
|
|
138
|
+
expect(result._display.type).toBe("diff");
|
|
139
|
+
expect(result._display.title).toBe("Update Plan");
|
|
140
|
+
expect(result._display.unified).toContain("-# Original");
|
|
141
|
+
expect(result._display.unified).toContain("+# Updated");
|
|
32
142
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
143
|
+
it("should throw error when plan does not exist", async () => {
|
|
144
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
145
|
+
const sessionId = "non-existent";
|
|
146
|
+
try {
|
|
147
|
+
await tool.execute(
|
|
148
|
+
{ content: "# Content" },
|
|
149
|
+
createToolContext(logger, { sessionId })
|
|
150
|
+
);
|
|
151
|
+
expect.fail("Should have thrown an error");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
154
|
+
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
155
|
+
}
|
|
40
156
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Path is now absolute, check it ends with the expected suffix
|
|
51
|
-
expect(preview.filename).toContain(sessionId);
|
|
52
|
-
expect(preview.filename).toMatch(/plan\.md$/);
|
|
53
|
-
expect(preview.unified).toContain('-1. First step');
|
|
54
|
-
expect(preview.unified).toContain('+1. First step');
|
|
55
|
-
expect(preview.unified).toContain('+2. Second step');
|
|
56
|
-
expect(preview.additions).toBeGreaterThan(0);
|
|
57
|
-
});
|
|
58
|
-
it('should throw error when plan does not exist', async () => {
|
|
59
|
-
const tool = createPlanUpdateTool(planService);
|
|
60
|
-
const sessionId = 'test-session';
|
|
61
|
-
try {
|
|
62
|
-
await tool.generatePreview({ content: '# New Content' }, { sessionId });
|
|
63
|
-
expect.fail('Should have thrown an error');
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
67
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
it('should throw error when sessionId is missing', async () => {
|
|
71
|
-
const tool = createPlanUpdateTool(planService);
|
|
72
|
-
try {
|
|
73
|
-
await tool.generatePreview({ content: '# Content' }, {});
|
|
74
|
-
expect.fail('Should have thrown an error');
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
78
|
-
expect(error.code).toBe(PlanErrorCode.SESSION_ID_REQUIRED);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
it('should show deletions in diff', async () => {
|
|
82
|
-
const tool = createPlanUpdateTool(planService);
|
|
83
|
-
const sessionId = 'test-session';
|
|
84
|
-
const originalContent = '# Plan\n\nLine to remove\nKeep this';
|
|
85
|
-
const newContent = '# Plan\n\nKeep this';
|
|
86
|
-
await planService.create(sessionId, originalContent);
|
|
87
|
-
const preview = (await tool.generatePreview({ content: newContent }, { sessionId }));
|
|
88
|
-
expect(preview.deletions).toBeGreaterThan(0);
|
|
89
|
-
expect(preview.unified).toContain('-Line to remove');
|
|
90
|
-
});
|
|
157
|
+
it("should throw error when sessionId is missing", async () => {
|
|
158
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
159
|
+
try {
|
|
160
|
+
await tool.execute({ content: "# Content" }, createToolContext(logger));
|
|
161
|
+
expect.fail("Should have thrown an error");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
164
|
+
expect(error.code).toBe(PlanErrorCode.SESSION_ID_REQUIRED);
|
|
165
|
+
}
|
|
91
166
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(result.path).toMatch(/plan\.md$/);
|
|
104
|
-
// Verify content was updated
|
|
105
|
-
const plan = await planService.read(sessionId);
|
|
106
|
-
expect(plan.content).toBe(newContent);
|
|
107
|
-
});
|
|
108
|
-
it('should include _display data with diff', async () => {
|
|
109
|
-
const tool = createPlanUpdateTool(planService);
|
|
110
|
-
const sessionId = 'test-session';
|
|
111
|
-
await planService.create(sessionId, '# Original');
|
|
112
|
-
const result = (await tool.execute({ content: '# Updated' }, { sessionId }));
|
|
113
|
-
expect(result._display).toBeDefined();
|
|
114
|
-
expect(result._display.type).toBe('diff');
|
|
115
|
-
expect(result._display.unified).toContain('-# Original');
|
|
116
|
-
expect(result._display.unified).toContain('+# Updated');
|
|
117
|
-
});
|
|
118
|
-
it('should throw error when plan does not exist', async () => {
|
|
119
|
-
const tool = createPlanUpdateTool(planService);
|
|
120
|
-
const sessionId = 'non-existent';
|
|
121
|
-
try {
|
|
122
|
-
await tool.execute({ content: '# Content' }, { sessionId });
|
|
123
|
-
expect.fail('Should have thrown an error');
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
127
|
-
expect(error.code).toBe(PlanErrorCode.PLAN_NOT_FOUND);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
it('should throw error when sessionId is missing', async () => {
|
|
131
|
-
const tool = createPlanUpdateTool(planService);
|
|
132
|
-
try {
|
|
133
|
-
await tool.execute({ content: '# Content' }, {});
|
|
134
|
-
expect.fail('Should have thrown an error');
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
expect(error).toBeInstanceOf(DextoRuntimeError);
|
|
138
|
-
expect(error.code).toBe(PlanErrorCode.SESSION_ID_REQUIRED);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
it('should preserve plan status after update', async () => {
|
|
142
|
-
const tool = createPlanUpdateTool(planService);
|
|
143
|
-
const sessionId = 'test-session';
|
|
144
|
-
await planService.create(sessionId, '# Plan');
|
|
145
|
-
await planService.updateMeta(sessionId, { status: 'approved' });
|
|
146
|
-
await tool.execute({ content: '# Updated Plan' }, { sessionId });
|
|
147
|
-
const plan = await planService.read(sessionId);
|
|
148
|
-
expect(plan.meta.status).toBe('approved');
|
|
149
|
-
});
|
|
167
|
+
it("should preserve plan status after update", async () => {
|
|
168
|
+
const tool = createPlanUpdateTool(async () => planService);
|
|
169
|
+
const sessionId = "test-session";
|
|
170
|
+
await planService.create(sessionId, "# Plan");
|
|
171
|
+
await planService.updateMeta(sessionId, { status: "approved" });
|
|
172
|
+
await tool.execute(
|
|
173
|
+
{ content: "# Updated Plan" },
|
|
174
|
+
createToolContext(logger, { sessionId })
|
|
175
|
+
);
|
|
176
|
+
const plan = await planService.read(sessionId);
|
|
177
|
+
expect(plan.meta.status).toBe("approved");
|
|
150
178
|
});
|
|
179
|
+
});
|
|
151
180
|
});
|