|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import test from 'node:test'; |
| 3 | +import { |
| 4 | + ATTENTION_TIERS, |
| 5 | + attentionAt, |
| 6 | + buildCurvePaths, |
| 7 | + createAttentionCurve, |
| 8 | + fillAtAttentionCrossing, |
| 9 | + jCurveStrength, |
| 10 | + projectCurve, |
| 11 | + tierAt, |
| 12 | + toneAt, |
| 13 | + zoneBandFill, |
| 14 | +} from './attentionModel.ts'; |
| 15 | + |
| 16 | +const compactMobilePlot = { left: 40, right: 316, top: 40, bottom: 176 }; |
| 17 | + |
| 18 | +/* ── Curve shape (ported from UShapeAttentionCurveGeometry.test) ── */ |
| 19 | + |
| 20 | +test('attention curve keeps both context edges above the middle at moderate fill', () => { |
| 21 | + const { points } = createAttentionCurve(50); |
| 22 | + const middle = points[points.length / 2 - 0.5]; |
| 23 | + |
| 24 | + assert.equal(points[0].position, 0); |
| 25 | + assert.equal(points.at(-1)?.position, 1); |
| 26 | + assert.ok(points[0].attention > middle.attention); |
| 27 | + assert.ok(points.at(-1)!.attention > middle.attention); |
| 28 | +}); |
| 29 | + |
| 30 | +test('long context turns the symmetric U toward recency', () => { |
| 31 | + const { points } = createAttentionCurve(100); |
| 32 | + |
| 33 | + assert.ok(points.at(-1)!.attention > points[0].attention); |
| 34 | + assert.equal(points.at(-1)!.cyanOpacity, 1); |
| 35 | +}); |
| 36 | + |
| 37 | +test('compact mobile projection retains context left-to-right and attention high-to-low', () => { |
| 38 | + const { points } = createAttentionCurve(50); |
| 39 | + const projected = projectCurve(points, compactMobilePlot); |
| 40 | + const middle = projected[projected.length / 2 - 0.5]; |
| 41 | + |
| 42 | + assert.equal(projected[0].x, compactMobilePlot.left); |
| 43 | + assert.equal(projected.at(-1)?.x, compactMobilePlot.right); |
| 44 | + assert.ok(projected[0].y < middle.y); |
| 45 | + assert.ok(projected.at(-1)!.y < middle.y); |
| 46 | +}); |
| 47 | + |
| 48 | +/* ── Unification invariants ─────────────────────────────────────── */ |
| 49 | + |
| 50 | +test('one shared primacy-decay coefficient: J-curve is zero at half fill and ramps to 1 at full', () => { |
| 51 | + assert.equal(jCurveStrength(0), 0); |
| 52 | + assert.equal(jCurveStrength(0.5), 0); |
| 53 | + assert.equal(jCurveStrength(1), 1); |
| 54 | +}); |
| 55 | + |
| 56 | +test('attention degrades monotonically as the window fills at every position', () => { |
| 57 | + for (const position of [0, 0.25, 0.5, 0.75, 1]) { |
| 58 | + for (let fill = 0; fill < 1; fill += 0.05) { |
| 59 | + assert.ok( |
| 60 | + attentionAt(position, fill) >= |
| 61 | + attentionAt(position, fill + 0.05) - 1e-9, |
| 62 | + `position ${position} fill ${fill}` |
| 63 | + ); |
| 64 | + } |
| 65 | + } |
| 66 | +}); |
| 67 | + |
| 68 | +test('recency edge stays strong at every fill while the middle collapses', () => { |
| 69 | + assert.ok(attentionAt(1, 1) >= ATTENTION_TIERS.strong); |
| 70 | + assert.ok(attentionAt(0.5, 1) < ATTENTION_TIERS.degraded); |
| 71 | +}); |
| 72 | + |
| 73 | +test('attention is full in an empty window regardless of position', () => { |
| 74 | + for (const position of [0, 0.25, 0.5, 0.75, 1]) { |
| 75 | + assert.equal(attentionAt(position, 0), 1); |
| 76 | + } |
| 77 | +}); |
| 78 | + |
| 79 | +/* ── Tiers, tones, band fills ───────────────────────────────────── */ |
| 80 | + |
| 81 | +test('tier and tone thresholds are consistent', () => { |
| 82 | + assert.equal(tierAt(ATTENTION_TIERS.strong), 'strong'); |
| 83 | + assert.equal(tierAt(ATTENTION_TIERS.degraded), 'degraded'); |
| 84 | + assert.equal(tierAt(0), 'collapsed'); |
| 85 | + assert.equal(toneAt(1), 'success'); |
| 86 | + assert.equal(toneAt(0.3), 'warning'); |
| 87 | + assert.equal(toneAt(0.1), 'error'); |
| 88 | +}); |
| 89 | + |
| 90 | +test('middle zone band deepens muted → warning → error as fill grows', () => { |
| 91 | + assert.equal(zoneBandFill('middle', 0), 'var(--surface-muted)'); |
| 92 | + assert.equal(zoneBandFill('middle', 0.5), 'var(--surface-muted)'); |
| 93 | + assert.equal(zoneBandFill('middle', 0.7), 'var(--visual-bg-warning)'); |
| 94 | + assert.equal(zoneBandFill('middle', 0.9), 'var(--visual-bg-error)'); |
| 95 | +}); |
| 96 | + |
| 97 | +test('recency band never degrades; primacy band fades only past half fill', () => { |
| 98 | + for (const fill of [0, 0.5, 0.8, 1]) { |
| 99 | + assert.equal(zoneBandFill('recency', fill), 'var(--visual-bg-cyan)'); |
| 100 | + } |
| 101 | + assert.ok(zoneBandFill('primacy', 0.4).includes('100%')); |
| 102 | + assert.ok(!zoneBandFill('primacy', 1).includes('100%')); |
| 103 | +}); |
| 104 | + |
| 105 | +test('fillAtAttentionCrossing inverts attentionAt', () => { |
| 106 | + for (const target of [ATTENTION_TIERS.strong, ATTENTION_TIERS.degraded]) { |
| 107 | + const fill = fillAtAttentionCrossing(0.5, target); |
| 108 | + assert.ok(Math.abs(attentionAt(0.5, fill) - target) < 0.01); |
| 109 | + } |
| 110 | + // The collapsed crossing happens at a deeper fill than the degraded one. |
| 111 | + assert.ok( |
| 112 | + fillAtAttentionCrossing(0.5, ATTENTION_TIERS.degraded) > |
| 113 | + fillAtAttentionCrossing(0.5, ATTENTION_TIERS.strong) |
| 114 | + ); |
| 115 | +}); |
| 116 | + |
| 117 | +/* ── Smooth curve paths (pressure diagram panel) ────────────────── */ |
| 118 | + |
| 119 | +test('buildCurvePaths reports the valley floor and spans the full plot', () => { |
| 120 | + const { minAttention, curvePath, mobileCurvePath } = buildCurvePaths(0.9); |
| 121 | + |
| 122 | + assert.ok(Math.abs(minAttention - attentionAt(0.5, 0.9)) < 0.02); |
| 123 | + assert.ok(curvePath.startsWith('M')); |
| 124 | + assert.ok(mobileCurvePath.startsWith('M')); |
| 125 | + // Empty window: no drop anywhere. |
| 126 | + assert.equal(buildCurvePaths(0).minAttention, 1); |
| 127 | +}); |
0 commit comments