Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6289,6 +6289,15 @@
"status",
"report",
"sources",
"progress",
"asyncTaskConversationId",
"widgetSessionId",
"asyncStatus",
"venusMessageType",
"venusStatus",
"waitingForUserUntil",
"planTitle",
"planId",
"url",
"method",
"diagnostics"
Expand Down
106 changes: 105 additions & 1 deletion clis/chatgpt/commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,24 @@ describe('chatgpt browser command registration', () => {
expect.objectContaining({ name: 'timeout', type: 'int', default: 120 }),
expect.objectContaining({ name: 'stable', type: 'int', default: 6 }),
]));
expect(command.columns).toEqual(['conversationId', 'status', 'report', 'sources', 'url', 'method', 'diagnostics']);
expect(command.columns).toEqual([
'conversationId',
'status',
'report',
'sources',
'progress',
'asyncTaskConversationId',
'widgetSessionId',
'asyncStatus',
'venusMessageType',
'venusStatus',
'waitingForUserUntil',
'planTitle',
'planId',
'url',
'method',
'diagnostics',
]);
});

it('does not return a success row when no completed deep research report exists', async () => {
Expand Down Expand Up @@ -178,6 +195,93 @@ describe('chatgpt browser command registration', () => {
.rejects.toBeInstanceOf(EmptyResultError);
});

it('returns structured deep research progress rows without a completed report', async () => {
const command = getRegistry().get('chatgpt/deep-research-result');
const payload = {
conversation_id: 'requested123',
mapping: {
progress_node: {
message: {
metadata: {
chatgpt_sdk: {
widget_state: JSON.stringify({
status: 'waiting_for_user_response_on_plan',
waiting_for_user_response_on_plan_until: '2026-07-02T02:29:48.298274Z',
plan: {
plan_id: 'plan-demo',
title: 'Research plan',
},
}),
response_metadata: {
async_task_conversation_id: 'async-conversation-123',
'openai/widgetSessionId': 'widget-session-123',
'openai/asyncStatus': 7,
venus_message_type: 'initial_loading_message',
},
},
},
},
},
},
};
const page = {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(true),
readNetworkCapture: vi.fn().mockResolvedValue([]),
getCookies: vi.fn().mockResolvedValue([]),
evaluate: vi.fn((script) => {
const s = String(script);
if (s === 'window.location.href') return Promise.resolve('https://chatgpt.com/');
if (s.includes("fetch('/backend-api/conversation/requested123'")) {
return Promise.resolve({
ok: true,
status: 200,
contentType: 'application/json',
text: JSON.stringify(payload),
});
}
if (s.includes("document.querySelectorAll('iframe')")) {
return Promise.resolve({
url: 'https://chatgpt.com/c/requested123',
title: 'ChatGPT',
iframes: [],
deepResearchIframe: null,
});
}
if (s.includes('composerSelectors') && s.includes('hasComposer')) {
return Promise.resolve({
url: 'https://chatgpt.com/c/requested123',
title: 'ChatGPT',
hasComposer: true,
isLoggedIn: true,
hasLoginGate: false,
});
}
if (s.includes('Stop generating') || s.includes('Thinking')) return Promise.resolve(false);
return Promise.resolve(undefined);
}),
};

await expect(command.func(page, { id: 'requested123' })).resolves.toEqual([
expect.objectContaining({
conversationId: 'requested123',
status: 'waiting_for_user',
report: '',
sources: [],
asyncTaskConversationId: 'async-conversation-123',
widgetSessionId: 'widget-session-123',
asyncStatus: 7,
venusMessageType: 'initial_loading_message',
venusStatus: 'waiting_for_user_response_on_plan',
waitingForUserUntil: '2026-07-02T02:29:48.298274Z',
planTitle: 'Research plan',
planId: 'plan-demo',
method: 'conversation-widget-progress',
}),
]);
});

it('typed-fails malformed deep research source rows instead of falling back to empty success', async () => {
const command = getRegistry().get('chatgpt/deep-research-result');
const report = `# Executive Summary\n\n${'Completed Deep Research report paragraph with enough detail to pass extraction heuristics. '.repeat(12)}\n\n## Sources`;
Expand Down
46 changes: 44 additions & 2 deletions clis/chatgpt/deep-research-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import {
waitForChatGPTDeepResearchResult,
} from './utils.js';

function hasDeepResearchProgress(result) {
return !!result
&& result.status !== 'completed'
&& result.progress
&& typeof result.progress === 'object'
&& !Array.isArray(result.progress)
&& Object.keys(result.progress).length > 0;
}

export const deepResearchResultCommand = cli({
site: 'chatgpt',
name: 'deep-research-result',
Expand All @@ -29,7 +38,24 @@ export const deepResearchResultCommand = cli({
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait when --wait is true' },
{ name: 'stable', type: 'int', default: 6, help: 'Seconds the report text must remain unchanged when --wait is true' },
],
columns: ['conversationId', 'status', 'report', 'sources', 'url', 'method', 'diagnostics'],
columns: [
'conversationId',
'status',
'report',
'sources',
'progress',
'asyncTaskConversationId',
'widgetSessionId',
'asyncStatus',
'venusMessageType',
'venusStatus',
'waitingForUserUntil',
'planTitle',
'planId',
'url',
'method',
'diagnostics',
],
func: async (page, kwargs) => {
const id = parseChatGPTConversationId(kwargs.id);
const shouldWait = normalizeBooleanFlag(kwargs.wait, false);
Expand Down Expand Up @@ -59,7 +85,14 @@ export const deepResearchResultCommand = cli({
? await waitForChatGPTDeepResearchResult(page, { conversationId: id, timeoutSeconds: timeout, stableSeconds })
: await getChatGPTDeepResearchResult(page, { conversationId: id, useBridgeProbes: true });

if (result.status !== 'completed' || !result.report) {
if (result.status !== 'completed' && !hasDeepResearchProgress(result)) {
throw new EmptyResultError(
'chatgpt deep-research-result',
`No completed Deep Research report was found for conversation ${id}.`,
);
}

if (result.status === 'completed' && !result.report) {
throw new EmptyResultError(
'chatgpt deep-research-result',
`No completed Deep Research report was found for conversation ${id}.`,
Expand All @@ -71,6 +104,15 @@ export const deepResearchResultCommand = cli({
status: result.status,
report: result.report || '',
sources: result.sources || [],
progress: result.progress || {},
asyncTaskConversationId: result.asyncTaskConversationId || '',
widgetSessionId: result.widgetSessionId || '',
asyncStatus: result.asyncStatus ?? '',
venusMessageType: result.venusMessageType || '',
venusStatus: result.venusStatus || '',
waitingForUserUntil: result.waitingForUserUntil || '',
planTitle: result.planTitle || '',
planId: result.planId || '',
url: result.url || targetUrl,
method: result.method || '',
diagnostics: result.diagnostics || {},
Expand Down
Loading
Loading