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
|
@@ -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:
|
|
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);
|