Files
sencho/backend/src/__tests__/self-update-data-mount.test.ts
T
Anso ee891b093b fix(self-update): resolve /app/data through bind or named volume (#1124)
SelfUpdateService scanned the container's Mounts list for Type='bind'
only when picking the host path to forward to the helper container.
A pilot enrolled with the recommended Docker Compose snippet uses a
named volume (sencho-agent-data:/app/data, Type='volume'), so the
lookup returned null and the boot log read "/app/data mount not found
- update error recovery will be unavailable". The helper still ran
on update but could not persist UPDATE_ERROR_FILE on a failed pull,
so the next gateway process had nothing to surface.

Extract findDataDirHost(mounts) and accept Type='bind' or
Type='volume'. Docker populates Source with the on-disk volume
directory for either type, so the existing :rw bind in the helper
spawn works unchanged. The independent hostBindMounts filter stays
strict to Type='bind' (forwarded operator-declared compose paths
only; named volumes are not in scope there).

Tests: pure unit coverage for the helper across 6 cases (bind hit,
volume hit, no /app/data, mixed list with siblings, empty Source,
tmpfs ignored).
2026-05-20 03:36:08 -04:00

57 lines
2.2 KiB
TypeScript

/**
* findDataDirHost detects the host-side path for /app/data across bind AND
* named-volume mounts. Pre-fix the helper bound the resolver to Type='bind'
* only, so a pilot-agent deployed with the recommended `sencho-agent-data:/
* app/data` named volume logged "/app/data mount not found - update error
* recovery will be unavailable" at boot.
*/
import { describe, expect, it } from 'vitest';
import { findDataDirHost } from '../services/SelfUpdateService';
describe('findDataDirHost', () => {
it('returns the host path for a bind mount at /app/data', () => {
const source = findDataDirHost([
{ Type: 'bind', Source: '/opt/sencho/data', Destination: '/app/data' },
]);
expect(source).toBe('/opt/sencho/data');
});
it('returns the host path for a named volume at /app/data', () => {
const source = findDataDirHost([
{ Type: 'volume', Source: '/var/lib/docker/volumes/sencho-agent-data/_data', Destination: '/app/data' },
]);
expect(source).toBe('/var/lib/docker/volumes/sencho-agent-data/_data');
});
it('returns null when no mount targets /app/data', () => {
const source = findDataDirHost([
{ Type: 'bind', Source: '/var/run/docker.sock', Destination: '/var/run/docker.sock' },
{ Type: 'bind', Source: '/opt/compose', Destination: '/app/compose' },
]);
expect(source).toBeNull();
});
it('picks the /app/data mount out of a mixed list and ignores siblings', () => {
const source = findDataDirHost([
{ Type: 'bind', Source: '/var/run/docker.sock', Destination: '/var/run/docker.sock' },
{ Type: 'volume', Source: '/var/lib/docker/volumes/sencho-agent-data/_data', Destination: '/app/data' },
{ Type: 'bind', Source: '/opt/compose', Destination: '/app/compose' },
]);
expect(source).toBe('/var/lib/docker/volumes/sencho-agent-data/_data');
});
it('returns null when a /app/data entry carries no Source', () => {
const source = findDataDirHost([
{ Type: 'bind', Source: '', Destination: '/app/data' },
]);
expect(source).toBeNull();
});
it('ignores tmpfs and other non-bind/volume types', () => {
const source = findDataDirHost([
{ Type: 'tmpfs', Source: '', Destination: '/app/data' },
]);
expect(source).toBeNull();
});
});