@witchpot/gmc 0.1.9 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.0 - 2026-07-04
8
+
9
+ - Added workspace game list commands for creating, listing, resolving, and
10
+ mutating saved cohorts from the CLI.
11
+ - Added saved game lists as a cohort source for agent workflows and evidence
12
+ packs, including manual-list item notes.
13
+ - Updated packaged schema/help output to include the new list routes and
14
+ commands.
15
+
7
16
  ## 0.1.9 - 2026-07-03
8
17
 
9
18
  - Clarified packaged Agent Skill docs so Claude Code uses `gmc skill install`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchpot/gmc",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Command line tools for Game Market Copilot market research.",
6
6
  "license": "UNLICENSED",
package/src/args.js CHANGED
@@ -56,6 +56,7 @@ function isCommandToken(command, token) {
56
56
  "creators",
57
57
  "cohort",
58
58
  "showcases",
59
+ "lists",
59
60
  "auth",
60
61
  "login",
61
62
  "logout",
@@ -95,6 +96,9 @@ function isCommandToken(command, token) {
95
96
  if (command[0] === "showcases") {
96
97
  return ["search", "fit", "patterns"].includes(token);
97
98
  }
99
+ if (command[0] === "lists") {
100
+ return ["create", "show", "games", "add", "remove", "delete"].includes(token);
101
+ }
98
102
  return false;
99
103
  }
100
104
 
package/src/constants.js CHANGED
@@ -8,7 +8,7 @@ export const EXIT_API = 4;
8
8
 
9
9
  export const CLI_PACKAGE_NAME = "@witchpot/gmc";
10
10
  export const CLI_BETA_PACKAGE_NAME = "@witchpot/gmc";
11
- export const CLI_VERSION = "0.1.9";
11
+ export const CLI_VERSION = "0.2.0";
12
12
 
13
13
  export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
14
14
  export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
@@ -33,6 +33,8 @@ export const SUPPORTED_BETA_COMMANDS = [
33
33
  "gmc games topics --source steam <externalId> --json",
34
34
  "gmc games compare --source steam <externalId> <externalId...> --focus market --json",
35
35
  "gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
36
+ "gmc lists --json",
37
+ "gmc lists games <id> --json",
36
38
  "gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
37
39
  "gmc showcases search --query <text> --json",
38
40
  "gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json"
@@ -47,6 +49,7 @@ export const PROVISIONAL_BETA_COMMANDS = [
47
49
  "gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json",
48
50
  "gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json",
49
51
  "gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json",
52
+ "gmc cohort brief --source steam --list <id> --json",
50
53
  "gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json"
51
54
  ];
52
55
 
package/src/help.js CHANGED
@@ -25,6 +25,10 @@ Usage:
25
25
  gmc skill print --format agents-md
26
26
  gmc skill status --json
27
27
  gmc schema --json
28
+ gmc lists --json
29
+ gmc lists create --name <name> --kind manual --json
30
+ gmc lists games <id> --json
31
+ gmc lists add <id> --app-ids <appid[,appid...]> --json
28
32
  gmc games search --source steam --query <text> --json
29
33
  gmc games count --source steam --release-year <year> --reviews-gt <number> --json
30
34
  gmc games aggregate --source steam --metric count,median_reviews --group-by release_year --json
@@ -47,6 +51,7 @@ Usage:
47
51
  gmc cohort pricing --source steam --tags <tag_slug[,tag_slug...]> --json
48
52
  gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json
49
53
  gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json
54
+ gmc cohort brief --source steam --list <id> --json
50
55
  gmc showcases search --query <text> --json
51
56
  gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json
52
57
  gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json
package/src/requests.js CHANGED
@@ -37,6 +37,80 @@ export function buildRequest(command, options, positional) {
37
37
  };
38
38
  }
39
39
 
40
+ if (matches(command, ["lists"])) {
41
+ return {
42
+ path: "/api/v1/lists",
43
+ query: {}
44
+ };
45
+ }
46
+
47
+ if (matches(command, ["lists", "create"])) {
48
+ const kind = assertListKind(options.kind ?? "manual");
49
+ return {
50
+ path: "/api/v1/lists",
51
+ method: "POST",
52
+ query: {},
53
+ body: {
54
+ name: assertRequiredOption(options.name, "--name"),
55
+ kind,
56
+ ...(options.description ? { description: options.description } : {}),
57
+ ...(kind === "filter" ? { filter: parseJsonOption(options.filter, "--filter") } : {}),
58
+ ...(options.sort ? { sort: parseJsonOption(options.sort, "--sort") } : {})
59
+ }
60
+ };
61
+ }
62
+
63
+ if (matches(command, ["lists", "show"])) {
64
+ const id = assertListId(positional[0], "gmc lists show <id> --json");
65
+ return {
66
+ path: `/api/v1/lists/${encodeURIComponent(id)}`,
67
+ query: {}
68
+ };
69
+ }
70
+
71
+ if (matches(command, ["lists", "games"])) {
72
+ const id = assertListId(positional[0], "gmc lists games <id> --json");
73
+ return {
74
+ path: `/api/v1/lists/${encodeURIComponent(id)}/games`,
75
+ query: pickQuery(options, {
76
+ limit: "limit",
77
+ offset: "offset"
78
+ })
79
+ };
80
+ }
81
+
82
+ if (matches(command, ["lists", "add"])) {
83
+ const id = assertListId(positional[0], "gmc lists add <id> --app-ids <ids> --json");
84
+ return {
85
+ path: `/api/v1/lists/${encodeURIComponent(id)}/items`,
86
+ method: "POST",
87
+ query: {},
88
+ body: {
89
+ appids: parseAppIdsOption(options["app-ids"] ?? options.appids),
90
+ ...(options.note ? { note: options.note } : {})
91
+ }
92
+ };
93
+ }
94
+
95
+ if (matches(command, ["lists", "remove"])) {
96
+ const id = assertListId(positional[0], "gmc lists remove <id> --app-ids <id> --json");
97
+ const [appid] = parseAppIdsOption(options["app-ids"] ?? options.appids);
98
+ return {
99
+ path: `/api/v1/lists/${encodeURIComponent(id)}/items/${encodeURIComponent(String(appid))}`,
100
+ method: "DELETE",
101
+ query: {}
102
+ };
103
+ }
104
+
105
+ if (matches(command, ["lists", "delete"])) {
106
+ const id = assertListId(positional[0], "gmc lists delete <id> --json");
107
+ return {
108
+ path: `/api/v1/lists/${encodeURIComponent(id)}`,
109
+ method: "DELETE",
110
+ query: {}
111
+ };
112
+ }
113
+
40
114
  if (matches(command, ["games", "search"])) {
41
115
  assertSource(options.source);
42
116
  return {
@@ -199,11 +273,20 @@ export function buildRequest(command, options, positional) {
199
273
  );
200
274
  const sentiment = options.sentiment == null ? undefined : assertReviewPatternSentiment(options.sentiment);
201
275
  const sampling = buildSamplingOptions(options, DEFAULT_REVIEW_PATTERNS_LIMIT);
202
- const query = {
203
- sort: "reviews_desc",
204
- limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
205
- ...buildGameSearchQuery(options, { includeTotal: false })
206
- };
276
+ const listId = options.list == null ? undefined : assertListId(options.list, "--list <id>");
277
+ const query = listId
278
+ ? {
279
+ list: listId,
280
+ limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
281
+ ...pickQuery(options, {
282
+ offset: "offset"
283
+ })
284
+ }
285
+ : {
286
+ sort: "reviews_desc",
287
+ limit: sampling?.pageLimit ?? DEFAULT_REVIEW_PATTERNS_LIMIT,
288
+ ...buildGameSearchQuery(options, { includeTotal: false })
289
+ };
207
290
  if (sampling && options.limit == null) query.limit = sampling.pageLimit;
208
291
 
209
292
  return {
@@ -211,14 +294,14 @@ export function buildRequest(command, options, positional) {
211
294
  source: "steam",
212
295
  query,
213
296
  sampling,
214
- countRequest: buildOptionalGameCountRequest(options),
297
+ countRequest: listId ? undefined : buildOptionalGameCountRequest(options),
215
298
  minGames,
216
299
  maxTopicsPerGame,
217
300
  sentiment,
218
301
  topic: options.topic,
219
302
  compareBy: isCompare ? String(options.segment ?? "price") : undefined,
220
303
  searchRequest: {
221
- path: "/api/v1/steam/games",
304
+ path: listId ? `/api/v1/lists/${encodeURIComponent(listId)}/games` : "/api/v1/steam/games",
222
305
  query
223
306
  },
224
307
  topicQuery: {
@@ -360,6 +443,38 @@ export function buildRequest(command, options, positional) {
360
443
  function buildCohortEvidenceRequest({ workflow, evidenceType, sections, options }) {
361
444
  assertSource(options.source);
362
445
  const sampling = buildSamplingOptions(options, DEFAULT_COHORT_WORKFLOW_LIMIT);
446
+ if (options.list != null) {
447
+ const listId = assertListId(options.list, "--list <id>");
448
+ const query = {
449
+ list: listId,
450
+ limit: sampling?.pageLimit ?? DEFAULT_COHORT_WORKFLOW_LIMIT,
451
+ ...pickQuery(options, {
452
+ offset: "offset"
453
+ })
454
+ };
455
+ if (sampling && options.limit == null) query.limit = sampling.pageLimit;
456
+ return {
457
+ kind: "cohortEvidence",
458
+ source: "steam",
459
+ workflow,
460
+ evidenceType,
461
+ sections,
462
+ query,
463
+ sampling,
464
+ searchRequest: {
465
+ path: `/api/v1/lists/${encodeURIComponent(listId)}/games`,
466
+ query
467
+ },
468
+ analysisQuery: sections.length
469
+ ? {
470
+ ...pickQuery(options, {
471
+ locale: "locale"
472
+ }),
473
+ sections: sections.join(",")
474
+ }
475
+ : undefined
476
+ };
477
+ }
363
478
  const query = {
364
479
  sort: "reviews_desc",
365
480
  limit: sampling?.pageLimit ?? DEFAULT_COHORT_WORKFLOW_LIMIT,
@@ -658,6 +773,73 @@ function assertResolveQuery(value) {
658
773
  }
659
774
  }
660
775
 
776
+ function assertRequiredOption(value, optionName) {
777
+ if (value == null || String(value).trim() === "") {
778
+ writeJsonError({
779
+ code: "CLI_USAGE_ERROR",
780
+ message: `Missing required option ${optionName}`
781
+ });
782
+ process.exit(EXIT_USAGE);
783
+ }
784
+ return String(value);
785
+ }
786
+
787
+ function assertListKind(value) {
788
+ if (value !== "manual" && value !== "filter") {
789
+ writeJsonError({
790
+ code: "CLI_USAGE_ERROR",
791
+ message: "--kind must be manual or filter"
792
+ });
793
+ process.exit(EXIT_USAGE);
794
+ }
795
+ return value;
796
+ }
797
+
798
+ function assertListId(value, usage) {
799
+ if (value == null || String(value).trim() === "") {
800
+ writeJsonError({
801
+ code: "CLI_USAGE_ERROR",
802
+ message: `Missing list id. Usage: ${usage}`
803
+ });
804
+ process.exit(EXIT_USAGE);
805
+ }
806
+ return String(value);
807
+ }
808
+
809
+ function parseJsonOption(value, optionName) {
810
+ if (value == null) {
811
+ writeJsonError({
812
+ code: "CLI_USAGE_ERROR",
813
+ message: `${optionName} is required`
814
+ });
815
+ process.exit(EXIT_USAGE);
816
+ }
817
+ try {
818
+ const parsed = JSON.parse(String(value));
819
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("expected object");
820
+ return parsed;
821
+ } catch {
822
+ writeJsonError({
823
+ code: "CLI_USAGE_ERROR",
824
+ message: `${optionName} must be a JSON object`
825
+ });
826
+ process.exit(EXIT_USAGE);
827
+ }
828
+ }
829
+
830
+ function parseAppIdsOption(value) {
831
+ const appids = parseCsvString(assertRequiredOption(value, "--app-ids"))
832
+ .map((item) => Number(item));
833
+ if (appids.length === 0 || appids.some((appid) => !Number.isSafeInteger(appid) || appid <= 0)) {
834
+ writeJsonError({
835
+ code: "CLI_USAGE_ERROR",
836
+ message: "--app-ids must be a comma-separated list of positive integers"
837
+ });
838
+ process.exit(EXIT_USAGE);
839
+ }
840
+ return appids;
841
+ }
842
+
661
843
  function assertCompareExternalIds(values) {
662
844
  if (values.length < 2) {
663
845
  writeJsonError({
@@ -119,6 +119,64 @@ test("reviews patterns forwards coverage into the cohort search request", () =>
119
119
  assert.equal(request.searchRequest.query.includeTotal, false);
120
120
  });
121
121
 
122
+ test("lists create builds a POST request body", () => {
123
+ const request = buildRequestFromArgv([
124
+ "lists",
125
+ "create",
126
+ "--name",
127
+ "Competitors",
128
+ "--kind",
129
+ "manual",
130
+ "--json"
131
+ ]);
132
+
133
+ assert.equal(request.path, "/api/v1/lists");
134
+ assert.equal(request.method, "POST");
135
+ assert.deepEqual(request.body, {
136
+ name: "Competitors",
137
+ kind: "manual"
138
+ });
139
+ });
140
+
141
+ test("lists add maps comma-separated app ids to item mutation body", () => {
142
+ const request = buildRequestFromArgv([
143
+ "lists",
144
+ "add",
145
+ "list-1",
146
+ "--app-ids",
147
+ "730,570",
148
+ "--note",
149
+ "genre refs",
150
+ "--json"
151
+ ]);
152
+
153
+ assert.equal(request.path, "/api/v1/lists/list-1/items");
154
+ assert.equal(request.method, "POST");
155
+ assert.deepEqual(request.body, {
156
+ appids: [730, 570],
157
+ note: "genre refs"
158
+ });
159
+ });
160
+
161
+ test("cohort brief can use a saved game list as its cohort source", () => {
162
+ const request = buildRequestFromArgv([
163
+ "cohort",
164
+ "brief",
165
+ "--source",
166
+ "steam",
167
+ "--list",
168
+ "list-1",
169
+ "--json"
170
+ ]);
171
+
172
+ assert.equal(request.kind, "cohortEvidence");
173
+ assert.equal(request.searchRequest.path, "/api/v1/lists/list-1/games");
174
+ assert.deepEqual(request.searchRequest.query, {
175
+ list: "list-1",
176
+ limit: "20"
177
+ });
178
+ });
179
+
122
180
  test("sampled workflows keep exact totals in a separate count request", () => {
123
181
  const request = buildRequestFromArgv([
124
182
  "games",
package/src/workflows.js CHANGED
@@ -25,7 +25,11 @@ export async function executeRequestPlan(baseUrl, request, { apiKey }) {
25
25
  return executeShowcaseFitRequest(baseUrl, request, { apiKey });
26
26
  }
27
27
 
28
- const response = await fetchJson(createRequestUrl(baseUrl, request), { apiKey });
28
+ const response = await fetchJson(createRequestUrl(baseUrl, request), {
29
+ apiKey,
30
+ method: request.method,
31
+ body: request.body
32
+ });
29
33
  return request.transform ? request.transform(response) : response;
30
34
  }
31
35
 
@@ -216,12 +220,12 @@ async function executeShowcaseFitRequest(baseUrl, request, { apiKey }) {
216
220
  async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey }) {
217
221
  if (!sampling) {
218
222
  return {
219
- searches: [await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey })],
223
+ searches: [normalizeSearchEnvelope(await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey }))],
220
224
  pageErrors: []
221
225
  };
222
226
  }
223
227
 
224
- const firstSearch = await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey });
228
+ const firstSearch = normalizeSearchEnvelope(await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey }));
225
229
  const searches = [firstSearch];
226
230
  const pageErrors = [];
227
231
  let nextOffset = firstSearch?.pagination?.nextOffset;
@@ -244,8 +248,9 @@ async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey })
244
248
  break;
245
249
  }
246
250
 
247
- searches.push(pageResult.payload);
248
- nextOffset = pageResult.payload?.pagination?.nextOffset;
251
+ const normalizedPage = normalizeSearchEnvelope(pageResult.payload);
252
+ searches.push(normalizedPage);
253
+ nextOffset = normalizedPage?.pagination?.nextOffset;
249
254
  }
250
255
 
251
256
  return {
@@ -254,6 +259,18 @@ async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey })
254
259
  };
255
260
  }
256
261
 
262
+ function normalizeSearchEnvelope(payload) {
263
+ if (Array.isArray(payload?.data?.games)) {
264
+ return {
265
+ ...payload,
266
+ data: payload.data.games,
267
+ list: payload.data.list,
268
+ notes: payload.data.notes
269
+ };
270
+ }
271
+ return payload;
272
+ }
273
+
257
274
  async function fetchOptionalCount(baseUrl, countRequest, { apiKey }) {
258
275
  if (!countRequest) return undefined;
259
276
  return fetchJson(createRequestUrl(baseUrl, countRequest), { apiKey });
@@ -682,7 +699,8 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
682
699
  const searches = Array.isArray(result.searches) ? result.searches : [result.search];
683
700
  const searchPageErrors = Array.isArray(result.searchPageErrors) ? result.searchPageErrors : [];
684
701
  const analysisResults = Array.isArray(result.analysisResults) ? result.analysisResults : [];
685
- const entries = analysisResults.map((entry) => buildCohortEvidenceGame(request, entry));
702
+ const listNotes = collectListNotes(searches);
703
+ const entries = analysisResults.map((entry) => buildCohortEvidenceGame(request, entry, listNotes));
686
704
  const included = entries.filter((entry) => !entry.skippedReason);
687
705
  const skipped = entries
688
706
  .filter((entry) => entry.skippedReason)
@@ -737,6 +755,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
737
755
  releaseDate: entry.releaseDate,
738
756
  coverage: entry.coverage,
739
757
  tags: entry.tags,
758
+ ...(entry.listNote ? { listNote: entry.listNote } : {}),
740
759
  patternEvidence: entry.patternEvidence
741
760
  }
742
761
  })),
@@ -760,6 +779,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
760
779
  releaseDate: entry.releaseDate,
761
780
  coverage: entry.coverage,
762
781
  tags: entry.tags,
782
+ ...(entry.listNote ? { listNote: entry.listNote } : {}),
763
783
  patternEvidence: entry.patternEvidence
764
784
  })),
765
785
  skipped
@@ -854,11 +874,16 @@ export function composeShowcaseFitPayload(request, result, options = {}) {
854
874
  };
855
875
  }
856
876
 
857
- async function fetchJson(url, { apiKey }) {
877
+ async function fetchJson(url, { apiKey, method = "GET", body } = {}) {
858
878
  let response;
859
879
  try {
860
880
  response = await fetch(url, {
861
- headers: buildRequestHeaders(apiKey)
881
+ method,
882
+ headers: {
883
+ ...buildRequestHeaders(apiKey),
884
+ ...(body == null ? {} : { "content-type": "application/json" })
885
+ },
886
+ ...(body == null ? {} : { body: JSON.stringify(body) })
862
887
  });
863
888
  } catch (error) {
864
889
  writeJsonError({
@@ -1357,7 +1382,7 @@ function buildReviewPatternSegments(games, segment) {
1357
1382
  }));
1358
1383
  }
1359
1384
 
1360
- function buildCohortEvidenceGame(request, entry) {
1385
+ function buildCohortEvidenceGame(request, entry, listNotes = new Map()) {
1361
1386
  const game = entry.game ?? {};
1362
1387
  const externalId = asString(entry.externalId ?? game?.externalId ?? game?.appid);
1363
1388
  const sections = getAnalysisSections(entry.analysis);
@@ -1378,6 +1403,7 @@ function buildCohortEvidenceGame(request, entry) {
1378
1403
  coverage: game?.coverage,
1379
1404
  tags: getRankedTagNames(game).slice(0, 8),
1380
1405
  genres: game?.genres,
1406
+ listNote: lookupListNote(listNotes, game?.appid, externalId),
1381
1407
  analysis: entry.analysis,
1382
1408
  sections,
1383
1409
  patternEvidence,
@@ -1386,6 +1412,23 @@ function buildCohortEvidenceGame(request, entry) {
1386
1412
  };
1387
1413
  }
1388
1414
 
1415
+ function collectListNotes(searches) {
1416
+ const notes = new Map();
1417
+ for (const search of searches) {
1418
+ if (!search?.notes || typeof search.notes !== "object") continue;
1419
+ for (const [appid, note] of Object.entries(search.notes)) {
1420
+ if (typeof note === "string" && note.trim()) notes.set(String(appid), note);
1421
+ }
1422
+ }
1423
+ return notes;
1424
+ }
1425
+
1426
+ function lookupListNote(notes, appid, externalId) {
1427
+ if (!notes?.size) return undefined;
1428
+ const appidKey = appid == null ? undefined : String(appid);
1429
+ return (appidKey ? notes.get(appidKey) : undefined) ?? (externalId ? notes.get(String(externalId)) : undefined);
1430
+ }
1431
+
1389
1432
  function buildCohortEvidenceSummary(request, entries, pagination) {
1390
1433
  return {
1391
1434
  sampleSize: entries.length,
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import { composeBenchmarkPayload, composeCohortEvidencePayload } from "./workflows.js";
4
+ import { composeBenchmarkPayload, composeCohortEvidencePayload, executeRequestPlan } from "./workflows.js";
5
5
 
6
6
  test("cohort evidence summarizes skipped games by reason", () => {
7
7
  const request = {
@@ -105,3 +105,58 @@ test("benchmark attaches exact total only when a count response is supplied", ()
105
105
  assert.equal(payload.pagination.total, 123);
106
106
  assert.deepEqual(payload.meta.upstreamRequests.map((entry) => entry.kind), ["search", "count"]);
107
107
  });
108
+
109
+ test("executeRequestPlan normalizes list game resolution for cohort workflows", async () => {
110
+ const originalFetch = globalThis.fetch;
111
+ const seenUrls = [];
112
+ globalThis.fetch = async (url) => {
113
+ seenUrls.push(String(url));
114
+ if (String(url).includes("/analysis")) {
115
+ return jsonResponse({
116
+ data: { appid: 730, sections: {} },
117
+ meta: { requestId: "analysis_req" }
118
+ });
119
+ }
120
+ return jsonResponse({
121
+ data: {
122
+ list: { id: "list-1", name: "Competitors", kind: "manual" },
123
+ games: [{ appid: 730, externalId: "730", title: "Example", reviews: 100 }],
124
+ notes: { 730: "Positioning reference" }
125
+ },
126
+ pagination: { limit: 20, offset: 0, total: 1, nextOffset: null },
127
+ meta: { requestId: "list_req", source: "static-fixture" }
128
+ });
129
+ };
130
+
131
+ try {
132
+ const payload = await executeRequestPlan("https://gmc.test", {
133
+ kind: "cohortEvidence",
134
+ source: "steam",
135
+ workflow: "cohortTrends",
136
+ evidenceType: "trends",
137
+ sections: [],
138
+ query: { list: "list-1", limit: "20" },
139
+ searchRequest: {
140
+ path: "/api/v1/lists/list-1/games",
141
+ query: { list: "list-1", limit: "20" }
142
+ }
143
+ }, { apiKey: "gmck_test" });
144
+
145
+ assert.equal(payload.data.cohort.returnedGames, 1);
146
+ assert.equal(payload.data.cohort.included[0].externalId, "730");
147
+ assert.equal(payload.data.cohort.included[0].evidence.listNote, "Positioning reference");
148
+ assert.equal(payload.data.games[0].listNote, "Positioning reference");
149
+ assert.ok(seenUrls.some((url) => url.includes("/api/v1/lists/list-1/games")));
150
+ } finally {
151
+ globalThis.fetch = originalFetch;
152
+ }
153
+ });
154
+
155
+ function jsonResponse(payload, status = 200) {
156
+ return {
157
+ ok: status >= 200 && status < 300,
158
+ status,
159
+ headers: new Headers({ "content-type": "application/json" }),
160
+ json: async () => payload
161
+ };
162
+ }