Files
Shade/packages/shade-observability/tests/with-tracer.test.ts
Sterister e6fdf31b49
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.

Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
  Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
  bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
  shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
  non-realtime stack.

Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
  version 4.0.0 on /health.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00

128 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import { NOOP_HOOK, withTracer, type OtelTracerLike } from '../src/index.ts';
interface RecordedSpan {
name: string;
attrs: Record<string, unknown>;
ended: boolean;
}
function makeFakeTracer(): { tracer: OtelTracerLike; spans: RecordedSpan[] } {
const spans: RecordedSpan[] = [];
const tracer: OtelTracerLike = {
startSpan(name, options) {
const rec: RecordedSpan = {
name,
attrs: { ...(options?.attributes ?? {}) },
ended: false,
};
spans.push(rec);
return {
setAttribute(k, v) {
rec.attrs[k] = v;
return undefined;
},
end() {
rec.ended = true;
return undefined;
},
};
},
};
return { tracer, spans };
}
describe('withTracer (off-by-default)', () => {
beforeEach(() => {
delete (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process?.env?.SHADE_OTEL_ENABLED;
});
test('returns NOOP_HOOK when tracer is undefined', () => {
const hook = withTracer(undefined);
expect(hook).toBe(NOOP_HOOK);
});
test('returns NOOP_HOOK when env-var is not set (default)', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer);
const span = hook.startSpan('shade.test');
span.setAttribute('foo', 'bar');
span.end();
expect(spans.length).toBe(0); // never reached the OTel tracer
});
test('force=true bypasses the env gate', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer, { force: true });
hook.startSpan('shade.test', { foo: 'bar' }).end();
expect(spans.length).toBe(1);
expect(spans[0]?.name).toBe('shade.test');
expect(spans[0]?.attrs.foo).toBe('bar');
expect(spans[0]?.ended).toBe(true);
});
});
describe('withTracer (env-enabled)', () => {
beforeEach(() => {
process.env.SHADE_OTEL_ENABLED = '1';
});
afterEach(() => {
delete process.env.SHADE_OTEL_ENABLED;
});
test('emits spans through the tracer when env is set', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer);
const span = hook.startSpan('shade.upload', { 'shade.bytes.bin': '110MB' });
span.setAttribute('shade.result', 'ok');
span.setStatus('ok');
span.end();
expect(spans.length).toBe(1);
expect(spans[0]?.name).toBe('shade.upload');
expect(spans[0]?.attrs['shade.bytes.bin']).toBe('110MB');
expect(spans[0]?.attrs['shade.result']).toBe('ok');
expect(spans[0]?.ended).toBe(true);
});
test('respects per-span sampling', () => {
const { tracer, spans } = makeFakeTracer();
let n = 0;
const random = () => {
// Alternates: 0.1 (sampled in), 0.9 (sampled out)
const v = n % 2 === 0 ? 0.1 : 0.9;
n++;
return v;
};
const hook = withTracer(tracer, { sample: 0.5, random });
for (let i = 0; i < 10; i++) hook.startSpan(`s${i}`).end();
// Half (5 of 10) should reach the OTel tracer.
expect(spans.length).toBe(5);
});
test('sample=0 means no spans even when env is on', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer, { sample: 0 });
hook.startSpan('shade.test').end();
expect(spans.length).toBe(0);
});
test('end() is idempotent', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer);
const span = hook.startSpan('shade.test');
span.end();
span.end();
expect(spans.length).toBe(1);
expect(spans[0]?.ended).toBe(true);
});
test('attribute mutations after end() are no-op', () => {
const { tracer, spans } = makeFakeTracer();
const hook = withTracer(tracer);
const span = hook.startSpan('shade.test', { a: 1 });
span.end();
span.setAttribute('after_end', 'oops');
expect(spans[0]?.attrs.after_end).toBeUndefined();
});
});