Files
FlowPilot/tests/step9-oauth-timeout-toggle.test.js
2026-05-21 07:21:34 +08:00

357 lines
10 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const step9ModuleSource = fs.readFileSync('flows/openai/background/steps/confirm-oauth.js', 'utf8');
test('step9 observes disabled oauth timeout while waiting for localhost callback', async () => {
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let step8PendingReject = null;
let cleanupCalls = 0;
let remainingCalls = 0;
let recoveryCalls = 0;
let completePayload = null;
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 25);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
async function addLog() {}
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
function throwIfStep8SettledOrStopped(resolved) {
if (resolved) throw new Error('already resolved');
}
async function getTabId() { return 123; }
async function isTabAlive() { return true; }
async function ensureStep8SignupPageReady() {}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
return 1;
}
return defaultTimeoutMs;
}
async function getOAuthFlowRemainingMs() {
remainingCalls += 1;
return null;
}
async function recoverOAuthLocalhostTimeout() {
recoveryCalls += 1;
throw new Error('should not recover when timeout has been disabled');
}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {}
async function waitForStep8ClickEffect() {
return { progressed: true, reason: 'url_changed', url: 'https://chatgpt.com/' };
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
}
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'URL 已变化'; }
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject(handler) { step8PendingReject = handler; }
async function completeNodeFromBackground(nodeId, payload) {
completePayload = { nodeId, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
const STEP8_MAX_ROUNDS = 1;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
recoverOAuthLocalhostTimeout,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
remainingCalls,
recoveryCalls,
completePayload,
hasPendingReject: Boolean(step8PendingReject),
};
},
};
`)(step9ModuleSource);
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 9,
});
const snapshot = api.snapshot();
assert.equal(snapshot.recoveryCalls, 0);
assert.equal(snapshot.remainingCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.equal(snapshot.hasPendingReject, false);
assert.deepEqual(snapshot.completePayload, {
nodeId: 'confirm-oauth',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
});
test('step9 defers callback timeout while auth security verification is still redirecting', async () => {
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let cleanupCalls = 0;
let deferCalls = 0;
let completePayload = null;
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 25);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
async function addLog() {}
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
function throwIfStep8SettledOrStopped(resolved) {
if (resolved) throw new Error('already resolved');
}
async function getTabId() { return 123; }
async function isTabAlive() { return true; }
async function ensureStep8SignupPageReady() {}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
return 1;
}
return defaultTimeoutMs;
}
async function shouldDeferStep9CallbackTimeout(details = {}) {
deferCalls += 1;
return Number(details.tabId) === 123;
}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {}
async function waitForStep8ClickEffect() {
return {
progressed: true,
reason: 'url_changed',
url: 'https://auth.openai.com/api/oauth/oauth2/auth?client_id=app_test',
};
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
}
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'URL 已变化'; }
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject() {}
async function completeNodeFromBackground(nodeId, payload) {
completePayload = { nodeId, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
const STEP8_MAX_ROUNDS = 1;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeNodeFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
shouldDeferStep9CallbackTimeout,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
deferCalls,
completePayload,
};
},
};
`)(step9ModuleSource);
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 12,
});
const snapshot = api.snapshot();
assert.equal(snapshot.deferCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.deepEqual(snapshot.completePayload, {
nodeId: 'confirm-oauth',
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
});