add deep equality checks and naming fixes
This commit is contained in:
57
CLAUDE.md
57
CLAUDE.md
@@ -25,7 +25,7 @@ design principle validated through comprehensive integration testing.
|
||||
|
||||
- **Build**: `npm run build` - Compiles TypeScript using esbuild
|
||||
- **Development**: `npm run dev` - Runs the CLI directly with tsx
|
||||
- **Test**: `npm run test` - Runs Vitest test suite (42 tests total)
|
||||
- **Test**: `npm run test` - Runs Vitest test suite (64 tests total)
|
||||
- **Type checking**: `npm run typecheck` - Runs TypeScript compiler in check
|
||||
mode
|
||||
- **Generate types**: `npm run typegen` - Generates TypeScript types from
|
||||
@@ -228,6 +228,61 @@ operation alongside other management tools.
|
||||
with tests
|
||||
6. **Respect authentication**: Always ask for API key, never store it
|
||||
|
||||
## Naming Convention Rules
|
||||
|
||||
**CRITICAL**: Follow this exact naming pattern for all OpenAPI schema
|
||||
integrations.
|
||||
|
||||
For any OpenAPI schema `components["schemas"]["XyzAbc"]`, create:
|
||||
|
||||
1. **`XyzAbcSchema`** - Direct type mapping from OpenAPI schema
|
||||
|
||||
```typescript
|
||||
export type XyzAbcSchema = components["schemas"]["XyzAbc"];
|
||||
```
|
||||
|
||||
2. **`XyzAbcConfig`** - User-facing config interface
|
||||
|
||||
```typescript
|
||||
export interface XyzAbcConfig {
|
||||
someField?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
3. **`XyzAbcConfigSchema`** - Zod validation schema for config
|
||||
|
||||
```typescript
|
||||
export const XyzAbcConfigSchema = z
|
||||
.object({
|
||||
someField: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
```
|
||||
|
||||
4. **`mapXyzAbcConfigToSchema`** - Mapper function from config to schema
|
||||
|
||||
```typescript
|
||||
export function mapXyzAbcConfigToSchema(
|
||||
desired: XyzAbcConfig,
|
||||
): Partial<XyzAbcSchema> { ... }
|
||||
```
|
||||
|
||||
5. **Variable names** - Must match the type name
|
||||
```typescript
|
||||
const currentXyzAbcSchema: XyzAbcSchema = ...;
|
||||
const updatedXyzAbcSchema: XyzAbcSchema = ...;
|
||||
```
|
||||
|
||||
**Example**: For `components["schemas"]["EncodingOptions"]`:
|
||||
|
||||
- ✅ `EncodingOptionsSchema`, `EncodingOptionsConfig`,
|
||||
`EncodingOptionsConfigSchema`
|
||||
- ✅ `mapEncodingOptionsConfigToSchema`
|
||||
- ✅ `currentEncodingOptionsSchema`, `updatedEncodingOptionsSchema`
|
||||
|
||||
**Never deviate from this pattern** - it ensures consistent, predictable naming
|
||||
across the entire codebase.
|
||||
|
||||
## Important Reminders
|
||||
|
||||
- **Never create files unless absolutely necessary** - prefer editing existing
|
||||
|
||||
@@ -252,10 +252,10 @@ nix build .#
|
||||
|
||||
**High Priority:**
|
||||
|
||||
- [ ] **Encoding Configuration**: Transcoding settings, hardware acceleration,
|
||||
codec preferences
|
||||
- [ ] **Deep Equality Checking**: Replace JSON.stringify with proper deep
|
||||
comparison
|
||||
- [x] **Encoding Configuration**: Basic transcoding settings
|
||||
(enableHardwareEncoding)
|
||||
- [x] **Deep Equality Checking**: Replaced JSON.stringify with fast-equals
|
||||
library
|
||||
- [ ] **Structured Error Handling**: Better error types and API response details
|
||||
|
||||
**Medium Priority:**
|
||||
|
||||
@@ -8,3 +8,6 @@ system:
|
||||
enabled: true
|
||||
trickplayOptions:
|
||||
enableHwAcceleration: true
|
||||
enableHwEncoding: true
|
||||
encoding:
|
||||
enableHardwareEncoding: true
|
||||
|
||||
@@ -44,7 +44,7 @@ pkgs.stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
|
||||
pnpmDeps = pkgs.pnpm.fetchDeps {
|
||||
fetcherVersion = 1;
|
||||
hash = "sha256-LjdZDFenfylIpKxRpVmPUten/1IyL/cmI6QjDfcmfDc=";
|
||||
hash = "sha256-EFshNgnzsgnJnXuhdbyZKsMQ2W7LWA58jNQEzJ7TTwU=";
|
||||
inherit (finalAttrs) pname src version;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"commander": "^14.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"fast-equals": "^5.3.2",
|
||||
"openapi-fetch": "^0.15.0",
|
||||
"undici": "^7.16.0",
|
||||
"yaml": "^2.8.1",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -13,6 +13,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
fast-equals:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
openapi-fetch:
|
||||
specifier: ^0.15.0
|
||||
version: 0.15.0
|
||||
@@ -1096,6 +1099,13 @@ packages:
|
||||
integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==,
|
||||
}
|
||||
|
||||
fast-equals@5.3.2:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==,
|
||||
}
|
||||
engines: { node: ">=6.0.0" }
|
||||
|
||||
fast-glob@3.3.3:
|
||||
resolution:
|
||||
{
|
||||
@@ -2439,6 +2449,8 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-equals@5.3.2: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
dependencies:
|
||||
"@nodelib/fs.stat": 2.0.5
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ServerConfigurationSchema } from "../types/schema/system";
|
||||
import type { EncodingConfigurationSchema } from "../types/schema/encoding";
|
||||
import type { EncodingOptionsSchema } from "../types/schema/encoding";
|
||||
|
||||
export interface JellyfinClient {
|
||||
getSystemConfiguration(): Promise<ServerConfigurationSchema>;
|
||||
updateSystemConfiguration(
|
||||
body: Partial<ServerConfigurationSchema>,
|
||||
): Promise<void>;
|
||||
getEncodingConfiguration(): Promise<EncodingConfigurationSchema>;
|
||||
getEncodingConfiguration(): Promise<EncodingOptionsSchema>;
|
||||
updateEncodingConfiguration(
|
||||
body: Partial<EncodingConfigurationSchema>,
|
||||
body: Partial<EncodingOptionsSchema>,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ServerConfigurationSchema } from "../types/schema/system";
|
||||
import type { EncodingConfigurationSchema } from "../types/schema/encoding";
|
||||
import type { EncodingOptionsSchema } from "../types/schema/encoding";
|
||||
import type { JellyfinClient } from "./jellyfin.types";
|
||||
import { makeClient } from "./client";
|
||||
import type { paths } from "../../generated/schema";
|
||||
@@ -44,7 +44,7 @@ export function createJellyfinClient(
|
||||
}
|
||||
},
|
||||
|
||||
async getEncodingConfiguration(): Promise<EncodingConfigurationSchema> {
|
||||
async getEncodingConfiguration(): Promise<EncodingOptionsSchema> {
|
||||
// eslint-disable-next-line @typescript-eslint/typedef
|
||||
const res = await client.GET("/System/Configuration/{key}", {
|
||||
params: { path: { key: "encoding" } },
|
||||
@@ -56,11 +56,11 @@ export function createJellyfinClient(
|
||||
);
|
||||
}
|
||||
|
||||
return res.data as EncodingConfigurationSchema;
|
||||
return res.data as EncodingOptionsSchema;
|
||||
},
|
||||
|
||||
async updateEncodingConfiguration(
|
||||
body: Partial<EncodingConfigurationSchema>,
|
||||
body: Partial<EncodingOptionsSchema>,
|
||||
): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/typedef
|
||||
const res = await client.POST("/System/Configuration/{key}", {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { logger } from "../lib/logger";
|
||||
import { mapEncodingConfigurationConfigToSchema } from "../mappers/encoding";
|
||||
import { type EncodingConfig } from "../types/config/encoding";
|
||||
import { type EncodingConfigurationSchema } from "../types/schema/encoding";
|
||||
import { mapEncodingOptionsConfigToSchema } from "../mappers/encoding";
|
||||
import { type EncodingOptionsConfig } from "../types/config/encoding";
|
||||
import { type EncodingOptionsSchema } from "../types/schema/encoding";
|
||||
|
||||
function hasEnableHardwareEncodingChanged(
|
||||
current: EncodingConfigurationSchema,
|
||||
desired: EncodingConfig,
|
||||
current: EncodingOptionsSchema,
|
||||
desired: EncodingOptionsConfig,
|
||||
): boolean {
|
||||
if (desired.enableHardwareEncoding === undefined) return false;
|
||||
|
||||
@@ -16,11 +16,11 @@ function hasEnableHardwareEncodingChanged(
|
||||
}
|
||||
|
||||
export function applyEncoding(
|
||||
current: EncodingConfigurationSchema,
|
||||
desired: EncodingConfig,
|
||||
): EncodingConfigurationSchema {
|
||||
const patch: Partial<EncodingConfigurationSchema> =
|
||||
mapEncodingConfigurationConfigToSchema(desired);
|
||||
current: EncodingOptionsSchema,
|
||||
desired: EncodingOptionsConfig,
|
||||
): EncodingOptionsSchema {
|
||||
const patch: Partial<EncodingOptionsSchema> =
|
||||
mapEncodingOptionsConfigToSchema(desired);
|
||||
|
||||
if (hasEnableHardwareEncodingChanged(current, desired)) {
|
||||
logger.info(
|
||||
@@ -28,7 +28,7 @@ export function applyEncoding(
|
||||
);
|
||||
}
|
||||
|
||||
const out: EncodingConfigurationSchema = { ...current };
|
||||
const out: EncodingOptionsSchema = { ...current };
|
||||
|
||||
if ("EnableHardwareEncoding" in patch) {
|
||||
out.EnableHardwareEncoding = patch.EnableHardwareEncoding;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type EncodingConfig } from "../types/config/encoding";
|
||||
import { type EncodingConfigurationSchema } from "../types/schema/encoding";
|
||||
import { type EncodingOptionsConfig } from "../types/config/encoding";
|
||||
import { type EncodingOptionsSchema } from "../types/schema/encoding";
|
||||
|
||||
export function mapEncodingConfigurationConfigToSchema(
|
||||
desired: EncodingConfig,
|
||||
): Partial<EncodingConfigurationSchema> {
|
||||
const out: Partial<EncodingConfigurationSchema> = {};
|
||||
export function mapEncodingOptionsConfigToSchema(
|
||||
desired: EncodingOptionsConfig,
|
||||
): Partial<EncodingOptionsSchema> {
|
||||
const out: Partial<EncodingOptionsSchema> = {};
|
||||
|
||||
if (typeof desired.enableHardwareEncoding !== "undefined") {
|
||||
out.EnableHardwareEncoding = desired.enableHardwareEncoding;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { promises as fs } from "fs";
|
||||
import YAML from "yaml";
|
||||
import { deepEqual } from "fast-equals";
|
||||
import { applySystem } from "../apply/system";
|
||||
import { applyEncoding } from "../apply/encoding";
|
||||
import { type ServerConfigurationSchema } from "../types/schema/system";
|
||||
import { type EncodingConfigurationSchema } from "../types/schema/encoding";
|
||||
import { type EncodingOptionsSchema } from "../types/schema/encoding";
|
||||
import { createJellyfinClient } from "../api/jellyfin_client";
|
||||
import { type JellyfinClient } from "../api/jellyfin.types";
|
||||
import {
|
||||
@@ -35,19 +36,21 @@ export async function runPipeline(path: string): Promise<void> {
|
||||
);
|
||||
|
||||
// Handle system configuration
|
||||
const currentSystem: ServerConfigurationSchema =
|
||||
const currentServerConfigurationSchema: ServerConfigurationSchema =
|
||||
await jellyfinClient.getSystemConfiguration();
|
||||
|
||||
const updatedSystem: ServerConfigurationSchema = applySystem(
|
||||
currentSystem,
|
||||
cfg.system,
|
||||
);
|
||||
const updatedServerConfigurationSchema: ServerConfigurationSchema =
|
||||
applySystem(currentServerConfigurationSchema, cfg.system);
|
||||
|
||||
const systemSame: boolean =
|
||||
JSON.stringify(updatedSystem) === JSON.stringify(currentSystem);
|
||||
if (!systemSame) {
|
||||
const isSystemSame: boolean = deepEqual(
|
||||
updatedServerConfigurationSchema,
|
||||
currentServerConfigurationSchema,
|
||||
);
|
||||
if (!isSystemSame) {
|
||||
console.log("→ updating system config");
|
||||
await jellyfinClient.updateSystemConfiguration(updatedSystem);
|
||||
await jellyfinClient.updateSystemConfiguration(
|
||||
updatedServerConfigurationSchema,
|
||||
);
|
||||
console.log("✓ updated system config");
|
||||
} else {
|
||||
console.log("✓ system config already up to date");
|
||||
@@ -55,19 +58,23 @@ export async function runPipeline(path: string): Promise<void> {
|
||||
|
||||
// Handle encoding configuration if provided
|
||||
if (cfg.encoding) {
|
||||
const currentEncoding: EncodingConfigurationSchema =
|
||||
const currentEncodingOptionsSchema: EncodingOptionsSchema =
|
||||
await jellyfinClient.getEncodingConfiguration();
|
||||
|
||||
const updatedEncoding: EncodingConfigurationSchema = applyEncoding(
|
||||
currentEncoding,
|
||||
const updatedEncodingOptionsSchema: EncodingOptionsSchema = applyEncoding(
|
||||
currentEncodingOptionsSchema,
|
||||
cfg.encoding,
|
||||
);
|
||||
|
||||
const encodingSame: boolean =
|
||||
JSON.stringify(updatedEncoding) === JSON.stringify(currentEncoding);
|
||||
if (!encodingSame) {
|
||||
const isEncodingSame: boolean = deepEqual(
|
||||
updatedEncodingOptionsSchema,
|
||||
currentEncodingOptionsSchema,
|
||||
);
|
||||
if (!isEncodingSame) {
|
||||
console.log("→ updating encoding config");
|
||||
await jellyfinClient.updateEncodingConfiguration(updatedEncoding);
|
||||
await jellyfinClient.updateEncodingConfiguration(
|
||||
updatedEncodingOptionsSchema,
|
||||
);
|
||||
console.log("✓ updated encoding config");
|
||||
} else {
|
||||
console.log("✓ encoding config already up to date");
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface EncodingConfig {
|
||||
export interface EncodingOptionsConfig {
|
||||
enableHardwareEncoding?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { components } from "../../../generated/schema";
|
||||
|
||||
export type EncodingConfigurationSchema =
|
||||
components["schemas"]["EncodingOptions"];
|
||||
export type EncodingOptionsSchema = components["schemas"]["EncodingOptions"];
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SystemConfigSchema: z.ZodObject<{
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const EncodingConfigSchema: z.ZodObject<{
|
||||
export const EncodingOptionsConfigSchema: z.ZodObject<{
|
||||
enableHardwareEncoding: z.ZodOptional<z.ZodBoolean>;
|
||||
}> = z
|
||||
.object({
|
||||
@@ -44,13 +44,13 @@ export const RootConfigSchema: z.ZodObject<{
|
||||
version: z.ZodNumber;
|
||||
base_url: ReturnType<typeof z.url>;
|
||||
system: typeof SystemConfigSchema;
|
||||
encoding: z.ZodOptional<typeof EncodingConfigSchema>;
|
||||
encoding: z.ZodOptional<typeof EncodingOptionsConfigSchema>;
|
||||
}> = z
|
||||
.object({
|
||||
version: z.number().int().positive("Version must be a positive integer"),
|
||||
base_url: z.url({ message: "Base URL must be a valid URL" }),
|
||||
system: SystemConfigSchema,
|
||||
encoding: EncodingConfigSchema.optional(),
|
||||
encoding: EncodingOptionsConfigSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -61,5 +61,7 @@ export type ValidatedTrickplayOptionsConfig = z.infer<
|
||||
typeof TrickplayOptionsConfigSchema
|
||||
>;
|
||||
export type ValidatedSystemConfig = z.infer<typeof SystemConfigSchema>;
|
||||
export type ValidatedEncodingConfig = z.infer<typeof EncodingConfigSchema>;
|
||||
export type ValidatedEncodingOptionsConfig = z.infer<
|
||||
typeof EncodingOptionsConfigSchema
|
||||
>;
|
||||
export type ValidatedRootConfig = z.infer<typeof RootConfigSchema>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
|
||||
import { applyEncoding } from "../../src/apply/encoding";
|
||||
import { type EncodingConfig } from "../../src/types/config/encoding";
|
||||
import { type EncodingConfigurationSchema } from "../../src/types/schema/encoding";
|
||||
import { type EncodingOptionsConfig } from "../../src/types/config/encoding";
|
||||
import { type EncodingOptionsSchema } from "../../src/types/schema/encoding";
|
||||
import * as loggerModule from "../../src/lib/logger";
|
||||
|
||||
// Mock the logger
|
||||
@@ -21,21 +21,18 @@ describe("apply/encoding", () => {
|
||||
describe("applyEncoding", () => {
|
||||
it("should update EnableHardwareEncoding when enableHardwareEncoding changes from false to true", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: false,
|
||||
EncodingThreadCount: -1,
|
||||
TranscodingTempPath: "/tmp",
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {
|
||||
const desired: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: EncodingConfigurationSchema = applyEncoding(
|
||||
current,
|
||||
desired,
|
||||
);
|
||||
const result: EncodingOptionsSchema = applyEncoding(current, desired);
|
||||
|
||||
// Assert
|
||||
expect(result.EnableHardwareEncoding).toBe(true);
|
||||
@@ -45,20 +42,17 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should update EnableHardwareEncoding when enableHardwareEncoding changes from true to false", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: true,
|
||||
EncodingThreadCount: 4,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {
|
||||
const desired: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: EncodingConfigurationSchema = applyEncoding(
|
||||
current,
|
||||
desired,
|
||||
);
|
||||
const result: EncodingOptionsSchema = applyEncoding(current, desired);
|
||||
|
||||
// Assert
|
||||
expect(result.EnableHardwareEncoding).toBe(false);
|
||||
@@ -67,18 +61,15 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should not modify EnableHardwareEncoding when enableHardwareEncoding is undefined", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: true,
|
||||
EncodingThreadCount: 2,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {};
|
||||
const desired: EncodingOptionsConfig = {};
|
||||
|
||||
// Act
|
||||
const result: EncodingConfigurationSchema = applyEncoding(
|
||||
current,
|
||||
desired,
|
||||
);
|
||||
const result: EncodingOptionsSchema = applyEncoding(current, desired);
|
||||
|
||||
// Assert
|
||||
expect(result.EnableHardwareEncoding).toBe(true); // Should remain unchanged
|
||||
@@ -87,20 +78,17 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should not modify EnableHardwareEncoding when value is the same", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: true,
|
||||
EncodingThreadCount: 1,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {
|
||||
const desired: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: EncodingConfigurationSchema = applyEncoding(
|
||||
current,
|
||||
desired,
|
||||
);
|
||||
const result: EncodingOptionsSchema = applyEncoding(current, desired);
|
||||
|
||||
// Assert
|
||||
expect(result.EnableHardwareEncoding).toBe(true);
|
||||
@@ -109,11 +97,11 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should log when EnableHardwareEncoding changes", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: false,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {
|
||||
const desired: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
@@ -133,11 +121,11 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should not log when EnableHardwareEncoding does not change", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: true,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {
|
||||
const desired: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
@@ -155,11 +143,11 @@ describe("apply/encoding", () => {
|
||||
|
||||
it("should not log when enableHardwareEncoding is undefined", () => {
|
||||
// Arrange
|
||||
const current: EncodingConfigurationSchema = {
|
||||
const current: EncodingOptionsSchema = {
|
||||
EnableHardwareEncoding: true,
|
||||
} as EncodingConfigurationSchema;
|
||||
} as EncodingOptionsSchema;
|
||||
|
||||
const desired: EncodingConfig = {};
|
||||
const desired: EncodingOptionsConfig = {};
|
||||
|
||||
const loggerSpy: Mock<(msg: string) => void> = vi.spyOn(
|
||||
loggerModule.logger,
|
||||
|
||||
77
tests/lib/deep-equality.spec.ts
Normal file
77
tests/lib/deep-equality.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { deepEqual } from "fast-equals";
|
||||
|
||||
describe("lib/deep-equality", () => {
|
||||
describe("deepEqual vs JSON.stringify", () => {
|
||||
it("should handle objects with different property order correctly", () => {
|
||||
// Arrange
|
||||
const obj1: { a: number; b: number; c: number } = { a: 1, b: 2, c: 3 };
|
||||
const obj2: { c: number; b: number; a: number } = { c: 3, b: 2, a: 1 };
|
||||
|
||||
// Act & Assert
|
||||
// deep equality should correctly identify these as equal
|
||||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||||
|
||||
// JSON.stringify would incorrectly identify these as different
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle undefined values correctly", () => {
|
||||
// Arrange
|
||||
const obj1: { a: number; b: undefined } = { a: 1, b: undefined };
|
||||
const obj2: { a: number; b: undefined } = { a: 1, b: undefined };
|
||||
|
||||
// Act & Assert
|
||||
// deep equality should correctly handle undefined
|
||||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||||
|
||||
// JSON.stringify would not serialize undefined values at all
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(true);
|
||||
|
||||
// But JSON.stringify would miss differences involving undefined
|
||||
const obj3: { a: number } = { a: 1 };
|
||||
expect(deepEqual(obj1, obj3)).toBe(false);
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj3)).toBe(true); // false positive!
|
||||
});
|
||||
|
||||
it("should handle nested objects correctly", () => {
|
||||
// Arrange
|
||||
const obj1: {
|
||||
system: { enableMetrics: boolean };
|
||||
encoding: { enableHardwareEncoding: boolean };
|
||||
} = {
|
||||
system: { enableMetrics: true },
|
||||
encoding: { enableHardwareEncoding: false },
|
||||
};
|
||||
const obj2: {
|
||||
encoding: { enableHardwareEncoding: boolean };
|
||||
system: { enableMetrics: boolean };
|
||||
} = {
|
||||
encoding: { enableHardwareEncoding: false },
|
||||
system: { enableMetrics: true },
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle Date objects correctly", () => {
|
||||
// Arrange
|
||||
const date: Date = new Date("2024-01-01");
|
||||
const obj1: { timestamp: Date } = { timestamp: date };
|
||||
const obj2: { timestamp: Date } = { timestamp: new Date("2024-01-01") };
|
||||
|
||||
// Act & Assert
|
||||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj2)).toBe(true); // works but converts to string
|
||||
|
||||
// But deepEqual preserves type information
|
||||
const obj3: { timestamp: string } = {
|
||||
timestamp: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
expect(deepEqual(obj1, obj3)).toBe(false); // correctly different types
|
||||
expect(JSON.stringify(obj1) === JSON.stringify(obj3)).toBe(true); // false positive!
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,19 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { mapEncodingConfigurationConfigToSchema } from "../../src/mappers/encoding";
|
||||
import { type EncodingConfig } from "../../src/types/config/encoding";
|
||||
import { type EncodingConfigurationSchema } from "../../src/types/schema/encoding";
|
||||
import { mapEncodingOptionsConfigToSchema } from "../../src/mappers/encoding";
|
||||
import { type EncodingOptionsConfig } from "../../src/types/config/encoding";
|
||||
import { type EncodingOptionsSchema } from "../../src/types/schema/encoding";
|
||||
|
||||
describe("mappers/encoding", () => {
|
||||
describe("mapEncodingConfigurationConfigToSchema", () => {
|
||||
describe("mapEncodingOptionsConfigToSchema", () => {
|
||||
it("should map enableHardwareEncoding to EnableHardwareEncoding", () => {
|
||||
// Arrange
|
||||
const config: EncodingConfig = {
|
||||
const config: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: Partial<EncodingConfigurationSchema> =
|
||||
mapEncodingConfigurationConfigToSchema(config);
|
||||
const result: Partial<EncodingOptionsSchema> =
|
||||
mapEncodingOptionsConfigToSchema(config);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
@@ -23,13 +23,13 @@ describe("mappers/encoding", () => {
|
||||
|
||||
it("should map enableHardwareEncoding false to EnableHardwareEncoding false", () => {
|
||||
// Arrange
|
||||
const config: EncodingConfig = {
|
||||
const config: EncodingOptionsConfig = {
|
||||
enableHardwareEncoding: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: Partial<EncodingConfigurationSchema> =
|
||||
mapEncodingConfigurationConfigToSchema(config);
|
||||
const result: Partial<EncodingOptionsSchema> =
|
||||
mapEncodingOptionsConfigToSchema(config);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
@@ -39,11 +39,11 @@ describe("mappers/encoding", () => {
|
||||
|
||||
it("should return empty object when enableHardwareEncoding is undefined", () => {
|
||||
// Arrange
|
||||
const config: EncodingConfig = {};
|
||||
const config: EncodingOptionsConfig = {};
|
||||
|
||||
// Act
|
||||
const result: Partial<EncodingConfigurationSchema> =
|
||||
mapEncodingConfigurationConfigToSchema(config);
|
||||
const result: Partial<EncodingOptionsSchema> =
|
||||
mapEncodingOptionsConfigToSchema(config);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({});
|
||||
@@ -51,11 +51,11 @@ describe("mappers/encoding", () => {
|
||||
|
||||
it("should not include EnableHardwareEncoding when field is not provided", () => {
|
||||
// Arrange
|
||||
const config: EncodingConfig = {};
|
||||
const config: EncodingOptionsConfig = {};
|
||||
|
||||
// Act
|
||||
const result: Partial<EncodingConfigurationSchema> =
|
||||
mapEncodingConfigurationConfigToSchema(config);
|
||||
const result: Partial<EncodingOptionsSchema> =
|
||||
mapEncodingOptionsConfigToSchema(config);
|
||||
|
||||
// Assert
|
||||
expect(result).not.toHaveProperty("EnableHardwareEncoding");
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
SystemConfigSchema,
|
||||
PluginRepositoryConfigSchema,
|
||||
TrickplayOptionsConfigSchema,
|
||||
EncodingConfigSchema,
|
||||
EncodingOptionsConfigSchema,
|
||||
type ValidatedPluginRepositoryConfig,
|
||||
type ValidatedTrickplayOptionsConfig,
|
||||
type ValidatedSystemConfig,
|
||||
type ValidatedEncodingConfig,
|
||||
type ValidatedEncodingOptionsConfig,
|
||||
type ValidatedRootConfig,
|
||||
} from "../../src/validation/config";
|
||||
|
||||
@@ -358,14 +358,14 @@ describe("validation/config — RootConfigSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation/config — EncodingConfigSchema", () => {
|
||||
describe("validation/config — EncodingOptionsConfigSchema", () => {
|
||||
it("should validate empty encoding config", () => {
|
||||
// Arrange
|
||||
const validConfig: z.input<typeof EncodingConfigSchema> = {};
|
||||
const validConfig: z.input<typeof EncodingOptionsConfigSchema> = {};
|
||||
|
||||
// Act
|
||||
const result: ZodSafeParseResult<ValidatedEncodingConfig> =
|
||||
EncodingConfigSchema.safeParse(validConfig);
|
||||
const result: ZodSafeParseResult<ValidatedEncodingOptionsConfig> =
|
||||
EncodingOptionsConfigSchema.safeParse(validConfig);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
@@ -376,13 +376,13 @@ describe("validation/config — EncodingConfigSchema", () => {
|
||||
|
||||
it("should validate encoding config with enableHardwareEncoding true", () => {
|
||||
// Arrange
|
||||
const validConfig: z.input<typeof EncodingConfigSchema> = {
|
||||
const validConfig: z.input<typeof EncodingOptionsConfigSchema> = {
|
||||
enableHardwareEncoding: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: ZodSafeParseResult<ValidatedEncodingConfig> =
|
||||
EncodingConfigSchema.safeParse(validConfig);
|
||||
const result: ZodSafeParseResult<ValidatedEncodingOptionsConfig> =
|
||||
EncodingOptionsConfigSchema.safeParse(validConfig);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
@@ -393,13 +393,13 @@ describe("validation/config — EncodingConfigSchema", () => {
|
||||
|
||||
it("should validate encoding config with enableHardwareEncoding false", () => {
|
||||
// Arrange
|
||||
const validConfig: z.input<typeof EncodingConfigSchema> = {
|
||||
const validConfig: z.input<typeof EncodingOptionsConfigSchema> = {
|
||||
enableHardwareEncoding: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: ZodSafeParseResult<ValidatedEncodingConfig> =
|
||||
EncodingConfigSchema.safeParse(validConfig);
|
||||
const result: ZodSafeParseResult<ValidatedEncodingOptionsConfig> =
|
||||
EncodingOptionsConfigSchema.safeParse(validConfig);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
@@ -410,14 +410,14 @@ describe("validation/config — EncodingConfigSchema", () => {
|
||||
|
||||
it("should reject non-boolean enableHardwareEncoding", () => {
|
||||
// Arrange
|
||||
const invalidConfig: z.input<typeof EncodingConfigSchema> = {
|
||||
const invalidConfig: z.input<typeof EncodingOptionsConfigSchema> = {
|
||||
// @ts-expect-error intentional bad type for test
|
||||
enableHardwareEncoding: "true",
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: ZodSafeParseResult<ValidatedEncodingConfig> =
|
||||
EncodingConfigSchema.safeParse(invalidConfig);
|
||||
const result: ZodSafeParseResult<ValidatedEncodingOptionsConfig> =
|
||||
EncodingOptionsConfigSchema.safeParse(invalidConfig);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
@@ -425,15 +425,15 @@ describe("validation/config — EncodingConfigSchema", () => {
|
||||
|
||||
it("should reject extra fields due to strict mode", () => {
|
||||
// Arrange
|
||||
const invalidConfig: z.input<typeof EncodingConfigSchema> = {
|
||||
const invalidConfig: z.input<typeof EncodingOptionsConfigSchema> = {
|
||||
enableHardwareEncoding: true,
|
||||
// @ts-expect-error intentional extra field for test
|
||||
unknownField: "should not be allowed",
|
||||
};
|
||||
|
||||
// Act
|
||||
const result: ZodSafeParseResult<ValidatedEncodingConfig> =
|
||||
EncodingConfigSchema.safeParse(invalidConfig);
|
||||
const result: ZodSafeParseResult<ValidatedEncodingOptionsConfig> =
|
||||
EncodingOptionsConfigSchema.safeParse(invalidConfig);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user