From 01f6c7302a40151b4421a4bbaf70004aeb16c1c5 Mon Sep 17 00:00:00 2001 From: Avinash Boppay Date: Mon, 6 Jul 2026 10:20:02 +0530 Subject: [PATCH] feat: add download-file for vcpm and gkv alias chunks update download-file to query vcpm and gkv alias data and save the chunks. Signed-off-by: Avinash Boppay --- .../download-vcpm-gkv-alias-design.md | 186 +++++++++++++++ .../services/acdb-file-serializer.ts | 82 ++++++- .../gkv-alias-chunk-serializer.ts | 141 ++++++++++++ .../bulk-read/bulk-read-query-service.ts | 27 +++ .../awsp-parser.integration.spec.ts | 2 +- .../gkv-alias-download.integration.spec.ts | 83 +++++++ ...e-calibration-download.integration.spec.ts | 81 +++++++ .../gkv-alias-chunk-serializer.spec.ts | 125 ++++++++++ .../typeorm-bulk-read-query-service.ts | 201 +++++++++++++++- .../vcpm-calibration-download.spec.ts | 216 ++++++++++++++++++ 10 files changed, 1141 insertions(+), 3 deletions(-) create mode 100644 docs/download-file/download-vcpm-gkv-alias-design.md create mode 100644 packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts create mode 100644 packages/core/tests/integration/application/file-operations/download-file/gkv-alias-download.integration.spec.ts create mode 100644 packages/core/tests/unit/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.spec.ts create mode 100644 packages/infrastructure/persistence/tests/integration/bulk-read/vcpm-calibration-download.spec.ts diff --git a/docs/download-file/download-vcpm-gkv-alias-design.md b/docs/download-file/download-vcpm-gkv-alias-design.md new file mode 100644 index 000000000..49e27ea7f --- /dev/null +++ b/docs/download-file/download-vcpm-gkv-alias-design.md @@ -0,0 +1,186 @@ + + +# Plan: Download VCPM Calibration Data + GKV Alias + +## Context + +The upload-file path already parses two pieces of data from an ACDB file that the download-file path does not yet reconstruct: + +1. **VCPM calibration data** — stored in `vcpm_instances` / `vcpm_ckv` / `vcpm_ckv_values` / `vcpm_parameter_payload` tables. The existing voice calibration download reads the `ckv` tables (SPF modules with `is_voice=true`) but never touches the VCPM-specific tables. The result: `VCPM_CALDATA` chunk family (`VCCD`, `VCMK`, `VCKT`, `VCLU`, `VCDE`) in the output is missing the VCPM module's calibration entries. + +2. **GKV alias** — stored in `use_cases.alias_id` / `use_cases.alias`. The download path already fetches every usecase row but drops these columns. The `GALS` chunk is never emitted. + +Both are reverse-of-upload operations. The goal is to complete the round-trip: upload → download → upload produces identical files. + +--- + +## Feature folder + +`docs/download-file/` — existing home for all download-file design docs. + +--- + +## Approach + +### A. VCPM Calibration Data Download + +**Key insight:** `VoiceCalibrationChunk` and `VoiceCalibrationChunkBuilder` already express the correct binary structure. The builder takes `CalibrationDataDownloadModel[]` — the same model shape that VCPM data naturally maps to. The serializer (`VoiceCalibrationChunkSerializer`) is already correct. No new chunk class or serializer needed. + +**What's missing:** a DB query that reads `vcpm_ckv → vcpm_ckv_values → value_definitions → arc_keys` + `vcpm_parameter_payload → vcpm_module_parameter_definitions` and returns `CalibrationDataDownloadModel[]`. + +**How VCPM merges with voice-CKV:** A single `VCPM_CALDATA` binary chunk exists in the output. After splitting calibration data into `audio` and `voice` arrays, VCPM data is merged into the `voice` array — specifically, for each voice subgraph that has a VCPM instance, the VCPM key-value combinations are appended to that subgraph's entry (or a new entry is created if no voice-CKV existed for that subgraph). The serializer then writes one combined `VCPM_CALDATA` chunk. + +**Sorting contract:** VCPM data must be sorted the same way as voice-CKV data: by subgraphId → keyIds → valueIds → moduleInstanceId → parameterId. This can be done at the application layer alongside the existing `sortCkvEntries()` logic. + +**What modules appear in the VCPM data:** During upload, `SPF_VCPM_MODULE_ID = 4` entries are **excluded** from the CKV path and stored in `vcpm_ckv` instead. The `moduleInstanceId` in the download model comes from `vcpm_module_definitions.module_definition_id` (the natural ID), not from SPF instance IDs. We set it to `SPF_VCPM_MODULE_ID` (= 4) during the query build. + +### B. GKV Alias Download + +**Key insight:** `GkvAliasChunk` already exists. The upload parser (`GkvAliasChunkParser`) shows the exact binary format. The download needs a new `GkvAliasChunkSerializer` that is the mirror. + +**Data source:** `use_cases.alias_id` and `use_cases.alias` — already fetched by `readUsecaseData()`. Add two optional fields to `UsecaseDataDownloadModel`. + +**Binary format (from parser):** +``` +GALS body: + NumKeyTables: uint32 + For each key table (grouped by numKeys): + NumKeys: uint32 + NumGkvs: uint32 + For each GKV: + numKeys × [keyId uint32, keyVal uint32] + DatapoolOffset: uint32 + +Datapool payload: + [uint32 innerStringLen][ASCII: "" or " | \0"] +``` + +Usecases are already sorted by numKeys → keyIds → valueIds by `readUsecaseData()`. The serializer groups them by numKeys to produce one `GkvAliasTable` per group. + +--- + +## Components to Create / Modify + +### 1. `BulkReadQueryService` interface (core) +**File:** `packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts` +- Add `vcpmCalibrationData?: CalibrationDataDownloadModel[]` to `DownloadEntities` +- Add `aliasId?: number` and `alias?: string` to `UsecaseDataDownloadModel` +- Add `readVcpmCalibrationData(fileSystemId: number): Promise` to `BulkReadQueryService` interface + +### 2. `TypeOrmBulkReadQueryService` — new query method (infrastructure) +**File:** `packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.ts` + +**`readVcpmCalibrationData(fileSystemId)`:** +- Fetch all VcpmCkv entries joined through VcpmInstance → Subgraph (filtered by `fileSystemId`) +- Fetch VcpmCkvValues for those IDs → join ValueDefinition → ArcKey (for `keyId`, `valueId`, `isDynamic`) +- Fetch VcpmParameterPayload for those IDs → join VcpmModuleParameterDefinition (for `paramId`, `payload`) +- Build `CalibrationDataDownloadModel[]` with the same grouping logic as `buildCalibrationModels()`: + - `moduleInstanceId` = `SPF_VCPM_MODULE_ID` (4) for all entries (VCPM module) + - `parameterId` from `VcpmModuleParameterDefinition.paramId` + - `payload` from `VcpmParameterPayload.payload` + - `pidType` = `''` (not used by voice builder; field required by type) + - Sort: subgraphId → keyIds → valueIds → parameterId + +**`readAllEntitiesForFile()` update:** +- Add `timed('readVcpmCalibrationData', this.readVcpmCalibrationData(fileSystemId))` to the `Promise.all()` call +- Include `vcpmCalibrationData` in the returned `DownloadEntities` + +**`readUsecaseData()` update:** +- Include `aliasId` and `alias` from `UseCaseRow` in the mapped model + +### 3. `GkvAliasChunkSerializer` (new, core) +**File:** `packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts` + +**`serialize(usecaseData: UsecaseDataDownloadModel[]): Uint8Array`:** +- Filter usecases to only those with `aliasId !== undefined` +- Group by `numKeys` (= `keyIds.length`) — one `GkvAliasTable` per group +- For each usecase in a group: build alias string `"${aliasId}"` or `"${aliasId} | ${alias}"` +- Write alias string to datapool: `[uint32 innerStringLen][ASCII bytes]` +- Write binary: numKeyTables → for each group: numKeys, numGkvs → for each GKV: keyId/keyVal pairs + datapoolOffset +- Uses shared `DatapoolChunk` passed in as parameter + +**Reuses:** `GkvAliasChunk` (data class), `DatapoolChunk.addOrReuse()`, `BinaryUtils` + +### 4. `AcdbFileSerializer` (modify, core) +**File:** `packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts` + +**VCPM merge into voice calibration:** +- After the `splitCalibrationData()` call, merge `entities.vcpmCalibrationData` into the `voice` array +- Merge strategy: for each VCPM entry by `subgraphId`, if a voice entry already exists for that subgraph, append its `keyValueCombinations`; otherwise push the VCPM entry as-is +- Re-sort the merged `voice` array by subgraphId → keyIds → valueIds before passing to builder + +**GKV alias serialization:** +- After `serializeUsecaseChunks()`, add a call to `serializeGkvAliasChunk(entities.usecaseData ?? [], chunkList, datapool)` +- New private method `serializeGkvAliasChunk()` creates a `GkvAliasChunkSerializer` and calls serialize +- Only emits the `GALS` chunk if at least one entry has `aliasId !== undefined` + +**Chunk order in file:** `GALS` chunk is emitted immediately after `GKV_TABLE` / `GKV_LUT` (the usecase chunks), before audio calibration. This matches the natural grouping of usecase-related chunks. + +### 5. Tests + +**Unit tests (new):** +- `packages/core/tests/unit/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.spec.ts` + - Empty data → no output + - Single entry with aliasId, no name + - Multiple entries same numKeys → single table + - Multiple entries different numKeys → multiple tables + - Alias string format: with and without usecaseName + +**Integration tests (extend):** +- `packages/core/tests/integration/application/file-operations/download-file/voice-calibration-download.integration.spec.ts` + - Add test: entities with `vcpmCalibrationData` → verify VCPM_CALDATA contains merged data +- New file: `packages/core/tests/integration/application/file-operations/download-file/gkv-alias-download.integration.spec.ts` + - Upload file with GKV alias data → download → verify GALS chunk present and parses correctly + +**Infrastructure integration tests (new):** +- `packages/infrastructure/persistence/tests/integration/bulk-read/vcpm-calibration-download.spec.ts` + - Insert vcpm_instances/vcpm_ckv/vcpm_ckv_values/vcpm_parameter_payload rows + - Call `readVcpmCalibrationData()` → verify model shape and values + +--- + +## Data Flow Summary + +``` +DB (vcpm_ckv + vcpm_ckv_values + vcpm_parameter_payload) + ↓ readVcpmCalibrationData() +CalibrationDataDownloadModel[] (vcpmCalibrationData) + ↓ merge into voice[] in AcdbFileSerializer +voice[] (voice-CKV + VCPM combined) + ↓ VoiceCalibrationChunkBuilder.buildChunk() +VoiceCalibrationChunk (offsets in datapool) + ↓ VoiceCalibrationChunkSerializer.serialize() +VCPM_CALDATA + VCMK + VCKT + VCLU + VCDE binary chunks + +DB (use_cases.alias_id, use_cases.alias) + ↓ readUsecaseData() (extended to include aliasId/alias) +UsecaseDataDownloadModel[] (with aliasId/alias) + ↓ GkvAliasChunkSerializer.serialize() +GALS binary chunk +``` + +--- + +## Verification + +1. Run existing tests: `nx test core` and `nx test infrastructure` — must pass +2. E2E round-trip test: upload a file containing VCPM data and GKV alias → download → parse the downloaded ACDB → verify `VCPM_CALDATA` and `GALS` chunks match the originals byte-for-byte +3. The fixture file `packages/api/tests/e2e/fixtures/acdb_cal.acdb` contains VCPM and GKV alias data — use this for the round-trip test + +--- + +## Files Changed Summary + +| File | Change | +|------|--------| +| `packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts` | Add `vcpmCalibrationData` to `DownloadEntities`; add `aliasId`/`alias` to `UsecaseDataDownloadModel`; add `readVcpmCalibrationData` to interface | +| `packages/infrastructure/persistence/src/.../typeorm-bulk-read-query-service.ts` | Add `readVcpmCalibrationData()`; extend `readUsecaseData()` to include `aliasId`/`alias`; add to `readAllEntitiesForFile()` | +| `packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts` | **New** — GKV alias binary serializer | +| `packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts` | Merge VCPM into voice array; emit GALS chunk after usecase chunks | +| `packages/core/tests/unit/.../gkv-alias-chunk-serializer.spec.ts` | **New** — unit tests | +| `packages/core/tests/integration/.../voice-calibration-download.integration.spec.ts` | Extend with VCPM merge test | +| `packages/core/tests/integration/.../gkv-alias-download.integration.spec.ts` | **New** — integration test | +| `packages/infrastructure/persistence/tests/.../vcpm-calibration-download.spec.ts` | **New** — DB integration test | diff --git a/packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts b/packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts index 8ee669117..d8e261603 100644 --- a/packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts +++ b/packages/core/src/application/file-operations/download-file/services/acdb-file-serializer.ts @@ -30,6 +30,7 @@ import {DriverCalibrationChunkSerializer} from './chunk-serializers/driver-calib import {TagDataChunkSerializer} from './chunk-serializers/tag-data-chunk-serializer.js'; import {TagKeysChunkSerializer} from './chunk-serializers/tag-keys-chunk-serializer.js'; import {TaggedModuleMapChunkSerializer} from './chunk-serializers/tagged-module-map-chunk-serializer.js'; +import {GkvAliasChunkSerializer} from './chunk-serializers/gkv-alias-chunk-serializer.js'; import {DatapoolChunk} from '../../shared/acdb-chunks/datapool-chunk.js'; import {DatapoolChunkSerializer} from './chunk-serializers/datapool-chunk-serializer.js'; import {BinaryUtils} from '../../../../shared/utilities/binary-utils.js'; @@ -37,6 +38,7 @@ import {ACDB_RAW_CHUNK_TYPES} from '../../shared/constants/chunk-types.js'; import {HANDLER_KEYS} from '../../shared/constants/registry-keys.js'; import {UsecaseDataChunkBuilder} from './chunk-builders/usecase-data-chunk-builder.js'; import {UsecaseDataChunk} from '../../shared/acdb-chunks/usecase-data-chunk.js'; +import {compareNumberArrays} from '../../../../shared/utilities/array-utils.js'; /** * Serializes domain entities to binary ACDB format. @@ -237,6 +239,74 @@ export class AcdbFileSerializer { return {audio, voice}; } + /** + * Merge VCPM calibration data into the voice array. + * + * VCPM entries share the VCPM_CALDATA chunk with voice-CKV entries. + * For each VCPM entry by subgraphId: + * - If a voice entry already exists for that subgraph, append keyValueCombinations. + * - Otherwise push the VCPM entry as a new voice entry. + * Re-sort the merged result. + */ + private mergeVcpmIntoVoice( + voice: CalibrationDataDownloadModel[], + vcpm: CalibrationDataDownloadModel[], + ): CalibrationDataDownloadModel[] { + if (vcpm.length === 0) return voice; + + const merged = [...voice]; + const bySubgraphId = new Map(merged.map(v => [v.naturalId, v])); + + for (const vcpmEntry of vcpm) { + const existing = bySubgraphId.get(vcpmEntry.naturalId); + if (existing) { + existing.keyValueCombinations = [ + ...existing.keyValueCombinations, + ...vcpmEntry.keyValueCombinations, + ]; + const mkMap = new Map( + existing.masterKeys.map(mk => [mk.keyNaturalId, mk.isDynamic]), + ); + for (const mk of vcpmEntry.masterKeys) { + if (!mkMap.has(mk.keyNaturalId)) + mkMap.set(mk.keyNaturalId, mk.isDynamic); + } + existing.masterKeys = [...mkMap.entries()] + .sort(([a], [b]) => a - b) + .map(([keyNaturalId, isDynamic]) => ({keyNaturalId, isDynamic})); + } else { + merged.push(vcpmEntry); + bySubgraphId.set(vcpmEntry.naturalId, vcpmEntry); + } + } + + merged.sort((a, b) => a.naturalId - b.naturalId); + for (const entry of merged) { + entry.keyValueCombinations.sort((a, b) => { + const kd = compareNumberArrays(a.keyIds, b.keyIds); + return kd !== 0 ? kd : compareNumberArrays(a.valueIds, b.valueIds); + }); + } + + return merged; + } + + /** + * Serialize GKV alias chunk (GALS) from usecase data. + * Only emits the chunk when at least one usecase has aliasId defined. + */ + private serializeGkvAliasChunk( + usecaseData: UsecaseDataDownloadModel[], + chunkList: Array<{id: string; data: Uint8Array}>, + datapool: DatapoolChunk, + ): void { + const serializer = new GkvAliasChunkSerializer(); + const galsData = serializer.serialize(usecaseData, datapool); + if (galsData.length > 0) { + this.addChunk(chunkList, ACDB_RAW_CHUNK_TYPES.GKV_ALIAS, galsData); + } + } + /** * Serialize entities to complete ACDB file. * @@ -257,6 +327,11 @@ export class AcdbFileSerializer { this.profiler?.start(PROFILER_OPERATIONS.ACDB_SERIALIZE_USECASE); await this.serializeUsecaseChunks(entities, chunkList, datapool); + this.serializeGkvAliasChunk( + entities.usecaseData ?? [], + chunkList, + datapool, + ); this.logSerializeStepMetrics( this.profiler?.end(PROFILER_OPERATIONS.ACDB_SERIALIZE_USECASE), ); @@ -266,6 +341,11 @@ export class AcdbFileSerializer { entities.subgraphData ?? [], ); + const mergedVoice = this.mergeVcpmIntoVoice( + voice, + entities.vcpmCalibrationData ?? [], + ); + this.profiler?.start( PROFILER_OPERATIONS.ACDB_SERIALIZE_AUDIO_CALIBRATION, ); @@ -279,7 +359,7 @@ export class AcdbFileSerializer { this.profiler?.start( PROFILER_OPERATIONS.ACDB_SERIALIZE_VOICE_CALIBRATION, ); - this.serializeVoiceCalibrationChunks(voice, chunkList, datapool); + this.serializeVoiceCalibrationChunks(mergedVoice, chunkList, datapool); this.logSerializeStepMetrics( this.profiler?.end( PROFILER_OPERATIONS.ACDB_SERIALIZE_VOICE_CALIBRATION, diff --git a/packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts b/packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts new file mode 100644 index 000000000..331afa5bf --- /dev/null +++ b/packages/core/src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {BinaryUtils} from '../../../../../shared/utilities/binary-utils.js'; +import type {DatapoolChunk} from '../../../shared/acdb-chunks/datapool-chunk.js'; +import type {UsecaseDataDownloadModel} from '../../../../ports/persistence/query-services/bulk-read/bulk-read-query-service.js'; + +const TEXT_ENCODER = new TextEncoder(); +const ALIAS_SEPARATOR = ' | '; + +/** + * Serializer for the GKV alias chunk (GALS). + * + * Binary format (mirrors GkvAliasChunkParser): + * GALS body: + * NumKeyTables: uint32 + * GkvAliasTable × NumKeyTables: + * NumKeys: uint32 + * NumGkvs: uint32 + * GkvEntry × NumGkvs: + * numKeys × [keyId uint32, keyVal uint32] + * DatapoolOffset: uint32 + * + * Datapool payload at each offset: + * [uint32 innerStringLength][ASCII bytes: "" or " | "] + * + * Usecases are expected to arrive pre-sorted by numKeys → keyIds → valueIds + * (the order produced by readUsecaseData()). + * + * Returns an empty Uint8Array when no entries have aliasId defined. + */ +export class GkvAliasChunkSerializer { + /** + * Serialize GKV alias data to binary GALS chunk. + * + * @param usecaseData - Download models (must include aliasId/alias fields) + * @param datapool - Shared datapool for alias string storage + * @returns Binary GALS chunk body, or empty Uint8Array if no aliases present + */ + serialize( + usecaseData: UsecaseDataDownloadModel[], + datapool: DatapoolChunk, + ): Uint8Array { + const withAlias = usecaseData.filter(uc => uc.aliasId !== undefined); + if (withAlias.length === 0) return new Uint8Array(0); + + const groups = this.groupByNumKeys(withAlias); + const totalSize = this.calculateTotalSize(groups); + const buffer = new Uint8Array(totalSize); + const view = new DataView( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength, + ); + let pos = 0; + + BinaryUtils.writeUint32(view, pos, groups.length); + pos += BinaryUtils.SIZEOF_UINT32; + + for (const group of groups) { + const numKeys = group[0].keyIds.length; + BinaryUtils.writeUint32(view, pos, numKeys); + pos += BinaryUtils.SIZEOF_UINT32; + + BinaryUtils.writeUint32(view, pos, group.length); + pos += BinaryUtils.SIZEOF_UINT32; + + for (const uc of group) { + for (let i = 0; i < uc.keyIds.length; i++) { + BinaryUtils.writeUint32(view, pos, uc.keyIds[i]); + pos += BinaryUtils.SIZEOF_UINT32; + BinaryUtils.writeUint32(view, pos, uc.valueIds[i]); + pos += BinaryUtils.SIZEOF_UINT32; + } + + const aliasPayload = this.buildAliasPayload(uc.aliasId!, uc.alias); + const dpOffset = datapool.addOrReuse(aliasPayload); + BinaryUtils.writeUint32(view, pos, dpOffset); + pos += BinaryUtils.SIZEOF_UINT32; + } + } + + return buffer; + } + + private groupByNumKeys( + entries: UsecaseDataDownloadModel[], + ): UsecaseDataDownloadModel[][] { + const groups: UsecaseDataDownloadModel[][] = []; + let currentGroup: UsecaseDataDownloadModel[] = []; + let currentNumKeys = -1; + + for (const entry of entries) { + const numKeys = entry.keyIds.length; + if (numKeys !== currentNumKeys) { + if (currentGroup.length > 0) groups.push(currentGroup); + currentGroup = []; + currentNumKeys = numKeys; + } + currentGroup.push(entry); + } + if (currentGroup.length > 0) groups.push(currentGroup); + + return groups; + } + + /** + * Build the datapool payload for one alias entry. + * + * Layout: [uint32 innerStringLength][ASCII bytes] + * Format: "" or " | " + * + * The outer size-prefix added by DatapoolChunk is separate from this payload. + */ + private buildAliasPayload(aliasId: number, alias?: string): Uint8Array { + const str = + alias !== undefined + ? `${aliasId}${ALIAS_SEPARATOR}${alias}` + : `${aliasId}`; + const strBytes = TEXT_ENCODER.encode(str); + const payload = new Uint8Array(BinaryUtils.SIZEOF_UINT32 + strBytes.length); + const view = new DataView(payload.buffer); + BinaryUtils.writeUint32(view, 0, strBytes.length); + payload.set(strBytes, BinaryUtils.SIZEOF_UINT32); + return payload; + } + + private calculateTotalSize(groups: UsecaseDataDownloadModel[][]): number { + let size = BinaryUtils.SIZEOF_UINT32; // numKeyTables + for (const group of groups) { + const numKeys = group[0].keyIds.length; + size += BinaryUtils.SIZEOF_UINT32; // numKeys + size += BinaryUtils.SIZEOF_UINT32; // numGkvs + size += group.length * numKeys * 2 * BinaryUtils.SIZEOF_UINT32; // keyId + keyVal per key + size += group.length * BinaryUtils.SIZEOF_UINT32; // datapoolOffset + } + return size; + } +} diff --git a/packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts b/packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts index f27e58e16..6bbbcdcba 100644 --- a/packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts +++ b/packages/core/src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.ts @@ -161,6 +161,12 @@ export interface UsecaseDataDownloadModel { sourceSubgraphNaturalId: number; destSubgraphNaturalId: number; }>; + + /** Usecase alias ID (natural ID stored in use_cases.alias_id). Optional — absent for files without GALS chunk. */ + aliasId?: number; + + /** Usecase alias name (stored in use_cases.alias). Optional — absent when no name was set. */ + alias?: string; } /** @@ -571,6 +577,7 @@ export interface DownloadEntities { subgraphData?: SubgraphDownloadModel[]; containerData?: ContainerDownloadModel[]; calibrationData?: CalibrationDataDownloadModel[]; + vcpmCalibrationData?: CalibrationDataDownloadModel[]; tagKeys?: TagKeysDownloadModel[]; tagData?: TagDataDownloadModel[]; taggedModules?: TaggedModuleDownloadModel[]; @@ -654,6 +661,26 @@ export interface BulkReadQueryService { fileSystemId: number, ): Promise; + /** + * Read VCPM calibration data from vcpm_ckv / vcpm_parameter_payload tables. + * Returns data structured identically to readCalibrationData() so it can be + * passed directly to VoiceCalibrationChunkBuilder. + * + * moduleInstanceId is always SPF_VCPM_MODULE_ID (4) for all returned entries. + * pidType is '' (unused by voice builder). + * + * Sorting order (must match voice-CKV contract): + * 1. subgraphId ASC + * 2. keyIds lexicographic ASC + * 3. valueIds lexicographic ASC + * 4. parameterId ASC + * + * @param fileSystemId - The file system ID to scope the query + */ + readVcpmCalibrationData( + fileSystemId: number, + ): Promise; + readTagKeys(fileSystemId: number): Promise; readTagData(fileSystemId: number): Promise; readTaggedModuleData( diff --git a/packages/core/tests/integration/application/file-operations/awsp-parser.integration.spec.ts b/packages/core/tests/integration/application/file-operations/awsp-parser.integration.spec.ts index 8d299a83b..0a31fc999 100644 --- a/packages/core/tests/integration/application/file-operations/awsp-parser.integration.spec.ts +++ b/packages/core/tests/integration/application/file-operations/awsp-parser.integration.spec.ts @@ -132,7 +132,7 @@ describe('AWSP Parser Integration Tests', () => { const typeCount = parsedAwsp.getDefinitionTypeCount(); expect(typeCount).toBeGreaterThan(0); - expect(typeCount).toBeLessThanOrEqual(8); + expect(typeCount).toBeLessThanOrEqual(10); const totalCount = parsedAwsp.getTotalDefinitionCount(); expect(totalCount).toBeGreaterThan(0); diff --git a/packages/core/tests/integration/application/file-operations/download-file/gkv-alias-download.integration.spec.ts b/packages/core/tests/integration/application/file-operations/download-file/gkv-alias-download.integration.spec.ts new file mode 100644 index 000000000..33ef8f39b --- /dev/null +++ b/packages/core/tests/integration/application/file-operations/download-file/gkv-alias-download.integration.spec.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {describe, it, expect} from '@jest/globals'; +import {AcdbFileSerializer} from '../../../../../src/application/file-operations/download-file/services/acdb-file-serializer.js'; +import type {DownloadEntities} from '../../../../../src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.js'; + +function containsChunkId(acdbFile: Uint8Array, chunkId: string): boolean { + const view = new DataView( + acdbFile.buffer, + acdbFile.byteOffset, + acdbFile.byteLength, + ); + let pos = 12; // skip file header: [4 magic][4 type][4 fileLen] + while (pos + 8 <= acdbFile.byteLength) { + const id = String.fromCharCode( + acdbFile[pos], + acdbFile[pos + 1], + acdbFile[pos + 2], + acdbFile[pos + 3], + ); + const len = view.getUint32(pos + 4, true); + if (id === chunkId) return true; + pos += 8 + len; + } + return false; +} + +describe('GKV Alias Download Integration', () => { + it('should emit GALS chunk when usecase data has aliasId', async () => { + const entities: DownloadEntities = { + headerMetadata: { + version: {major: 2, minor: 0, revision: 0, cplInfo: 0}, + codecInfos: [], + modifiedDate: 0, + oemInfo: '', + }, + usecaseData: [ + { + systemId: 1, + keyIds: [1, 2], + valueIds: [10, 20], + subgraphIds: [], + subgraphPairs: [], + aliasId: 42, + alias: 'VoiceCall', + }, + ], + }; + + const serializer = new AcdbFileSerializer(); + const result = await serializer.serialize(entities); + + expect(containsChunkId(result, 'GALS')).toBe(true); + }); + + it('should NOT emit GALS chunk when no usecase has aliasId', async () => { + const entities: DownloadEntities = { + headerMetadata: { + version: {major: 2, minor: 0, revision: 0, cplInfo: 0}, + codecInfos: [], + modifiedDate: 0, + oemInfo: '', + }, + usecaseData: [ + { + systemId: 1, + keyIds: [1], + valueIds: [5], + subgraphIds: [], + subgraphPairs: [], + }, + ], + }; + + const serializer = new AcdbFileSerializer(); + const result = await serializer.serialize(entities); + + expect(containsChunkId(result, 'GALS')).toBe(false); + }); +}); diff --git a/packages/core/tests/integration/application/file-operations/download-file/voice-calibration-download.integration.spec.ts b/packages/core/tests/integration/application/file-operations/download-file/voice-calibration-download.integration.spec.ts index faecd24bb..1ac4e6484 100644 --- a/packages/core/tests/integration/application/file-operations/download-file/voice-calibration-download.integration.spec.ts +++ b/packages/core/tests/integration/application/file-operations/download-file/voice-calibration-download.integration.spec.ts @@ -20,6 +20,27 @@ function makeScenarioPayload(value: number): Uint8Array { return buf; } +function containsChunkId(acdbFile: Uint8Array, chunkId: string): boolean { + const view = new DataView( + acdbFile.buffer, + acdbFile.byteOffset, + acdbFile.byteLength, + ); + let pos = 12; // skip file header: [4 magic][4 type][4 fileLen] + while (pos + 8 <= acdbFile.byteLength) { + const id = String.fromCharCode( + acdbFile[pos], + acdbFile[pos + 1], + acdbFile[pos + 2], + acdbFile[pos + 3], + ); + const len = view.getUint32(pos + 4, true); + if (id === chunkId) return true; + pos += 8 + len; + } + return false; +} + describe('Voice Calibration Download Integration', () => { it('should serialize voice calibration data to ACDB file', async () => { // Arrange: subgraphData includes scenario property so the serializer can @@ -95,4 +116,64 @@ describe('Voice Calibration Download Integration', () => { const fileId = view.getUint32(0, true); expect(fileId).toBe(0x42444341); // 'ACDB' in little-endian }); + + it('should include vcpm calibration data merged into voice calibration output', async () => { + const entities: DownloadEntities = { + headerMetadata: { + version: {major: 2, minor: 3, revision: 4, cplInfo: 5}, + codecInfos: [], + modifiedDate: Date.now(), + oemInfo: 'Test OEM', + }, + subgraphData: [ + { + subgraphId: 100, + properties: [ + { + propertyId: SUB_GRAPH_PROP_ID_SCENARIO_ID, + payload: makeScenarioPayload( + SUB_GRAPH_PROP_ID_SCENARIO_VALUE_VOICE_CALL, + ), + }, + ], + modules: [], + dataLinks: [], + controlLinks: [], + voiceTags: [], + }, + ], + calibrationData: [], + vcpmCalibrationData: [ + { + subgraphId: 100, + masterKeys: [{keyId: 1, isDynamic: true}], + keyValueCombinations: [ + { + keyIds: [1], + valueIds: [10], + modules: [ + { + moduleInstanceId: 4, // SPF_VCPM_MODULE_ID + parameters: [ + { + parameterId: 0x08001163, + payload: new Uint8Array([0xca, 0xfe]), + pidType: '', + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const serializer = new AcdbFileSerializer(); + const result = await serializer.serialize(entities); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result.length).toBeGreaterThan(0); + expect(containsChunkId(result, 'VCCD')).toBe(true); + }); }); diff --git a/packages/core/tests/unit/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.spec.ts b/packages/core/tests/unit/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.spec.ts new file mode 100644 index 000000000..69bed1f23 --- /dev/null +++ b/packages/core/tests/unit/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.spec.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {describe, it, expect} from '@jest/globals'; +import {GkvAliasChunkSerializer} from '../../../../../../../src/application/file-operations/download-file/services/chunk-serializers/gkv-alias-chunk-serializer.js'; +import {DatapoolChunk} from '../../../../../../../src/application/file-operations/shared/acdb-chunks/datapool-chunk.js'; +import type {UsecaseDataDownloadModel} from '../../../../../../../src/application/ports/persistence/query-services/bulk-read/bulk-read-query-service.js'; + +function makeUsecase( + keyIds: number[], + valueIds: number[], + aliasId?: number, + alias?: string, +): UsecaseDataDownloadModel { + return { + systemId: 1, + keyIds, + valueIds, + subgraphIds: [], + subgraphPairs: [], + aliasId, + alias, + }; +} + +describe('GkvAliasChunkSerializer', () => { + it('returns empty Uint8Array when no entries have aliasId', () => { + const datapool = new DatapoolChunk(); + const serializer = new GkvAliasChunkSerializer(); + const result = serializer.serialize( + [makeUsecase([1], [10])], // no aliasId + datapool, + ); + expect(result).toBeInstanceOf(Uint8Array); + expect(result.length).toBe(0); + }); + + it('serializes a single entry with aliasId only (no name)', () => { + const datapool = new DatapoolChunk(); + const serializer = new GkvAliasChunkSerializer(); + const result = serializer.serialize( + [makeUsecase([1, 2], [10, 20], 42)], + datapool, + ); + expect(result.length).toBeGreaterThan(0); + + const view = new DataView( + result.buffer, + result.byteOffset, + result.byteLength, + ); + let pos = 0; + // numKeyTables + expect(view.getUint32(pos, true)).toBe(1); + pos += 4; + // numKeys for this table + expect(view.getUint32(pos, true)).toBe(2); + pos += 4; + // numGkvs + expect(view.getUint32(pos, true)).toBe(1); + pos += 4; + // keyId[0], keyVal[0] + expect(view.getUint32(pos, true)).toBe(1); + pos += 4; + expect(view.getUint32(pos, true)).toBe(10); + pos += 4; + // keyId[1], keyVal[1] + expect(view.getUint32(pos, true)).toBe(2); + pos += 4; + expect(view.getUint32(pos, true)).toBe(20); + pos += 4; + // datapoolOffset (some non-negative number) + const dpOffset = view.getUint32(pos, true); + expect(dpOffset).toBeGreaterThanOrEqual(0); + }); + + it('serializes a single entry with aliasId and alias name without error', () => { + const datapool = new DatapoolChunk(); + const serializer = new GkvAliasChunkSerializer(); + const result = serializer.serialize( + [makeUsecase([1], [5], 99, 'MyUsecase')], + datapool, + ); + expect(result.length).toBeGreaterThan(0); + }); + + it('groups entries with same numKeys into one table', () => { + const datapool = new DatapoolChunk(); + const serializer = new GkvAliasChunkSerializer(); + const result = serializer.serialize( + [makeUsecase([1], [10], 1), makeUsecase([2], [20], 2)], + datapool, + ); + const view = new DataView( + result.buffer, + result.byteOffset, + result.byteLength, + ); + // numKeyTables should be 1 (both have numKeys=1) + expect(view.getUint32(0, true)).toBe(1); + // numGkvs should be 2 + expect(view.getUint32(8, true)).toBe(2); + }); + + it('creates separate tables for entries with different numKeys', () => { + const datapool = new DatapoolChunk(); + const serializer = new GkvAliasChunkSerializer(); + const result = serializer.serialize( + [ + makeUsecase([1], [10], 1), // numKeys = 1 + makeUsecase([1, 2], [10, 20], 2), // numKeys = 2 + ], + datapool, + ); + const view = new DataView( + result.buffer, + result.byteOffset, + result.byteLength, + ); + // numKeyTables should be 2 + expect(view.getUint32(0, true)).toBe(2); + }); +}); diff --git a/packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.ts b/packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.ts index 236254cbf..a78ea7b4c 100644 --- a/packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.ts +++ b/packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.ts @@ -44,7 +44,12 @@ import type { UiFileExtrasDownloadModel, Logger, } from '@arc/core'; -import {compareNumberArrays, LINK_TYPE, PORT_IO_TYPE} from '@arc/core'; +import { + compareNumberArrays, + LINK_TYPE, + PORT_IO_TYPE, + SPF_VCPM_MODULE_ID, +} from '@arc/core'; import type {DataSource, SelectQueryBuilder, ObjectLiteral} from 'typeorm'; import {DbFileQuery} from '../db-file-query.js'; import {ENTITY_NAMES} from '../../entity-schema/entity-table-names.js'; @@ -97,6 +102,11 @@ import type {ModulePropertyRow} from '../../entity-schema/definitions/module/spf import type {ProcessorDefinitionRow} from '../../entity-schema/definitions/common/processor-definition.schema.js'; import type {ContainerTypeRow} from '../../entity-schema/definitions/container/container-definition.schema.js'; import type {ConfigurationRow} from '../../entity-schema/project-data/configuration.schema.js'; +import type { + VcpmCkvRow, + VcpmCkvValuesRow, + VcpmParameterPayloadRow, +} from '../../entity-schema/usecase-data/subgraph/subgraph-vcpm-data.js'; /** * TypeORM implementation of BulkReadQueryService. @@ -145,6 +155,7 @@ export class TypeOrmBulkReadQueryService implements BulkReadQueryService { tagData, taggedModules, driverCalibrationData, + vcpmCalibrationData, configurationData, keyDefinitions, tagDefinitions, @@ -169,6 +180,10 @@ export class TypeOrmBulkReadQueryService implements BulkReadQueryService { 'readDriverCalibrationData', this.readDriverCalibrationData(fileSystemId), ), + timed( + 'readVcpmCalibrationData', + this.readVcpmCalibrationData(fileSystemId), + ), timed('readConfiguration', this.readConfiguration(fileSystemId)), timed('readKeyDefinitions', this.readKeyDefinitions(fileSystemId)), timed('readTagDefinitions', this.readTagDefinitions(fileSystemId)), @@ -209,6 +224,7 @@ export class TypeOrmBulkReadQueryService implements BulkReadQueryService { subgraphData, containerData, calibrationData, + vcpmCalibrationData, tagKeys, tagData, taggedModules, @@ -277,6 +293,8 @@ export class TypeOrmBulkReadQueryService implements BulkReadQueryService { .map(membership => membership.subgraph!.naturalId) .sort((a, b) => a - b), subgraphPairs: pairsMap.get(uc.systemId) ?? [], + aliasId: uc.aliasId || undefined, + alias: uc.alias || undefined, })); } @@ -904,6 +922,187 @@ export class TypeOrmBulkReadQueryService implements BulkReadQueryService { return result; } + // ─── VCPM Calibration ──────────────────────────────────────────────────────── + + async readVcpmCalibrationData( + fileSystemId: number, + ): Promise { + const ckvRows = await this.fetchAllVcpmCkvEntries(fileSystemId); + if (ckvRows.length === 0) return []; + + const vcpmCkvIds = ckvRows.map(r => r.systemId); + + const [valRows, paramRows] = await Promise.all([ + this.fetchVcpmCkvValues(vcpmCkvIds), + this.fetchVcpmParameterPayloads(vcpmCkvIds), + ]); + + const valMap = new Map(); + for (const v of valRows) { + if (!valMap.has(v.vcpmCkvSystemId)) valMap.set(v.vcpmCkvSystemId, []); + valMap.get(v.vcpmCkvSystemId)!.push(v); + } + for (const ckv of ckvRows) { + (ckv as VcpmCkvRow & {values: VcpmCkvValuesRow[]}).values = + valMap.get(ckv.systemId) ?? []; + } + + const sorted = this.sortVcpmCkvEntries(ckvRows); + return this.buildVcpmCalibrationModels(sorted, paramRows); + } + + private async fetchAllVcpmCkvEntries( + fileSystemId: number, + ): Promise { + return this.dataSource + .getRepository(ENTITY_NAMES.VcpmCkv) + .createQueryBuilder('vckv') + .innerJoinAndSelect('vckv.vcpmInstance', 'vi') + .innerJoinAndSelect('vi.subgraph', 'sg') + .where('sg.fileSystemId = :fileSystemId', {fileSystemId}) + .orderBy('sg.naturalId', 'ASC') + .getMany() as Promise; + } + + private fetchVcpmCkvValues( + vcpmCkvIds: number[], + ): Promise { + return this.queryInChunks(vcpmCkvIds, ids => + this.dataSource + .getRepository(ENTITY_NAMES.VcpmCkvValues) + .createQueryBuilder('vcv') + .leftJoinAndSelect('vcv.valueDef', 'vd') + .leftJoinAndSelect('vd.keys', 'k') + .where('vcv.vcpmCkvSystemId IN (:...ids)', {ids}), + ) as Promise; + } + + private fetchVcpmParameterPayloads( + vcpmCkvIds: number[], + ): Promise { + return this.queryInChunks(vcpmCkvIds, ids => + this.dataSource + .getRepository(ENTITY_NAMES.VcpmParameterPayload) + .createQueryBuilder('vpp') + .leftJoinAndSelect('vpp.vcpmParameter', 'param') + .where('vpp.vcpmCkvSystemId IN (:...ids)', {ids}) + .orderBy('vpp.vcpmCkvSystemId', 'ASC') + .addOrderBy('param.naturalId', 'ASC'), + ) as Promise; + } + + private sortVcpmCkvEntries(entries: VcpmCkvRow[]): VcpmCkvRow[] { + type Augmented = VcpmCkvRow & {values: VcpmCkvValuesRow[]}; + const prepared = (entries as Augmented[]).map(e => ({ + entry: e, + vals: [...(e.values ?? [])].sort( + (x, y) => + (x.valueDef?.keys?.naturalId ?? 0) - + (y.valueDef?.keys?.naturalId ?? 0), + ), + })); + + prepared.sort((a, b) => { + const sgA = a.entry.vcpmInstance?.subgraph?.naturalId ?? 0; + const sgB = b.entry.vcpmInstance?.subgraph?.naturalId ?? 0; + if (sgA !== sgB) return sgA - sgB; + + const keyDiff = compareNumberArrays( + a.vals.map(v => v.valueDef?.keys?.naturalId ?? 0), + b.vals.map(v => v.valueDef?.keys?.naturalId ?? 0), + ); + if (keyDiff !== 0) return keyDiff; + return compareNumberArrays( + a.vals.map(v => v.valueDef?.naturalId ?? 0), + b.vals.map(v => v.valueDef?.naturalId ?? 0), + ); + }); + + return prepared.map(p => p.entry); + } + + private buildVcpmCalibrationModels( + sortedEntries: VcpmCkvRow[], + paramRows: VcpmParameterPayloadRow[], + ): CalibrationDataDownloadModel[] { + type Augmented = VcpmCkvRow & {values: VcpmCkvValuesRow[]}; + + const paramMap = new Map< + number, + Array<{naturalId: number; payload: Uint8Array; pidType: string}> + >(); + for (const row of paramRows) { + if (!paramMap.has(row.vcpmCkvSystemId)) + paramMap.set(row.vcpmCkvSystemId, []); + paramMap.get(row.vcpmCkvSystemId)!.push({ + naturalId: row.vcpmParameter.naturalId, + payload: row.payload, + pidType: '', + }); + } + + const result: CalibrationDataDownloadModel[] = []; + let currentSg: CalibrationDataDownloadModel | null = null; + let currentKvCombo: + | CalibrationDataDownloadModel['keyValueCombinations'][0] + | null = null; + const masterKeyTracker = new Map>(); + + for (const ckv of sortedEntries as Augmented[]) { + const subgraphId = ckv.vcpmInstance.subgraph.naturalId; + const vals = [...(ckv.values ?? [])].sort( + (x, y) => + (x.valueDef?.keys?.naturalId ?? 0) - + (y.valueDef?.keys?.naturalId ?? 0), + ); + + const keyIds = vals.map(v => v.valueDef!.keys.naturalId); + const valueIds = vals.map(v => v.valueDef!.naturalId); + + if (!currentSg || currentSg.naturalId !== subgraphId) { + currentSg = { + naturalId: subgraphId, + masterKeys: [], + keyValueCombinations: [], + }; + result.push(currentSg); + currentKvCombo = null; + masterKeyTracker.set(subgraphId, new Map()); + } + + const mkMap = masterKeyTracker.get(subgraphId)!; + for (const val of vals) { + const keyId = val.valueDef!.keys.naturalId; + if (!mkMap.has(keyId)) { + mkMap.set(keyId, val.valueDef!.keys.isDynamic ?? false); + } + } + + if ( + !currentKvCombo || + compareNumberArrays(currentKvCombo.keyIds, keyIds) !== 0 || + compareNumberArrays(currentKvCombo.valueIds, valueIds) !== 0 + ) { + currentKvCombo = {keyIds, valueIds, modules: []}; + currentSg.keyValueCombinations.push(currentKvCombo); + } + + currentKvCombo.modules.push({ + naturalId: SPF_VCPM_MODULE_ID, + parameters: paramMap.get(ckv.systemId) ?? [], + }); + } + + for (const sg of result) { + const mkMap = masterKeyTracker.get(sg.naturalId)!; + sg.masterKeys = [...mkMap.entries()] + .sort(([a], [b]) => a - b) + .map(([keyNaturalId, isDynamic]) => ({keyNaturalId, isDynamic})); + } + + return result; + } + // ─── Tag Keys ───────────────────────────────────────────────────────────── async readTagKeys(fileSystemId: number): Promise { diff --git a/packages/infrastructure/persistence/tests/integration/bulk-read/vcpm-calibration-download.spec.ts b/packages/infrastructure/persistence/tests/integration/bulk-read/vcpm-calibration-download.spec.ts new file mode 100644 index 000000000..f1523789a --- /dev/null +++ b/packages/infrastructure/persistence/tests/integration/bulk-read/vcpm-calibration-download.spec.ts @@ -0,0 +1,216 @@ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {TypeOrmBulkReadQueryService} from '../../../src/persistence-typeorm-sqllite/queries/bulk-read/typeorm-bulk-read-query-service.js'; +import { + setupIntegrationTest, + teardownIntegrationTest, + setupEachTest, + getTestRepository, + getTestDataSource, +} from '../helpers/test-database-setup.js'; +import {Repository} from 'typeorm'; +import {ArcDbFileSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/project-data/arc-db-file.schema.js'; +import {ProjectSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/project-data/project.schema.js'; +import {KeyDefinitionSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/key-value/key-definition.schema.js'; +import {ValueDefinitionSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/key-value/value-definition.schema.js'; +import {SubgraphSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/usecase-data/subgraph/subgraph.schema.js'; +import { + VcpmInstanceSchema, + VcpmCkvSchema, + VcpmCkvValuesSchema, +} from '../../../src/persistence-typeorm-sqllite/entity-schema/usecase-data/subgraph/subgraph-vcpm-data.js'; +import {VcpmModuleDefinitionSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/subgraph/vcpm/vcpm-module-definition.schema.js'; +import {VcpmModuleParameterDefinitionSchema} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/subgraph/vcpm/vcpm-module-parameter-definition.schema.js'; +import type {ArcDbFileRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/project-data/arc-db-file.schema.js'; +import type {ProjectRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/project-data/project.schema.js'; +import type {KeyDefinitionRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/key-value/key-definition.schema.js'; +import type {ValueDefinitionRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/key-value/value-definition.schema.js'; +import type {SubgraphRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/usecase-data/subgraph/subgraph.schema.js'; +import type { + VcpmInstanceRow, + VcpmCkvRow, +} from '../../../src/persistence-typeorm-sqllite/entity-schema/usecase-data/subgraph/subgraph-vcpm-data.js'; +import type {VcpmModuleDefinitionRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/subgraph/vcpm/vcpm-module-definition.schema.js'; +import type {VcpmModuleParameterDefinitionRow} from '../../../src/persistence-typeorm-sqllite/entity-schema/definitions/subgraph/vcpm/vcpm-module-parameter-definition.schema.js'; +import {SPF_VCPM_MODULE_ID} from '@arc/core'; + +describe('TypeOrmBulkReadQueryService.readVcpmCalibrationData', () => { + let service: TypeOrmBulkReadQueryService; + let fileRepository: Repository; + let projectRepository: Repository; + let keyRepository: Repository; + let valueRepository: Repository; + let subgraphRepository: Repository; + let vcpmModuleDefRepository: Repository; + let vcpmModuleParamRepository: Repository; + let vcpmInstanceRepository: Repository; + let vcpmCkvRepository: Repository; + let testFileSystemId: number; + let nextId: number; + + beforeAll(async () => { + await setupIntegrationTest(); + const dataSource = getTestDataSource(); + service = new TypeOrmBulkReadQueryService(dataSource); + fileRepository = getTestRepository(ArcDbFileSchema); + projectRepository = getTestRepository(ProjectSchema); + keyRepository = getTestRepository(KeyDefinitionSchema); + valueRepository = getTestRepository( + ValueDefinitionSchema, + ); + subgraphRepository = getTestRepository(SubgraphSchema); + vcpmModuleDefRepository = getTestRepository( + VcpmModuleDefinitionSchema, + ); + vcpmModuleParamRepository = + getTestRepository( + VcpmModuleParameterDefinitionSchema, + ); + vcpmInstanceRepository = + getTestRepository(VcpmInstanceSchema); + vcpmCkvRepository = getTestRepository(VcpmCkvSchema); + }); + + afterAll(async () => { + await teardownIntegrationTest(); + }); + + beforeEach(async () => { + await setupEachTest(); + nextId = 1; + + const project = await projectRepository.save({ + name: 'Test Project VCPM', + description: 'Test project for VCPM calibration download', + type: 'Offline', + }); + + const file = await fileRepository.save({ + projectSystemId: project.systemId, + fileName: JSON.stringify({acdb: 'test.acdb', awsp: 'test.awsp'}), + description: 'Test file', + metadata: '{}', + isTarget: false, + lastReservedId: 0, + openStatus: 'READY', + headerVersion: 1, + acdbVersionMajor: 1, + acdbVersionMinor: 0, + acdbVersionRevision: 0, + acdbVersionCplInfo: 0, + codecInfos: JSON.stringify([]), + modifiedDate: Date.now(), + oemInfo: 'Test OEM', + }); + testFileSystemId = file.systemId; + }); + + it('returns empty array when no vcpm_instances exist for the file', async () => { + const result = await service.readVcpmCalibrationData(testFileSystemId); + expect(result).toEqual([]); + }); + + it('returns CalibrationDataDownloadModel for a single vcpm ckv entry', async () => { + const dataSource = getTestDataSource(); + + // Create key and value definitions + const key = await keyRepository.save({ + systemId: nextId++, + fileSystemId: testFileSystemId, + naturalId: 1, + name: 'VoiceKey1', + isDynamic: true, + }); + const value = await valueRepository.save({ + systemId: nextId++, + keySystemId: key.systemId, + naturalId: 10, + name: 'Value10', + }); + + // Create subgraph + const subgraph = await subgraphRepository.save({ + systemId: nextId++, + fileSystemId: testFileSystemId, + naturalId: 100, + name: 'VoiceSubgraph', + isImported: false, + }); + + // Create VCPM module definition + const vcpmModuleDef = await vcpmModuleDefRepository.save({ + systemId: nextId++, + naturalId: SPF_VCPM_MODULE_ID, + name: 'VCPM', + fileSystemId: testFileSystemId, + }); + + // Create VCPM module parameter definition + const vcpmParam = await vcpmModuleParamRepository.save({ + systemId: nextId++, + naturalId: 0x08001163, + name: 'VoiceCalTbl', + maxSize: 1024, + pidType: 'SharedPersistent', + isPersistent: true, + isReadOnly: false, + vcpmModuleDefinitionSystemId: vcpmModuleDef.systemId, + elementsStructure: '{}', + }); + + // Create VCPM instance + const vcpmInstance = await vcpmInstanceRepository.save({ + systemId: nextId++, + subgraphSystemId: subgraph.systemId, + vcpmDefinitionId: vcpmModuleDef.systemId, + }); + + // Create VCPM CKV + const vcpmCkv = await vcpmCkvRepository.save({ + systemId: nextId++, + vcpmInstanceSystemId: vcpmInstance.systemId, + }); + + // Link CKV to value definition + await dataSource.query( + 'INSERT INTO vcpm_ckv_values (vcpm_ckv_system_id, value_def_system_id) VALUES (?, ?)', + [vcpmCkv.systemId, value.systemId], + ); + + // Create VCPM parameter payload via raw SQL (VcpmParameterPayloadSchema is a factory function) + await dataSource.query( + `INSERT INTO vcpm_parameter_payload (system_id, vcpm_ckv_system_id, vcpm_parameter_system_id, payload) + VALUES (?, ?, ?, ?)`, + [ + nextId++, + vcpmCkv.systemId, + vcpmParam.systemId, + Buffer.from([0xde, 0xad]), + ], + ); + + const result = await service.readVcpmCalibrationData(testFileSystemId); + + expect(result).toHaveLength(1); + const sg = result[0]; + expect(sg.naturalId).toBe(100); + expect(sg.masterKeys).toEqual([{keyNaturalId: 1, isDynamic: true}]); + expect(sg.keyValueCombinations).toHaveLength(1); + const kvCombo = sg.keyValueCombinations[0]; + expect(kvCombo.keyIds).toEqual([1]); + expect(kvCombo.valueIds).toEqual([10]); + expect(kvCombo.modules).toHaveLength(1); + const mod = kvCombo.modules[0]; + expect(mod.naturalId).toBe(SPF_VCPM_MODULE_ID); + expect(mod.parameters).toHaveLength(1); + expect(mod.parameters[0].naturalId).toBe(0x08001163); + // SQLite may return payload as Buffer or Uint8Array depending on transformer config + expect( + Buffer.from(mod.parameters[0].payload).equals(Buffer.from([0xde, 0xad])), + ).toBe(true); + expect(mod.parameters[0].pidType).toBe(''); + }); +});