合并 Grok SSO 多 flow 重构
吸收 Grok 注册与 webchat2api SSO Cookie 导出流程,并把 Kiro/Grok 验证码轮询统一到共享 flow 邮件轮询服务。保留 master 上 Kiro 页面恢复、active verify-otp 接管和重注入修复,补齐对应测试适配。
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadFlowMailPollingApi() {
|
||||
const source = fs.readFileSync('background/flow-mail-polling.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${source}; return self;`)(globalScope);
|
||||
return globalScope.MultiPageBackgroundFlowMailPolling;
|
||||
}
|
||||
|
||||
test('flow mail polling service dispatches API mail providers through shared helper', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
const logs = [];
|
||||
let buildCall = null;
|
||||
let hotmailCall = null;
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async (message, level, options) => {
|
||||
logs.push({ message, level, options });
|
||||
},
|
||||
buildVerificationPollPayloadForNode: (nodeId, state, overrides) => {
|
||||
buildCall = { nodeId, state, overrides };
|
||||
return {
|
||||
flowId: state.activeFlowId,
|
||||
nodeId,
|
||||
step: 4,
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 2,
|
||||
intervalMs: 100,
|
||||
...overrides,
|
||||
};
|
||||
},
|
||||
getMailConfig: () => ({ provider: 'hotmail-api', label: 'Hotmail' }),
|
||||
pollHotmailVerificationCode: async (step, state, payload) => {
|
||||
hotmailCall = { step, state, payload };
|
||||
return { code: '123456', emailTimestamp: 456 };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: { activeFlowId: 'kiro', email: 'user@example.com' },
|
||||
step: 4,
|
||||
filterAfterTimestamp: 123,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
assert.equal(buildCall.nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(buildCall.overrides.filterAfterTimestamp, 123);
|
||||
assert.equal(hotmailCall.step, 4);
|
||||
assert.equal(hotmailCall.payload.filterAfterTimestamp, 123);
|
||||
assert.equal(logs.some((entry) => entry.message.includes('Hotmail')), true);
|
||||
});
|
||||
|
||||
test('flow mail polling service prepares browser mail provider sessions and payload timeouts', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
let ensured2925 = null;
|
||||
let mailMessage = null;
|
||||
let mailOptions = null;
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayloadForNode: () => ({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
step: 4,
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 2,
|
||||
intervalMs: 100,
|
||||
}),
|
||||
ensureMail2925MailboxSession: async (options) => {
|
||||
ensured2925 = options;
|
||||
},
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
label: '2925 邮箱',
|
||||
}),
|
||||
sendToMailContentScriptResilient: async (_mail, message, options) => {
|
||||
mailMessage = message;
|
||||
mailOptions = options;
|
||||
return { code: '654321', emailTimestamp: 789 };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: {
|
||||
activeFlowId: 'kiro',
|
||||
currentMail2925AccountId: 'acct-1',
|
||||
mail2925UseAccountPool: true,
|
||||
mail2925Accounts: [
|
||||
{ id: 'acct-1', email: 'pool@example.com' },
|
||||
],
|
||||
},
|
||||
step: 4,
|
||||
logStepKey: 'kiro-submit-verification-code',
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.equal(ensured2925.accountId, 'acct-1');
|
||||
assert.equal(ensured2925.expectedMailboxEmail, 'pool@example.com');
|
||||
assert.equal(mailMessage.type, 'POLL_EMAIL');
|
||||
assert.equal(mailMessage.payload.targetEmail, 'user@example.com');
|
||||
assert.equal(mailOptions.logStepKey, 'kiro-submit-verification-code');
|
||||
assert.equal(mailOptions.responseTimeoutMs, 45000);
|
||||
});
|
||||
|
||||
test('flow mail polling service lets 2925 limit errors flow through shared recovery', async () => {
|
||||
const api = loadFlowMailPollingApi();
|
||||
const limitError = new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限');
|
||||
const recoveredError = new Error('switched-account');
|
||||
const service = api.createFlowMailPollingService({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayloadForNode: () => ({
|
||||
step: 4,
|
||||
maxAttempts: 1,
|
||||
intervalMs: 100,
|
||||
}),
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
source: 'mail-2925',
|
||||
label: '2925 邮箱',
|
||||
}),
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
isMail2925LimitReachedError: (error) => error === limitError,
|
||||
handleMail2925LimitReachedError: async (step, error) => {
|
||||
assert.equal(step, 4);
|
||||
assert.equal(error, limitError);
|
||||
return recoveredError;
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => {
|
||||
throw limitError;
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => service.pollFlowVerificationCode({
|
||||
flowId: 'kiro',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
state: { activeFlowId: 'kiro' },
|
||||
step: 4,
|
||||
}),
|
||||
/switched-account/
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGrokStateApi() {
|
||||
const source = fs.readFileSync('flows/grok/background/state.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundGrokState;`)(globalScope);
|
||||
}
|
||||
|
||||
test('grok state view projects canonical runtime state into legacy flat read fields', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const view = api.buildStateView({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
openai: {
|
||||
preserved: true,
|
||||
},
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 42,
|
||||
pageState: 'profile_entry',
|
||||
pageUrl: 'https://accounts.x.ai/sign-up',
|
||||
},
|
||||
register: {
|
||||
email: 'USER@EXAMPLE.COM',
|
||||
firstName: 'Ada',
|
||||
lastName: 'Lovelace',
|
||||
password: 'Secret123!',
|
||||
verificationRequestedAt: 1000,
|
||||
verificationCode: 'ABC123',
|
||||
status: 'verified',
|
||||
completedAt: 2000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a', 'cookie-b', 'cookie-a'],
|
||||
extractedAt: 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(view.grokRegisterTabId, 42);
|
||||
assert.equal(view.grokPageState, 'profile_entry');
|
||||
assert.equal(view.grokEmail, 'user@example.com');
|
||||
assert.equal(view.grokFirstName, 'Ada');
|
||||
assert.equal(view.grokLastName, 'Lovelace');
|
||||
assert.equal(view.grokPassword, 'Secret123!');
|
||||
assert.equal(view.grokVerificationRequestedAt, 1000);
|
||||
assert.equal(view.grokVerificationCode, 'ABC123');
|
||||
assert.equal(view.grokRegisterStatus, 'verified');
|
||||
assert.equal(view.grokCompletedAt, 2000);
|
||||
assert.equal(view.grokSsoCookie, 'cookie-a');
|
||||
assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']);
|
||||
assert.equal(view.grokSsoExtractedAt, 3000);
|
||||
assert.equal(view.runtimeState.flowState.openai.preserved, true);
|
||||
assert.equal(view.runtimeState.flowState.grok.register.email, 'user@example.com');
|
||||
assert.equal(view.flowState.grok.sso.currentCookie, 'cookie-a');
|
||||
assert.equal(view.flows.grok.sso.cookies.length, 2);
|
||||
});
|
||||
|
||||
test('grok completion payloads update canonical runtime state and flat compatibility fields', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const patch = api.applyNodeCompletionPayload({}, {
|
||||
grokEmail: 'GROK@EXAMPLE.COM',
|
||||
grokVerificationRequestedAt: 123,
|
||||
grokSsoCookie: 'cookie-z',
|
||||
grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'],
|
||||
grokCompletedAt: 456,
|
||||
});
|
||||
|
||||
assert.equal(patch.grokEmail, 'grok@example.com');
|
||||
assert.equal(patch.grokVerificationRequestedAt, 123);
|
||||
assert.equal(patch.grokSsoCookie, 'cookie-z');
|
||||
assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']);
|
||||
assert.equal(patch.grokCompletedAt, 456);
|
||||
assert.equal(patch.grokSsoExtractedAt, 456);
|
||||
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
|
||||
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
|
||||
});
|
||||
|
||||
test('grok fresh keep-state reset preserves SSO cookies but clears registration runtime', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const patch = api.buildFreshKeepState({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 42,
|
||||
pageState: 'profile_entry',
|
||||
},
|
||||
register: {
|
||||
email: 'grok@example.com',
|
||||
status: 'completed',
|
||||
completedAt: 1000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a', 'cookie-b'],
|
||||
extractedAt: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(patch.grokRegisterTabId, null);
|
||||
assert.equal(patch.grokPageState, '');
|
||||
assert.equal(patch.grokEmail, '');
|
||||
assert.equal(patch.grokRegisterStatus, '');
|
||||
assert.equal(patch.grokCompletedAt, 0);
|
||||
assert.equal(patch.grokSsoCookie, 'cookie-a');
|
||||
assert.deepEqual(patch.grokSsoCookies, ['cookie-a', 'cookie-b']);
|
||||
assert.equal(patch.grokSsoExtractedAt, 2000);
|
||||
assert.equal(patch.runtimeState.flowState.grok.register.email, '');
|
||||
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-a');
|
||||
});
|
||||
|
||||
test('grok downstream reset clears only the state owned by the restarted tail', () => {
|
||||
const api = loadGrokStateApi();
|
||||
const currentState = {
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 7,
|
||||
pageState: 'signed_in',
|
||||
pageUrl: 'https://grok.com/',
|
||||
lastError: 'old-error',
|
||||
},
|
||||
register: {
|
||||
email: 'grok@example.com',
|
||||
status: 'completed',
|
||||
completedAt: 1000,
|
||||
},
|
||||
sso: {
|
||||
currentCookie: 'cookie-a',
|
||||
cookies: ['cookie-a'],
|
||||
extractedAt: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const profilePatch = api.buildDownstreamResetPatch('grok-submit-profile', currentState);
|
||||
assert.equal(profilePatch.grokEmail, 'grok@example.com');
|
||||
assert.equal(profilePatch.grokRegisterStatus, 'completed');
|
||||
assert.equal(profilePatch.grokSsoCookie, '');
|
||||
assert.deepEqual(profilePatch.grokSsoCookies, []);
|
||||
|
||||
const emailPatch = api.buildDownstreamResetPatch('grok-submit-email', currentState);
|
||||
assert.equal(emailPatch.grokEmail, '');
|
||||
assert.equal(emailPatch.grokRegisterStatus, '');
|
||||
assert.equal(emailPatch.grokRegisterTabId, 7);
|
||||
});
|
||||
@@ -91,6 +91,18 @@ test('kiro desktop authorize runner uses a shared 3-minute page-load timeout bud
|
||||
assert.match(source, /finalizeDesktopAuthorizeCallback/);
|
||||
});
|
||||
|
||||
test('kiro desktop authorize runner delegates OTP mail polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/desktop-authorize-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildDesktopOtpPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('background wires the Kiro register injector into desktop authorization runner', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
|
||||
|
||||
@@ -37,6 +37,18 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', ()
|
||||
assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/);
|
||||
});
|
||||
|
||||
test('kiro register runner delegates verification mail polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildKiroVerificationPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
|
||||
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
|
||||
@@ -231,8 +243,8 @@ test('kiro verification polling uses the registration email field instead of pag
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 103,
|
||||
isTabAlive: async () => true,
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '123456', emailTimestamp: 2000, mailId: 'mail-1' };
|
||||
},
|
||||
sendToContentScriptResilient: async (_sourceId, message) => {
|
||||
@@ -266,8 +278,11 @@ test('kiro verification polling uses the registration email field instead of pag
|
||||
});
|
||||
|
||||
assert.equal(pollPayloads.length, 1);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'skater-twine-carve@duck.com');
|
||||
assert.deepEqual(pollPayloads[0].targetEmailHints, ['skater-twine-carve@duck.com']);
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].filterAfterTimestamp, 1000);
|
||||
assert.equal(pollPayloads[0].state.email, 'skater-twine-carve@duck.com');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'skater-twine-carve@duck.com');
|
||||
assert.equal(sentMessages.some((message) => (
|
||||
message.type === 'EXECUTE_NODE'
|
||||
&& message.nodeId === 'kiro-submit-verification-code'
|
||||
@@ -315,8 +330,8 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-active' };
|
||||
},
|
||||
registerTab: async (source, tabId) => {
|
||||
@@ -355,7 +370,10 @@ test('kiro verification step can adopt the active AWS verify-otp page without st
|
||||
|
||||
assert.deepEqual(registeredTabs, [{ source: 'kiro-register-page', tabId: 301 }]);
|
||||
assert.equal(getKiroRuntime(statePatches[0]).session?.registerTabId, 301);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(sentMessages.some((message) => (
|
||||
message.type === 'EXECUTE_NODE'
|
||||
&& message.nodeId === 'kiro-submit-verification-code'
|
||||
@@ -419,8 +437,8 @@ test('kiro verification step reinjects the register driver when only the generic
|
||||
'content/utils.js',
|
||||
'flows/kiro/content/register-page.js',
|
||||
],
|
||||
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
|
||||
pollPayloads.push(payload);
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
pollPayloads.push(options);
|
||||
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-reinject' };
|
||||
},
|
||||
sendToContentScriptResilient: async (_sourceId, message) => {
|
||||
@@ -464,7 +482,10 @@ test('kiro verification step reinjects the register driver when only the generic
|
||||
'content/utils.js',
|
||||
'flows/kiro/content/register-page.js',
|
||||
]);
|
||||
assert.equal(pollPayloads[0].targetEmail, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(pollPayloads[0].flowId, 'kiro');
|
||||
assert.equal(pollPayloads[0].nodeId, 'kiro-submit-verification-code');
|
||||
assert.equal(pollPayloads[0].state.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(pollPayloads[0].state).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
assert.equal(getKiroRuntime(completedPayload).register?.email, 'tmp3x58ft2ivc@edu.email.qlhazycoder.tech');
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports mail rule registry and OpenAI mail rules modules', () => {
|
||||
test('background imports mail rule registry and flow mail rules modules', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/mail-rule-registry\.js/);
|
||||
assert.match(source, /flows\/openai\/mail-rules\.js/);
|
||||
assert.match(source, /flows\/kiro\/mail-rules\.js/);
|
||||
assert.match(source, /flows\/grok\/mail-rules\.js/);
|
||||
assert.match(source, /background\/flow-mail-polling\.js/);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
|
||||
@@ -133,3 +136,185 @@ test('mail rule registry rejects unknown active flow ids instead of silently usi
|
||||
/未找到 flow=site-a 的邮件轮询规则构造器/
|
||||
);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes Kiro AWS verification poll payloads by node', () => {
|
||||
const stateSource = fs.readFileSync('flows/kiro/background/state.js', 'utf8');
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const kiroSource = fs.readFileSync('flows/kiro/mail-rules.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${stateSource}; ${registrySource}; ${kiroSource}; return self;`)(globalScope);
|
||||
|
||||
const kiroMailRules = globalScope.MultiPageKiroMailRules.createKiroMailRules({
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
|
||||
});
|
||||
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {
|
||||
kiro: kiroMailRules,
|
||||
},
|
||||
});
|
||||
const baseState = {
|
||||
activeFlowId: 'kiro',
|
||||
mailProvider: '2925',
|
||||
mail2925Mode: 'receive',
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
kiro: {
|
||||
register: {
|
||||
email: 'kiro-user@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayloadForNode(
|
||||
'kiro-submit-verification-code',
|
||||
baseState,
|
||||
{ filterAfterTimestamp: 12345 }
|
||||
),
|
||||
{
|
||||
flowId: 'kiro',
|
||||
ruleId: 'kiro-submit-verification-code',
|
||||
nodeId: 'kiro-submit-verification-code',
|
||||
step: 4,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '^\\s*(\\d{6})\\s*$',
|
||||
flags: 'gm',
|
||||
},
|
||||
{
|
||||
source: '>\\s*(\\d{6})\\s*<',
|
||||
flags: 'g',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 12345,
|
||||
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
|
||||
senderFilters: [
|
||||
'no-reply@signin.aws',
|
||||
'no-reply@login.awsapps.com',
|
||||
'noreply@amazon.com',
|
||||
'account-update@amazon.com',
|
||||
'no-reply@aws.amazon.com',
|
||||
'noreply@aws.amazon.com',
|
||||
'aws',
|
||||
],
|
||||
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
|
||||
targetEmail: 'kiro-user@example.com',
|
||||
targetEmailHints: ['kiro-user@example.com'],
|
||||
mail2925MatchTargetEmail: true,
|
||||
maxAttempts: 17,
|
||||
intervalMs: 16000,
|
||||
}
|
||||
);
|
||||
|
||||
const desktopPayload = registry.buildVerificationPollPayloadForNode(
|
||||
'kiro-complete-desktop-authorize',
|
||||
{
|
||||
...baseState,
|
||||
mailProvider: 'luckmail-api',
|
||||
mail2925Mode: 'provide',
|
||||
}
|
||||
);
|
||||
assert.equal(desktopPayload.ruleId, 'kiro-complete-desktop-authorize');
|
||||
assert.equal(desktopPayload.step, 8);
|
||||
assert.equal(desktopPayload.mail2925MatchTargetEmail, false);
|
||||
assert.equal(desktopPayload.maxAttempts, 3);
|
||||
assert.equal(desktopPayload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
test('mail rule registry exposes Grok xAI verification poll payloads by node', () => {
|
||||
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
|
||||
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
|
||||
const grokSource = fs.readFileSync('flows/grok/mail-rules.js', 'utf8');
|
||||
const globalScope = {};
|
||||
new Function('self', `${stateSource}; ${registrySource}; ${grokSource}; return self;`)(globalScope);
|
||||
|
||||
const grokMailRules = globalScope.MultiPageGrokMailRules.createGrokMailRules({
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
|
||||
});
|
||||
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
|
||||
defaultFlowId: 'openai',
|
||||
flowBuilders: {
|
||||
grok: grokMailRules,
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
registry.buildVerificationPollPayloadForNode(
|
||||
'grok-submit-verification-code',
|
||||
{
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: '2925',
|
||||
mail2925Mode: 'receive',
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
register: {
|
||||
email: 'grok-user@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ filterAfterTimestamp: 12345 }
|
||||
),
|
||||
{
|
||||
flowId: 'grok',
|
||||
ruleId: 'grok-submit-verification-code',
|
||||
nodeId: 'grok-submit-verification-code',
|
||||
step: 3,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[::\\s]*(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?:验证码|代码|确认码)[::\\s为]+(\\d{6})',
|
||||
flags: 'gi',
|
||||
},
|
||||
{
|
||||
source: '(?<!#)\\b(\\d{6})\\b',
|
||||
flags: 'g',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 12345,
|
||||
requiredKeywords: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
|
||||
senderFilters: ['x.ai', 'xai', 'grok'],
|
||||
subjectFilters: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
|
||||
targetEmail: 'grok-user@example.com',
|
||||
targetEmailHints: ['grok-user@example.com'],
|
||||
mail2925MatchTargetEmail: true,
|
||||
maxAttempts: 17,
|
||||
intervalMs: 16000,
|
||||
}
|
||||
);
|
||||
|
||||
const luckmailPayload = registry.buildVerificationPollPayloadForNode(
|
||||
'grok-submit-verification-code',
|
||||
{
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: 'luckmail-api',
|
||||
mail2925Mode: 'provide',
|
||||
grokEmail: 'fallback@example.com',
|
||||
}
|
||||
);
|
||||
assert.equal(luckmailPayload.mail2925MatchTargetEmail, false);
|
||||
assert.equal(luckmailPayload.maxAttempts, 3);
|
||||
assert.equal(luckmailPayload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
@@ -625,6 +625,47 @@ test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow'
|
||||
});
|
||||
});
|
||||
|
||||
test('CLEAR_GROK_SSO_COOKIES delegates canonical runtime cleanup to background', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
let called = false;
|
||||
const router = api.createMessageRouter({
|
||||
clearGrokSsoCookies: async () => {
|
||||
called = true;
|
||||
return {
|
||||
ok: true,
|
||||
state: {
|
||||
grokSsoCookie: '',
|
||||
grokSsoCookies: [],
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
sso: {
|
||||
currentCookie: '',
|
||||
cookies: [],
|
||||
extractedAt: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'CLEAR_GROK_SSO_COOKIES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
assert.equal(called, true);
|
||||
assert.equal(response.ok, true);
|
||||
assert.deepStrictEqual(response.state.grokSsoCookies, []);
|
||||
assert.equal(response.state.runtimeState.flowState.grok.sso.currentCookie, '');
|
||||
});
|
||||
|
||||
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
|
||||
@@ -17,11 +17,14 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
|
||||
assert.match(source, /flows\/kiro\/background\/desktop-client\.js/);
|
||||
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/);
|
||||
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/state\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
|
||||
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
|
||||
|
||||
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
|
||||
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
|
||||
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
|
||||
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
|
||||
|
||||
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
|
||||
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
|
||||
@@ -32,11 +35,20 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
|
||||
assert.match(source, /'kiro-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
|
||||
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/);
|
||||
assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/);
|
||||
assert.match(source, /'grok-open-signup-page': \(state\) => grokRegisterRunner\.executeGrokOpenSignupPage\(state\)/);
|
||||
assert.match(source, /'grok-submit-email': \(state\) => grokRegisterRunner\.executeGrokSubmitEmail\(state\)/);
|
||||
assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/);
|
||||
assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/);
|
||||
assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie'/
|
||||
);
|
||||
});
|
||||
|
||||
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
|
||||
|
||||
@@ -69,9 +69,23 @@ test('contribution registry resolves the combined tutorial entry per flow', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('contribution registry keeps Grok SSO export flow out of published contribution checks', () => {
|
||||
const api = loadApi();
|
||||
|
||||
assert.equal(api.getContributionTutorialEntry('grok'), null);
|
||||
assert.deepEqual(api.getContributionAdapterIds('grok'), []);
|
||||
assert.deepEqual(api.getPublishedContributionFlowIds(['openai', 'kiro', 'grok']), ['openai', 'kiro']);
|
||||
assert.throws(
|
||||
() => api.assertPublishedFlowsHaveContributionAdapters(['grok']),
|
||||
/缺少账号贡献适配器:grok/
|
||||
);
|
||||
});
|
||||
|
||||
test('contribution registry fails fast when a published flow has no adapter', () => {
|
||||
const api = loadApi();
|
||||
|
||||
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
|
||||
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
|
||||
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
|
||||
assert.throws(
|
||||
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
|
||||
|
||||
@@ -98,6 +98,36 @@ test('flow capability registry exposes Kiro as an independent flow with its own
|
||||
);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes Grok as an independent SSO flow without OpenAI-only modes', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'grok',
|
||||
targetId: 'webchat2api',
|
||||
signupMethod: 'phone',
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(capabilityState.activeFlowId, 'grok');
|
||||
assert.equal(capabilityState.canShowPhoneSettings, false);
|
||||
assert.equal(capabilityState.canShowPlusSettings, false);
|
||||
assert.equal(capabilityState.canShowContributionMode, false);
|
||||
assert.equal(capabilityState.canShowLuckmail, false);
|
||||
assert.equal(capabilityState.effectiveSignupMethod, 'email');
|
||||
assert.equal(capabilityState.effectiveTargetId, 'webchat2api');
|
||||
assert.deepEqual(capabilityState.supportedTargetIds, ['webchat2api']);
|
||||
assert.deepEqual(capabilityState.flowCapabilities.contributionAdapterIds, []);
|
||||
assert.deepEqual(
|
||||
capabilityState.visibleGroupIds,
|
||||
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes shared auto-run validation for phone locks and target support', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry({
|
||||
|
||||
@@ -16,12 +16,14 @@ function loadApis() {
|
||||
test('flow registry exposes canonical flow and target metadata', () => {
|
||||
const { flowRegistry } = loadApis();
|
||||
|
||||
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
|
||||
assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
|
||||
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
|
||||
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
|
||||
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
|
||||
assert.equal(flowRegistry.normalizeTargetId('openai', 'sub2api'), 'sub2api');
|
||||
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
|
||||
assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api');
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('openai', 'cpa'),
|
||||
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
|
||||
@@ -30,10 +32,18 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
|
||||
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getVisibleGroupIds('grok', 'webchat2api'),
|
||||
['grok-runtime-status', 'grok-target-webchat2api', 'service-account', 'service-email', 'service-proxy']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
|
||||
['cpa', 'sub2api', 'codex2api']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
|
||||
['webchat2api']
|
||||
);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
|
||||
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
|
||||
@@ -41,6 +51,8 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
assert.equal(flowRegistry.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
|
||||
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
|
||||
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
|
||||
assert.equal(flowRegistry.getFlowCapabilities('grok').supportsAccountContribution, false);
|
||||
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').supportedTargetIds, ['webchat2api']);
|
||||
assert.deepEqual(
|
||||
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
|
||||
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
|
||||
@@ -49,6 +61,7 @@ test('flow registry exposes canonical flow and target metadata', () => {
|
||||
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
|
||||
['kiro-builder-id']
|
||||
);
|
||||
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
|
||||
});
|
||||
|
||||
test('settings schema normalizes view input into canonical nested namespaces', () => {
|
||||
@@ -68,6 +81,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
stepExecutionRangeByFlow: {
|
||||
openai: { enabled: true, fromStep: 2, toStep: 9 },
|
||||
kiro: { enabled: true, fromStep: 1, toStep: 9 },
|
||||
grok: { enabled: true, fromStep: 2, toStep: 4 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,6 +92,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
assert.equal(normalized.flows.openai.selectedTargetId, 'cpa');
|
||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
|
||||
assert.equal(normalized.flows.grok.selectedTargetId, 'webchat2api');
|
||||
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
|
||||
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
|
||||
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
|
||||
@@ -85,6 +100,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
|
||||
fromStep: 1,
|
||||
toStep: 9,
|
||||
});
|
||||
assert.deepEqual(normalized.flows.grok.autoRun.stepExecutionRange, {
|
||||
enabled: true,
|
||||
fromStep: 2,
|
||||
toStep: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('settings schema lets explicit flat step range override stale canonical range', () => {
|
||||
@@ -130,6 +150,11 @@ test('settings schema can project canonical state into a read view without legac
|
||||
assert.equal(view.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(view.settingsSchemaVersion, 5);
|
||||
assert.equal(view.settingsState.activeFlowId, 'kiro');
|
||||
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
|
||||
enabled: false,
|
||||
fromStep: 1,
|
||||
toStep: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('settings schema preserves CPA session strategy in canonical state and read view', () => {
|
||||
@@ -143,3 +168,99 @@ test('settings schema preserves CPA session strategy in canonical state and read
|
||||
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('settings schema preserves registered custom flow settings without openai/kiro hardcoding', () => {
|
||||
const { settingsSchema } = loadApis();
|
||||
const customFlowRegistry = {
|
||||
DEFAULT_FLOW_ID: 'openai',
|
||||
getRegisteredFlowIds: () => ['openai', 'kiro', 'sample'],
|
||||
getDefaultTargetId(flowId) {
|
||||
return flowId === 'sample' ? 'sample-target' : (flowId === 'kiro' ? 'kiro-rs' : 'cpa');
|
||||
},
|
||||
getFlowDefinition(flowId) {
|
||||
if (flowId !== 'sample') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: 'sample',
|
||||
defaultTargetId: 'sample-target',
|
||||
settingsDefaults: {
|
||||
targets: {
|
||||
'sample-target': {
|
||||
endpoint: 'https://sample.example.com',
|
||||
},
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 3 },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getTargetDefinitions(flowId) {
|
||||
if (flowId === 'sample') {
|
||||
return {
|
||||
'sample-target': { id: 'sample-target', label: 'Sample Target' },
|
||||
};
|
||||
}
|
||||
if (flowId === 'kiro') {
|
||||
return {
|
||||
'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs' },
|
||||
};
|
||||
}
|
||||
return {
|
||||
cpa: { id: 'cpa', label: 'CPA' },
|
||||
sub2api: { id: 'sub2api', label: 'SUB2API' },
|
||||
codex2api: { id: 'codex2api', label: 'Codex2API' },
|
||||
};
|
||||
},
|
||||
normalizeFlowId(value = '', fallback = 'openai') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return ['openai', 'kiro', 'sample'].includes(normalized)
|
||||
? normalized
|
||||
: (['openai', 'kiro', 'sample'].includes(fallback) ? fallback : 'openai');
|
||||
},
|
||||
normalizeTargetId(flowId, targetId = '', fallback = '') {
|
||||
const targets = Object.keys(customFlowRegistry.getTargetDefinitions(flowId));
|
||||
const normalized = String(targetId || '').trim().toLowerCase();
|
||||
if (targets.includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
if (targets.includes(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
return customFlowRegistry.getDefaultTargetId(flowId);
|
||||
},
|
||||
};
|
||||
const schema = settingsSchema.createSettingsSchema({ flowRegistry: customFlowRegistry });
|
||||
|
||||
const normalized = schema.normalizeSettingsState({
|
||||
activeFlowId: 'sample',
|
||||
targetId: 'sample-target',
|
||||
settingsState: {
|
||||
flows: {
|
||||
sample: {
|
||||
selectedTargetId: 'sample-target',
|
||||
targets: {
|
||||
'sample-target': {
|
||||
endpoint: 'https://custom.example.com',
|
||||
},
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: { enabled: true, fromStep: 2, toStep: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const view = schema.buildSettingsView(normalized);
|
||||
|
||||
assert.equal(normalized.activeFlowId, 'sample');
|
||||
assert.equal(normalized.flows.sample.selectedTargetId, 'sample-target');
|
||||
assert.equal(normalized.flows.sample.targets['sample-target'].endpoint, 'https://custom.example.com');
|
||||
assert.deepEqual(view.stepExecutionRangeByFlow.sample, {
|
||||
enabled: true,
|
||||
fromStep: 2,
|
||||
toStep: 3,
|
||||
});
|
||||
assert.equal(schema.getSelectedTargetId(normalized, 'sample'), 'sample-target');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGrokRunnerApi() {
|
||||
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundGrokRegisterRunner;`)(globalScope);
|
||||
}
|
||||
|
||||
function getGrokRuntime(state = {}) {
|
||||
return state?.runtimeState?.flowState?.grok || {};
|
||||
}
|
||||
|
||||
test('grok runner delegates verification polling to the shared flow mail service', () => {
|
||||
const source = fs.readFileSync('flows/grok/background/register-runner.js', 'utf8');
|
||||
assert.match(source, /pollFlowVerificationCode/);
|
||||
assert.doesNotMatch(source, /buildGrokVerificationPollPayload/);
|
||||
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
|
||||
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
|
||||
});
|
||||
|
||||
test('grok content script does not patch global MouseEvent prototypes', () => {
|
||||
const source = fs.readFileSync('flows/grok/content/register-page.js', 'utf8');
|
||||
assert.doesNotMatch(source, /MouseEvent\.prototype/);
|
||||
assert.doesNotMatch(source, /Object\.defineProperty\(MouseEvent/);
|
||||
assert.match(source, /screenX:/);
|
||||
assert.match(source, /screenY:/);
|
||||
});
|
||||
|
||||
test('grok verification runner polls by flow node and submits normalized code', async () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
const calls = [];
|
||||
let completedPayload = null;
|
||||
let currentState = {
|
||||
activeFlowId: 'grok',
|
||||
mailProvider: '2925',
|
||||
grokRegisterTabId: 101,
|
||||
grokEmail: 'grok-user@example.com',
|
||||
grokVerificationRequestedAt: 1000000,
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 101,
|
||||
},
|
||||
register: {
|
||||
email: 'grok-user@example.com',
|
||||
verificationRequestedAt: 1000000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runner = api.createGrokRegisterRunner({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async (tabId) => ({ id: tabId }),
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (_nodeId, payload) => {
|
||||
completedPayload = payload;
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 101,
|
||||
isTabAlive: async () => true,
|
||||
pollFlowVerificationCode: async (options) => {
|
||||
calls.push({ type: 'poll', options });
|
||||
return { code: 'ABC-123', messageId: 'mail-001' };
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendToContentScriptResilient: async (sourceId, message) => {
|
||||
calls.push({ type: 'send', sourceId, message });
|
||||
return { submitted: true, state: 'profile_entry', url: 'https://accounts.x.ai/profile' };
|
||||
},
|
||||
setState: async (patch) => {
|
||||
currentState = { ...currentState, ...patch };
|
||||
},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await runner.executeGrokSubmitVerificationCode({ nodeId: 'grok-submit-verification-code', ...currentState });
|
||||
|
||||
const pollCall = calls.find((entry) => entry.type === 'poll');
|
||||
assert.equal(pollCall.options.flowId, 'grok');
|
||||
assert.equal(pollCall.options.nodeId, 'grok-submit-verification-code');
|
||||
assert.equal(pollCall.options.step, 3);
|
||||
assert.equal(pollCall.options.logStep, 3);
|
||||
assert.equal(pollCall.options.filterAfterTimestamp, 400000);
|
||||
assert.equal(pollCall.options.state.activeFlowId, 'grok');
|
||||
assert.equal(pollCall.options.state.visibleStep, 3);
|
||||
|
||||
const sendCall = calls.find((entry) => entry.type === 'send');
|
||||
assert.equal(sendCall.sourceId, 'grok-register-page');
|
||||
assert.equal(sendCall.message.type, 'EXECUTE_NODE');
|
||||
assert.equal(sendCall.message.nodeId, 'grok-submit-verification-code');
|
||||
assert.deepEqual(sendCall.message.payload, { code: 'ABC123' });
|
||||
|
||||
assert.equal(completedPayload.grokVerificationCode, 'ABC123');
|
||||
assert.equal(completedPayload.grokVerificationRawCode, 'ABC-123');
|
||||
assert.equal(completedPayload.grokVerificationMessageId, 'mail-001');
|
||||
assert.equal(getGrokRuntime(completedPayload).register.verificationCode, 'ABC123');
|
||||
assert.equal(getGrokRuntime(completedPayload).register.status, 'verified');
|
||||
});
|
||||
|
||||
test('grok SSO extraction accumulates unique cookies without logging the secret value', async () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
const logs = [];
|
||||
let completedPayload = null;
|
||||
let markUsedPayload = null;
|
||||
let currentState = {
|
||||
activeFlowId: 'grok',
|
||||
grokRegisterTabId: 202,
|
||||
grokSsoCookies: ['existing-cookie', 'new-cookie'],
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
session: {
|
||||
registerTabId: 202,
|
||||
},
|
||||
sso: {
|
||||
cookies: ['existing-cookie', 'new-cookie'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runner = api.createGrokRegisterRunner({
|
||||
addLog: async (message, level) => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
cookies: {
|
||||
get: async () => ({ value: 'new-cookie' }),
|
||||
},
|
||||
tabs: {
|
||||
get: async (tabId) => ({ id: tabId }),
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (_nodeId, payload) => {
|
||||
completedPayload = payload;
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getState: async () => currentState,
|
||||
getTabId: async () => 202,
|
||||
isTabAlive: async () => true,
|
||||
markCurrentRegistrationAccountUsed: async (state) => {
|
||||
markUsedPayload = state;
|
||||
},
|
||||
registerTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('content fallback should not be used when chrome.cookies finds sso');
|
||||
},
|
||||
setState: async (patch) => {
|
||||
currentState = { ...currentState, ...patch };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabStableComplete: async () => {},
|
||||
});
|
||||
|
||||
await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState });
|
||||
|
||||
assert.equal(completedPayload.grokSsoCookie, 'new-cookie');
|
||||
assert.deepEqual(completedPayload.grokSsoCookies, ['existing-cookie', 'new-cookie']);
|
||||
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
|
||||
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['existing-cookie', 'new-cookie']);
|
||||
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
|
||||
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
|
||||
});
|
||||
|
||||
test('grok register runner requires background node completion dependency', () => {
|
||||
const api = loadGrokRunnerApi();
|
||||
assert.throws(
|
||||
() => api.createGrokRegisterRunner({}),
|
||||
/requires completeNodeFromBackground/
|
||||
);
|
||||
});
|
||||
@@ -3,12 +3,14 @@ const fs = require('node:fs');
|
||||
const FLOW_DEFINITION_FILES = Object.freeze([
|
||||
'flows/openai/index.js',
|
||||
'flows/kiro/index.js',
|
||||
'flows/grok/index.js',
|
||||
'flows/index.js',
|
||||
]);
|
||||
|
||||
const FLOW_WORKFLOW_FILES = Object.freeze([
|
||||
'flows/openai/workflow.js',
|
||||
'flows/kiro/workflow.js',
|
||||
'flows/grok/workflow.js',
|
||||
]);
|
||||
|
||||
function readBundle(files = []) {
|
||||
|
||||
@@ -36,6 +36,7 @@ function extractFunction(source, name) {
|
||||
test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
[
|
||||
'id="select-flow"',
|
||||
'<option value="grok">Grok</option>',
|
||||
'id="label-source-selector"',
|
||||
'id="row-step6-cookie-settings"',
|
||||
'id="row-kiro-rs-url"',
|
||||
@@ -46,9 +47,90 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
|
||||
'id="row-kiro-web-status"',
|
||||
'id="row-kiro-login-url"',
|
||||
'id="row-kiro-upload-status"',
|
||||
'id="row-grok-register-status"',
|
||||
'id="row-grok-sso-status"',
|
||||
'id="row-grok-sso-settings"',
|
||||
'id="btn-copy-grok-sso"',
|
||||
'id="btn-export-grok-sso"',
|
||||
'id="btn-clear-grok-sso"',
|
||||
'<script src="../flows/grok/index.js"></script>',
|
||||
'<script src="../flows/grok/workflow.js"></script>',
|
||||
].forEach((snippet) => {
|
||||
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
});
|
||||
assert.ok(
|
||||
sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>')
|
||||
< sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>')
|
||||
);
|
||||
assert.ok(
|
||||
sidepanelHtml.indexOf('<script src="../flows/grok/workflow.js"></script>')
|
||||
< sidepanelHtml.indexOf('<script src="../flows/index.js"></script>')
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel Grok SSO clear action goes through background message instead of direct storage writes', () => {
|
||||
const clearButtonIndex = sidepanelSource.indexOf("btnClearGrokSso?.addEventListener('click'");
|
||||
assert.notEqual(clearButtonIndex, -1);
|
||||
const nextManagerIndex = sidepanelSource.indexOf('const hotmailManager', clearButtonIndex);
|
||||
assert.notEqual(nextManagerIndex, -1);
|
||||
const block = sidepanelSource.slice(clearButtonIndex, nextManagerIndex);
|
||||
|
||||
assert.match(block, /type:\s*'CLEAR_GROK_SSO_COOKIES'/);
|
||||
assert.match(block, /chrome\.runtime\.sendMessage/);
|
||||
assert.doesNotMatch(block, /chrome\.storage/);
|
||||
assert.doesNotMatch(block, /storage\.local\.set/);
|
||||
});
|
||||
|
||||
test('sidepanel renders Grok SSO status from canonical runtime state', () => {
|
||||
const bundle = [
|
||||
extractFunction(sidepanelSource, 'getGrokRuntimeState'),
|
||||
extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'),
|
||||
extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'),
|
||||
extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = {};
|
||||
const displayGrokRegisterStatus = { textContent: '' };
|
||||
const displayGrokSsoStatus = { textContent: '' };
|
||||
const displayGrokSsoCookie = { textContent: '', title: '' };
|
||||
const buttons = [];
|
||||
const btnCopyGrokSso = { disabled: false };
|
||||
const btnExportGrokSso = { disabled: false };
|
||||
const btnClearGrokSso = { disabled: false };
|
||||
${bundle}
|
||||
return {
|
||||
displayGrokRegisterStatus,
|
||||
displayGrokSsoStatus,
|
||||
displayGrokSsoCookie,
|
||||
btnCopyGrokSso,
|
||||
btnExportGrokSso,
|
||||
btnClearGrokSso,
|
||||
renderGrokRuntimeState,
|
||||
};
|
||||
`)();
|
||||
|
||||
api.renderGrokRuntimeState({
|
||||
runtimeState: {
|
||||
flowState: {
|
||||
grok: {
|
||||
register: { status: 'completed' },
|
||||
sso: {
|
||||
currentCookie: '1234567890abcdef',
|
||||
cookies: ['1234567890abcdef', 'second-cookie'],
|
||||
extractedAt: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
|
||||
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
|
||||
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
|
||||
assert.equal(api.btnCopyGrokSso.disabled, false);
|
||||
assert.equal(api.btnExportGrokSso.disabled, false);
|
||||
assert.equal(api.btnClearGrokSso.disabled, false);
|
||||
});
|
||||
|
||||
test('sidepanel Kiro GitHub button opens the configured fork', () => {
|
||||
|
||||
@@ -525,6 +525,7 @@ function updateAccountRunHistorySettingsUI() {}
|
||||
function updatePhoneVerificationSettingsUI() {}
|
||||
function updatePanelModeUI() {}
|
||||
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
|
||||
function renderGrokRuntimeState() {}
|
||||
function renderSub2ApiGroupOptions() {}
|
||||
function isLuckmailProvider() { return false; }
|
||||
function updateButtonStates() {}
|
||||
|
||||
@@ -15,6 +15,11 @@ test('background imports shared source registry module', () => {
|
||||
assert.match(source, /core\/flow-kernel\/settings-schema\.js/);
|
||||
assert.match(source, /core\/flow-kernel\/source-registry\.js/);
|
||||
assert.match(source, /shared\/kiro-timeouts\.js/);
|
||||
assert.match(source, /flows\/grok\/index\.js/);
|
||||
assert.match(source, /flows\/grok\/workflow\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/state\.js/);
|
||||
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
|
||||
assert.match(source, /flows\/grok\/mail-rules\.js/);
|
||||
});
|
||||
|
||||
test('manifest loads shared source registry before content utils in static bundles', () => {
|
||||
@@ -41,15 +46,38 @@ test('manifest no longer ships a static Kiro content bundle', () => {
|
||||
assert.equal(hasStaticKiroBundle, false);
|
||||
});
|
||||
|
||||
test('manifest loads Grok flow definition in static bundles but not Grok content runtime', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
for (const entry of manifest.content_scripts || []) {
|
||||
const scripts = Array.isArray(entry.js) ? entry.js : [];
|
||||
if (!scripts.includes('flows/index.js')) continue;
|
||||
assert.ok(scripts.includes('flows/kiro/index.js'));
|
||||
assert.ok(scripts.includes('flows/grok/index.js'));
|
||||
assert.ok(
|
||||
scripts.indexOf('flows/kiro/index.js') < scripts.indexOf('flows/grok/index.js'),
|
||||
'Kiro definition should load before Grok definition'
|
||||
);
|
||||
assert.ok(
|
||||
scripts.indexOf('flows/grok/index.js') < scripts.indexOf('flows/index.js'),
|
||||
'Grok definition must load before flows/index.js'
|
||||
);
|
||||
assert.equal(scripts.includes('flows/grok/content/register-page.js'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('background injects shared Kiro timeout module before Kiro content scripts', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(
|
||||
source,
|
||||
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
|
||||
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
|
||||
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const GROK_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'content\/utils\.js', 'flows\/grok\/content\/register-page\.js'\];/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,6 +111,20 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
}),
|
||||
'kiro-desktop-authorize'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://accounts.x.ai/sign-up',
|
||||
hostname: 'accounts.x.ai',
|
||||
}),
|
||||
'grok-register-page'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://grok.com/',
|
||||
hostname: 'grok.com',
|
||||
}),
|
||||
'grok-register-page'
|
||||
);
|
||||
assert.equal(
|
||||
registry.detectSourceFromLocation({
|
||||
url: 'https://example.com/',
|
||||
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'grok-register-page',
|
||||
'https://accounts.x.ai/sign-up',
|
||||
'https://grok.com/'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'grok-register-page',
|
||||
'https://grok.com/',
|
||||
'https://accounts.x.ai/sign-up'
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
registry.matchesSourceUrlFamily(
|
||||
'kiro-desktop-authorize',
|
||||
@@ -137,4 +195,6 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/register-runner', 'kiro-open-register-page'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
|
||||
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
|
||||
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
|
||||
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
|
||||
|
||||
assert.equal(Array.isArray(steps), true);
|
||||
assert.equal(steps.length, 11);
|
||||
@@ -162,8 +163,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
|
||||
assert.equal(api.hasFlow('openai'), true);
|
||||
assert.equal(api.hasFlow('kiro'), true);
|
||||
assert.equal(api.hasFlow('grok'), true);
|
||||
assert.equal(api.hasFlow('site-a'), false);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
|
||||
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
|
||||
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
||||
assert.deepStrictEqual(
|
||||
@@ -210,6 +212,36 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
[],
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
grokSteps.map((step) => step.key),
|
||||
[
|
||||
'grok-open-signup-page',
|
||||
'grok-submit-email',
|
||||
'grok-submit-verification-code',
|
||||
'grok-submit-profile',
|
||||
'grok-extract-sso-cookie',
|
||||
]
|
||||
);
|
||||
assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true);
|
||||
assert.equal(grokSteps[0].driverId, 'flows/grok/background/register-runner');
|
||||
assert.equal(grokSteps[0].sourceId, 'grok-register-page');
|
||||
assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code');
|
||||
assert.deepStrictEqual(
|
||||
grokSteps.map((step) => step.title),
|
||||
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie']
|
||||
);
|
||||
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]);
|
||||
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5);
|
||||
assert.deepStrictEqual(
|
||||
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
|
||||
[
|
||||
['grok-submit-email'],
|
||||
['grok-submit-verification-code'],
|
||||
['grok-submit-profile'],
|
||||
['grok-extract-sso-cookie'],
|
||||
[],
|
||||
]
|
||||
);
|
||||
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user