product-discovery-cli 0.0.14 → 0.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "product-discovery-cli",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "CLI to consume the Product Discovery Agent API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "AuronForge",
@@ -0,0 +1,222 @@
1
+ const { RunDiscoveryFlow } = require("./RunDiscoveryFlow");
2
+
3
+ const mockI18n = {
4
+ t: jest.fn((key) => key)
5
+ };
6
+
7
+ describe("RunDiscoveryFlow", () => {
8
+ test("runs discovery, saves, and stops", async () => {
9
+ const prompt = {
10
+ askInput: jest.fn().mockResolvedValue("My idea"),
11
+ askYesNo: jest.fn().mockResolvedValue(false)
12
+ };
13
+ const presenter = {
14
+ printHeader: jest.fn(),
15
+ info: jest.fn(),
16
+ error: jest.fn(),
17
+ json: jest.fn(),
18
+ success: jest.fn(),
19
+ goodbye: jest.fn(),
20
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
21
+ };
22
+ const apiClient = {
23
+ runDiscovery: jest.fn().mockResolvedValue({ name: "Test Product" })
24
+ };
25
+ const storage = {
26
+ saveJson: jest.fn().mockResolvedValue({ saved: true, fullPath: "/tmp/file.json" })
27
+ };
28
+
29
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
30
+ await useCase.execute({
31
+ apiUrl: "http://localhost/api",
32
+ lang: "pt-br",
33
+ saveDefaults: { autoSave: true },
34
+ i18n: mockI18n
35
+ });
36
+
37
+ expect(presenter.printHeader).toHaveBeenCalled();
38
+ expect(prompt.askInput).toHaveBeenCalled();
39
+ expect(apiClient.runDiscovery).toHaveBeenCalledWith("My idea", "http://localhost/api", "pt-br");
40
+ expect(storage.saveJson).toHaveBeenCalled();
41
+ expect(presenter.goodbye).toHaveBeenCalled();
42
+ });
43
+
44
+ test("asks to improve when not saving and exits on no", async () => {
45
+ const prompt = {
46
+ askInput: jest.fn().mockResolvedValue("My idea"),
47
+ askYesNo: jest.fn().mockResolvedValueOnce(false)
48
+ };
49
+ const presenter = {
50
+ printHeader: jest.fn(),
51
+ info: jest.fn(),
52
+ error: jest.fn(),
53
+ json: jest.fn(),
54
+ success: jest.fn(),
55
+ goodbye: jest.fn(),
56
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
57
+ };
58
+ const apiClient = {
59
+ runDiscovery: jest.fn().mockResolvedValue({ name: "Test Product" })
60
+ };
61
+ const storage = {
62
+ saveJson: jest.fn()
63
+ };
64
+
65
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
66
+ await useCase.execute({
67
+ apiUrl: "http://localhost/api",
68
+ lang: "pt-br",
69
+ saveDefaults: { autoSave: false },
70
+ i18n: mockI18n
71
+ });
72
+
73
+ expect(prompt.askYesNo).toHaveBeenCalledWith("askImprove");
74
+ expect(storage.saveJson).not.toHaveBeenCalled();
75
+ expect(presenter.goodbye).toHaveBeenCalled();
76
+ });
77
+
78
+ test("retries when discovery fails and user chooses to retry", async () => {
79
+ const prompt = {
80
+ askInput: jest.fn().mockResolvedValue("My idea"),
81
+ askYesNo: jest
82
+ .fn()
83
+ .mockResolvedValueOnce(true)
84
+ .mockResolvedValueOnce(false)
85
+ };
86
+ const presenter = {
87
+ printHeader: jest.fn(),
88
+ info: jest.fn(),
89
+ error: jest.fn(),
90
+ json: jest.fn(),
91
+ success: jest.fn(),
92
+ goodbye: jest.fn(),
93
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
94
+ };
95
+ const apiClient = {
96
+ runDiscovery: jest
97
+ .fn()
98
+ .mockRejectedValueOnce(new Error("API down"))
99
+ .mockResolvedValueOnce({ name: "Recovered" })
100
+ };
101
+ const storage = {
102
+ saveJson: jest.fn().mockResolvedValue({ saved: true, fullPath: "/tmp/file.json" })
103
+ };
104
+
105
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
106
+ await useCase.execute({
107
+ apiUrl: "http://localhost/api",
108
+ lang: "pt-br",
109
+ saveDefaults: { autoSave: true },
110
+ i18n: mockI18n
111
+ });
112
+
113
+ expect(apiClient.runDiscovery).toHaveBeenCalledTimes(2);
114
+ expect(presenter.error).toHaveBeenCalledWith("API down");
115
+ expect(presenter.goodbye).toHaveBeenCalled();
116
+ });
117
+
118
+ test("prompts to improve after save declined", async () => {
119
+ const prompt = {
120
+ askInput: jest.fn().mockResolvedValue("My idea"),
121
+ askYesNo: jest
122
+ .fn()
123
+ .mockResolvedValueOnce(true)
124
+ .mockResolvedValueOnce(false)
125
+ };
126
+ const presenter = {
127
+ printHeader: jest.fn(),
128
+ info: jest.fn(),
129
+ error: jest.fn(),
130
+ json: jest.fn(),
131
+ success: jest.fn(),
132
+ goodbye: jest.fn(),
133
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
134
+ };
135
+ const apiClient = {
136
+ runDiscovery: jest.fn().mockResolvedValue({ name: "Test Product" })
137
+ };
138
+ const storage = {
139
+ saveJson: jest.fn().mockResolvedValue({ saved: false })
140
+ };
141
+
142
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
143
+ await useCase.execute({
144
+ apiUrl: "http://localhost/api",
145
+ lang: "pt-br",
146
+ saveDefaults: { autoSave: true },
147
+ i18n: mockI18n
148
+ });
149
+
150
+ expect(storage.saveJson).toHaveBeenCalled();
151
+ expect(prompt.askYesNo).toHaveBeenCalledWith("askImprove");
152
+ expect(presenter.goodbye).toHaveBeenCalled();
153
+ });
154
+
155
+ test("stops when discovery fails and user declines retry", async () => {
156
+ const prompt = {
157
+ askInput: jest.fn().mockResolvedValue("My idea"),
158
+ askYesNo: jest.fn().mockResolvedValue(false)
159
+ };
160
+ const presenter = {
161
+ printHeader: jest.fn(),
162
+ info: jest.fn(),
163
+ error: jest.fn(),
164
+ json: jest.fn(),
165
+ success: jest.fn(),
166
+ goodbye: jest.fn(),
167
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
168
+ };
169
+ const apiClient = {
170
+ runDiscovery: jest.fn().mockRejectedValue(new Error("API down"))
171
+ };
172
+ const storage = {
173
+ saveJson: jest.fn()
174
+ };
175
+
176
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
177
+ await useCase.execute({
178
+ apiUrl: "http://localhost/api",
179
+ lang: "pt-br",
180
+ saveDefaults: { autoSave: true },
181
+ i18n: mockI18n
182
+ });
183
+
184
+ expect(prompt.askYesNo).toHaveBeenCalledWith("askRetry");
185
+ expect(storage.saveJson).not.toHaveBeenCalled();
186
+ expect(presenter.goodbye).toHaveBeenCalled();
187
+ });
188
+
189
+ test("continues when autoSave false and user wants to improve", async () => {
190
+ const prompt = {
191
+ askInput: jest.fn().mockResolvedValue("My idea"),
192
+ askYesNo: jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false)
193
+ };
194
+ const presenter = {
195
+ printHeader: jest.fn(),
196
+ info: jest.fn(),
197
+ error: jest.fn(),
198
+ json: jest.fn(),
199
+ success: jest.fn(),
200
+ goodbye: jest.fn(),
201
+ spinner: jest.fn().mockReturnValue({ succeed: jest.fn(), fail: jest.fn() })
202
+ };
203
+ const apiClient = {
204
+ runDiscovery: jest.fn().mockResolvedValue({ name: "Test Product" })
205
+ };
206
+ const storage = {
207
+ saveJson: jest.fn()
208
+ };
209
+
210
+ const useCase = new RunDiscoveryFlow({ prompt, apiClient, storage, presenter });
211
+ await useCase.execute({
212
+ apiUrl: "http://localhost/api",
213
+ lang: "pt-br",
214
+ saveDefaults: { autoSave: false },
215
+ i18n: mockI18n
216
+ });
217
+
218
+ expect(apiClient.runDiscovery).toHaveBeenCalledTimes(2);
219
+ expect(prompt.askYesNo).toHaveBeenCalledWith("askImprove");
220
+ expect(presenter.goodbye).toHaveBeenCalled();
221
+ });
222
+ });
@@ -0,0 +1,12 @@
1
+ const { DiscoverySession } = require("./DiscoverySession");
2
+
3
+ describe("DiscoverySession", () => {
4
+ test("appends additional details after base idea", () => {
5
+ const session = new DiscoverySession();
6
+ const first = session.addIdea("Base idea");
7
+ const second = session.addIdea("More details");
8
+
9
+ expect(first).toBe("Base idea");
10
+ expect(second).toBe("Base idea\n\nAdditional details: More details");
11
+ });
12
+ });
@@ -0,0 +1,26 @@
1
+ const {
2
+ sanitizeForFolderName,
3
+ timestampForPath,
4
+ ensureJsonExtension
5
+ } = require("./PathNaming");
6
+
7
+ describe("PathNaming", () => {
8
+ test("sanitizeForFolderName normalizes input", () => {
9
+ expect(sanitizeForFolderName("My Product!! 2024")).toBe("my-product-2024");
10
+ });
11
+
12
+ test("sanitizeForFolderName falls back to product", () => {
13
+ expect(sanitizeForFolderName("###")).toBe("product");
14
+ });
15
+
16
+ test("timestampForPath returns a safe string", () => {
17
+ const value = timestampForPath();
18
+ expect(value).toMatch(/\d{4}-\d{2}-\d{2}T/);
19
+ expect(value).not.toMatch(/[:.]/);
20
+ });
21
+
22
+ test("ensureJsonExtension appends extension", () => {
23
+ expect(ensureJsonExtension("discovery")).toBe("discovery.json");
24
+ expect(ensureJsonExtension("report.JSON")).toBe("report.JSON");
25
+ });
26
+ });
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ const { ProductDiscoveryApi } = require("./infrastructure/ProductDiscoveryApi");
7
7
  const { JsonFileStorage } = require("./infrastructure/JsonFileStorage");
8
8
  const { RunDiscoveryFlow } = require("./application/RunDiscoveryFlow");
9
9
 
10
- const DEFAULT_API_URL = process.env.API_URL || "http://localhost:3001/api/v1/discovery";
10
+ const DEFAULT_API_URL = process.env.API_URL || "http://localhost:3000/api/v1/discovery";
11
11
 
12
12
  const prompt = new PromptService();
13
13
  const presenter = new ConsolePresenter(null);