Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d9ee3a8
fix(cli): Preserve mid-turn image messages
doudouOUC Jun 16, 2026
4edf714
Merge branch 'main' into fix/mid-turn-image-preservation
yiliang114 Jun 16, 2026
50c7576
codex: address PR review feedback (#5183)
doudouOUC Jun 16, 2026
b23bfd2
fix(cli): Harden mid-turn message drain
doudouOUC Jun 16, 2026
25d04fb
fix(cli): Enforce mid-turn resolve timeout
doudouOUC Jun 16, 2026
f188796
fix(cli): Address mid-turn review suggestions
doudouOUC Jun 16, 2026
d030bee
codex: address PR review feedback (#5183)
doudouOUC Jun 16, 2026
f4e2205
codex: address PR review feedback (#5183)
doudouOUC Jun 17, 2026
f12054d
codex: address PR review feedback (#5183)
doudouOUC Jun 17, 2026
e221102
Merge branch 'main' into fix/mid-turn-image-preservation
doudouOUC Jun 17, 2026
3540048
Merge branch 'main' into fix/mid-turn-image-preservation
doudouOUC Jun 17, 2026
8c663de
fix(cli): Address mid-turn review feedback
doudouOUC Jun 17, 2026
869f9f2
fix(cli): Harden mid-turn drain edge cases
doudouOUC Jun 17, 2026
9344672
test(cli): Cover mid-turn drain review cases
doudouOUC Jun 17, 2026
c5d637c
fix(desktop): Remove unused mid-turn attachment argument
doudouOUC Jun 17, 2026
4dd05a6
fix(desktop): Harden mid-turn drain acknowledgements
doudouOUC Jun 17, 2026
e9269af
fix(cli): Skip failed mid-turn at-command injection
doudouOUC Jun 17, 2026
0fae7cf
fix(desktop): Simplify mid-turn drain acknowledgements
doudouOUC Jun 17, 2026
ba634c6
chore: merge main into mid-turn image branch
doudouOUC Jun 17, 2026
df5da5f
fix(desktop): Use stable mid-turn drain ack keys
doudouOUC Jun 17, 2026
b26aa48
fix(cli): Handle mid-turn image drain failures
doudouOUC Jun 17, 2026
7d6a0e3
fix(cli): Bound mid-turn drain fallbacks
doudouOUC Jun 17, 2026
8f6f642
fix: Bound mid-turn attachment handling
doudouOUC Jun 17, 2026
855a268
fix: Harden mid-turn attachment edge cases
doudouOUC Jun 17, 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
189 changes: 186 additions & 3 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2509,7 +2509,8 @@ describe('Session', () => {
mockToolRegistry.getTool.mockReturnValue(tool);
mockConfig.getApprovalMode = vi.fn().mockReturnValue(ApprovalMode.YOLO);
mockClient.extMethod = vi.fn().mockResolvedValue({
messages: ['please also check tests'],
items: [],
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
messages: [' please also check tests '],
});
mockChat.sendMessageStream = vi
.fn()
Expand Down Expand Up @@ -2542,14 +2543,196 @@ describe('Session', () => {
);
const secondCall = vi.mocked(mockChat.sendMessageStream).mock.calls[1];
const midTurnPart = {
text: '\n[User message received during tool execution]: please also check tests',
text: '\n[User message received during tool execution]: please also check tests ',
};
expect(secondCall?.[1].message).toEqual(
expect.arrayContaining([midTurnPart]),
);
expect(
mockChatRecordingService.recordMidTurnUserMessage,
).toHaveBeenCalledWith([midTurnPart], 'please also check tests');
).toHaveBeenCalledWith([midTurnPart], ' please also check tests ');
});

it('injects drained structured mid-turn user messages with images', async () => {
const executeSpy = vi.fn().mockResolvedValue({
llmContent: 'file contents',
returnDisplay: 'file contents',
});
const tool = {
name: 'read_file',
kind: core.Kind.Read,
build: vi.fn().mockReturnValue({
params: { path: '/tmp/test.txt' },
getDefaultPermission: vi.fn().mockResolvedValue('allow'),
getDescription: vi.fn().mockReturnValue('Read file'),
toolLocations: vi.fn().mockReturnValue([]),
execute: executeSpy,
}),
};

mockToolRegistry.getTool.mockReturnValue(tool);
mockConfig.getApprovalMode = vi.fn().mockReturnValue(ApprovalMode.YOLO);
mockClient.extMethod = vi.fn().mockResolvedValue({
items: [
{
content: [
{ type: 'text', text: 'please inspect this image' },
{
type: 'image',
mimeType: 'image/png',
data: 'iVBORw0KGgo=',
},
],
displayText: 'please inspect this image',
},
],
});
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValueOnce(
createStreamWithChunks([
{
type: core.StreamEventType.CHUNK,
value: {
functionCalls: [
{
id: 'call-1',
name: 'read_file',
args: { path: '/tmp/test.txt' },
},
],
},
},
]),
)
.mockResolvedValueOnce(createEmptyStream());

await session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: 'read file' }],
});

const midTurnParts: Part[] = [
{
text: '\n[User message received during tool execution]: please inspect this image',
},
{
inlineData: {
mimeType: 'image/png',
data: 'iVBORw0KGgo=',
},
},
];
const secondCall = vi.mocked(mockChat.sendMessageStream).mock.calls[1];
expect(secondCall?.[1].message).toEqual(
expect.arrayContaining(midTurnParts),
);
expect(
mockChatRecordingService.recordMidTurnUserMessage,
).toHaveBeenCalledWith(midTurnParts, 'please inspect this image');
});

it('rejects mid-turn resource links and keeps valid messages in the same batch', async () => {
const readManyFilesSpy = vi
.spyOn(core, 'readManyFiles')
.mockResolvedValue({
contentParts: 'secret file',
files: [],
});
const executeSpy = vi.fn().mockResolvedValue({
llmContent: 'file contents',
returnDisplay: 'file contents',
});
const tool = {
name: 'read_file',
kind: core.Kind.Read,
build: vi.fn().mockReturnValue({
params: { path: '/tmp/test.txt' },
getDefaultPermission: vi.fn().mockResolvedValue('allow'),
getDescription: vi.fn().mockReturnValue('Read file'),
toolLocations: vi.fn().mockReturnValue([]),
execute: executeSpy,
}),
};

mockToolRegistry.getTool.mockReturnValue(tool);
mockConfig.getApprovalMode = vi.fn().mockReturnValue(ApprovalMode.YOLO);
mockClient.extMethod = vi.fn().mockResolvedValue({
items: [
{
content: [
{ type: 'text', text: 'mixed safe follow-up' },
{
type: 'resource_link',
uri: 'file:///etc/passwd',
name: 'passwd',
},
],
displayText: 'mixed safe follow-up',
},
{
content: [
{
type: 'resource_link',
uri: 'file:///etc/passwd',
name: 'passwd',
},
],
displayText: 'secret file',
},
{
content: [{ type: 'text', text: 'safe follow-up' }],
displayText: 'safe follow-up',
},
],
});
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValueOnce(
createStreamWithChunks([
{
type: core.StreamEventType.CHUNK,
value: {
functionCalls: [
{
id: 'call-1',
name: 'read_file',
args: { path: '/tmp/test.txt' },
},
],
},
},
]),
)
.mockResolvedValueOnce(createEmptyStream());

try {
await session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: 'read file' }],
});

const mixedMidTurnPart = {
text: '\n[User message received during tool execution]: mixed safe follow-up',
};
const midTurnPart = {
text: '\n[User message received during tool execution]: safe follow-up',
};
const secondCall = vi.mocked(mockChat.sendMessageStream).mock
.calls[1];
expect(secondCall?.[1].message).toEqual(
expect.arrayContaining([mixedMidTurnPart, midTurnPart]),
);
expect(readManyFilesSpy).not.toHaveBeenCalled();
expect(
mockChatRecordingService.recordMidTurnUserMessage,
).toHaveBeenCalledWith([mixedMidTurnPart], 'mixed safe follow-up');
expect(
mockChatRecordingService.recordMidTurnUserMessage,
).toHaveBeenCalledWith([midTurnPart], 'safe follow-up');
} finally {
readManyFilesSpy.mockRestore();
}
});

it('latches mid-turn drain off after a permanent (-32601) error', async () => {
Expand Down
Loading
Loading