feat(f6.8): harden offline field flow and act documents
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m41s
DH V2 CI / API · typecheck, tests, build (push) Successful in 31s
DH V2 CI / WEB · typecheck, build (push) Successful in 19s
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 8s
DH V2 CI / Docker / scripts contract (push) Successful in 1m14s

This commit is contained in:
DH V2
2026-09-15 15:56:50 -03:00
parent 47cd985931
commit 079728aa6d
62 changed files with 2240 additions and 338 deletions
@@ -34,9 +34,9 @@ test('F4 Android uses only lock and seal endpoints for the active Act lifecycle'
assert.doesNotMatch(mobileActs, /suspend fun closeAct\(/);
});
test('F4 Android ViewModel calls lock and seal directly', () => {
assert.match(viewModel, /actsRepository\.lock\(actId, urgency\)/);
assert.match(viewModel, /actsRepository\.seal\(actId\)/);
test('F6.8 Android ViewModel persists lock and seal through the offline queue', () => {
assert.match(viewModel, /offlineQueue\.enqueueActLock\(actId, urgency\)/);
assert.match(viewModel, /offlineQueue\.enqueueSeal\(actId\)/);
assert.doesNotMatch(viewModel, /actsRepository\.prepare\(/);
assert.doesNotMatch(viewModel, /actsRepository\.reopen\(/);
assert.doesNotMatch(viewModel, /actsRepository\.closeAct\(/);
+3 -3
View File
@@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { API_PHASE, API_VERSION } from '../../src/version';
test('health metadata reports the current F6.7 release', () => {
assert.equal(API_PHASE, 'F6.7');
test('health metadata reports the current F6.8 release', () => {
assert.equal(API_PHASE, 'F6.8');
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { version: string };
assert.equal(API_VERSION, pkg.version);
assert.equal(API_VERSION, '0.29.0-7');
assert.equal(API_VERSION, '0.29.0-8');
});
@@ -63,7 +63,7 @@ test('F5.1 Finding Catalog defaults to associated findings and exposes all items
assert.match(panel, /available\.filter\(\(item\) => savedIds\.has\(item\.id\)\)/);
assert.match(panel, /Buscar dentro de \{viewMode === 'ASSOCIATED' \? 'los asociados' : 'todo el Catálogo'\}/);
assert.match(panel, /Todos para vincular/);
assert.match(panel, /Esta clasificación todavía no tiene Hallazgos asociados/);
assert.match(panel, /Este tipo todavía no tiene Hallazgos asociados/);
});
test('F7 Inventory Configuration exposes only simple types, containment and fields', () => {
@@ -116,7 +116,7 @@ test('F7 canonical detail uses one short everyday profile and keeps advanced adm
assert.match(detail, />Fotos</);
assert.match(detail, />Cambios</);
assert.match(detail, /Empresa operadora/);
assert.match(detail, /Tipo técnico/);
assert.match(detail, /Tipo de instalación \/ subinstalación/);
assert.match(detail, /AssetTechnicalDataPanel/);
assert.match(detail, /AssetDossierPanel/);
assert.match(detail, /Administración avanzada/);
+2 -2
View File
@@ -10,8 +10,8 @@ function mountedRepoFile(path: string): string {
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
assert.match(gradle, /versionCode = 37/);
assert.match(gradle, /versionName = "0\.19\.9"/);
assert.match(gradle, /versionCode = 38/);
assert.match(gradle, /versionName = "0\.19\.10"/);
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
});
@@ -13,7 +13,7 @@ test('F6.1 presentation metadata keeps the visible WEB version aligned with pack
const visibleVersion = version.match(/APP_VERSION\s*=\s*'([^']+)'/)?.[1];
assert.equal(visibleVersion, pkg.version);
assert.match(version, /APP_PHASE\s*=\s*'F6\.7 · Cierre y firma por Acta'/);
assert.match(version, /APP_PHASE\s*=\s*'F6\.8/);
});
test('F6.1 presentation keeps Relevamientos retired from WEB navigation and routes', () => {
@@ -49,7 +49,7 @@ test('F6.1 presentation keeps Android start, Otro and chronological merge flows
assert.match(mobileApi, /field-inventory\/\{assetId\}\/merge/);
assert.match(overview, /"Iniciar inspección"/);
assert.match(overview, /Otro \/ no catalogado/);
assert.match(viewModel, /repository\.startVisit\(id\)/);
assert.match(viewModel, /enqueueVisitStart\(id\)|repository\.startVisit\(id\)/);
assert.match(viewModel, /selectedFieldAsset = repository\.selectFieldAsset\(visitId, result\.canonical\.id\)/);
assert.match(viewModel, /la historia de \$\{result\.source\.code\} permanece trazable/);
});
+2 -2
View File
@@ -36,11 +36,11 @@ test('F6.7 Android goes Draft to Para firmar to company manifestation without in
assert.match(labels, /"LOCKED" -> "Para firmar"/);
assert.match(android, /Cerrar contenido y pasar a firma/);
assert.match(android, /Firma del representante de la empresa \(acompañante\)/);
assert.match(android, /Aplicar mi firma y cerrar Acta/);
assert.match(android, /cerrar Acta/);
assert.doesNotMatch(android, /Firmá como inspector\/a/);
assert.doesNotMatch(android, /inspectorSigned/);
assert.doesNotMatch(android, /Text\(selected\.code/);
assert.match(android, /companyOutcome\?\.status == "ABSENT"/);
assert.match(android, /attendanceStatus == "ABSENT"/);
});
test('F6.7 Dashboard exposes the reusable inspector signature clearly', () => {
assert.match(profile, /FIRMA DEL INSPECTOR/);
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import test from 'node:test';
const api = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8');
const android = (path: string) => readFileSync(resolve(process.cwd(), '..', 'android-app', path), 'utf8');
const web = (path: string) => readFileSync(resolve(process.cwd(), '..', 'web-v2', path), 'utf8');
test('F6.8 persists field mutations locally and retries only when network is available', () => {
const queue = android('app/src/main/java/com/korexlabs/dhinspeccion/data/offline/OfflineQueue.kt');
assert.match(queue, /@Database\(entities = \[PendingMobileOperation::class, OfflineCacheEntry::class\]/);
assert.match(queue, /NetworkType\.CONNECTED/);
assert.match(queue, /OneTimeWorkRequestBuilder<OfflineSyncWorker>/);
for (const operation of [
'VISIT_START', 'VISIT_CLOSE', 'FIELD_ASSET_CREATE', 'FIELD_ASSET_PHOTO',
'ACT_CREATE', 'ACT_ASSET_ENSURE', 'FINDING_CREATE', 'FINDING_PHOTO',
'ACT_RESPONSIBLE', 'ACT_LOCK', 'COMPANY_OUTCOME', 'COMPANY_SIGNATURE', 'ACT_SEAL',
]) assert.match(queue, new RegExp(`"${operation}"`));
});
test('F6.8 isolates offline work and cached inspection data by authenticated user', () => {
const queue = android('app/src/main/java/com/korexlabs/dhinspeccion/data/offline/OfflineQueue.kt');
const mobile = android('app/src/main/java/com/korexlabs/dhinspeccion/data/DhMobile.kt');
const acts = android('app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt');
const findings = android('app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt');
assert.match(queue, /val ownerUserId: String/);
assert.match(queue, /ownerUserId=:ownerUserId/);
assert.match(queue, /SecureSessionStore/);
assert.match(mobile, /userCacheKey/);
assert.match(acts, /userCacheKey/);
assert.match(findings, /userCacheKey/);
});
test('F6.8 gives offline-created objects idempotent client identities', () => {
const actDto = api('src/inspection-acts/dto/create-inspection-act.dto.ts');
const findingDto = api('src/inspection-findings/dto/create-inspection-finding.dto.ts');
const inventoryDto = api('src/inspection-visits/dto/create-field-inventory.dto.ts');
const migration = api('src/database/migrations/1790128200000-f6-8-offline-evidence-idempotency.ts');
assert.match(actDto, /clientGeneratedId/);
assert.match(findingDto, /clientGeneratedId/);
assert.match(inventoryDto, /clientGeneratedId/);
assert.match(migration, /inspection_finding_evidence/);
assert.match(migration, /asset_field_capture_events/);
assert.equal((migration.match(/client_operation_id/g) ?? []).length >= 6, true);
});
test('F6.8 reviews photos before queueing and never sends the retired ONLINE upload mode', () => {
const photo = android('app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt');
const acts = android('app/src/main/java/com/korexlabs/dhinspeccion/data/MobileActs.kt');
assert.match(photo, /Revisar fotografía/);
assert.match(photo, /Usar esta foto/);
assert.match(photo, /Volver a tomar/);
assert.match(photo, /Eliminar foto/);
assert.doesNotMatch(acts, /uploadMode: String = "ONLINE"/);
assert.match(acts, /uploadMode: String = "IMMEDIATE"/);
});
test('F6.8 makes the real Acta PDF and photographic record first-class in Dashboard', () => {
const controller = api('src/inspection-reports/inspection-reports.controller.ts');
const page = web('src/pages/InspectionActEditorPage.tsx');
const media = web('src/features/inspections/InspectionActMediaPanel.tsx');
assert.match(controller, /inspection-acts\/:actId\/pdf/);
assert.match(api('src/inspection-acts/inspection-acts.controller.ts'), /:id\/field-media/);
assert.match(api('src/inspection-acts/inspection-acts.service.ts'), /capture\.visit_id=act\.visit_id/);
assert.match(page, /Abrir PDF del Acta/);
assert.match(page, /Descargar PDF/);
assert.match(media, /Fotos y Hallazgos/);
assert.match(media, /Evidencia fotográfica/);
assert.match(media, /Fotos tomadas durante esta inspección/);
});
test('F6.8 presents technical families to users as installation and subinstallation types', () => {
const config = web('src/pages/AssetTypesPage.tsx');
const detail = web('src/pages/SimpleInventoryDetailPage.tsx');
const field = api('src/inspection-visits/f3-field-inventory-structure.service.ts');
assert.match(config, /Tipos de Instalación/);
assert.match(config, /Tipos de Subinstalación/);
assert.match(detail, /Tipo de instalación \/ subinstalación/);
assert.match(field, /Tipo de instalación\/subinstalación/);
assert.doesNotMatch(config, /clasificación técnica/i);
assert.doesNotMatch(detail, /clasificación técnica/i);
});