# Redux API Testing

> Working with kcsapi game-API payloads in redux — typing action creators, sourcing real response-saver fixtures, writing slice/middleware tests, and avoiding `as` / `any`. Use when editing views/redux/**, adding an API response action, writing or fixing tests under __tests__, needing a real API payload sample, or inspecting api_start2 master data.

- Skill: `poooi/redux-api-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add poooi/redux-api-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/poooi/redux-api-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: poooi (https://skillmd.com/u/poooi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/poooi/redux-api-testing

---


# Redux / kcsapi API Work

AGENTS.md carries the canonical typing and fixture rules; this skill adds the operational
parts — where real payloads come from and how to work with them.

## Where real payloads live

poi's response-saver writes every game API response to disk. On Windows the directory is:

```
%APPDATA%\poi\response-saver\kcsapi
```

(that is `<user home>\AppData\Roaming\poi\response-saver\kcsapi`). The exact root is
machine-specific — if it is not there, ask the user rather than guessing.

- Layout: one directory per API path (e.g. `api_req_nyukyo/start/`), files named by timestamp.
  Newest first: `ls -t | head`.
- File shape is exactly the fixture shape: `{ method, path, body, postBody, time }`.
- **Prefer a real capture over a synthetic payload** whenever the payload shape affects
  behaviour. Copy the JSON into `views/redux/info/__tests__/__fixtures__/` **unchanged** — no
  reformatting or minifying — so it stays byte-comparable with the original.
- Captures only go back about 10 days.

### `api_start2/getData/` — master data

The authoritative source for ship `api_ctype` / `api_stype`, equipment ids, and everything
else in `api_mst_ship` / `api_mst_slotitem`. Player ships are `api_id < 1500`. Use it to
validate ctype lists (combat predicates, quest nationality lists) rather than trusting a wiki
summary.

### Privacy

**Never quote the absolute capture path** in commit messages, PR bodies, issue comments or
release notes — AGENTS.md's privacy rule. Write "from a response-saver capture" instead, and
use repo-relative paths for fixtures.

## Fixture naming

Name fixtures **behaviour first**, not just by endpoint — many endpoints have several
interesting shapes. Consult the field semantics (what `api_locked`, `api_state` etc. actually
mean) so the name reflects the payload's meaning.

- `api_req_nyukyo_start_highspeed_bucket_repairs_immediately.json`
- `api_get_member_ndock_instant_completion_shows_empty.json`
- `api_req_hensei_lock_unlock_ship.json` — the path says `lock`, but `api_locked: 0` means the
  captured _behaviour_ is unlock; the filename reflects the behaviour.
- `api_port_port_typical.json`

If you rename a fixture, rename its import variable and every path reference in the tests, then
run `npm test -- --testPathPattern="views/redux/info/__tests__"`.

## Test conventions

- Reducers migrated to RTK `createSlice` with
  `extraReducers(builder.addCase(actionCreator, ...))` match on the **action creator**, so
  tests must dispatch the real creator from `views/redux/actions/` (API response creators live
  in `response.ts`, re-exported from `index.ts`), not a raw
  `{ type: '...' }` object.
- Let TypeScript infer fixture types by assigning to a typed variable rather than casting:

  ```ts
  const payload: GameResponsePayload<APIGetMemberNdockResponse[], APIGetMemberNdockRequest> =
    ndockFixture
  dispatch(createAPIGetMemberNdockResponseAction(payload))
  ```

- For intentionally-invalid payloads that exercise a guard branch, use `@ts-expect-error` on
  the specific invalid field with a reason — never `as unknown as` on the whole object.
- Electron/remote dependencies: `jest.mock('@electron/remote', () => ({ require }))`.

## Cross-slice behaviour

Behaviour spanning multiple endpoints or slices goes in a small middleware that listens to API
response actions and dispatches an **internal domain action** (e.g. `@@info.ships@RepairCompleted`),
defined alongside the API actions under `views/redux/actions/` (the ships repair action lives in
`quest.ts`). See
`views/redux/middlewares/ships-cross-slice.ts`.

One notable edge case is instant docking completion: when repair finishes in under 60 seconds,
the following `api_get_member/ndock` shows the dock empty (`api_ship_id === 0`,
`api_state === 0`) as if it never happened. `views/redux/info/ships.ts` bridges this with a
short-lived module-level `instantDockingCompletionState` set by `api_req_nyukyo/start` and
consumed on the next `ndock`. Test it with a real fixture pair and assert the repair applies
exactly once.

## Type suppressions — reviewed, do not re-attempt

A full audit (2026-07-05) removed ~30 suppressions via better typing. The remaining ones were
judged inherent. `tsc` errors on an _unused_ `@ts-expect-error`, so a green typecheck proves
every remaining one still suppresses a live error.

Do not try to fix these:

- `webview.tsx` JSX attributes (`nodeintegration={'on'}` etc.) — `@types/react` wrongly
  declares them boolean.
- Settings config components (checkbox / switch / radio / text / integer / folder-picker):
  `config.set(configName as ConfigPath, v as never)` — these accept arbitrary plugin config
  paths (plain strings) by design, and `ConfigPath` cannot cover plugin keys. (`ShortcutConfig`
  was fixable only because its paths are a closed union.)
- Generic plumbing: `lib/config.ts` emit/get internals, `views/utils/tools.ts` generics
  (`pickExisting`, `compareUpdate`, `copyIfSame`),
  `views/redux/{reducer-factory,combine-reducers,create-store}.ts`, `data-resolver.ts`,
  `lib/ipc.ts`, `views/redux/layout/index.ts` `getIntegerSize`.
  (The audit also listed `views/redux/info/quests.ts`; that file was since split into
  `views/redux/info/quests/`, which now carries no suppressions.)
- Boundary assertions kept deliberately per the "assert once at the boundary" rule:
  `lib/constant.ts` (CSON), `views/env-parts/const.ts` (`remote.require`), `update.tsx`
  (fetch json), `views/redux/info/index.ts` (admiral-change reset).
- Upstream/internal: `lib/touchbar.ts`, `lib/module-path.ts`, polyfills
  (react-bootstrap / react-window), the `tab-area` private-method hack,
  `plugin-wrapper.tsx` styled `forwardRef`, `left-panel.tsx` React 19 `RefObject` compat,
  `i18next.ts` `store.emit`.

Related gotcha found in that audit: `electron-react-titlebar` invokes a menu `click` handler as
`(item, MouseEvent)` — the second argument is **not** a window, unlike Electron's
`remote.Menu`, which passes `(item, BaseWindow | undefined, event)`. Handlers that need a
window must check `instanceof remote.BrowserWindow` and fall back to
`remote.getCurrentWindow()`.

