合并 Grok SSO 多 flow 重构

吸收 Grok 注册与 webchat2api SSO Cookie 导出流程,并把 Kiro/Grok 验证码轮询统一到共享 flow 邮件轮询服务。保留 master 上 Kiro 页面恢复、active verify-otp 接管和重注入修复,补齐对应测试适配。
This commit is contained in:
QLHazyCoder
2026-05-23 04:51:41 +08:00
37 changed files with 4364 additions and 852 deletions
@@ -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/
);
});
+158
View File
@@ -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 };
+12
View File
@@ -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', () => {
+14
View File
@@ -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']),
+30
View File
@@ -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({
+122 -1
View File
@@ -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');
});
+185
View File
@@ -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/
);
});
+2
View File
@@ -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', () => {
+1
View File
@@ -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() {}
+62 -2
View File
@@ -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);
});
+33 -1
View File
@@ -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 登录与授权');