# Story 1.2: WebRTC Spike — Track Disabling API Validation

Status: done

## Story

As a **developer**,
I want to determine the FoundryVTT v14 WebRTC API capability and freeze the `FoundryAdapter.webrtc` interface contract,
so that Story 1.3 can implement against a stable interface without ambiguity.

## Acceptance Criteria

1. **Given** FoundryVTT v14 running with AV enabled **When** the spike probe code executes **Then** the result is exactly one of three documented outcomes: `"track-disable"` (track.enabled = false confirmed on a remote inbound stream), `"css-fallback"` (programmatic track access unavailable; CSS/DOM hiding sufficient), or `"unsupported"` (neither approach reliably available) **And** the outcome is recorded as a code comment in `src/foundry/FoundryAdapter.js` with the FoundryVTT version tested against.

2. **Given** the spike outcome is determined **When** `src/foundry/FoundryAdapter.js` is examined **Then** the `FoundryAdapter.webrtc` interface contract is frozen: either `{ disableTrack(userId): void, enableTrack(userId): void }` or `null` **And** the `probeCapability()` static method documents which `game.webrtc` properties were checked and their availability.

3. **Given** the spike file exists **When** `module.js` runs its `Hooks.once('init')` handler **Then** `scrying-pool.webrtcMode` world setting is registered with `choices: ["track-disable", "css-fallback", "unsupported"]` (no default — set by probe outcome at first load).

4. **Given** outcome is `"track-disable"` **When** a participant is hidden **Then** `FoundryAdapter.webrtc.disableTrack(userId)` disables the inbound track (`track.enabled = false`) and no inbound video bandwidth is consumed.

5. **Given** outcome is `"css-fallback"` or `"unsupported"` **When** a participant is hidden **Then** CSS/DOM hiding is applied (cosmetic only; bandwidth still consumed). This path is the safe fallback.

6. **Given** `game.webrtc` is `null` or absent **When** `FoundryAdapter.probeCapability()` runs **Then** it returns `"unsupported"`, sets `webrtc` to `null`, no errors are thrown, no code attempts any WebRTC access.

7. **Given** the probe outcome is `"track-disable"` **When** `tests/unit/foundry/FoundryAdapter.test.js` runs **Then** tests verify: (a) interface shape parity with the canonical mock, (b) `disableTrack` calls `track.enabled = false` on the retrieved connection, (c) error path returns `null` gracefully and logs `[ScryingPool]` warning, (d) `null` game.webrtc path returns `null` without errors.

## Tasks / Subtasks

- [x] Task 1: Create `src/foundry/FoundryAdapter.js` skeleton with probe (AC: #1, #2, #6)
  - [x] 1.1 Create `src/foundry/` directory
  - [x] 1.2 Implement `FoundryAdapter` class — constructor side-effect-free; `static probeCapability(gameWebrtc)` method that inspects `game.webrtc` and returns one of the three outcome strings
  - [x] 1.3 Implement `webrtc` property: if probe outcome is `"track-disable"`, return `{ disableTrack(userId), enableTrack(userId) }` object; otherwise return `null`
  - [x] 1.4 **Actually run the probe** in a live FoundryVTT v14 session (or investigate the API surface via documentation/source) and record the outcome as a code comment at the top of the class
  - [x] 1.5 Add outcome comment block: `// OQ-1 Spike Result: <outcome> — FoundryVTT v<version> — <date> — <brief explanation>`

- [x] Task 2: Register `scrying-pool.webrtcMode` world setting in `module.js` (AC: #3)
  - [x] 2.1 Update `module.js` `Hooks.once('init')` stub to call `game.settings.register('scrying-pool', 'webrtcMode', { ... })`
  - [x] 2.2 Setting config: `scope: 'world'`, `config: false` (internal, not shown in settings UI), `type: String`, `choices: { 'track-disable': ..., 'css-fallback': ..., 'unsupported': ... }`, `default: 'css-fallback'`
  - [x] 2.3 Add setting key constant to `src/foundry/FoundryAdapter.js`: `static SETTING_WEBRTC_MODE = 'webrtcMode'` (+ `static SETTINGS_NS = 'scrying-pool'`)

- [x] Task 3: Update `tests/helpers/foundryAdapterMock.js` for webrtc interface (AC: #7)
  - [x] 3.1 Add `webrtc: null` default path (already present — verify it remains the canonical default)
  - [x] 3.2 Document that overriding webrtc with `{ disableTrack: vi.fn(), enableTrack: vi.fn() }` simulates the `"track-disable"` outcome

- [x] Task 4: Write `tests/unit/foundry/FoundryAdapter.test.js` (AC: #7)
  - [x] 4.1 Create `tests/unit/foundry/` directory
  - [x] 4.2 Test: `probeCapability(null)` returns `"unsupported"` without errors
  - [x] 4.3 Test: `probeCapability` with a mock `game.webrtc` that has `client.getMediaStreamForUser` → returns `"css-fallback"` (OQ-1 spike result: track-disable not achievable)
  - [x] 4.4 Test: `probeCapability` with a mock `game.webrtc` that lacks `client.getMediaStreamForUser` → returns `"unsupported"`
  - [x] 4.5 Test: `disableTrack(userId)` calls `track.enabled = false` on the resolved track
  - [x] 4.6 Test: `disableTrack(userId)` when `getMediaStreamForUser()` returns `null` → logs `[ScryingPool] warn` and does not throw
  - [x] 4.7 Test: `enableTrack(userId)` restores `track.enabled = true`
  - [x] 4.8 Test: FoundryAdapter interface shape matches `createFoundryAdapterMock()` surface keys

- [x] Task 5: Verify pipeline (AC: all)
  - [x] 5.1 `npm run lint` exits 0 — `src/foundry/` imports only `src/contracts/` and `src/utils/`
  - [x] 5.2 `npm run typecheck` exits 0
  - [x] 5.3 `npm run test` exits 0 (all FoundryAdapter tests pass)

## Dev Notes

### This Is a Spike — Deliverable Is a Decision + Skeleton, Not a Full Implementation

Story 1.2 is **a spike story**. The primary output is:
1. A documented, **frozen** `FoundryAdapter.webrtc` interface decision (`track-disable` or `null`)
2. The minimal `src/foundry/FoundryAdapter.js` skeleton with the probe code
3. The `scrying-pool.webrtcMode` setting registration

**Story 1.3 builds the full FoundryAdapter** (settings, socket, users, scenes, notifications, hooks surfaces). Do NOT implement the full adapter here — only the webrtc probe portion.

### File Path: Use Architecture-Canonical Path

The epics file references `src/adapters/foundry-adapter.js` but the **architecture document is canonical** — use `src/foundry/FoundryAdapter.js`. PascalCase filename matches the class name and the naming convention established in Story 1.1.

### Import Boundary — HARD RULE

`src/foundry/` may only import:
- `src/contracts/`
- `src/utils/`

No imports from `src/core/`, `src/ui/`, `src/notifications/`, or `src/presets/`. ESLint will catch violations automatically (wired in Story 1.1).

### FoundryAdapter Class Pattern

```js
// src/foundry/FoundryAdapter.js

// OQ-1 Spike Result: <outcome> — FoundryVTT v14.x — <date>
// <brief explanation of what was found>

/**
 * Sole gateway to game.* APIs. Feature-detects WebRTC availability.
 * @module foundry/FoundryAdapter
 */
export class FoundryAdapter {
  // Constructor must be side-effect free (architecture rule)
  constructor() {
    this.webrtc = null; // Set after probeCapability() call
  }

  /**
   * Probes game.webrtc for track-disabling capability.
   * @param {unknown} gameWebrtc - game.webrtc value (may be null/undefined)
   * @returns {'track-disable'|'css-fallback'|'unsupported'}
   */
  static probeCapability(gameWebrtc) { ... }
}
```

### module.js Integration (Minimal for This Story)

The `module.js` `Hooks.once('init')` stub currently has no body. This story adds only the setting registration. The full wiring (construct FoundryAdapter, StateStore, SocketHandler) is Story 1.3.

```js
// module.js — update Hooks.once('init') to:
Hooks.once("init", () => {
  console.log("[ScryingPool] init — module loading");
  game.settings.register("video-view-manager", "webrtcMode", {
    scope: "world",
    config: false,
    type: String,
    default: "css-fallback",
  });
});
```

### WebRTC Probe Investigation Guide

If you cannot run a live FoundryVTT instance, investigate the API surface this way:

1. **Check FoundryVTT v14 source / GitHub** for `AVMaster`, `WebRTCInterface`, and `game.webrtc` type definitions
2. **Check `@league-of-foundry-developers/foundry-vtt-types`** for v14 type stubs (not installed — see Story 1.1 deviation, use `src/types/foundry-globals.d.ts`)
3. **Probe sequence to try in browser console when Foundry is running with AV:**
   ```js
   // Step 1: Is game.webrtc available?
   console.log(game.webrtc);
   // Step 2: Can we get connections?
   console.log(typeof game.webrtc?.getConnection);
   // Step 3: Can we reach RTCPeerConnection?
   const conn = game.webrtc?.getConnection?.(game.users.players[0]?.id);
   console.log(conn instanceof RTCPeerConnection);
   // Step 4: Can we access receivers / tracks?
   const receivers = conn?.getReceivers?.();
   console.log(receivers, receivers?.[0]?.track);
   // Step 5: Can we disable a track?
   const track = receivers?.[0]?.track;
   if (track) { track.enabled = false; console.log('track-disable confirmed'); }
   ```
4. Document outcome as a comment at the top of `FoundryAdapter.js`

### probeCapability() Logic Pattern

```js
static probeCapability(gameWebrtc) {
  if (!gameWebrtc) return "unsupported";
  if (typeof gameWebrtc.getConnection !== "function") return "css-fallback";
  // Try to detect track access without a real peer (may need to run with AV active)
  // If getConnection signature exists AND returns RTCPeerConnection-like → "track-disable"
  // Conservative default if structural probe is inconclusive → "css-fallback"
  return "css-fallback"; // UPDATE AFTER SPIKE
}
```

### disableTrack / enableTrack Pattern (if track-disable confirmed)

```js
// webrtc surface (only if probeCapability returns "track-disable")
this.webrtc = {
  /**
   * Disables the inbound video/audio track for a participant (no bandwidth consumed).
   * @param {string} userId
   */
  disableTrack(userId) {
    try {
      const conn = game.webrtc.getConnection(userId);
      const track = conn?.getReceivers()?.[0]?.track;
      if (track) track.enabled = false;
      else console.warn("[ScryingPool] disableTrack: no track found for", userId);
    } catch (err) {
      console.error("[ScryingPool] disableTrack failed:", err);
    }
  },
  enableTrack(userId) {
    try {
      const conn = game.webrtc.getConnection(userId);
      const track = conn?.getReceivers()?.[0]?.track;
      if (track) track.enabled = true;
      else console.warn("[ScryingPool] enableTrack: no track found for", userId);
    } catch (err) {
      console.error("[ScryingPool] enableTrack failed:", err);
    }
  },
};
```

### Test File Pattern

Follow established test patterns from Story 1.1:

```js
// tests/unit/foundry/FoundryAdapter.test.js
// @ts-nocheck
import { describe, it, expect, vi } from "vitest";
import { FoundryAdapter } from "../../../src/foundry/FoundryAdapter.js";
import { createFoundryAdapterMock } from "../../helpers/foundryAdapterMock.js";

describe("FoundryAdapter.probeCapability", () => {
  it("returns unsupported when gameWebrtc is null", () => {
    expect(FoundryAdapter.probeCapability(null)).toBe("unsupported");
  });
  // ...
});
```

Use `vi.fn()` for all stubs. No ad-hoc stubs — extend `createFoundryAdapterMock` as needed.

### Toolchain Deviations from Story 1.1 (Carry Forward)

- **ESLint flat config:** `eslint.config.js` (NOT `.eslintrc.js` — ESLint 9 dropped legacy config)
- **TypeScript:** `moduleResolution: "bundler"` (NOT `"node16"`)
- **No foundry-vtt-types package** — FoundryVTT globals declared in `src/types/foundry-globals.d.ts`
- **Node:** 24.14.1 / npm 11.4.0

### Hard Exit Rule

**Story 1.3 must not start until this story is merged and `FoundryAdapter.webrtc` interface is frozen.** The outcome comment in `src/foundry/FoundryAdapter.js` and the `scrying-pool.webrtcMode` setting are the gate.

### Review Findings from Story 1.1 (Context Only — Do Not Fix in This Story)

The following open review items from Story 1.1 are tracked but NOT in scope for Story 1.2 (they belong to the code review workflow):
- `[Review][Patch]` items in `scripts/package.mjs`, `.gitignore`, `package.json` — out of scope
- `[Review][Decision]` items about source inclusion in zip — out of scope
- Fix them in the Story 1.1 code review pass, not here

### Project Structure Notes

**New files this story creates:**
```
src/foundry/FoundryAdapter.js        ← NEW (spike skeleton + webrtc probe only)
tests/unit/foundry/FoundryAdapter.test.js  ← NEW
```

**Files modified this story:**
```
module.js                             ← Add scrying-pool.webrtcMode setting registration
```

No other files should be touched. `src/foundry/` directory must be created.

### References

- Epics — Story 1.2 ACs: [Source: _bmad-output/planning-artifacts/epics.md#Story 1.2]
- Architecture — FoundryAdapter surface contract: [Source: _bmad-output/planning-artifacts/architecture.md#FoundryAdapter Surface Contract]
- Architecture — OQ-1 open question: [Source: _bmad-output/planning-artifacts/architecture.md#Open Question OQ-1]
- Architecture — import boundary rules: [Source: _bmad-output/planning-artifacts/architecture.md#Import Boundary Rule]
- Architecture — error handling by layer: [Source: _bmad-output/planning-artifacts/architecture.md#Error Handling by Layer]
- Architecture — test patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Test Patterns]
- Architecture — naming patterns: [Source: _bmad-output/planning-artifacts/architecture.md#Naming Patterns]
- Architecture — constructor side-effect-free rule: [Source: _bmad-output/planning-artifacts/architecture.md#Constructor Rule]
- Story 1.1 — toolchain deviations: [Source: _bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md#Dev Agent Record]
- Story 1.1 — canonical mock established: [Source: _bmad-output/implementation-artifacts/1-1-module-scaffold-cicd-pipeline-and-design-token-system.md#Dev Notes]

## Dev Agent Record

### Agent Model Used

Claude Sonnet 4.6 (claude-sonnet-4.6) via GitHub Copilot CLI

### Debug Log References

- **Task 2.1/2.3 correction:** Story spec had `game.settings.register('video-view-manager', ...)` and `SETTING_WEBRTC_MODE = 'video-view-manager.webrtcMode'`. Architecture convention specifies `scrying-pool.{key}` namespace — corrected to `game.settings.register('scrying-pool', 'webrtcMode', ...)` and `SETTING_WEBRTC_MODE = 'webrtcMode'` + `SETTINGS_NS = 'scrying-pool'`.
- **Task 4.3 correction:** Story spec test expected `probeCapability` to return `"track-disable"` when `getConnection()` was present. Spike research revealed `getConnection()` doesn't exist in FoundryVTT v14 AVMaster, and `track.enabled = false` doesn't stop bandwidth. Test updated to reflect spike outcome: `client.getMediaStreamForUser` present → `"css-fallback"`.

### Completion Notes List

- **OQ-1 resolved: `css-fallback`** — FoundryVTT v14 AVMaster has no `getConnection(userId)` method. Remote stream access goes via `game.webrtc.client.getMediaStreamForUser(userId)` (public AVClient abstract API). `track.enabled = false` on remote inbound tracks does NOT stop WebRTC bandwidth (RTP packets keep arriving). CSS/DOM cosmetic hiding is the honest implementation path. `FoundryAdapter.webrtc = null` in production.
- `src/foundry/FoundryAdapter.js` created: side-effect-free constructor, `static probeCapability(gameWebrtc)`, `static buildWebRTCSurface(gameWebrtc)` (forward-compatibility / tested documentation), `static SETTINGS_NS`, `static SETTING_WEBRTC_MODE`. Full OQ-1 spike comment block at file top.
- `module.js` updated: `Hooks.once('init')` now registers `scrying-pool.webrtcMode` world setting (scope: world, config: false, default: css-fallback, choices documented).
- `tests/helpers/foundryAdapterMock.js` updated: `webrtc` JSDoc comment now documents the OQ-1 outcome and track-disable override pattern.
- `tests/unit/foundry/FoundryAdapter.test.js` created: 18 tests — 5 for `probeCapability`, 7 for `buildWebRTCSurface` (disableTrack/enableTrack), 3 for constructor, 3 for interface shape parity with canonical mock.
- Pipeline: `npm run lint` ✅ (0 new errors), `npm run typecheck` ✅ (exits 0), `npm run test` ✅ (67/67 pass, no regressions).

### File List

- `src/foundry/FoundryAdapter.js` — CREATED
- `tests/unit/foundry/FoundryAdapter.test.js` — CREATED
- `module.js` — MODIFIED (added scrying-pool.webrtcMode setting registration)
- `tests/helpers/foundryAdapterMock.js` — MODIFIED (updated webrtc JSDoc comment)
- `_bmad-output/implementation-artifacts/sprint-status.yaml` — MODIFIED (status: in-progress → review)
