@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.
Files changed (62) hide show
  1. package/dist/errors.cjs +126 -0
  2. package/dist/errors.js +99 -64
  3. package/dist/index.cjs +36 -0
  4. package/dist/index.d.cts +224 -0
  5. package/dist/index.d.ts +1 -25
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +9 -39
  8. package/dist/plan-service-getter.cjs +16 -0
  9. package/dist/plan-service-getter.d.ts +4 -0
  10. package/dist/plan-service-getter.d.ts.map +1 -0
  11. package/dist/plan-service-getter.js +0 -0
  12. package/dist/plan-service.cjs +247 -0
  13. package/dist/plan-service.d.ts +2 -2
  14. package/dist/plan-service.d.ts.map +1 -1
  15. package/dist/plan-service.js +201 -215
  16. package/dist/plan-service.test.cjs +227 -0
  17. package/dist/plan-service.test.js +200 -216
  18. package/dist/tool-factory-config.cjs +38 -0
  19. package/dist/tool-factory-config.d.ts +32 -0
  20. package/dist/tool-factory-config.d.ts.map +1 -0
  21. package/dist/tool-factory-config.js +13 -0
  22. package/dist/tool-factory.cjs +71 -0
  23. package/dist/tool-factory.d.ts +4 -0
  24. package/dist/tool-factory.d.ts.map +1 -0
  25. package/dist/tool-factory.js +40 -0
  26. package/dist/tool-factory.test.cjs +96 -0
  27. package/dist/tool-factory.test.d.ts +7 -0
  28. package/dist/tool-factory.test.d.ts.map +1 -0
  29. package/dist/tool-factory.test.js +95 -0
  30. package/dist/tools/plan-create-tool.cjs +102 -0
  31. package/dist/tools/plan-create-tool.d.ts +15 -3
  32. package/dist/tools/plan-create-tool.d.ts.map +1 -1
  33. package/dist/tools/plan-create-tool.js +77 -71
  34. package/dist/tools/plan-create-tool.test.cjs +174 -0
  35. package/dist/tools/plan-create-tool.test.js +142 -109
  36. package/dist/tools/plan-read-tool.cjs +65 -0
  37. package/dist/tools/plan-read-tool.d.ts +6 -3
  38. package/dist/tools/plan-read-tool.d.ts.map +1 -1
  39. package/dist/tools/plan-read-tool.js +39 -38
  40. package/dist/tools/plan-read-tool.test.cjs +109 -0
  41. package/dist/tools/plan-read-tool.test.js +78 -75
  42. package/dist/tools/plan-review-tool.cjs +98 -0
  43. package/dist/tools/plan-review-tool.d.ts +14 -5
  44. package/dist/tools/plan-review-tool.d.ts.map +1 -1
  45. package/dist/tools/plan-review-tool.js +73 -83
  46. package/dist/tools/plan-update-tool.cjs +92 -0
  47. package/dist/tools/plan-update-tool.d.ts +12 -3
  48. package/dist/tools/plan-update-tool.d.ts.map +1 -1
  49. package/dist/tools/plan-update-tool.js +65 -69
  50. package/dist/tools/plan-update-tool.test.cjs +203 -0
  51. package/dist/tools/plan-update-tool.test.js +171 -142
  52. package/dist/types.cjs +44 -0
  53. package/dist/types.js +17 -24
  54. package/package.json +8 -8
  55. package/.dexto-plugin/plugin.json +0 -7
  56. package/dist/tool-provider.d.ts +0 -44
  57. package/dist/tool-provider.d.ts.map +0 -1
  58. package/dist/tool-provider.js +0 -81
  59. package/dist/tool-provider.test.d.ts +0 -7
  60. package/dist/tool-provider.test.d.ts.map +0 -1
  61. package/dist/tool-provider.test.js +0 -185
  62. package/skills/plan/SKILL.md +0 -102
@@ -1,72 +1,68 @@
1
- /**
2
- * Plan Update Tool
3
- *
4
- * Updates the implementation plan for the current session.
5
- * Shows a diff preview for approval before saving.
6
- */
7
- import { z } from 'zod';
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
- const unified = createPatch(filePath, originalContent, newContent, 'before', 'after', {
20
- context: 3,
21
- });
22
- const additions = (unified.match(/^\+[^+]/gm) || []).length;
23
- const deletions = (unified.match(/^-[^-]/gm) || []).length;
24
- return {
25
- type: 'diff',
26
- unified,
27
- filename: filePath,
28
- additions,
29
- deletions,
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
- * Creates the plan_update tool
34
- */
35
- export function createPlanUpdateTool(planService) {
36
- return {
37
- id: 'plan_update',
38
- 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).',
39
- inputSchema: PlanUpdateInputSchema,
40
- /**
41
- * Generate diff preview for approval UI
42
- */
43
- generatePreview: async (input, context) => {
44
- const { content: newContent } = input;
45
- if (!context?.sessionId) {
46
- throw PlanError.sessionIdRequired();
47
- }
48
- // Read existing plan
49
- const existing = await planService.read(context.sessionId);
50
- if (!existing) {
51
- throw PlanError.planNotFound(context.sessionId);
52
- }
53
- // Generate diff preview
54
- const planPath = planService.getPlanPath(context.sessionId);
55
- return generateDiffPreview(planPath, existing.content, newContent);
56
- },
57
- execute: async (input, context) => {
58
- const { content } = input;
59
- if (!context?.sessionId) {
60
- throw PlanError.sessionIdRequired();
61
- }
62
- const result = await planService.update(context.sessionId, content);
63
- const planPath = planService.getPlanPath(context.sessionId);
64
- return {
65
- success: true,
66
- path: planPath,
67
- status: result.meta.status,
68
- _display: generateDiffPreview(planPath, result.oldContent, result.newContent),
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
- * Plan Update Tool Tests
3
- *
4
- * Tests for the plan_update tool including diff preview generation.
5
- */
6
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
- import * as path from 'node:path';
8
- import * as fs from 'node:fs/promises';
9
- import * as os from 'node:os';
10
- import { createPlanUpdateTool } from './plan-update-tool.js';
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
- createChild: vi.fn().mockReturnThis(),
21
- });
22
- describe('plan_update tool', () => {
23
- let mockLogger;
24
- let tempDir;
25
- let planService;
26
- beforeEach(async () => {
27
- mockLogger = createMockLogger();
28
- const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dexto-plan-update-test-'));
29
- tempDir = await fs.realpath(rawTempDir);
30
- planService = new PlanService({ basePath: tempDir }, mockLogger);
31
- vi.clearAllMocks();
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
- afterEach(async () => {
34
- try {
35
- await fs.rm(tempDir, { recursive: true, force: true });
36
- }
37
- catch {
38
- // Ignore cleanup errors
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
- describe('generatePreview', () => {
42
- it('should return DiffDisplayData with unified diff', async () => {
43
- const tool = createPlanUpdateTool(planService);
44
- const sessionId = 'test-session';
45
- const originalContent = '# Plan\n\n## Steps\n1. First step';
46
- const newContent = '# Plan\n\n## Steps\n1. First step\n2. Second step';
47
- await planService.create(sessionId, originalContent);
48
- const preview = (await tool.generatePreview({ content: newContent }, { sessionId }));
49
- expect(preview.type).toBe('diff');
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
- describe('execute', () => {
93
- it('should update plan content and return success', async () => {
94
- const tool = createPlanUpdateTool(planService);
95
- const sessionId = 'test-session';
96
- const originalContent = '# Original Plan';
97
- const newContent = '# Updated Plan';
98
- await planService.create(sessionId, originalContent);
99
- const result = (await tool.execute({ content: newContent }, { sessionId }));
100
- expect(result.success).toBe(true);
101
- // Path is now absolute, check it ends with the expected suffix
102
- expect(result.path).toContain(sessionId);
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
  });