mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-06 06:41:56 +00:00
* fix(cli): filter raw SSE event JSON from leaking into chat messages
Two types of internal JSON were appearing as visible text in Telegram
Mini App and web chat:
1. `rate_limit_event` — the rate limit parser returned `null` for
unknown statuses, causing raw JSON to pass through as assistant text.
Changed to `{ suppress: true }` so all rate_limit_event variants are
handled; new statuses that need display can be added explicitly.
2. `{ type: "output", data: { ... } }` — internal session metadata
envelopes leaked through the ACP text chunk pipeline. Added an
`isInternalEventJson` filter that catches JSON objects with known
internal envelope types (output, event, queue-operation) before they
enter the text buffer.
Closes #386
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): narrow internal event filter to match only leaked metadata shape
Address review feedback: the broad type-based filter could suppress
legitimate assistant JSON with type "event" or "queue-operation".
Narrow the check to only match the specific leaked metadata envelope:
{ type: "output", data: { parentUuid, sessionId, userType } }
Add negative tests confirming other JSON types pass through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): suppress malformed rate_limit_event without resetsAt
Address review: rate_limit_event payloads missing resetsAt still leaked
as raw JSON because parseRateLimitText returned null before reaching the
unknown-status suppress. Move the allowed check before the resetsAt
guard and suppress malformed payloads instead of passing them through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): handle parentUuid: null in internal event filter
Root/first-message metadata envelopes have parentUuid: null rather than
a string, so the filter missed them. Accept both string and null.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(cli): add e2e regression tests for metadata envelope filtering
Add AcpMessageHandler tests that verify leaked { type: "output", data }
metadata envelopes (both parentUuid string and null) are dropped before
reaching the text buffer.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix when cumulative metadata chunk arrives
When a leaked metadata envelope arrives as cumulative streaming chunks
(first an incomplete JSON prefix, then the full blob), the filter
dropped the full chunk but left the prefix in bufferedText. Clear the
buffer when the detected internal JSON starts with the buffered prefix.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): round resetsAt to integer for pipe-delimited format
The web-side regex uses \d+ to parse the timestamp, so a float value
would silently fail to match. Apply Math.round to ensure integer output.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix for cumulative rate_limit_event chunks
The prefix-clearing logic only applied to the isInternalEventJson
branch but not to the parseRateLimitText branch, so cumulative
rate_limit_event chunks could leave a raw JSON prefix in the buffer.
Hoist the prefix check before both filters and apply uniformly.
Add regression tests for suppressed and displayable cumulative
rate_limit_event scenarios.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { parseRateLimitText } from './rateLimitParser';
|
|
|
|
describe('parseRateLimitText', () => {
|
|
it('returns null for non-JSON text', () => {
|
|
expect(parseRateLimitText('Hello world')).toBeNull();
|
|
});
|
|
|
|
it('returns null for JSON that is not a rate_limit_event', () => {
|
|
expect(parseRateLimitText('{"type":"other"}')).toBeNull();
|
|
});
|
|
|
|
it('converts allowed_warning to pipe-delimited warning text', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'allowed_warning',
|
|
resetsAt: 1774278000,
|
|
rateLimitType: 'five_hour',
|
|
utilization: 0.9,
|
|
isUsingOverage: false,
|
|
surpassedThreshold: 0.9,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: 'Claude AI usage limit warning|1774278000|90|five_hour',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('includes seven_day rateLimitType', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'allowed_warning',
|
|
resetsAt: 1774850400,
|
|
rateLimitType: 'seven_day',
|
|
utilization: 0.85,
|
|
surpassedThreshold: 0.75,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: 'Claude AI usage limit warning|1774850400|85|seven_day',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('handles missing rateLimitType gracefully', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'allowed_warning',
|
|
resetsAt: 1774278000,
|
|
utilization: 0.9,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: 'Claude AI usage limit warning|1774278000|90|',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('converts rejected to existing pipe-delimited reached text', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'rejected',
|
|
resetsAt: 1774278000,
|
|
rateLimitType: 'five_hour',
|
|
overageStatus: 'rejected',
|
|
isUsingOverage: false,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: 'Claude AI usage limit reached|1774278000|five_hour',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('suppresses allowed status', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'allowed',
|
|
resetsAt: 1774278000,
|
|
utilization: 0.3,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({ suppress: true });
|
|
});
|
|
|
|
it('suppresses unknown statuses to prevent raw JSON leaking', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'some_future_status',
|
|
resetsAt: 1774278000,
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({ suppress: true });
|
|
});
|
|
|
|
it('handles wrapped { type: "output", data: { ... } } format', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'output',
|
|
data: {
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'allowed_warning',
|
|
resetsAt: 1774278000,
|
|
utilization: 1,
|
|
},
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: 'Claude AI usage limit warning|1774278000|100|',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('suppresses when resetsAt is missing to prevent raw JSON leak', () => {
|
|
const result = parseRateLimitText(JSON.stringify({
|
|
type: 'rate_limit_event',
|
|
rate_limit_info: {
|
|
status: 'rejected',
|
|
},
|
|
}));
|
|
|
|
expect(result).toEqual({ suppress: true });
|
|
});
|
|
});
|