@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,227 +1,213 @@
1
- /**
2
- * Plan Service
3
- *
4
- * Handles storage and retrieval of implementation plans.
5
- * Plans are stored in .dexto/plans/{sessionId}/ with:
6
- * - plan.md: The plan content
7
- * - plan-meta.json: Metadata (status, checkpoints, timestamps)
8
- */
9
- import * as fs from 'node:fs/promises';
10
- import * as path from 'node:path';
11
- import { existsSync } from 'node:fs';
12
- import { PlanMetaSchema } from './types.js';
13
- import { PlanError } from './errors.js';
14
- const PLAN_FILENAME = 'plan.md';
15
- const META_FILENAME = 'plan-meta.json';
16
- /**
17
- * Service for managing implementation plans.
18
- */
19
- export class PlanService {
20
- basePath;
21
- logger;
22
- constructor(options, logger) {
23
- this.basePath = options.basePath;
24
- this.logger = logger;
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { PlanMetaSchema } from "./types.js";
5
+ import { PlanError } from "./errors.js";
6
+ const PLAN_FILENAME = "plan.md";
7
+ const META_FILENAME = "plan-meta.json";
8
+ class PlanService {
9
+ basePath;
10
+ logger;
11
+ constructor(options, logger) {
12
+ this.basePath = options.basePath;
13
+ this.logger = logger;
14
+ }
15
+ /**
16
+ * Resolves and validates a session directory path.
17
+ * Prevents path traversal attacks by ensuring the resolved path stays within basePath.
18
+ */
19
+ resolveSessionDir(sessionId) {
20
+ const base = path.resolve(this.basePath);
21
+ const resolved = path.resolve(base, sessionId);
22
+ const rel = path.relative(base, resolved);
23
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
24
+ throw PlanError.invalidSessionId(sessionId);
25
25
  }
26
- /**
27
- * Resolves and validates a session directory path.
28
- * Prevents path traversal attacks by ensuring the resolved path stays within basePath.
29
- */
30
- resolveSessionDir(sessionId) {
31
- const base = path.resolve(this.basePath);
32
- const resolved = path.resolve(base, sessionId);
33
- const rel = path.relative(base, resolved);
34
- // Check for path traversal (upward traversal)
35
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
36
- throw PlanError.invalidSessionId(sessionId);
37
- }
38
- return resolved;
26
+ return resolved;
27
+ }
28
+ /**
29
+ * Gets the directory path for a session's plan
30
+ */
31
+ getPlanDir(sessionId) {
32
+ return this.resolveSessionDir(sessionId);
33
+ }
34
+ /**
35
+ * Gets the path to the plan content file.
36
+ * Public accessor for tools that need to display the path.
37
+ */
38
+ getPlanPath(sessionId) {
39
+ return path.join(this.getPlanDir(sessionId), PLAN_FILENAME);
40
+ }
41
+ /**
42
+ * Gets the path to the plan metadata file
43
+ */
44
+ getMetaPath(sessionId) {
45
+ return path.join(this.getPlanDir(sessionId), META_FILENAME);
46
+ }
47
+ /**
48
+ * Checks if a plan exists for the given session
49
+ */
50
+ async exists(sessionId) {
51
+ const planPath = this.getPlanPath(sessionId);
52
+ return existsSync(planPath);
53
+ }
54
+ /**
55
+ * Creates a new plan for the session
56
+ *
57
+ * @throws PlanError.planAlreadyExists if plan already exists
58
+ * @throws PlanError.storageError on filesystem errors
59
+ */
60
+ async create(sessionId, content, options) {
61
+ if (await this.exists(sessionId)) {
62
+ throw PlanError.planAlreadyExists(sessionId);
39
63
  }
40
- /**
41
- * Gets the directory path for a session's plan
42
- */
43
- getPlanDir(sessionId) {
44
- return this.resolveSessionDir(sessionId);
64
+ const planDir = this.getPlanDir(sessionId);
65
+ const now = Date.now();
66
+ const meta = {
67
+ sessionId,
68
+ status: "draft",
69
+ title: options?.title,
70
+ createdAt: now,
71
+ updatedAt: now
72
+ };
73
+ try {
74
+ await fs.mkdir(planDir, { recursive: true });
75
+ await Promise.all([
76
+ fs.writeFile(this.getPlanPath(sessionId), content, "utf-8"),
77
+ fs.writeFile(this.getMetaPath(sessionId), JSON.stringify(meta, null, 2), "utf-8")
78
+ ]);
79
+ this.logger.debug(`Created plan for session ${sessionId}`);
80
+ return { content, meta };
81
+ } catch (error) {
82
+ throw PlanError.storageError("create", sessionId, error);
45
83
  }
46
- /**
47
- * Gets the path to the plan content file.
48
- * Public accessor for tools that need to display the path.
49
- */
50
- getPlanPath(sessionId) {
51
- return path.join(this.getPlanDir(sessionId), PLAN_FILENAME);
84
+ }
85
+ /**
86
+ * Reads the plan for the given session
87
+ *
88
+ * @returns The plan or null if not found
89
+ */
90
+ async read(sessionId) {
91
+ if (!await this.exists(sessionId)) {
92
+ return null;
52
93
  }
53
- /**
54
- * Gets the path to the plan metadata file
55
- */
56
- getMetaPath(sessionId) {
57
- return path.join(this.getPlanDir(sessionId), META_FILENAME);
58
- }
59
- /**
60
- * Checks if a plan exists for the given session
61
- */
62
- async exists(sessionId) {
63
- const planPath = this.getPlanPath(sessionId);
64
- return existsSync(planPath);
65
- }
66
- /**
67
- * Creates a new plan for the session
68
- *
69
- * @throws PlanError.planAlreadyExists if plan already exists
70
- * @throws PlanError.storageError on filesystem errors
71
- */
72
- async create(sessionId, content, options) {
73
- // Check if plan already exists
74
- if (await this.exists(sessionId)) {
75
- throw PlanError.planAlreadyExists(sessionId);
76
- }
77
- const planDir = this.getPlanDir(sessionId);
78
- const now = Date.now();
79
- // Create metadata
80
- const meta = {
94
+ try {
95
+ const [content, metaContent] = await Promise.all([
96
+ fs.readFile(this.getPlanPath(sessionId), "utf-8"),
97
+ fs.readFile(this.getMetaPath(sessionId), "utf-8")
98
+ ]);
99
+ const metaParsed = JSON.parse(metaContent);
100
+ const metaResult = PlanMetaSchema.safeParse(metaParsed);
101
+ if (!metaResult.success) {
102
+ this.logger.warn(`Invalid plan metadata for session ${sessionId}, using defaults`);
103
+ return {
104
+ content,
105
+ meta: {
81
106
  sessionId,
82
- status: 'draft',
83
- title: options?.title,
84
- createdAt: now,
85
- updatedAt: now,
107
+ status: "draft",
108
+ createdAt: Date.now(),
109
+ updatedAt: Date.now()
110
+ }
86
111
  };
87
- try {
88
- // Ensure directory exists
89
- await fs.mkdir(planDir, { recursive: true });
90
- // Write plan content and metadata
91
- await Promise.all([
92
- fs.writeFile(this.getPlanPath(sessionId), content, 'utf-8'),
93
- fs.writeFile(this.getMetaPath(sessionId), JSON.stringify(meta, null, 2), 'utf-8'),
94
- ]);
95
- this.logger?.debug(`Created plan for session ${sessionId}`);
96
- return { content, meta };
97
- }
98
- catch (error) {
99
- throw PlanError.storageError('create', sessionId, error);
100
- }
112
+ }
113
+ return { content, meta: metaResult.data };
114
+ } catch (error) {
115
+ const err = error;
116
+ if (err.code === "ENOENT") {
117
+ return null;
118
+ }
119
+ if (error instanceof SyntaxError) {
120
+ this.logger.error(`Failed to read plan for session ${sessionId}: ${error.message}`);
121
+ return null;
122
+ }
123
+ this.logger.error(
124
+ `Failed to read plan for session ${sessionId}: ${err.message ?? String(err)}`
125
+ );
126
+ throw PlanError.storageError("read", sessionId, err);
101
127
  }
102
- /**
103
- * Reads the plan for the given session
104
- *
105
- * @returns The plan or null if not found
106
- */
107
- async read(sessionId) {
108
- if (!(await this.exists(sessionId))) {
109
- return null;
110
- }
111
- try {
112
- const [content, metaContent] = await Promise.all([
113
- fs.readFile(this.getPlanPath(sessionId), 'utf-8'),
114
- fs.readFile(this.getMetaPath(sessionId), 'utf-8'),
115
- ]);
116
- const metaParsed = JSON.parse(metaContent);
117
- const metaResult = PlanMetaSchema.safeParse(metaParsed);
118
- if (!metaResult.success) {
119
- this.logger?.warn(`Invalid plan metadata for session ${sessionId}, using defaults`);
120
- // Return with minimal metadata if parsing fails
121
- return {
122
- content,
123
- meta: {
124
- sessionId,
125
- status: 'draft',
126
- createdAt: Date.now(),
127
- updatedAt: Date.now(),
128
- },
129
- };
130
- }
131
- return { content, meta: metaResult.data };
132
- }
133
- catch (error) {
134
- const err = error;
135
- // ENOENT means file doesn't exist - return null (expected case)
136
- if (err.code === 'ENOENT') {
137
- return null;
138
- }
139
- // JSON parse errors (SyntaxError) mean corrupted data - treat as not found
140
- // but log for debugging
141
- if (error instanceof SyntaxError) {
142
- this.logger?.error(`Failed to read plan for session ${sessionId}: ${error.message}`);
143
- return null;
144
- }
145
- // For real I/O errors (permission denied, disk issues), throw to surface the issue
146
- this.logger?.error(`Failed to read plan for session ${sessionId}: ${err.message ?? String(err)}`);
147
- throw PlanError.storageError('read', sessionId, err);
148
- }
128
+ }
129
+ /**
130
+ * Updates the plan content for the given session
131
+ *
132
+ * @throws PlanError.planNotFound if plan doesn't exist
133
+ * @throws PlanError.storageError on filesystem errors
134
+ */
135
+ async update(sessionId, content) {
136
+ const existing = await this.read(sessionId);
137
+ if (!existing) {
138
+ throw PlanError.planNotFound(sessionId);
149
139
  }
150
- /**
151
- * Updates the plan content for the given session
152
- *
153
- * @throws PlanError.planNotFound if plan doesn't exist
154
- * @throws PlanError.storageError on filesystem errors
155
- */
156
- async update(sessionId, content) {
157
- const existing = await this.read(sessionId);
158
- if (!existing) {
159
- throw PlanError.planNotFound(sessionId);
160
- }
161
- const oldContent = existing.content;
162
- const now = Date.now();
163
- // Update metadata
164
- const updatedMeta = {
165
- ...existing.meta,
166
- updatedAt: now,
167
- };
168
- try {
169
- await Promise.all([
170
- fs.writeFile(this.getPlanPath(sessionId), content, 'utf-8'),
171
- fs.writeFile(this.getMetaPath(sessionId), JSON.stringify(updatedMeta, null, 2), 'utf-8'),
172
- ]);
173
- this.logger?.debug(`Updated plan for session ${sessionId}`);
174
- return {
175
- oldContent,
176
- newContent: content,
177
- meta: updatedMeta,
178
- };
179
- }
180
- catch (error) {
181
- throw PlanError.storageError('update', sessionId, error);
182
- }
140
+ const oldContent = existing.content;
141
+ const now = Date.now();
142
+ const updatedMeta = {
143
+ ...existing.meta,
144
+ updatedAt: now
145
+ };
146
+ try {
147
+ await Promise.all([
148
+ fs.writeFile(this.getPlanPath(sessionId), content, "utf-8"),
149
+ fs.writeFile(
150
+ this.getMetaPath(sessionId),
151
+ JSON.stringify(updatedMeta, null, 2),
152
+ "utf-8"
153
+ )
154
+ ]);
155
+ this.logger.debug(`Updated plan for session ${sessionId}`);
156
+ return {
157
+ oldContent,
158
+ newContent: content,
159
+ meta: updatedMeta
160
+ };
161
+ } catch (error) {
162
+ throw PlanError.storageError("update", sessionId, error);
183
163
  }
184
- /**
185
- * Updates the plan metadata (status, title)
186
- *
187
- * @throws PlanError.planNotFound if plan doesn't exist
188
- * @throws PlanError.storageError on filesystem errors
189
- */
190
- async updateMeta(sessionId, updates) {
191
- const existing = await this.read(sessionId);
192
- if (!existing) {
193
- throw PlanError.planNotFound(sessionId);
194
- }
195
- const updatedMeta = {
196
- ...existing.meta,
197
- ...updates,
198
- updatedAt: Date.now(),
199
- };
200
- try {
201
- await fs.writeFile(this.getMetaPath(sessionId), JSON.stringify(updatedMeta, null, 2), 'utf-8');
202
- this.logger?.debug(`Updated plan metadata for session ${sessionId}`);
203
- return updatedMeta;
204
- }
205
- catch (error) {
206
- throw PlanError.storageError('update metadata', sessionId, error);
207
- }
164
+ }
165
+ /**
166
+ * Updates the plan metadata (status, title)
167
+ *
168
+ * @throws PlanError.planNotFound if plan doesn't exist
169
+ * @throws PlanError.storageError on filesystem errors
170
+ */
171
+ async updateMeta(sessionId, updates) {
172
+ const existing = await this.read(sessionId);
173
+ if (!existing) {
174
+ throw PlanError.planNotFound(sessionId);
175
+ }
176
+ const updatedMeta = {
177
+ ...existing.meta,
178
+ ...updates,
179
+ updatedAt: Date.now()
180
+ };
181
+ try {
182
+ await fs.writeFile(
183
+ this.getMetaPath(sessionId),
184
+ JSON.stringify(updatedMeta, null, 2),
185
+ "utf-8"
186
+ );
187
+ this.logger.debug(`Updated plan metadata for session ${sessionId}`);
188
+ return updatedMeta;
189
+ } catch (error) {
190
+ throw PlanError.storageError("update metadata", sessionId, error);
191
+ }
192
+ }
193
+ /**
194
+ * Deletes the plan for the given session
195
+ *
196
+ * @throws PlanError.planNotFound if plan doesn't exist
197
+ * @throws PlanError.storageError on filesystem errors
198
+ */
199
+ async delete(sessionId) {
200
+ if (!await this.exists(sessionId)) {
201
+ throw PlanError.planNotFound(sessionId);
208
202
  }
209
- /**
210
- * Deletes the plan for the given session
211
- *
212
- * @throws PlanError.planNotFound if plan doesn't exist
213
- * @throws PlanError.storageError on filesystem errors
214
- */
215
- async delete(sessionId) {
216
- if (!(await this.exists(sessionId))) {
217
- throw PlanError.planNotFound(sessionId);
218
- }
219
- try {
220
- await fs.rm(this.getPlanDir(sessionId), { recursive: true, force: true });
221
- this.logger?.debug(`Deleted plan for session ${sessionId}`);
222
- }
223
- catch (error) {
224
- throw PlanError.storageError('delete', sessionId, error);
225
- }
203
+ try {
204
+ await fs.rm(this.getPlanDir(sessionId), { recursive: true, force: true });
205
+ this.logger.debug(`Deleted plan for session ${sessionId}`);
206
+ } catch (error) {
207
+ throw PlanError.storageError("delete", sessionId, error);
226
208
  }
209
+ }
227
210
  }
211
+ export {
212
+ PlanService
213
+ };
@@ -0,0 +1,227 @@
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_service = require("./plan-service.js");
29
+ var import_errors = require("./errors.js");
30
+ var import_core = require("@dexto/core");
31
+ const createMockLogger = () => ({
32
+ debug: import_vitest.vi.fn(),
33
+ silly: import_vitest.vi.fn(),
34
+ info: import_vitest.vi.fn(),
35
+ warn: import_vitest.vi.fn(),
36
+ error: import_vitest.vi.fn(),
37
+ trackException: import_vitest.vi.fn(),
38
+ createChild: import_vitest.vi.fn().mockReturnThis(),
39
+ setLevel: import_vitest.vi.fn(),
40
+ getLevel: import_vitest.vi.fn(() => "debug"),
41
+ getLogFilePath: import_vitest.vi.fn(() => null),
42
+ destroy: import_vitest.vi.fn(async () => void 0)
43
+ });
44
+ (0, import_vitest.describe)("PlanService", () => {
45
+ let mockLogger;
46
+ let tempDir;
47
+ let planService;
48
+ (0, import_vitest.beforeEach)(async () => {
49
+ mockLogger = createMockLogger();
50
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-plan-test-"));
51
+ tempDir = await fs.realpath(rawTempDir);
52
+ planService = new import_plan_service.PlanService({ basePath: tempDir }, mockLogger);
53
+ import_vitest.vi.clearAllMocks();
54
+ });
55
+ (0, import_vitest.afterEach)(async () => {
56
+ try {
57
+ await fs.rm(tempDir, { recursive: true, force: true });
58
+ } catch {
59
+ }
60
+ });
61
+ (0, import_vitest.describe)("exists", () => {
62
+ (0, import_vitest.it)("should return false for non-existent plan", async () => {
63
+ const exists = await planService.exists("non-existent-session");
64
+ (0, import_vitest.expect)(exists).toBe(false);
65
+ });
66
+ (0, import_vitest.it)("should return true for existing plan", async () => {
67
+ const sessionId = "test-session";
68
+ await planService.create(sessionId, "# Test Plan");
69
+ const exists = await planService.exists(sessionId);
70
+ (0, import_vitest.expect)(exists).toBe(true);
71
+ });
72
+ });
73
+ (0, import_vitest.describe)("create", () => {
74
+ (0, import_vitest.it)("should create a new plan with content and metadata", async () => {
75
+ const sessionId = "test-session";
76
+ const content = "# Implementation Plan\n\n## Steps\n1. First step";
77
+ const title = "Test Plan";
78
+ const plan = await planService.create(sessionId, content, { title });
79
+ (0, import_vitest.expect)(plan.content).toBe(content);
80
+ (0, import_vitest.expect)(plan.meta.sessionId).toBe(sessionId);
81
+ (0, import_vitest.expect)(plan.meta.status).toBe("draft");
82
+ (0, import_vitest.expect)(plan.meta.title).toBe(title);
83
+ (0, import_vitest.expect)(plan.meta.createdAt).toBeGreaterThan(0);
84
+ (0, import_vitest.expect)(plan.meta.updatedAt).toBeGreaterThan(0);
85
+ });
86
+ (0, import_vitest.it)("should throw error when plan already exists", async () => {
87
+ const sessionId = "test-session";
88
+ await planService.create(sessionId, "# First Plan");
89
+ try {
90
+ await planService.create(sessionId, "# Second Plan");
91
+ import_vitest.expect.fail("Should have thrown an error");
92
+ } catch (error) {
93
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
94
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_ALREADY_EXISTS);
95
+ }
96
+ });
97
+ (0, import_vitest.it)("should store plan files on disk", async () => {
98
+ const sessionId = "test-session";
99
+ const content = "# Test Plan";
100
+ await planService.create(sessionId, content);
101
+ const planPath = path.join(tempDir, sessionId, "plan.md");
102
+ const storedContent = await fs.readFile(planPath, "utf-8");
103
+ (0, import_vitest.expect)(storedContent).toBe(content);
104
+ const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
105
+ const metaContent = await fs.readFile(metaPath, "utf-8");
106
+ const meta = JSON.parse(metaContent);
107
+ (0, import_vitest.expect)(meta.sessionId).toBe(sessionId);
108
+ });
109
+ });
110
+ (0, import_vitest.describe)("read", () => {
111
+ (0, import_vitest.it)("should return null for non-existent plan", async () => {
112
+ const plan = await planService.read("non-existent-session");
113
+ (0, import_vitest.expect)(plan).toBeNull();
114
+ });
115
+ (0, import_vitest.it)("should read existing plan with content and metadata", async () => {
116
+ const sessionId = "test-session";
117
+ const content = "# Test Plan";
118
+ const title = "My Plan";
119
+ await planService.create(sessionId, content, { title });
120
+ const plan = await planService.read(sessionId);
121
+ (0, import_vitest.expect)(plan).not.toBeNull();
122
+ (0, import_vitest.expect)(plan.content).toBe(content);
123
+ (0, import_vitest.expect)(plan.meta.sessionId).toBe(sessionId);
124
+ (0, import_vitest.expect)(plan.meta.title).toBe(title);
125
+ });
126
+ (0, import_vitest.it)("should handle invalid metadata schema gracefully", async () => {
127
+ const sessionId = "test-session";
128
+ await planService.create(sessionId, "# Test");
129
+ const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
130
+ await fs.writeFile(metaPath, JSON.stringify({ invalidField: "value" }));
131
+ const plan = await planService.read(sessionId);
132
+ (0, import_vitest.expect)(plan).not.toBeNull();
133
+ (0, import_vitest.expect)(plan.meta.sessionId).toBe(sessionId);
134
+ (0, import_vitest.expect)(plan.meta.status).toBe("draft");
135
+ (0, import_vitest.expect)(mockLogger.warn).toHaveBeenCalled();
136
+ });
137
+ (0, import_vitest.it)("should return null for corrupted JSON metadata", async () => {
138
+ const sessionId = "test-session";
139
+ await planService.create(sessionId, "# Test");
140
+ const metaPath = path.join(tempDir, sessionId, "plan-meta.json");
141
+ await fs.writeFile(metaPath, "{ invalid json }");
142
+ const plan = await planService.read(sessionId);
143
+ (0, import_vitest.expect)(plan).toBeNull();
144
+ (0, import_vitest.expect)(mockLogger.error).toHaveBeenCalled();
145
+ });
146
+ });
147
+ (0, import_vitest.describe)("update", () => {
148
+ (0, import_vitest.it)("should update plan content", async () => {
149
+ const sessionId = "test-session";
150
+ await planService.create(sessionId, "# Original Content");
151
+ const result = await planService.update(sessionId, "# Updated Content");
152
+ (0, import_vitest.expect)(result.oldContent).toBe("# Original Content");
153
+ (0, import_vitest.expect)(result.newContent).toBe("# Updated Content");
154
+ (0, import_vitest.expect)(result.meta.updatedAt).toBeGreaterThan(0);
155
+ });
156
+ (0, import_vitest.it)("should preserve metadata when updating content", async () => {
157
+ const sessionId = "test-session";
158
+ const plan = await planService.create(sessionId, "# Original", { title: "My Title" });
159
+ const originalCreatedAt = plan.meta.createdAt;
160
+ await planService.update(sessionId, "# Updated");
161
+ const updatedPlan = await planService.read(sessionId);
162
+ (0, import_vitest.expect)(updatedPlan.meta.title).toBe("My Title");
163
+ (0, import_vitest.expect)(updatedPlan.meta.createdAt).toBe(originalCreatedAt);
164
+ });
165
+ (0, import_vitest.it)("should throw error when plan does not exist", async () => {
166
+ try {
167
+ await planService.update("non-existent", "# Content");
168
+ import_vitest.expect.fail("Should have thrown an error");
169
+ } catch (error) {
170
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
171
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
172
+ }
173
+ });
174
+ });
175
+ (0, import_vitest.describe)("updateMeta", () => {
176
+ (0, import_vitest.it)("should update plan status", async () => {
177
+ const sessionId = "test-session";
178
+ await planService.create(sessionId, "# Plan");
179
+ const meta = await planService.updateMeta(sessionId, { status: "approved" });
180
+ (0, import_vitest.expect)(meta.status).toBe("approved");
181
+ });
182
+ (0, import_vitest.it)("should update plan title", async () => {
183
+ const sessionId = "test-session";
184
+ await planService.create(sessionId, "# Plan");
185
+ const meta = await planService.updateMeta(sessionId, { title: "New Title" });
186
+ (0, import_vitest.expect)(meta.title).toBe("New Title");
187
+ });
188
+ (0, import_vitest.it)("should throw error when plan does not exist", async () => {
189
+ try {
190
+ await planService.updateMeta("non-existent", { status: "approved" });
191
+ import_vitest.expect.fail("Should have thrown an error");
192
+ } catch (error) {
193
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
194
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
195
+ }
196
+ });
197
+ });
198
+ (0, import_vitest.describe)("delete", () => {
199
+ (0, import_vitest.it)("should delete existing plan", async () => {
200
+ const sessionId = "test-session";
201
+ await planService.create(sessionId, "# Plan");
202
+ await planService.delete(sessionId);
203
+ const exists = await planService.exists(sessionId);
204
+ (0, import_vitest.expect)(exists).toBe(false);
205
+ });
206
+ (0, import_vitest.it)("should throw error when plan does not exist", async () => {
207
+ try {
208
+ await planService.delete("non-existent");
209
+ import_vitest.expect.fail("Should have thrown an error");
210
+ } catch (error) {
211
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_core.DextoRuntimeError);
212
+ (0, import_vitest.expect)(error.code).toBe(import_errors.PlanErrorCode.PLAN_NOT_FOUND);
213
+ }
214
+ });
215
+ (0, import_vitest.it)("should remove plan directory from disk", async () => {
216
+ const sessionId = "test-session";
217
+ await planService.create(sessionId, "# Plan");
218
+ const planDir = path.join(tempDir, sessionId);
219
+ await planService.delete(sessionId);
220
+ try {
221
+ await fs.access(planDir);
222
+ import_vitest.expect.fail("Directory should not exist");
223
+ } catch {
224
+ }
225
+ });
226
+ });
227
+ });