Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
68b14d4
feat(privacy): strip listing payloads on external transcript egress
ussoewwin Aug 9, 2026
525c929
refactor(privacy): rename egress listing helpers off prefix-cache labels
ussoewwin Aug 9, 2026
1e0229e
fix(privacy): close CodeRabbit findings on external egress projection
ussoewwin Aug 10, 2026
24f9260
fix(privacy): close CodeRabbit review 4892951252 (3 findings)
ussoewwin Aug 10, 2026
545a74a
fix(privacy): close jatmn review 4893077611 on external egress
ussoewwin Aug 10, 2026
de683fa
fix(privacy): close jatmn review 4893077611 (all four findings)
ussoewwin Aug 10, 2026
25bc648
fix(privacy): use OMISSION_REBUILD_TAIL_BYTES for egress rebuild (Cod…
ussoewwin Aug 10, 2026
60993e4
fix(privacy): bound omission rebuild reads and preserve line-boundary…
ussoewwin Aug 10, 2026
cbd5416
fix(privacy): close maintainer review 4899061317
ussoewwin Aug 11, 2026
401a74d
fix(privacy): bound evicted egress set and own test lock
ussoewwin Aug 11, 2026
7071e31
fix(privacy): retain compact ancestry after double omission eviction
ussoewwin Aug 11, 2026
385ca39
fix(privacy): store actual compact omission ancestors
ussoewwin Aug 11, 2026
426bb47
fix(privacy): tail-window on-demand ancestry and known-omitted scan gate
ussoewwin Aug 11, 2026
be4c02e
fix(privacy): close maintainer review 4907143023 egress omissions
ussoewwin Aug 12, 2026
9fca2a5
fix(privacy): tighten egress persist after review 4907143023 tests
ussoewwin Aug 12, 2026
ca2ab10
fix(privacy): close CodeRabbit review 4912132735 persist and bound eg…
ussoewwin Aug 12, 2026
b48bdb1
fix(privacy): tighten grandchild persist-skip assertion for 3762934396
ussoewwin Aug 12, 2026
c4b7239
fix(privacy): fail-close cyclic omitted parents and cache parent_safe
ussoewwin Aug 12, 2026
7376d6e
test(privacy): fix seed startingParentUuid type in external egress test
ussoewwin Aug 18, 2026
e488ac4
fix(privacy): unified fail-closed egress classifier, explicit deliver…
ussoewwin Aug 19, 2026
e814390
fix(privacy): guard live egress projection on active sink, add test t…
ussoewwin Aug 19, 2026
343abfc
fix(privacy): recursively resolve chained omitted/evicted ancestors a…
ussoewwin Aug 19, 2026
7d1d9a9
fix(privacy): treat null target parent as safe projected root and com…
ussoewwin Aug 19, 2026
88aadf1
fix(privacy): strict remote delivery witness tracking, fail-closed an…
ussoewwin Aug 20, 2026
d1591cc
fix(privacy): bound parent_safe lookups, track lock ownership in feed…
ussoewwin Aug 20, 2026
df78c5b
fix(privacy): unify external egress projection contract and register …
ussoewwin Aug 21, 2026
0dbfd93
fix(privacy): bound on-demand ancestry scans and retry incomplete reb…
ussoewwin Aug 21, 2026
039efa0
fix(privacy): bound incomplete rebuild retries and isolate diagnostic…
ussoewwin Aug 21, 2026
ea96c51
fix(privacy): recompute parent projection and clear cached misses on …
ussoewwin Aug 21, 2026
bc22983
fix(privacy): bound on-demand ancestry scan budget to 8 MiB in append…
ussoewwin Aug 21, 2026
262eafd
fix(privacy): close jatmn 5000432029 external egress findings
ussoewwin Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions src/components/Feedback.egress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { afterAll, beforeAll, expect, mock, test } from 'bun:test'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Message } from '../types/message.js'
import {
acquireSharedMutationLock,
releaseSharedMutationLock,
} from '../test/sharedMutationLock.js'

type AxiosModule = typeof import('axios')
type ProvidersModule = typeof import('../utils/model/providers.js')
type AuthModule = typeof import('../utils/auth.js')
type HttpModule = typeof import('../utils/http.js')
type PrivacyModule = typeof import('../utils/privacyLevel.js')

let originalAxiosModule: AxiosModule | undefined
let originalProvidersModule: ProvidersModule | undefined
let originalAuthModule: AuthModule | undefined
let originalHttpModule: HttpModule | undefined
let originalPrivacyModule: PrivacyModule | undefined
let originalUserType: string | undefined
let hadMacro = false
let originalMacro: unknown
let tempDir: string | undefined
let transcriptPath: string | undefined
let postedBodies: Array<{ content?: string }> = []
let ownsSharedMutationLock = false

function buildAxiosModuleStub(
post: (...args: unknown[]) => Promise<unknown>,
): AxiosModule {
const instance = {
get: async () => ({ status: 200 }),
post,
isAxiosError: () => false,
isCancel: () => false,
defaults: {} as Record<string, unknown>,
interceptors: {
request: { use: () => 0, eject: () => {} },
response: { use: () => 0, eject: () => {} },
},
}
return { default: instance } as unknown as AxiosModule
}

beforeAll(async () => {
ownsSharedMutationLock = false
await acquireSharedMutationLock('Feedback.egress')
ownsSharedMutationLock = true
originalAxiosModule = await import('axios')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
originalUserType = process.env.USER_TYPE
hadMacro = Object.prototype.hasOwnProperty.call(globalThis, 'MACRO')
originalMacro = (globalThis as { MACRO?: unknown }).MACRO
;(globalThis as { MACRO?: { VERSION: string } }).MACRO = {
VERSION: 'test-version',
}

originalProvidersModule = await import('../utils/model/providers.js')
mock.module('../utils/model/providers.js', () => ({
...originalProvidersModule!,
getAPIProvider: () => 'firstParty',
isFirstPartyAnthropicBaseUrl: () => true,
}))

originalAuthModule = await import('../utils/auth.js')
mock.module('../utils/auth.js', () => ({
...originalAuthModule!,
checkAndRefreshOAuthTokenIfNeeded: async () => {},
}))

originalHttpModule = await import('../utils/http.js')
mock.module('../utils/http.js', () => ({
...originalHttpModule!,
getAuthHeaders: () => ({
headers: { Authorization: 'Bearer test' },
error: undefined,
}),
getUserAgent: () => 'test-agent',
}))

originalPrivacyModule = await import('../utils/privacyLevel.js')
mock.module('../utils/privacyLevel.js', () => ({
...originalPrivacyModule!,
isEssentialTrafficOnly: () => false,
}))

tempDir = await mkdtemp(join(tmpdir(), 'openclaude-feedback-egress-'))
transcriptPath = join(tempDir, 'session.jsonl')
// f001 user → f002 listing (omitted) → f003 user (parent=f002, must reparent to f001)
await writeFile(
transcriptPath,
`${JSON.stringify({
type: 'user',
uuid: '00000000-0000-4000-8000-00000000f001',
parentUuid: null,
timestamp: '2026-08-07T00:00:00.000Z',
message: { role: 'user', content: 'feedback main turn' },
})}\n${JSON.stringify({
type: 'attachment',
uuid: '00000000-0000-4000-8000-00000000f002',
parentUuid: '00000000-0000-4000-8000-00000000f001',
timestamp: '2026-08-07T00:00:00.000Z',
attachment: {
type: 'skill_listing',
content: 'Available skills:\n- /leak-me-please',
skillCount: 1,
isInitial: true,
},
})}\n${JSON.stringify({
type: 'user',
uuid: '00000000-0000-4000-8000-00000000f003',
parentUuid: '00000000-0000-4000-8000-00000000f002',
timestamp: '2026-08-07T00:00:01.000Z',
message: { role: 'user', content: 'after listing turn' },
})}\n`,
)

// Do NOT mock.module sessionStorage — that leaks into later suites under
// --max-concurrency=1. Pass transcript / subagent data via test seams.
mock.module('axios', () =>
buildAxiosModuleStub(async (_url: unknown, body: unknown) => {
postedBodies.push(body as { content?: string })
return { status: 200, data: { feedback_id: 'fb-egress-1' } }
}),
)
})

afterAll(async () => {
try {
if (originalUserType === undefined) {
delete process.env.USER_TYPE
} else {
process.env.USER_TYPE = originalUserType
}
if (!hadMacro) {
delete (globalThis as { MACRO?: unknown }).MACRO
} else {
;(globalThis as { MACRO?: unknown }).MACRO = originalMacro
}
if (originalAxiosModule) {
mock.module('axios', () => originalAxiosModule!)
}
if (originalProvidersModule) {
mock.module('../utils/model/providers.js', () => originalProvidersModule!)
}
if (originalAuthModule) {
mock.module('../utils/auth.js', () => originalAuthModule!)
}
if (originalHttpModule) {
mock.module('../utils/http.js', () => originalHttpModule!)
}
if (originalPrivacyModule) {
mock.module('../utils/privacyLevel.js', () => originalPrivacyModule!)
}
if (tempDir) {
await rm(tempDir, { recursive: true, force: true })
}
} finally {
if (ownsSharedMutationLock) {
ownsSharedMutationLock = false
releaseSharedMutationLock()
}
}
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('Feedback upload strips listing payloads from the posted content body', async () => {
postedBodies = []
process.env.USER_TYPE = 'external'

const { assembleFeedbackEgressReportData, submitFeedback } = await import(
'./Feedback.js'
)

const listing = {
type: 'attachment',
uuid: '00000000-0000-4000-8000-00000000m101',
attachment: {
type: 'skill_listing',
content: 'Available skills:\n- /leak-me-please',
skillCount: 1,
isInitial: true,
},
} as unknown as Message
const user = {
type: 'user',
uuid: '00000000-0000-4000-8000-00000000m102',
message: { role: 'user', content: 'plain turn' },
} as unknown as Message

const report = await assembleFeedbackEgressReportData({
messages: [listing, user],
description: 'egress regression',
transcriptPathForTesting: transcriptPath,
subagentTranscriptsForTesting: {
'agent-leak': [
{
type: 'attachment',
uuid: '00000000-0000-4000-8000-00000000a201',
attachment: {
type: 'agent_listing_delta',
addedTypes: ['Explore'],
addedLines: ['- Explore: /leak-agent-listing'],
removedTypes: [],
isInitial: true,
showConcurrencyNote: false,
},
} as unknown as Message,
{
type: 'user',
uuid: '00000000-0000-4000-8000-00000000a202',
message: { role: 'user', content: 'subagent turn' },
} as unknown as Message,
],
},
})
const result = await submitFeedback(report)

expect(result.success).toBe(true)
expect(postedBodies).toHaveLength(1)
const content = postedBodies[0]?.content ?? ''
expect(content).toContain('plain turn')
expect(content).toContain('feedback main turn')
expect(content).toContain('after listing turn')
expect(content).toContain('subagent turn')
expect(content).not.toContain('leak-me-please')
expect(content).not.toContain('leak-agent-listing')

// Posted report embeds filtered rawTranscriptJsonl — survivor f003 must
// reparent from omitted f002 onto retained ancestor f001.
const parsed = JSON.parse(content) as { rawTranscriptJsonl?: string }
expect(parsed.rawTranscriptJsonl).toBeDefined()
const lines = (parsed.rawTranscriptJsonl ?? '')
.split('\n')
.filter(l => l.length > 0)
.map(
line =>
JSON.parse(line) as {
uuid?: string
parentUuid?: string | null
},
)
const afterListing = lines.find(
e => e.uuid === '00000000-0000-4000-8000-00000000f003',
)
expect(afterListing).toBeDefined()
expect(afterListing?.parentUuid).toBe(
'00000000-0000-4000-8000-00000000f001',
)
expect(
lines.some(e => e.uuid === '00000000-0000-4000-8000-00000000f002'),
).toBe(false)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading