Add native local infrastructure integrations

This commit is contained in:
2026-05-05 19:06:21 +00:00
parent cfab8c593e
commit a144ef687c
70 changed files with 11607 additions and 183 deletions
+135
View File
@@ -0,0 +1,135 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { IppClient, IppIntegration } from '../../ts/integrations/ipp/index.js';
tap.test('reads IPP printer attributes with a native Get-Printer-Attributes request', async () => {
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; method?: string; body: Buffer }> = [];
globalThis.fetch = (async (urlArg: URL | RequestInfo, initArg?: RequestInit) => {
calls.push({ url: String(urlArg), method: initArg?.method, body: Buffer.from(initArg?.body as ArrayBuffer) });
return new Response(arrayBuffer(ippResponse([
stringAttribute(0x42, 'printer-name', ['TCP Printer']),
stringAttribute(0x41, 'printer-info', ['Ready over IPP']),
stringAttribute(0x41, 'printer-make-and-model', ['Brother HL-L2350DW']),
stringAttribute(0x41, 'printer-device-id', ['MFG:Brother;MDL:HL-L2350DW;CMD:PCL;SERIALNUMBER:BR123;']),
stringAttribute(0x45, 'printer-uri-supported', ['ipp://printer.local:631/ipp/print']),
enumAttribute('printer-state', [4]),
stringAttribute(0x41, 'printer-state-message', ['Printing']),
stringAttribute(0x44, 'printer-state-reasons', ['none']),
booleanAttribute('printer-is-accepting-jobs', true),
integerAttribute('queued-job-count', 1),
stringAttribute(0x42, 'marker-names', ['Black Toner']),
stringAttribute(0x44, 'marker-types', ['toner-cartridge']),
integerAttribute('marker-levels', 73),
])), { status: 200, headers: { 'content-type': 'application/ipp' } });
}) as typeof globalThis.fetch;
try {
const snapshot = await new IppClient({ host: 'printer.local', port: 631, basePath: '/ipp/print', timeoutMs: 1000 }).getSnapshot();
expect(snapshot.online).toBeTrue();
expect(snapshot.printer.manufacturer).toEqual('Brother');
expect(snapshot.printer.serialNumber).toEqual('BR123');
expect(snapshot.status.printerState).toEqual('printing');
expect(snapshot.status.queuedJobCount).toEqual(1);
expect(snapshot.markers[0].level).toEqual(73);
expect(calls[0].url).toEqual('http://printer.local:631/ipp/print');
expect(calls[0].method).toEqual('POST');
expect(calls[0].body.readUInt16BE(2)).toEqual(0x000b);
expect(calls[0].body.includes(Buffer.from('printer-uri'))).toBeTrue();
} finally {
globalThis.fetch = originalFetch;
}
});
tap.test('read-only IPP runtime exposes snapshot and refuses unsupported writes', async () => {
const runtime = await new IppIntegration().setup({
snapshot: {
printer: { id: 'snapshot-printer', name: 'Snapshot Printer' },
status: { printerState: 'idle', stateReasons: [] },
markers: [{ index: 0, name: 'Black Toner', kind: 'toner', level: 55 }],
jobs: [],
online: true,
},
}, {});
const snapshotResult = await runtime.callService?.({ domain: 'ipp', service: 'snapshot', target: {} });
expect(snapshotResult?.success).toBeTrue();
const unsupported = await runtime.callService?.({ domain: 'switch', service: 'turn_on', target: {} });
expect(unsupported?.success).toBeFalse();
expect((await runtime.entities()).find((entityArg) => entityArg.id === 'sensor.black_toner')?.state).toEqual(55);
await runtime.destroy();
});
tap.test('refresh reports failure for configs without host, client, attributes, or snapshot', async () => {
const runtime = await new IppIntegration().setup({ name: 'No Source Printer' }, {});
const result = await runtime.callService?.({ domain: 'ipp', service: 'refresh', target: {} });
expect(result?.success).toBeFalse();
await runtime.destroy();
});
tap.test('refresh reports failed live IPP reads instead of faking success', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => {
throw new Error('connection refused');
}) as typeof globalThis.fetch;
try {
const runtime = await new IppIntegration().setup({ host: 'printer.local', timeoutMs: 1000 }, {});
const result = await runtime.callService?.({ domain: 'ipp', service: 'refresh', target: {} });
expect(result?.success).toBeFalse();
expect(result?.error).toEqual('connection refused');
await runtime.destroy();
} finally {
globalThis.fetch = originalFetch;
}
});
const ippResponse = (attributesArg: Buffer[]): Buffer => {
return Buffer.concat([
Buffer.from([0x01, 0x01]),
uint16(0x0000),
uint32(1),
Buffer.from([0x04]),
...attributesArg,
Buffer.from([0x03]),
]);
};
const stringAttribute = (tagArg: number, nameArg: string, valuesArg: string[]): Buffer => {
return Buffer.concat(valuesArg.map((valueArg, indexArg) => value(tagArg, indexArg === 0 ? nameArg : '', Buffer.from(valueArg, 'utf8'))));
};
const integerAttribute = (nameArg: string, valueArg: number): Buffer => {
const buffer = Buffer.alloc(4);
buffer.writeInt32BE(valueArg, 0);
return value(0x21, nameArg, buffer);
};
const enumAttribute = (nameArg: string, valuesArg: number[]): Buffer => {
return Buffer.concat(valuesArg.map((valueArg, indexArg) => {
const buffer = Buffer.alloc(4);
buffer.writeInt32BE(valueArg, 0);
return value(0x23, indexArg === 0 ? nameArg : '', buffer);
}));
};
const booleanAttribute = (nameArg: string, valueArg: boolean): Buffer => value(0x22, nameArg, Buffer.from([valueArg ? 1 : 0]));
const value = (tagArg: number, nameArg: string, valueArg: Buffer): Buffer => {
const name = Buffer.from(nameArg, 'utf8');
return Buffer.concat([Buffer.from([tagArg]), uint16(name.length), name, uint16(valueArg.length), valueArg]);
};
const uint16 = (valueArg: number): Buffer => {
const buffer = Buffer.alloc(2);
buffer.writeUInt16BE(valueArg, 0);
return buffer;
};
const uint32 = (valueArg: number): Buffer => {
const buffer = Buffer.alloc(4);
buffer.writeUInt32BE(valueArg, 0);
return buffer;
};
const arrayBuffer = (bufferArg: Buffer): ArrayBuffer => bufferArg.buffer.slice(bufferArg.byteOffset, bufferArg.byteOffset + bufferArg.byteLength) as ArrayBuffer;
export default tap.start();
+58
View File
@@ -0,0 +1,58 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { IppConfigFlow, createIppDiscoveryDescriptor } from '../../ts/integrations/ipp/index.js';
tap.test('matches IPP and IPPS mDNS printer records', async () => {
const descriptor = createIppDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ipp-mdns-match');
const result = await matcher!.matches({
type: '_ipps._tcp.local.',
name: 'Office Printer._ipps._tcp.local.',
host: 'office-printer.local',
port: 631,
txt: {
rp: 'ipp/print',
UUID: 'printer-uuid-1',
ty: 'HP Color LaserJet Pro',
usb_MFG: 'HP',
usb_MDL: 'Color LaserJet Pro',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.normalizedDeviceId).toEqual('printer-uuid-1');
expect(result.candidate?.integrationDomain).toEqual('ipp');
expect(result.candidate?.metadata?.basePath).toEqual('/ipp/print');
expect(result.candidate?.metadata?.tls).toBeTrue();
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.confidence).toEqual('certain');
});
tap.test('matches manual IPP URLs and carries defaults into config flow', async () => {
const descriptor = createIppDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'ipp-manual-match');
const match = await matcher!.matches({ host: 'ipps://printer.local:631/ipp/print', name: 'Manual Printer' }, {});
expect(match.matched).toBeTrue();
expect(match.candidate?.host).toEqual('printer.local');
expect(match.candidate?.metadata?.basePath).toEqual('/ipp/print');
expect(match.candidate?.metadata?.tls).toBeTrue();
const step = await new IppConfigFlow().start(match.candidate!, {});
const done = await step.submit!({ host: 'printer.local' });
expect(done.kind).toEqual('done');
expect(done.config?.port).toEqual(631);
expect(done.config?.basePath).toEqual('/ipp/print');
expect(done.config?.tls).toBeTrue();
});
tap.test('rejects IPP-looking candidates without a usable source', async () => {
const descriptor = createIppDiscoveryDescriptor();
const validator = descriptor.getValidators()[0];
const validation = await validator.validate({ source: 'manual', integrationDomain: 'ipp', name: 'Printer without host' }, {});
expect(validation.matched).toBeFalse();
expect(validation.reason).toEqual('IPP candidate lacks host, snapshot, attributes, or client information.');
});
export default tap.start();
+74
View File
@@ -0,0 +1,74 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { IppClient, IppMapper, type IIppAttributeRecord, type IIppSnapshot } from '../../ts/integrations/ipp/index.js';
const attributes: IIppAttributeRecord = {
'printer-name': 'Office Printer',
'printer-info': 'Office color printer',
'printer-location': 'Office',
'printer-make-and-model': 'HP Color LaserJet Pro',
'printer-device-id': 'MFG:HP;MDL:Color LaserJet Pro;CMD:PCL,POSTSCRIPT;SERIALNUMBER:CN123456;',
'printer-uuid': 'urn:uuid:printer-1',
'printer-state': 3,
'printer-state-message': 'Ready',
'printer-state-reasons': ['none'],
'printer-is-accepting-jobs': true,
'queued-job-count': 2,
'printer-up-time': 3600,
'printer-current-time': '2026-01-01T01:00:00.000Z',
'marker-names': ['Black Toner', 'Cyan Ink'],
'marker-types': ['toner-cartridge', 'ink-cartridge'],
'marker-colors': ['black', 'cyan'],
'marker-levels': [88, 42],
'marker-low-levels': [5, 10],
'marker-high-levels': [100, 100],
'job-id': [12],
'job-name': ['Test Page'],
'job-state': [5],
'job-originating-user-name': ['phil'],
};
tap.test('maps IPP attributes to printer device, marker, status, and job sensors', async () => {
const snapshot = IppClient.attributesToSnapshot(attributes, { host: 'printer.local', port: 631, basePath: '/ipp/print' }, true);
expect(snapshot.printer.manufacturer).toEqual('HP');
expect(snapshot.printer.serialNumber).toEqual('CN123456');
expect(snapshot.status.printerState).toEqual('idle');
expect(snapshot.status.bootedAt).toEqual('2026-01-01T00:00:00.000Z');
expect(snapshot.markers[0].kind).toEqual('toner');
expect(snapshot.markers[1].kind).toEqual('ink');
expect(snapshot.jobs[0].state).toEqual('processing');
const devices = IppMapper.toDevices(snapshot);
const entities = IppMapper.toEntities(snapshot);
expect(devices[0].id).toEqual('ipp.printer.urn_uuid_printer_1');
expect(devices[0].features.some((featureArg) => featureArg.id === 'marker_0')).toBeTrue();
expect(entities.find((entityArg) => entityArg.id === 'sensor.office_printer_status')?.state).toEqual('idle');
expect(entities.find((entityArg) => entityArg.id === 'sensor.black_toner')?.attributes?.markerKind).toEqual('toner');
expect(entities.find((entityArg) => entityArg.id === 'sensor.cyan_ink')?.state).toEqual(42);
expect(entities.find((entityArg) => entityArg.id === 'sensor.office_printer_queued_jobs')?.state).toEqual(2);
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.office_printer_accepting_jobs')?.state).toEqual('on');
expect(entities.find((entityArg) => entityArg.id === 'sensor.test_page')?.attributes?.owner).toEqual('phil');
});
tap.test('keeps hostless IPP runtime snapshots offline instead of faking live success', async () => {
const snapshot = await new IppClient({ name: 'Hostless Printer' }).getSnapshot();
expect(snapshot.online).toBeFalse();
expect(snapshot.source).toEqual('runtime');
expect(await new IppClient({ name: 'Hostless Printer' }).ping()).toBeFalse();
});
tap.test('maps offline snapshots without inventing marker or job values', async () => {
const snapshot: IIppSnapshot = {
printer: { id: 'offline-printer', name: 'Offline Printer' },
status: { printerState: 'unknown', stateReasons: [] },
markers: [],
jobs: [],
online: false,
updatedAt: '2026-01-01T00:00:00.000Z',
};
const entities = IppMapper.toEntities(snapshot);
expect(entities.find((entityArg) => entityArg.id === 'sensor.offline_printer_status')?.available).toBeFalse();
expect(entities.some((entityArg) => entityArg.id.includes('marker'))).toBeFalse();
expect(entities.some((entityArg) => entityArg.id.includes('job'))).toBeFalse();
});
export default tap.start();