@witchpot/gmc 0.5.0 → 0.8.0

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/src/workflows.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  composeResolvePayload,
12
12
  composeReviewPatternsPayload,
13
13
  composeShowcaseFitPayload,
14
+ composeShowcaseHistoryPayload,
14
15
  getPreferredShowcaseTags,
15
16
  mergeSearchPages
16
17
  } from "@witchpot/gmc-core";
@@ -22,7 +23,8 @@ export {
22
23
  composeComparePayload,
23
24
  composeResolvePayload,
24
25
  composeReviewPatternsPayload,
25
- composeShowcaseFitPayload
26
+ composeShowcaseFitPayload,
27
+ composeShowcaseHistoryPayload
26
28
  };
27
29
 
28
30
  export async function executeRequestPlan(baseUrl, request, { apiKey }) {
@@ -47,6 +49,9 @@ export async function executeRequestPlan(baseUrl, request, { apiKey }) {
47
49
  if (request.kind === "showcaseFit") {
48
50
  return executeShowcaseFitRequest(baseUrl, request, { apiKey });
49
51
  }
52
+ if (request.kind === "showcaseHistory") {
53
+ return executeShowcaseHistoryRequest(baseUrl, request, { apiKey });
54
+ }
50
55
 
51
56
  const response = await fetchJson(createRequestUrl(baseUrl, request), {
52
57
  apiKey,
@@ -240,6 +245,15 @@ async function executeShowcaseFitRequest(baseUrl, request, { apiKey }) {
240
245
  });
241
246
  }
242
247
 
248
+ async function executeShowcaseHistoryRequest(baseUrl, request, { apiKey }) {
249
+ const detailRequest = {
250
+ path: `/api/v1/steam/games/${encodeURIComponent(request.externalId)}/analysis`,
251
+ query: request.detailQuery
252
+ };
253
+ const detail = await fetchJson(createRequestUrl(baseUrl, detailRequest), { apiKey });
254
+ return composeShowcaseHistoryPayload(request, { detail });
255
+ }
256
+
243
257
  async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey }) {
244
258
  if (!sampling) {
245
259
  return {
@@ -152,6 +152,162 @@ test("executeRequestPlan normalizes list game resolution for cohort workflows",
152
152
  }
153
153
  });
154
154
 
155
+ test("games language-topics passes the insufficient-evidence envelope through untouched", async () => {
156
+ // NAK-455: the CLI is a transport for this endpoint. Evidence state,
157
+ // coverage, denominators, and the raw/weighted split must arrive exactly as
158
+ // the API produced them — an insufficient_evidence answer must never be
159
+ // flattened into empty rows or a zero.
160
+ const envelope = {
161
+ data: {
162
+ appid: 730,
163
+ evidenceState: "insufficient_evidence",
164
+ snapshot: {
165
+ periodStart: "2026-02-06",
166
+ periodEnd: "2026-08-06",
167
+ analysisVersion: "v1",
168
+ generatedAt: "2026-08-06T04:00:00.000Z"
169
+ },
170
+ estimateCaveat: "Weighted values are estimates.",
171
+ coverage: {
172
+ totalRows: 226,
173
+ rowsAtThreshold: 0,
174
+ languagesAtThreshold: 0,
175
+ topicsAtThreshold: 0,
176
+ appliedMinReviews: 30,
177
+ insufficientRowsReturned: 1,
178
+ insufficientRowsTruncated: false
179
+ },
180
+ rows: [
181
+ {
182
+ appid: 730,
183
+ clusterId: 11,
184
+ topic: "performance",
185
+ language: "german",
186
+ counts: {
187
+ sourceReviews: 400,
188
+ sampledReviews: 90,
189
+ analyzedReviews: 21,
190
+ distinctReviews: 12
191
+ },
192
+ estimates: { samplingWeightMethod: "source_over_analyzed_by_language_voted_up_v1" },
193
+ evidence: { minimumN: 30, status: "insufficient_evidence", meetsRequestedThreshold: false }
194
+ }
195
+ ]
196
+ },
197
+ meta: { requestId: "req_test" },
198
+ warnings: []
199
+ };
200
+
201
+ const originalFetch = globalThis.fetch;
202
+ const seenUrls = [];
203
+ globalThis.fetch = async (url) => {
204
+ seenUrls.push(String(url));
205
+ return jsonResponse(envelope);
206
+ };
207
+
208
+ try {
209
+ const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
210
+ path: "/api/v1/steam/games/730/review-topics/languages",
211
+ query: { include_insufficient: "true" }
212
+ }, { apiKey: "gmck_test" });
213
+
214
+ assert.deepEqual(payload, envelope);
215
+ assert.equal(payload.data.evidenceState, "insufficient_evidence");
216
+ assert.equal(payload.data.coverage.totalRows, 226);
217
+ assert.equal(payload.data.rows[0].counts.distinctReviews, 12);
218
+ assert.deepEqual(seenUrls, [
219
+ "https://gamemarketcopilot.com/api/v1/steam/games/730/review-topics/languages?include_insufficient=true"
220
+ ]);
221
+ } finally {
222
+ globalThis.fetch = originalFetch;
223
+ }
224
+ });
225
+
226
+ test("games review-history passes the three arrays through untouched", async () => {
227
+ // NAK-428: the CLI is a transport for this endpoint. An empty events or
228
+ // discountWindows array is a COLLECTION state the agent has to see as-is —
229
+ // the CLI must never drop, merge, or window the arrays, and must never turn
230
+ // an unmatched (open) discount window into a closed one.
231
+ const envelope = {
232
+ data: {
233
+ series: [
234
+ { date: "2026-08-01", totalReviews: 1000 },
235
+ { date: "2026-08-03", totalReviews: 1040 }
236
+ ],
237
+ events: [
238
+ { type: "release", date: "2026-07-01" },
239
+ {
240
+ type: "announcement",
241
+ date: "2026-08-02",
242
+ count: 3,
243
+ label: "Patch 1.2 is live",
244
+ url: "https://store.steampowered.com/news/app/730/view/1"
245
+ }
246
+ ],
247
+ discountWindows: [
248
+ { startDate: "2026-08-01", endDate: "2026-08-05", discountPercent: 33 },
249
+ { startDate: "2026-08-20", endDate: null, discountPercent: null }
250
+ ]
251
+ },
252
+ meta: { requestId: "req_test" },
253
+ warnings: []
254
+ };
255
+
256
+ const originalFetch = globalThis.fetch;
257
+ const seenUrls = [];
258
+ globalThis.fetch = async (url) => {
259
+ seenUrls.push(String(url));
260
+ return jsonResponse(envelope);
261
+ };
262
+
263
+ try {
264
+ const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
265
+ path: "/api/v1/steam/games/730/review-history",
266
+ query: {}
267
+ }, { apiKey: "gmck_test" });
268
+
269
+ assert.deepEqual(payload, envelope);
270
+ // The gap between 08-01 and 08-03 survives: collection gaps are real and
271
+ // consumers must scale by date, not by array index.
272
+ assert.deepEqual(payload.data.series.map((point) => point.date), ["2026-08-01", "2026-08-03"]);
273
+ assert.equal(payload.data.discountWindows[1].endDate, null);
274
+ // No query string appended for an endpoint that takes no parameters.
275
+ assert.deepEqual(seenUrls, [
276
+ "https://gamemarketcopilot.com/api/v1/steam/games/730/review-history"
277
+ ]);
278
+ } finally {
279
+ globalThis.fetch = originalFetch;
280
+ }
281
+ });
282
+
283
+ test("games review-history passes an all-empty payload through as-is", async () => {
284
+ // Empty arrays mean "nothing collected", never "nothing happened". The CLI
285
+ // must hand the agent the same empty arrays the API produced rather than
286
+ // omitting the keys, which would read as absent fields.
287
+ const envelope = {
288
+ data: { series: [], events: [], discountWindows: [] },
289
+ meta: { requestId: "req_test" },
290
+ warnings: ["static_fixture_data"]
291
+ };
292
+
293
+ const originalFetch = globalThis.fetch;
294
+ globalThis.fetch = async () => jsonResponse(envelope);
295
+
296
+ try {
297
+ const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
298
+ path: "/api/v1/steam/games/730/review-history",
299
+ query: {}
300
+ }, { apiKey: "gmck_test" });
301
+
302
+ assert.deepEqual(payload, envelope);
303
+ assert.deepEqual(payload.data.events, []);
304
+ assert.deepEqual(payload.data.discountWindows, []);
305
+ assert.deepEqual(payload.warnings, ["static_fixture_data"]);
306
+ } finally {
307
+ globalThis.fetch = originalFetch;
308
+ }
309
+ });
310
+
155
311
  function jsonResponse(payload, status = 200) {
156
312
  return {
157
313
  ok: status >= 200 && status < 300,