# Testing without XLibrary

The SDK includes a deterministic in-memory host. It runs the normal entry module
in Vitest/Node without Electron, a window, a dev server, an application database,
or a real website.

## Project setup

```json
{
  "scripts": {
    "test": "vitest run",
    "build": "tsc -p tsconfig.json"
  },
  "devDependencies": {
    "@xlibrary/plugin-sdk": "^2.1.0",
    "typescript": "^5.4.2",
    "vitest": "^2.1.9"
  }
}
```

`tsconfig.json` should build the entry module into `runtime/`, because the
manifest expects `runtime/index.js`.

## Test host

```ts
const testHost = createPluginTestHost({
  permissions: ['game-sources.providers', 'network.http'],
  games: [],
  sessions: [],
  settings: {endpoint: 'https://catalog.example'},
  cache: {'search:example': {items: []}},
  network: ({url}) => ({status: 200, body: JSON.stringify({url})}),
});
```

The host fails closed: if a capability is missing from `permissions`, the call
throws. This catches manifest/code mismatches before packaging.

Supported fixtures:

| Option | Provides |
| --- | --- |
| `games` | The result of `host.games.list` |
| `sessions` | The result of `host.sessions.list` |
| `gameFields` | Values by game id |
| `settings` | Current plugin settings |
| `storage` | Persistent storage before the test |
| `cache` | Cache entries with controllable time |
| `importInputs` | UTF-8 strings or `Uint8Array` values by input id; the host returns base64 chunks |
| `network` | A mock for `host.network.request` |
| `auth` | Auth-account statuses |
| `authRequest` | A mock for authenticated requests |

The test host also records `calls`, `notifications`, `diagnostics`,
`exportChunks`, `storage`, `cache`, and `importInputs` for assertions.

## Invocation helpers

Use the contract-validating helpers for game sources:

```ts
import {
  createBrowserPageCapture,
  createPluginTestHost,
  runGameSourceResolve,
  runGameSourceSearch,
} from '@xlibrary/plugin-sdk/testing';

const host = createPluginTestHost();
const search = await runGameSourceSearch(plugin, {
  sourceId: 'catalog',
  query: 'example',
  limit: 20,
}, host);

const draft = await runGameSourceResolve(plugin, {
  sourceId: 'catalog',
  page: createBrowserPageCapture({
    url: 'https://catalog.example/games/example',
    title: 'Example',
  }),
}, host);
```

`runGameSourceRefresh` is available as well. For other invocation methods, use
`runPluginInvocation` and assert the result required by your contract:

```ts
const result = await runPluginInvocation(plugin, {
  method: 'imports.parse',
  payload: {
    sourceId: 'json',
    input: {id: 'input-1', fileName: 'games.json', size: 128},
  },
}, createPluginTestHost({
  permissions: ['imports.sources'],
  importInputs: {'input-1': '{"games":[{"name":"Example"}]}'},
}));
expect(result).toEqual({
  games: [{name: 'Example'}],
  warnings: [],
});
```

`createBrowserPageCapture` provides a valid snapshot and lets you override only
the fields relevant to a test. Do not use a real website in unit tests.

## Contract errors

`runGameSourceSearch`, `runGameSourceResolve`, and `runGameSourceRefresh` throw
`PluginContractValidationError` with a machine-readable `issues` array:

```ts
try {
  await runGameSourceSearch(plugin, payload, host);
} catch (error) {
  if (error instanceof PluginContractValidationError) {
    expect(error.issues[0]).toMatchObject({
      path: ['candidates', 0, 'title'],
      code: 'required',
    });
  }
  throw error;
}
```

Every issue contains a `path`, stable `code`, message, and optional `hint`. This
makes a failure point to a concrete field and correction instead of only saying
that the result is invalid.

## Permission and side-effect checks

Minimum tests for every capability:

1. successful call with the permission;
2. denied call without the permission;
3. malformed input/result;
4. size and empty-value boundaries;
5. repeated calls and idempotency;
6. dry run without writes;
7. safe error without a token/cookie/path in diagnostics.

Example permission denial:

```ts
await expect(host.host.network.request({url: 'https://catalog.example'}))
  .rejects.toThrow('network.http');
```

Use `testHost.setNow(...)` to test cache TTL without sleeping:

```ts
testHost.setNow('2026-01-01T00:00:00.000Z');
await host.host.cache.set('key', {value: 1}, {ttlSeconds: 60});
testHost.setNow('2026-01-01T00:01:01.000Z');
await expect(host.host.cache.get('key')).resolves.toBeUndefined();
```

## What the application still validates

The SDK host does not replace package validation. Before installation, XLibrary
also checks:

- `.xlplugin` zip, manifest, entry, and safe archive paths;
- SemVer, minAppVersion, and permission/contribution consistency;
- allowlisted network and browser hosts;
- runtime JSON limits and result schemas;
- staged import/export, backup rollback, and diagnostics redaction.

Final CI should therefore run `pnpm test`, `pnpm build`, and a packaging smoke
test with the real `manifest.json`.
