import { describe, expect, it } from 'vitest' import { EN_SURFACES } from '@/i18n/en-surfaces' import { TR_SURFACES } from '@/i18n/tr-surfaces' import type { StarmapGraph } from '@/types/hermes' import { decodeShareCode, encodeShareCode, ShareCodeError, shareCodeErrorText } from './share-code' function sampleGraph(): StarmapGraph { return { clusters: [], edges: [ { source: 'skill-a', target: 'skill-b' }, { source: 'skill-b', target: 'memory:profile:0' } ], memory: [ { body: 'Prefers concise answers.', source: 'profile', timestamp: 1_700_000_000, title: 'Tone' }, { body: 'Uses a worktree.', source: 'memory', timestamp: null, title: 'Env' } ], nodes: [ { category: 'devops', createdBy: 'agent', id: 'skill-a', kind: 'skill', label: 'skill-a', pinned: true, state: 'active', timestamp: 1_699_900_000, useCount: 7 }, { category: 'devops', createdBy: null, id: 'skill-b', kind: 'skill', label: 'skill-b', pinned: false, state: 'draft', timestamp: 1_699_950_000, useCount: 0 }, { category: 'memory', createdBy: null, id: 'memory:profile:0', kind: 'memory', label: 'A fact', memorySource: 'profile', pinned: false, state: 'active', timestamp: 1_700_000_000, useCount: 0 } ], stats: {} } } // Decoded edges compared by node POSITION (ids are synthesized), so topology is // the invariant, not the literal id strings. const topology = (g: StarmapGraph): [number, number][] => { const idx = new Map(g.nodes.map((n, i) => [n.id, i])) return g.edges.map(e => [idx.get(e.source)!, idx.get(e.target)!]) } describe('share-code', () => { // The viz contract: everything the star map RENDERS survives — kinds, radius // inputs, time position, edge topology — while text is dropped (it's a loadout, // not a backup). it('preserves the visualization', () => { const g = sampleGraph() const decoded = decodeShareCode(encodeShareCode(g)) const span = 1_700_000_000 - 1_699_900_000 const tol = Math.ceil(span / 4095) + 1 expect(decoded.nodes).toHaveLength(g.nodes.length) decoded.nodes.forEach((d, i) => { const o = g.nodes[i]! expect(d.kind).toBe(o.kind) expect(d.label).toBe(o.label) expect(d.useCount).toBe(o.useCount) expect(d.state).toBe(o.state) expect(d.pinned).toBe(o.pinned) expect(d.category).toBe(o.category) expect(d.memorySource).toBe(o.memorySource) expect(d.createdBy).toBe(o.createdBy) if (o.timestamp == null) { expect(d.timestamp).toBeNull() } else { expect(Math.abs((d.timestamp ?? 0) - o.timestamp)).toBeLessThanOrEqual(tol) } }) expect(topology(decoded)).toEqual(topology(g)) }) it('drops memory prose (loadout is viz-only)', () => { expect(decodeShareCode(encodeShareCode(sampleGraph())).memory).toHaveLength(0) }) it('rebuilds clusters from node categories', () => { const decoded = decodeShareCode(encodeShareCode(sampleGraph())) expect(decoded.clusters.find(c => c.category === 'devops')?.count).toBe(2) }) it('produces a short, opaque, prefixed code', () => { const code = encodeShareCode(sampleGraph()) expect(code.startsWith('HML')).toBe(true) expect(code.slice(3)).toMatch(/^[A-Za-z0-9_-]+$/) // Strictly smaller than the naive JSON it replaces — the whole point. expect(code.length).toBeLessThan(JSON.stringify(sampleGraph()).length) }) it('stays compact on a large graph (no string bloat)', () => { const nodes = Array.from({ length: 500 }, (_, i) => ({ category: `cat-${i % 8}`, createdBy: 'agent' as const, id: `s${i}`, kind: 'skill' as const, label: `A fairly verbose skill label number ${i}`, pinned: false, state: 'active', timestamp: 1_700_000_000 + i * 3600, useCount: i % 50 })) const graph: StarmapGraph = { clusters: [], edges: [], memory: [], nodes, stats: {} } const code = encodeShareCode(graph) // Deflate keeps even verbose, repetitive labels far below the naive JSON. expect(code.length).toBeLessThan(JSON.stringify(graph).length / 5) }) it('handles an empty graph', () => { const decoded = decodeShareCode(encodeShareCode({ clusters: [], edges: [], memory: [], nodes: [], stats: {} })) expect(decoded.nodes).toHaveLength(0) expect(decoded.edges).toHaveLength(0) }) it('drops edges whose endpoints are missing', () => { const g = sampleGraph() g.edges.push({ source: 'skill-a', target: 'does-not-exist' }) expect(decodeShareCode(encodeShareCode(g)).edges).toHaveLength(2) }) it('rejects garbage with a ShareCodeError', () => { expect(() => decodeShareCode('not a real code !!!')).toThrow(ShareCodeError) expect(() => decodeShareCode('')).toThrow(ShareCodeError) }) it('formats real decoding failures in the selected language without changing valid codes', () => { const graph = sampleGraph() const valid = encodeShareCode(graph) const capture = (code: string): unknown => { try { decodeShareCode(code) } catch (error) { return error } throw new Error('Expected a decoding failure') } const invalid = capture('not a real code !!!') expect(shareCodeErrorText(invalid, TR_SURFACES.mapErrors)).toBe('Bu metin bir harita koduna benzemiyor.') expect(shareCodeErrorText(invalid, EN_SURFACES.mapErrors)).toBe("That doesn't look like a map code.") const raw = atob(valid.slice(3).replace(/-/g, '+').replace(/_/g, '/')) const expected = raw.charCodeAt(0) const actual = (expected + 1) % 256 const otherVersion = 'HML' + btoa(String.fromCharCode(actual) + raw.slice(1)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') expect(shareCodeErrorText(capture(otherVersion), TR_SURFACES.mapErrors)) .toBe(TR_SURFACES.mapErrors.version(actual, expected)) const middle = Math.floor(valid.length / 2) const corrupted = valid.slice(0, middle) + (valid[middle] === 'A' ? 'B' : 'A') + valid.slice(middle + 1) expect(shareCodeErrorText(capture(corrupted), TR_SURFACES.mapErrors)).toBe(TR_SURFACES.mapErrors.corrupted) expect(shareCodeErrorText(new Error('internal details'), TR_SURFACES.mapErrors)).toBe('Harita kodu okunamadı.') expect(encodeShareCode(graph)).toBe(valid) expect(decodeShareCode(valid).nodes).toHaveLength(graph.nodes.length) }) it('rejects a corrupted (bit-flipped) code', () => { const code = encodeShareCode(sampleGraph()) // Flip a mid-payload char (trailing base64 bits can be dropped on decode). const i = Math.floor(code.length / 2) const corrupted = code.slice(0, i) + (code[i] === 'A' ? 'B' : 'A') + code.slice(i + 1) expect(() => decodeShareCode(corrupted)).toThrow(ShareCodeError) }) it('tolerates whitespace, including internal wraps', () => { const code = encodeShareCode(sampleGraph()) const wrapped = ` ${code.slice(0, 20)}\n${code.slice(20)}\t` expect(() => decodeShareCode(wrapped)).not.toThrow() expect(decodeShareCode(wrapped).nodes).toHaveLength(sampleGraph().nodes.length) }) })