Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ WHISPARR_API_KEY=
# Lidarr
LIDARR_URL=
LIDARR_API_KEY=
# Readarr (retired upstream - best effort)
READARR_URL=
READARR_API_KEY=

# =============================================================================
# SECURITY & AUTHENTICATION
Expand Down Expand Up @@ -156,6 +159,11 @@ WHISPARR_LOGS_PATH=
# Linux: ~/.config/Lidarr/logs
# Docker: /path/to/lidarr/config/logs
LIDARR_LOGS_PATH=
# Readarr examples (container path: /readarr-logs):
# Windows: C:/ProgramData/Readarr/logs
# Linux: ~/.config/Readarr/logs
# Docker: /path/to/readarr/config/logs
READARR_LOGS_PATH=

# =============================================================================
# MULTIPLE INSTANCES (optional - for multiple servers of the same type)
Expand Down Expand Up @@ -205,3 +213,6 @@ WHISPARR_LOGS_PATH_3=
LIDARR_LOGS_PATH_1=
LIDARR_LOGS_PATH_2=
LIDARR_LOGS_PATH_3=
READARR_LOGS_PATH_1=
READARR_LOGS_PATH_2=
READARR_LOGS_PATH_3=
5 changes: 5 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ WHISPARR_API_KEY={{ op://Development/Logarr-Dev/WHISPARR_API_KEY }}
LIDARR_URL={{ op://Development/Logarr-Dev/LIDARR_URL }}
LIDARR_API_KEY={{ op://Development/Logarr-Dev/LIDARR_API_KEY }}

# Readarr (retired upstream) - add matching items to the 1Password vault, then uncomment
# READARR_URL={{ op://Development/Logarr-Dev/READARR_URL }}
# READARR_API_KEY={{ op://Development/Logarr-Dev/READARR_API_KEY }}

# =============================================================================
# SECURITY & AUTHENTICATION
# =============================================================================
Expand All @@ -111,3 +115,4 @@ RADARR_LOGS_PATH={{ op://Development/Logarr-Dev/RADARR_LOGS_PATH }}
PROWLARR_LOGS_PATH={{ op://Development/Logarr-Dev/PROWLARR_LOGS_PATH }}
WHISPARR_LOGS_PATH={{ op://Development/Logarr-Dev/WHISPARR_LOGS_PATH }}
LIDARR_LOGS_PATH={{ op://Development/Logarr-Dev/LIDARR_LOGS_PATH }}
# READARR_LOGS_PATH={{ op://Development/Logarr-Dev/READARR_LOGS_PATH }}
2 changes: 1 addition & 1 deletion .github/agents/my-agent.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ This is a pnpm + Turborepo TypeScript monorepo.
- `apps/backend`: NestJS 11 API with Drizzle/Postgres, Redis, Socket.IO, Swagger, auth/RBAC, audit logging, retention, ingestion, settings, and proxying.
- `apps/frontend`: Next.js 16 App Router UI with React 19, Tailwind 4, Radix UI, React Query, and realtime socket hooks.
- `packages/core`: shared provider interfaces and normalized cross-service types.
- `packages/provider-*`: provider adapters for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, plus shared `provider-arr` base logic.
- `packages/provider-*`: provider adapters for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, Readarr, plus shared `provider-arr` base logic.
- `e2e`: Playwright coverage for key UI flows.
- `.github/workflows/docker-publish.yml`: CI gates and container publishing.

Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://www.docker.com/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

Unified logging, intelligent issue detection, and AI-powered analysis for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, and more.
Unified logging, intelligent issue detection, and AI-powered analysis for Plex, Jellyfin, Emby, Sonarr, Radarr, Prowlarr, Whisparr, Lidarr, Readarr, and more.

[Quick Start](#quick-start) · [Features](#features) · [Screenshots](#screenshots) · [Documentation](#documentation)

Expand Down Expand Up @@ -95,13 +95,15 @@ _Configure multiple AI providers for issue analysis (Anthropic, OpenAI, Google,

## Supported Servers

| Server | Status | Server | Status |
| ------------ | ------------ | ------------ | ------------ |
| **Jellyfin** | ✅ Supported | **Plex** | ✅ Supported |
| **Sonarr** | ✅ Supported | **Emby** | ✅ Supported |
| **Radarr** | ✅ Supported | **Whisparr** | ✅ Supported |
| **Prowlarr** | ✅ Supported | **Lidarr** | ✅ Supported |
| **Kodi** | 🚧 Planned | | |
| Server | Status | Server | Status |
| ------------ | -------------- | ------------ | ------------ |
| **Jellyfin** | ✅ Supported | **Plex** | ✅ Supported |
| **Sonarr** | ✅ Supported | **Emby** | ✅ Supported |
| **Radarr** | ✅ Supported | **Whisparr** | ✅ Supported |
| **Prowlarr** | ✅ Supported | **Lidarr** | ✅ Supported |
| **Readarr** | ⚠️ Supported\* | **Kodi** | 🚧 Planned |

> \*Readarr was retired upstream (archived June 2025, last release v0.4.18). Logarr supports it on a best-effort basis; expect permanent metadata (BookInfo) health errors on most installs.

> Logarr uses a provider architecture—adding support for new servers is straightforward. [Contributions welcome!](#contributing)

Expand Down Expand Up @@ -179,13 +181,14 @@ flowchart TB
PR[Prowlarr]
WH[Whisparr]
LI[Lidarr]
RE[Readarr]
end

FE <--> API
FE <--> WS
API <--> PG
API <--> RD
API <--> PX & JF & EM & SO & RA & PR & WH & LI
API <--> PX & JF & EM & SO & RA & PR & WH & LI & RE
```

### How It Works
Expand Down Expand Up @@ -274,6 +277,7 @@ RADARR_LOGS_PATH=/path/to/radarr/config/logs
PROWLARR_LOGS_PATH=/path/to/prowlarr/config/logs
WHISPARR_LOGS_PATH=/path/to/whisparr/config/logs
LIDARR_LOGS_PATH=/path/to/lidarr/config/logs
READARR_LOGS_PATH=/path/to/readarr/config/logs
```

2. **Restart Logarr** — The docker-compose mounts these paths automatically.
Expand All @@ -290,6 +294,7 @@ LIDARR_LOGS_PATH=/path/to/lidarr/config/logs
| Prowlarr | `/prowlarr-logs` |
| Whisparr | `/whisp-logs` |
| Lidarr | `/lidarr-logs` |
| Readarr | `/readarr-logs` |

#### Multiple Instances (e.g., Movies vs Shorts, 1080p vs 4K)

Expand Down Expand Up @@ -335,6 +340,7 @@ Simply add multiple servers in the UI using the same server type but different U
| Prowlarr | `/config/logs` | `~/.config/Prowlarr/logs` | `C:\ProgramData\Prowlarr\logs` |
| Whisparr | `/config/logs` | `~/.config/Whisparr/logs` | `C:\ProgramData\Whisparr\logs` |
| Lidarr | `/config/logs` | `~/.config/Lidarr/logs` | `C:\ProgramData\Lidarr\logs` |
| Readarr | `/config/logs` | `~/.config/Readarr/logs` | `C:\ProgramData\Readarr\logs` |

---

Expand Down Expand Up @@ -377,6 +383,7 @@ logarr/
│ ├── provider-prowlarr/ # Prowlarr integration
│ ├── provider-whisparr/ # Whisparr integration
│ ├── provider-lidarr/ # Lidarr integration
│ ├── provider-readarr/ # Readarr integration (retired upstream)
│ └── provider-arr/ # Base *arr provider
└── docker-compose.yml
```
Expand Down Expand Up @@ -488,6 +495,7 @@ pnpm test:e2e # Run Playwright E2E tests
| `@logarr/core` | Core HTTP utilities |
| `@logarr/provider-arr` | \*arr application log parser |
| `@logarr/provider-emby` | Emby log parser |
| `@logarr/provider-readarr` | Readarr provider |
| `@logarr/provider-whisparr` | Whisparr provider |
| `@logarr/provider-lidarr` | Lidarr provider |

Expand Down
2 changes: 2 additions & 0 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-whisparr/packag
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-whisparr/dist ./packages/provider-whisparr/dist
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-lidarr/package.json ./packages/provider-lidarr/
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-lidarr/dist ./packages/provider-lidarr/dist
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-readarr/package.json ./packages/provider-readarr/
COPY --from=builder --chown=nestjs:nodejs /app/packages/provider-readarr/dist ./packages/provider-readarr/dist

# Copy backend
COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/package.json ./apps/backend/
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ COPY apps/backend/ ./apps/backend/
RUN pnpm install

# Build only the packages (not backend itself - that will be done in watch mode)
RUN pnpm build --filter @logarr/core --filter @logarr/provider-arr --filter @logarr/provider-jellyfin --filter @logarr/provider-sonarr --filter @logarr/provider-radarr --filter @logarr/provider-prowlarr --filter @logarr/provider-plex --filter @logarr/provider-emby --filter @logarr/provider-whisparr --filter @logarr/provider-lidarr
RUN pnpm build --filter @logarr/core --filter @logarr/provider-arr --filter @logarr/provider-jellyfin --filter @logarr/provider-sonarr --filter @logarr/provider-radarr --filter @logarr/provider-prowlarr --filter @logarr/provider-plex --filter @logarr/provider-emby --filter @logarr/provider-whisparr --filter @logarr/provider-lidarr --filter @logarr/provider-readarr

EXPOSE 4002

Expand Down
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@logarr/provider-plex": "workspace:*",
"@logarr/provider-prowlarr": "workspace:*",
"@logarr/provider-radarr": "workspace:*",
"@logarr/provider-readarr": "workspace:*",
"@logarr/provider-sonarr": "workspace:*",
"@logarr/provider-whisparr": "workspace:*",
"@nestjs/axios": "^4.0.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/modules/ingestion/ingestion.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LidarrProvider } from '@logarr/provider-lidarr';
import { PlexProvider, PlexClient } from '@logarr/provider-plex';
import { ProwlarrProvider } from '@logarr/provider-prowlarr';
import { RadarrProvider } from '@logarr/provider-radarr';
import { ReadarrProvider } from '@logarr/provider-readarr';
import { SonarrProvider } from '@logarr/provider-sonarr';
import { WhisparrProvider } from '@logarr/provider-whisparr';
import { Injectable, Inject, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
Expand Down Expand Up @@ -85,6 +86,9 @@ export class IngestionService implements OnModuleInit, OnModuleDestroy {

const lidarrProvider = new LidarrProvider();
this.providers.set(lidarrProvider.id, lidarrProvider);

const readarrProvider = new ReadarrProvider();
this.providers.set(readarrProvider.id, readarrProvider);
}

async onModuleInit() {
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/src/modules/servers/server-seed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ const ENV_SERVER_CONFIGS: EnvServerConfig[] = [
name: 'Lidarr',
containerLogPath: '/lidarr-logs',
},
{
urlEnvKey: 'READARR_URL',
apiKeyEnvKey: 'READARR_API_KEY',
providerId: 'readarr',
name: 'Readarr',
containerLogPath: '/readarr-logs',
},
];

@Injectable()
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/modules/servers/servers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LidarrProvider } from '@logarr/provider-lidarr';
import { PlexProvider } from '@logarr/provider-plex';
import { ProwlarrProvider } from '@logarr/provider-prowlarr';
import { RadarrProvider } from '@logarr/provider-radarr';
import { ReadarrProvider } from '@logarr/provider-readarr';
import { SonarrProvider } from '@logarr/provider-sonarr';
import { WhisparrProvider } from '@logarr/provider-whisparr';
import { Injectable, Inject, NotFoundException, forwardRef, Logger } from '@nestjs/common';
Expand Down Expand Up @@ -55,6 +56,9 @@ export class ServersService {

const lidarrProvider = new LidarrProvider();
this.providers.set(lidarrProvider.id, lidarrProvider);

const readarrProvider = new ReadarrProvider();
this.providers.set(readarrProvider.id, readarrProvider);
}

async findAll() {
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/app/(dashboard)/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function LogDetailModal({
case 'sonarr':
case 'prowlarr':
case 'lidarr':
case 'readarr':
// *arr apps: system/events shows searchable event log
return `${baseUrl}/system/events`;
default:
Expand Down
43 changes: 43 additions & 0 deletions apps/frontend/src/lib/integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,44 @@ describe('Integrations Registry', () => {
});
});

describe('Readarr integration', () => {
it('should be available and not coming soon', () => {
const readarr = getIntegrationById('readarr');
expect(readarr).toBeDefined();
expect(readarr?.name).toBe('Readarr');
expect(readarr?.status).toBe('available');
expect(readarr?.connectionType).toBe('api');
expect(readarr?.category).toBe('arr_stack');
expect(readarr?.defaultPort).toBe(8787);
});

it('should not be duplicated in the registry', () => {
expect(integrations.filter((i) => i.id === 'readarr')).toHaveLength(1);
});

it('should state that it is retired upstream', () => {
const readarr = getIntegrationById('readarr');
expect(readarr?.description).toMatch(/retired/i);
expect(readarr?.description).toMatch(/best-effort/i);
});

it('should have standard API config fields', () => {
const fieldNames = getIntegrationById('readarr')?.configFields.map((f) => f.name);
expect(fieldNames).toEqual(['name', 'url', 'apiKey', 'logPath']);
});

it('should use the dashboard-icons readarr icon', () => {
const readarr = getIntegrationById('readarr');
expect(readarr?.icon).toBe('readarr');
expect(readarr?.iconType).toBe('dashboard-icons');
});

it('should be findable by name and by the retired notice', () => {
expect(searchIntegrations('readarr').some((i) => i.id === 'readarr')).toBe(true);
expect(searchIntegrations('retired').some((i) => i.id === 'readarr')).toBe(true);
});
});

describe('getIntegrationsByCategory', () => {
it('should include Whisparr in arr_stack category', () => {
const arrIntegrations = getIntegrationsByCategory('arr_stack');
Expand Down Expand Up @@ -180,6 +218,11 @@ describe('Integrations Registry', () => {
expect(ids).toContain('whisparr');
expect(ids).toContain('lidarr');
});

it('should include Readarr in available integrations', () => {
const ids = getAvailableIntegrations().map((i) => i.id);
expect(ids).toContain('readarr');
});
});

describe('searchIntegrations', () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/lib/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,10 @@ export const integrations: Integration[] = [
{
id: 'readarr',
name: 'Readarr',
description: 'Book, audiobook, and comic collection manager',
description:
'Book, audiobook, and comic collection manager (retired upstream - best-effort support)',
category: 'arr_stack',
status: 'coming_soon',
status: 'available',
connectionType: 'api',
icon: 'readarr',
iconType: 'dashboard-icons',
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ services:
- ./packages/provider-emby/src:/app/packages/provider-emby/src
- ./packages/provider-whisparr/src:/app/packages/provider-whisparr/src
- ./packages/provider-lidarr/src:/app/packages/provider-lidarr/src
- ./packages/provider-readarr/src:/app/packages/provider-readarr/src
- ./apps/backend/src:/app/apps/backend/src
# Mount config files for changes
- ./apps/backend/nest-cli.json:/app/apps/backend/nest-cli.json
Expand All @@ -92,6 +93,7 @@ services:
- ${PROWLARR_LOGS_PATH:-./logs/prowlarr}:/prowlarr-logs:ro
- ${WHISPARR_LOGS_PATH:-./logs/whisparr}:/whisparr-logs:ro
- ${LIDARR_LOGS_PATH:-./logs/lidarr}:/lidarr-logs:ro
- ${READARR_LOGS_PATH:-./logs/readarr}:/readarr-logs:ro
# Multiple instance mounts for development
- ${PLEX_LOGS_PATH_1:-./logs/plex-1}:/plex-logs-1:ro
- ${JELLYFIN_LOGS_PATH_1:-./logs/jellyfin-1}:/jellyfin-logs-1:ro
Expand All @@ -101,6 +103,7 @@ services:
- ${PROWLARR_LOGS_PATH_1:-./logs/prowlarr-1}:/prowlarr-logs-1:ro
- ${WHISPARR_LOGS_PATH_1:-./logs/whisparr-1}:/whisparr-logs-1:ro
- ${LIDARR_LOGS_PATH_1:-./logs/lidarr-1}:/lidarr-logs-1:ro
- ${READARR_LOGS_PATH_1:-./logs/readarr-1}:/readarr-logs-1:ro
- ${PLEX_LOGS_PATH_2:-./logs/plex-2}:/plex-logs-2:ro
- ${JELLYFIN_LOGS_PATH_2:-./logs/jellyfin-2}:/jellyfin-logs-2:ro
- ${EMBY_LOGS_PATH_2:-./logs/emby-2}:/emby-logs-2:ro
Expand All @@ -109,6 +112,7 @@ services:
- ${PROWLARR_LOGS_PATH_2:-./logs/prowlarr-2}:/prowlarr-logs-2:ro
- ${WHISPARR_LOGS_PATH_2:-./logs/whisparr-2}:/whisparr-logs-2:ro
- ${LIDARR_LOGS_PATH_2:-./logs/lidarr-2}:/lidarr-logs-2:ro
- ${READARR_LOGS_PATH_2:-./logs/readarr-2}:/readarr-logs-2:ro
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:4002/api']
interval: 30s
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ services:
WHISPARR_API_KEY: ${WHISPARR_API_KEY:-}
LIDARR_URL: ${LIDARR_URL:-}
LIDARR_API_KEY: ${LIDARR_API_KEY:-}
READARR_URL: ${READARR_URL:-}
READARR_API_KEY: ${READARR_API_KEY:-}
# Log paths - set to non-empty to enable file ingestion for that server
# The actual path inside container is fixed (e.g., /jellyfin-logs)
JELLYFIN_LOGS_PATH: ${JELLYFIN_LOGS_PATH:-}
Expand All @@ -87,6 +89,7 @@ services:
PROWLARR_LOGS_PATH: ${PROWLARR_LOGS_PATH:-}
WHISPARR_LOGS_PATH: ${WHISPARR_LOGS_PATH:-}
LIDARR_LOGS_PATH: ${LIDARR_LOGS_PATH:-}
READARR_LOGS_PATH: ${READARR_LOGS_PATH:-}
# Multiple instance support - add numbered suffixes for additional instances
# Example: RADARR_LOGS_PATH_1=/path/to/movies, RADARR_LOGS_PATH_2=/path/to/shorts
JELLYFIN_LOGS_PATH_1: ${JELLYFIN_LOGS_PATH_1:-}
Expand All @@ -113,6 +116,9 @@ services:
LIDARR_LOGS_PATH_1: ${LIDARR_LOGS_PATH_1:-}
LIDARR_LOGS_PATH_2: ${LIDARR_LOGS_PATH_2:-}
LIDARR_LOGS_PATH_3: ${LIDARR_LOGS_PATH_3:-}
READARR_LOGS_PATH_1: ${READARR_LOGS_PATH_1:-}
READARR_LOGS_PATH_2: ${READARR_LOGS_PATH_2:-}
READARR_LOGS_PATH_3: ${READARR_LOGS_PATH_3:-}
# Admin password reset - set to 'true' to reset admin account on startup
ADMIN_PASSWORD_RESET: ${ADMIN_PASSWORD_RESET:-}
volumes:
Expand All @@ -126,6 +132,7 @@ services:
- ${PROWLARR_LOGS_PATH:-/dev/null}:/prowlarr-logs:ro
- ${WHISPARR_LOGS_PATH:-/dev/null}:/whisp-logs:ro
- ${LIDARR_LOGS_PATH:-/dev/null}:/lidarr-logs:ro
- ${READARR_LOGS_PATH:-/dev/null}:/readarr-logs:ro
# Multiple instance mounts - numbered instances map to /<server>-logs-<n>
- ${JELLYFIN_LOGS_PATH_1:-/dev/null}:/jellyfin-logs-1:ro
- ${JELLYFIN_LOGS_PATH_2:-/dev/null}:/jellyfin-logs-2:ro
Expand All @@ -151,6 +158,9 @@ services:
- ${LIDARR_LOGS_PATH_1:-/dev/null}:/lidarr-logs-1:ro
- ${LIDARR_LOGS_PATH_2:-/dev/null}:/lidarr-logs-2:ro
- ${LIDARR_LOGS_PATH_3:-/dev/null}:/lidarr-logs-3:ro
- ${READARR_LOGS_PATH_1:-/dev/null}:/readarr-logs-1:ro
- ${READARR_LOGS_PATH_2:-/dev/null}:/readarr-logs-2:ro
- ${READARR_LOGS_PATH_3:-/dev/null}:/readarr-logs-3:ro
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:4000/api']
interval: 30s
Expand Down
14 changes: 13 additions & 1 deletion packages/provider-arr/src/arr.events.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { describeArrEvent, resolveArrEventName, toArrItemId } from './arr.events.js';
import { describeArrEvent, resolveArrEventName, stableArrHash, toArrItemId } from './arr.events.js';

import type { ArrEventTable } from './arr.events.js';

Expand Down Expand Up @@ -100,3 +100,15 @@ describe('toArrItemId', () => {
}
);
});

describe('stableArrHash', () => {
it('is deterministic and 16 hex chars', () => {
expect(stableArrHash('a', 'b')).toBe(stableArrHash('a', 'b'));
expect(stableArrHash('a', 'b')).toMatch(/^[0-9a-f]{16}$/);
});

it('differs for different input and for different part boundaries', () => {
expect(stableArrHash('a', 'b')).not.toBe(stableArrHash('a', 'c'));
expect(stableArrHash('ab', 'c')).not.toBe(stableArrHash('a', 'bc'));
});
});
Loading