mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-26 16:58:28 +00:00
current.pullRequests survived both sanitizeForSharing and redactProjectNames(include_project_names: false) verbatim, so each row's full GitHub url and owner/repo#123 label named the repository to a paired peer device and to a name-redacted MCP client — the same class of leak Sharing drops the block like it drops the project, session and per-branch rows. The MCP pass pseudonymizes url and label from the url (the aggregation key) with the same salted, domain-separated hash as the other pseudonyms, so rows referring to the same PR still line up and every number stays untouched. Closes #1346
112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
|
|
import { sanitizeForSharing } from '../../src/sharing/sanitize.js'
|
|
import type { MenubarPayload } from '../../src/menubar-json.js'
|
|
|
|
function fixture(): MenubarPayload {
|
|
return {
|
|
generated: 'now',
|
|
current: {
|
|
label: 'June',
|
|
cost: 100,
|
|
calls: 5,
|
|
sessions: 2,
|
|
oneShotRate: 1,
|
|
inputTokens: 10,
|
|
outputTokens: 20,
|
|
cacheHitPercent: 90,
|
|
codexCredits: 0,
|
|
topActivities: [{ name: 'Coding', cost: 50, savingsUSD: 0, turns: 3, oneShotRate: 1 }],
|
|
topModels: [{ name: 'Opus', cost: 80, savingsUSD: 0, savingsBaselineModel: '', calls: 4 }],
|
|
providers: { claude: 100 },
|
|
topProjects: [
|
|
{ name: 'secret-project', cost: 100, savingsUSD: 0, sessions: 2, avgCostPerSession: 50, sessionDetails: [] },
|
|
],
|
|
tools: [{ name: 'Bash', calls: 9 }],
|
|
topSessions: [{ project: 'secret-project', cost: 100, savingsUSD: 0, calls: 5, date: '2026-06-01', projectKey: '-Users-me-Projects-secret-project' }],
|
|
byBranch: [
|
|
{ branch: 'exp/extraction-arms', cost: 60, calls: 3, sessions: 1 },
|
|
{ branch: null, cost: 40, calls: 2, sessions: 1 },
|
|
],
|
|
pullRequests: {
|
|
rows: [{
|
|
url: 'https://github.com/secret-client/repo/pull/42', label: 'secret-client/repo#42',
|
|
cost: 60, savingsUSD: 0, sessions: 1, calls: 5,
|
|
firstStarted: '2026-06-01T10:00:00.000Z', lastEnded: '2026-06-01T12:00:00.000Z',
|
|
approx: false, models: ['Opus'],
|
|
}],
|
|
distinctCost: 60, distinctSessions: 1, attributedCost: 60, unattributedCost: 0,
|
|
},
|
|
},
|
|
history: {
|
|
daily: [],
|
|
timeline: {
|
|
bucketMinutes: 15,
|
|
modelSeries: [{ id: 'model_0', label: 'claude-opus-4-6' }],
|
|
sessionSeries: [{ id: 'session_0', label: 'secret-project · abc123…cdef (claude)' }],
|
|
points: [{
|
|
timestamp: '2026-06-01T10:00:00.000Z',
|
|
cost: 5,
|
|
tokens: 150,
|
|
models: [{ seriesId: 'model_0', cost: 5, tokens: 150 }],
|
|
sessions: [{ seriesId: 'session_0', cost: 5, tokens: 150 }],
|
|
}],
|
|
},
|
|
},
|
|
} as unknown as MenubarPayload
|
|
}
|
|
|
|
describe('sanitizeForSharing', () => {
|
|
it('strips project names and session detail but keeps aggregates', () => {
|
|
const clean = sanitizeForSharing(fixture())
|
|
expect(clean.current.topProjects).toEqual([])
|
|
expect(clean.current.topSessions).toEqual([])
|
|
expect(clean.current.cost).toBe(100)
|
|
expect(clean.current.topModels[0]!.name).toBe('Opus')
|
|
expect(clean.current.providers).toEqual({ claude: 100 })
|
|
expect(clean.history.timeline?.sessionSeries).toEqual([])
|
|
expect(clean.history.timeline?.points[0]!.sessions).toEqual([])
|
|
expect(clean.history.timeline?.modelSeries).toHaveLength(1)
|
|
expect(clean.history.timeline?.points[0]!.models).toHaveLength(1)
|
|
})
|
|
|
|
it('leaks no project name anywhere in the shared payload', () => {
|
|
const clean = sanitizeForSharing(fixture())
|
|
expect(JSON.stringify(clean)).not.toContain('secret-project')
|
|
})
|
|
|
|
it('drops the per-branch rows: branch names encode tickets, customers and codenames', () => {
|
|
const clean = sanitizeForSharing(fixture())
|
|
expect(clean.current.byBranch).toBeUndefined()
|
|
expect(JSON.stringify(clean)).not.toContain('exp/extraction-arms')
|
|
})
|
|
|
|
it('leaks no working-directory key from the session rows', () => {
|
|
const clean = sanitizeForSharing(fixture())
|
|
expect(JSON.stringify(clean)).not.toContain('-Users-me-Projects-secret-project')
|
|
})
|
|
|
|
it('drops the pull-request rows: their urls and labels name the repository', () => {
|
|
const clean = sanitizeForSharing(fixture())
|
|
expect(clean.current.pullRequests).toBeUndefined()
|
|
expect(JSON.stringify(clean)).not.toContain('github.com/secret-client/repo')
|
|
expect(JSON.stringify(clean)).not.toContain('secret-client/repo#42')
|
|
})
|
|
|
|
it('drops the live-session block, which names the project and branch in flight', () => {
|
|
const withSessions = {
|
|
...fixture(),
|
|
liveSessions: {
|
|
windowSeconds: 120,
|
|
sessions: [{
|
|
id: 'a', provider: 'claude', project: 'secret-project', branch: 'secret-branch',
|
|
model: 'Opus 4.8', contextTokens: 1, contextWindow: 200_000,
|
|
startedAt: '2026-09-01T10:00:00.000Z', lastActivityAt: '2026-09-01T12:00:00.000Z',
|
|
}],
|
|
},
|
|
}
|
|
const clean = sanitizeForSharing(withSessions)
|
|
expect(clean.liveSessions).toBeUndefined()
|
|
expect(JSON.stringify(clean)).not.toContain('secret-branch')
|
|
})
|
|
})
|