import { describe, expect, it } from 'vitest'; import { mergeReplicaLines, parseSinceToSeconds, toShortReplicaName, toTimestampSortKey, type ServiceReplicaLog, } from './service-logs.js'; const SERVICE_ID = 'channel-service'; /** * Builds a line in the shape SDU returns: the RFC3339 instant Kubernetes * recorded, a space, then the container's own output. */ const k8sLine = (timestamp: string, text: string): string => `${timestamp} ${text}`; const replica = (suffix: string, lines: string[]): ServiceReplicaLog => ({ replicaName: `d-8ip51mklekzyxmtk-5ihjg3ycvjkpnmb2-${SERVICE_ID}-${suffix}`, lines: [...lines, ''], }); describe('toShortReplicaName', () => { it('keeps only the segment that distinguishes one replica from another', () => { expect( toShortReplicaName( `d-8ip51mklekzyxmtk-5ihjg3ycvjkpnmb2-${SERVICE_ID}-bb9c8cfmfq`, SERVICE_ID, ), ).toBe('bb9c8cfmfq'); }); it('falls back to the full name when it does not follow the convention', () => { expect(toShortReplicaName('some-other-pod', SERVICE_ID)).toBe( 'some-other-pod', ); }); }); describe('mergeReplicaLines', () => { it('orders lines from all replicas by the time Kubernetes recorded them', () => { const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [ k8sLine('2026-07-17T10:12:25.130421355Z', 'first'), k8sLine('2026-07-17T10:12:27.000000000Z', 'third'), ]), replica('bbbbbbbbbb', [ k8sLine('2026-07-17T10:12:26.000000000Z', 'second'), k8sLine('2026-07-17T10:12:28.000000000Z', 'fourth'), ]), ]); expect(merged.map((l) => l.message)).toEqual([ 'first', 'second', 'third', 'fourth', ]); expect( merged.map((l) => toShortReplicaName(l.replicaName, SERVICE_ID)), ).toEqual(['aaaaaaaaaa', 'bbbbbbbbbb', 'aaaaaaaaaa', 'bbbbbbbbbb']); }); it('orders timestamps whose fractional part has a different length', () => { // RFC3339Nano allows a variable-length fraction. `.12Z` is earlier than // `.1201Z`, but compares after it as a plain string. const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [k8sLine('2026-07-17T10:12:25.1201Z', 'later')]), replica('bbbbbbbbbb', [k8sLine('2026-07-17T10:12:25.12Z', 'earlier')]), ]); expect(merged.map((l) => l.message)).toEqual(['earlier', 'later']); }); it('orders a timestamp with no fractional part before one within the same second', () => { const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [ k8sLine('2026-07-17T10:12:25.000000001Z', 'later'), ]), replica('bbbbbbbbbb', [k8sLine('2026-07-17T10:12:25Z', 'earlier')]), ]); expect(merged.map((l) => l.message)).toEqual(['earlier', 'later']); }); it('orders sub-millisecond timestamps that a Date would collapse', () => { const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [ k8sLine('2026-07-17T10:12:25.000000900Z', 'later'), ]), replica('bbbbbbbbbb', [ k8sLine('2026-07-17T10:12:25.000000100Z', 'earlier'), ]), ]); expect(merged.map((l) => l.message)).toEqual(['earlier', 'later']); }); it('separates the timestamp from the service output without altering it', () => { const json = '{"logtime":"2026-07-17T10:12:25.130Z","loglevel":"INFO","message":"up"}'; const [line] = mergeReplicaLines([ replica('aaaaaaaaaa', [k8sLine('2026-07-17T10:12:25.130421355Z', json)]), ]); expect(line.timestamp).toBe('2026-07-17T10:12:25.130421355Z'); expect(line.message).toBe(json); }); it('keeps a line without a timestamp beside the line it belongs to', () => { // A service not using the Mosaic logger can emit output that arrives // without a usable stamp; it must not sort to the top of the stream. const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [ k8sLine('2026-07-17T10:12:25.000000000Z', 'Traceback:'), ' at line 2 of the stack', k8sLine('2026-07-17T10:12:26.000000000Z', 'after'), ]), ]); expect(merged.map((l) => l.message)).toEqual([ 'Traceback:', ' at line 2 of the stack', 'after', ]); }); it('preserves blank lines inside the log but drops the trailing newline', () => { const merged = mergeReplicaLines([ replica('aaaaaaaaaa', [ k8sLine('2026-07-17T10:12:25.000000000Z', 'before'), k8sLine('2026-07-17T10:12:25.500000000Z', ''), k8sLine('2026-07-17T10:12:26.000000000Z', 'after'), ]), ]); expect(merged.map((l) => l.message)).toEqual(['before', '', 'after']); }); }); describe('parseSinceToSeconds', () => { it.each([ ['45s', 45], ['5m', 300], ['2h', 7200], ['1d', 86400], ['90', 90], ])('reads %s as %i seconds', (since, expected) => { expect(parseSinceToSeconds(since)).toBe(expected); }); it.each(['0', '0m', '-5m', '5 m', 'five minutes', '5w', '1.5h', ''])( 'rejects %s rather than silently sending a wrong window', (since) => { expect(parseSinceToSeconds(since)).toBeUndefined(); }, ); it('is undefined when not supplied, so no window is sent at all', () => { expect(parseSinceToSeconds(undefined)).toBeUndefined(); }); }); describe('toTimestampSortKey', () => { it.each([ ['2026-07-17T10:12:25.12Z', '2026-07-17T10:12:25.120000000Z'], ['2026-07-17T10:12:25.1201Z', '2026-07-17T10:12:25.120100000Z'], ['2026-07-17T10:12:25Z', '2026-07-17T10:12:25.000000000Z'], ['2026-07-17T10:12:25.130421355Z', '2026-07-17T10:12:25.130421355Z'], ])('pads %s to %s', (input, expected) => { expect(toTimestampSortKey(input)).toBe(expected); }); it('leaves a value it cannot parse untouched', () => { expect(toTimestampSortKey('not-a-timestamp')).toBe('not-a-timestamp'); }); });