feat: add flow-aware kiro device auth workflow

This commit is contained in:
QLHazyCoder
2026-05-18 00:34:51 +08:00
parent b3da7c9e7f
commit 0c034c232d
26 changed files with 4085 additions and 383 deletions
@@ -121,6 +121,7 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
@@ -132,6 +133,28 @@ const FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const self = {
MultiPageFlowRegistry: {
DEFAULT_KIRO_REGION: 'us-east-1',
DEFAULT_KIRO_RS_URL: 'https://kiro.leftcode.xyz/admin',
normalizeFlowId(value, fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'kiro') {
return 'kiro';
}
if (normalized === 'codex' || normalized === 'openai') {
return 'openai';
}
return String(fallback || 'openai').trim().toLowerCase() === 'kiro' ? 'kiro' : 'openai';
},
normalizeSourceId(flowId, sourceId, fallback = 'kiro-rs') {
const normalizedFlowId = this.normalizeFlowId(flowId);
if (normalizedFlowId !== 'kiro') {
return 'cpa';
}
const normalizedSourceId = String(sourceId || '').trim().toLowerCase();
return normalizedSourceId === 'kiro-rs' ? normalizedSourceId : fallback;
},
},
GoPayUtils: {
normalizeGoPayCountryCode(value) {
const digits = String(value || '').replace(/\\D/g, '');
@@ -252,6 +275,12 @@ return {
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email');
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'codex'), 'openai');
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'kiro'), 'kiro');
assert.equal(api.normalizePersistentSettingValue('kiroSourceId', 'unknown'), 'kiro-rs');
assert.equal(api.normalizePersistentSettingValue('kiroRsUrl', ''), 'https://kiro.leftcode.xyz/admin');
assert.equal(api.normalizePersistentSettingValue('kiroRegion', ''), 'us-east-1');
assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), ' key-1 ');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
@@ -0,0 +1,328 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadKiroDeviceAuthApi() {
const source = fs.readFileSync('background/steps/kiro-device-auth.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundKiroDeviceAuth;`)({});
}
function createResponse({ ok = true, status = 200, json = null, text = '' } = {}) {
const bodyText = text || (json ? JSON.stringify(json) : '');
return {
ok,
status,
statusText: bodyText || `HTTP ${status}`,
async text() {
return bodyText;
},
};
}
function mergeUpdates(updatesList = []) {
return updatesList.reduce((acc, item) => Object.assign(acc, item), {});
}
test('kiro device auth module exposes a factory', () => {
const api = loadKiroDeviceAuthApi();
assert.equal(typeof api?.createKiroDeviceAuthExecutor, 'function');
assert.equal(typeof api?.startBuilderIdDeviceLogin, 'function');
assert.equal(typeof api?.uploadBuilderIdCredential, 'function');
});
test('kiro start device login registers client, opens auth tab, and completes with runtime payload', async () => {
const api = loadKiroDeviceAuthApi();
const fetchCalls = [];
const stateUpdates = [];
const registerCalls = [];
const reuseCalls = [];
const completeCalls = [];
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({
url,
method: options.method || 'GET',
body: options.body ? JSON.parse(options.body) : null,
});
if (url.endsWith('/client/register')) {
return createResponse({
ok: true,
status: 200,
json: {
clientId: 'client-001',
clientSecret: 'secret-001',
},
});
}
if (url.endsWith('/device_authorization')) {
return createResponse({
ok: true,
status: 200,
json: {
deviceCode: 'device-code-001',
userCode: 'ABCD-1234',
verificationUri: 'https://device.example.com/start',
verificationUriComplete: 'https://device.example.com/complete',
interval: 7,
expiresIn: 900,
},
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
},
getState: async () => ({
kiroRegion: 'eu-west-1',
}),
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
reuseOrCreateTab: async (source, url) => {
reuseCalls.push({ source, url });
return 88;
},
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await executor.executeKiroStartDeviceLogin({
nodeId: 'kiro-start-device-login',
kiroRegion: 'eu-west-1',
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://oidc.eu-west-1.amazonaws.com/client/register');
assert.equal(fetchCalls[1].url, 'https://oidc.eu-west-1.amazonaws.com/device_authorization');
assert.deepEqual(fetchCalls[1].body, {
clientId: 'client-001',
clientSecret: 'secret-001',
startUrl: 'https://view.awsapps.com/start',
});
assert.deepEqual(reuseCalls, [{
source: 'kiro-device-auth',
url: 'https://device.example.com/complete',
}]);
assert.deepEqual(registerCalls, [{
source: 'kiro-device-auth',
tabId: 88,
}]);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroClientId, 'client-001');
assert.equal(finalState.kiroClientSecret, 'secret-001');
assert.equal(finalState.kiroDeviceAuthorizationCode, 'device-code-001');
assert.equal(finalState.kiroDeviceCode, 'ABCD-1234');
assert.equal(finalState.kiroLoginUrl, 'https://device.example.com/complete');
assert.equal(finalState.kiroAuthRegion, 'eu-west-1');
assert.equal(finalState.kiroAuthIntervalSeconds, 7);
assert.equal(finalState.kiroAuthStatus, 'waiting_user');
assert.equal(finalState.kiroUploadStatus, 'waiting_login');
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-start-device-login');
assert.equal(completeCalls[0].payload.kiroDeviceCode, 'ABCD-1234');
assert.equal(completeCalls[0].payload.kiroLoginUrl, 'https://device.example.com/complete');
});
test('kiro await device login polls until refresh token is captured', async () => {
const api = loadKiroDeviceAuthApi();
const fetchCalls = [];
const stateUpdates = [];
const sleepCalls = [];
const completeCalls = [];
let pollCount = 0;
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({
url,
method: options.method || 'GET',
body: options.body ? JSON.parse(options.body) : null,
});
pollCount += 1;
if (pollCount === 1) {
return createResponse({
ok: false,
status: 400,
json: { error: 'authorization_pending' },
});
}
return createResponse({
ok: true,
status: 200,
json: {
accessToken: 'access-001',
refreshToken: 'refresh-001',
expiresIn: 3600,
},
});
},
getState: async () => ({
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroDeviceAuthorizationCode: 'device-code-001',
kiroAuthRegion: 'us-east-1',
kiroAuthExpiresAt: Date.now() + 60000,
kiroAuthIntervalSeconds: 5,
}),
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async (ms) => {
sleepCalls.push(ms);
},
throwIfStopped: () => {},
});
await executor.executeKiroAwaitDeviceLogin({
nodeId: 'kiro-await-device-login',
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroDeviceAuthorizationCode: 'device-code-001',
kiroAuthRegion: 'us-east-1',
kiroAuthExpiresAt: Date.now() + 60000,
kiroAuthIntervalSeconds: 5,
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://oidc.us-east-1.amazonaws.com/token');
assert.deepEqual(fetchCalls[0].body, {
clientId: 'client-001',
clientSecret: 'secret-001',
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
deviceCode: 'device-code-001',
});
assert.deepEqual(sleepCalls, [5000]);
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroAuthStatus, 'authorized');
assert.equal(finalState.kiroRefreshToken, 'refresh-001');
assert.equal(finalState.kiroAccessToken, 'access-001');
assert.equal(finalState.kiroUploadStatus, 'ready_to_upload');
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-await-device-login');
assert.equal(completeCalls[0].payload.kiroRefreshToken, 'refresh-001');
});
test('kiro upload credential checks connection and uploads builder id credential to kiro.rs', async () => {
const api = loadKiroDeviceAuthApi();
const fetchCalls = [];
const stateUpdates = [];
const completeCalls = [];
const executor = api.createKiroDeviceAuthExecutor({
addLog: async () => {},
completeNodeFromBackground: async (nodeId, payload) => {
completeCalls.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({
url,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body ? JSON.parse(options.body) : null,
});
if (options.method === 'GET') {
return createResponse({
ok: true,
status: 200,
json: { success: true },
});
}
return createResponse({
ok: true,
status: 200,
json: {
success: true,
message: 'uploaded',
credentialId: 321,
email: 'aws-user@example.com',
},
});
},
getState: async () => ({
kiroRefreshToken: 'refresh-001',
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroAuthRegion: 'ap-southeast-1',
kiroAuthorizedEmail: 'cached@example.com',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'admin-key-001',
ipProxyEnabled: true,
ipProxyProtocol: 'socks5',
ipProxyHost: '127.0.0.1',
ipProxyPort: '1080',
ipProxyUsername: 'proxy-user',
ipProxyPassword: 'proxy-pass',
}),
setState: async (updates) => {
stateUpdates.push(updates);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await executor.executeKiroUploadCredential({
nodeId: 'kiro-upload-credential',
kiroRefreshToken: 'refresh-001',
kiroClientId: 'client-001',
kiroClientSecret: 'secret-001',
kiroAuthRegion: 'ap-southeast-1',
kiroAuthorizedEmail: 'cached@example.com',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'admin-key-001',
ipProxyEnabled: true,
ipProxyProtocol: 'socks5',
ipProxyHost: '127.0.0.1',
ipProxyPort: '1080',
ipProxyUsername: 'proxy-user',
ipProxyPassword: 'proxy-pass',
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://kiro.example.com/api/admin/credentials');
assert.equal(fetchCalls[0].method, 'GET');
assert.equal(fetchCalls[0].headers['x-api-key'], 'admin-key-001');
assert.equal(fetchCalls[1].url, 'https://kiro.example.com/api/admin/credentials');
assert.equal(fetchCalls[1].method, 'POST');
assert.equal(fetchCalls[1].headers['x-api-key'], 'admin-key-001');
assert.deepEqual(fetchCalls[1].body, {
refreshToken: 'refresh-001',
clientId: 'client-001',
clientSecret: 'secret-001',
region: 'ap-southeast-1',
email: 'cached@example.com',
priority: 0,
authMethod: 'IdC',
provider: 'BuilderId',
proxyUrl: 'socks5://127.0.0.1:1080',
proxyUsername: 'proxy-user',
proxyPassword: 'proxy-pass',
});
const finalState = mergeUpdates(stateUpdates);
assert.equal(finalState.kiroLastConnectionMessage, 'kiro.rs connection ok (HTTP 200)');
assert.equal(finalState.kiroAuthorizedEmail, 'aws-user@example.com');
assert.equal(finalState.kiroCredentialId, 321);
assert.equal(finalState.kiroUploadStatus, 'uploaded');
assert.equal(typeof finalState.kiroLastUploadAt, 'number');
assert.equal(finalState.kiroLastUploadAt > 0, true);
assert.equal(completeCalls.length, 1);
assert.equal(completeCalls[0].nodeId, 'kiro-upload-credential');
assert.equal(completeCalls[0].payload.kiroCredentialId, 321);
assert.equal(completeCalls[0].payload.kiroUploadStatus, 'uploaded');
});
@@ -0,0 +1,407 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
const backgroundSource = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => backgroundSource.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < backgroundSource.length; i += 1) {
const ch = backgroundSource[i];
if (ch === '(') parenDepth += 1;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < backgroundSource.length; end += 1) {
const ch = backgroundSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return backgroundSource.slice(start, end);
}
function buildHarness(extra = '') {
return new Function(`
const self = {};
${flowRegistrySource}
${settingsSchemaSource}
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_SUB2API_GROUP_NAMES = ['codex', 'openai-plus'];
const PERSISTED_SETTING_DEFAULTS = {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
panelMode: 'cpa',
signupMethod: 'email',
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
phoneVerificationEnabled: false,
mailProvider: '163',
ipProxyEnabled: false,
ipProxyService: '711proxy',
ipProxyMode: 'account',
kiroSourceId: 'kiro-rs',
kiroRsUrl: 'https://kiro.leftcode.xyz/admin',
kiroRsKey: '',
kiroRegion: 'us-east-1',
stepExecutionRangeByFlow: {},
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
const PERSISTED_SETTINGS_SCHEMA_KEYS = ['settingsSchemaVersion', 'settingsState'];
const LEGACY_AUTO_STEP_DELAY_KEYS = [];
const LEGACY_VERIFICATION_RESEND_COUNT_KEYS = [];
function normalizePanelMode(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa';
}
function normalizeSignupMethod(value = '') {
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'gopay' || normalized === 'gpc-helper' ? normalized : 'paypal';
}
function normalizeSub2ApiGroupNames(value) {
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
}
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value : []; }
function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; }
function normalizeCloudMailDomains(value) { return Array.isArray(value) ? value : []; }
function normalizeMailProvider(value = '') { return String(value || '163').trim().toLowerCase() || '163'; }
function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; }
function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
function buildIpProxyServiceProfileFromState() {
return {
mode: 'account',
apiUrl: '',
accountList: '',
accountSessionPrefix: '',
accountLifeMinutes: '',
poolTargetCount: '20',
host: '',
port: '',
protocol: 'http',
username: '',
password: '',
region: '',
};
}
function normalizeIpProxyAccountList(value) { return String(value || ''); }
function normalizeIpProxyAccountSessionPrefix(value) { return String(value || ''); }
function normalizeIpProxyAccountLifeMinutes(value) { return String(value || ''); }
function normalizeIpProxyPoolTargetCount(value) { return String(value || '20'); }
function normalizeIpProxyPort(value) { return String(value || '').trim(); }
function normalizeIpProxyProtocol(value) { return String(value || 'http').trim() || 'http'; }
function resolveSignupMethod(state = {}) {
const activeFlowId = String(state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID;
if (activeFlowId === 'kiro') {
return 'email';
}
return String(state?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${extractFunction('normalizePersistentSettingValue')}
${extractFunction('getSettingsSchemaApi')}
${extractFunction('buildPersistentSettingsPayload')}
${extractFunction('getPersistedSettings')}
${extractFunction('setPersistentSettings')}
${extra}
return {
buildPersistentSettingsPayload,
getPersistedSettings,
setPersistentSettings,
getRequestedKeys: typeof getRequestedKeys === 'function' ? getRequestedKeys : () => [],
getPersistedWrites: typeof getPersistedWrites === 'function' ? getPersistedWrites : () => [],
};
`)();
}
test('buildPersistentSettingsPayload writes canonical settings schema into persisted payloads when defaults are materialized', () => {
const api = buildHarness();
const payload = api.buildPersistentSettingsPayload({
activeFlowId: 'kiro',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'secret-key',
kiroRegion: 'eu-west-1',
}, { fillDefaults: true });
assert.equal(payload.activeFlowId, 'kiro');
assert.equal(payload.kiroSourceId, 'kiro-rs');
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'secret-key');
assert.equal(payload.kiroRegion, 'eu-west-1');
assert.equal(payload.settingsSchemaVersion, 3);
assert.equal(payload.settingsState.activeFlowId, 'kiro');
assert.equal(payload.settingsState.flows.kiro.source.selected, 'kiro-rs');
});
test('buildPersistentSettingsPayload accepts schema-only input when requireKnownKeys is enabled', () => {
const api = buildHarness();
const payload = api.buildPersistentSettingsPayload({
settingsSchemaVersion: 3,
settingsState: {
activeFlowId: 'kiro',
services: {
email: { provider: '163' },
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: { selected: 'cpa', entries: {} },
account: { customPassword: '' },
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'schema-only-key',
},
},
},
options: {
kiroRegion: 'eu-west-1',
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
},
},
},
},
}, { requireKnownKeys: true });
assert.equal(payload.activeFlowId, 'kiro');
assert.equal(payload.kiroSourceId, 'kiro-rs');
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'schema-only-key');
assert.equal(payload.kiroRegion, 'eu-west-1');
assert.equal(payload.settingsSchemaVersion, 3);
});
test('getPersistedSettings reads schema keys alongside legacy flat settings keys', async () => {
const api = buildHarness(`
let requestedKeys = [];
const chrome = {
storage: {
local: {
async get(keys) {
requestedKeys = Array.isArray(keys) ? [...keys] : [];
return {};
},
},
},
};
function getRequestedKeys() {
return requestedKeys;
}
`);
const state = await api.getPersistedSettings();
assert.ok(api.getRequestedKeys().includes('settingsSchemaVersion'));
assert.ok(api.getRequestedKeys().includes('settingsState'));
assert.equal(state.settingsSchemaVersion, 3);
assert.equal(state.settingsState.activeFlowId, 'openai');
});
test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => {
const api = buildHarness(`
const chrome = {
storage: {
local: {
async get() {
return {
settingsSchemaVersion: 3,
settingsState: {
activeFlowId: 'kiro',
services: {
email: { provider: 'hotmail' },
proxy: { enabled: true, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: {
selected: 'sub2api',
entries: {},
},
account: { customPassword: '' },
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'stored-key',
},
},
},
options: {
kiroRegion: 'ap-southeast-1',
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
},
},
},
},
};
},
},
},
};
`);
const state = await api.getPersistedSettings();
assert.equal(state.activeFlowId, 'kiro');
assert.equal(state.panelMode, 'sub2api');
assert.equal(state.mailProvider, 'hotmail');
assert.equal(state.ipProxyEnabled, true);
assert.equal(state.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(state.kiroRsKey, 'stored-key');
assert.equal(state.kiroRegion, 'ap-southeast-1');
assert.deepEqual(state.stepExecutionRangeByFlow.kiro, {
enabled: true,
fromStep: 1,
toStep: 3,
});
});
test('setPersistentSettings materializes canonical schema keys for schema-only updates', async () => {
const api = buildHarness(`
const persistedWrites = [];
const chrome = {
storage: {
local: {
async get() {
return {};
},
async set(payload) {
persistedWrites.push(JSON.parse(JSON.stringify(payload)));
},
},
},
};
function getPersistedWrites() {
return persistedWrites;
}
`);
const persisted = await api.setPersistentSettings({
settingsSchemaVersion: 3,
settingsState: {
activeFlowId: 'kiro',
services: {
email: { provider: '163' },
proxy: { enabled: false, provider: '711proxy', mode: 'account' },
},
flows: {
openai: {
source: { selected: 'cpa', entries: {} },
account: { customPassword: '' },
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 11 },
},
},
kiro: {
source: {
selected: 'kiro-rs',
entries: {
'kiro-rs': {
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'nested-only-key',
},
},
},
options: {
kiroRegion: 'us-west-2',
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 1, toStep: 3 },
},
},
},
},
});
const write = api.getPersistedWrites().at(-1);
assert.equal(persisted.activeFlowId, 'kiro');
assert.equal(persisted.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(persisted.kiroRsKey, 'nested-only-key');
assert.equal(persisted.kiroRegion, 'us-west-2');
assert.equal(persisted.settingsSchemaVersion, 3);
assert.equal(write.activeFlowId, 'kiro');
assert.equal(write.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(write.kiroRsKey, 'nested-only-key');
assert.equal(write.kiroRegion, 'us-west-2');
assert.equal(write.settingsSchemaVersion, 3);
assert.equal(write.settingsState.activeFlowId, 'kiro');
});
+2 -1
View File
@@ -2,7 +2,7 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background imports step 1~10 modules', () => {
test('background imports workflow step modules including Kiro device auth', () => {
const source = fs.readFileSync('background.js', 'utf8');
[
@@ -16,6 +16,7 @@ test('background imports step 1~10 modules', () => {
'background/steps/fetch-login-code.js',
'background/steps/confirm-oauth.js',
'background/steps/platform-verify.js',
'background/steps/kiro-device-auth.js',
].forEach((path) => {
assert.match(source, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
@@ -0,0 +1,64 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('background node registry preserves node metadata even before an executor is registered', () => {
const source = fs.readFileSync('background/steps/registry.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({});
const registry = api.createNodeRegistry([
{
flowId: 'kiro',
legacyStepId: 1,
nodeId: 'kiro-start-device-login',
displayOrder: 10,
executeKey: 'kiro-start-device-login',
title: 'Start device login',
},
]);
const node = registry.getNodeDefinition('kiro-start-device-login');
assert.equal(node.flowId, 'kiro');
assert.equal(node.legacyStepId, 1);
assert.equal(node.title, 'Start device login');
assert.throws(
() => registry.executeNode('kiro-start-device-login', {}),
/Missing node executor: kiro-start-device-login/
);
});
test('background node registry executes registered nodes in display order', async () => {
const source = fs.readFileSync('background/steps/registry.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundStepRegistry;`)({});
const events = [];
const registry = api.createNodeRegistry([
{
flowId: 'openai',
legacyStepId: 2,
nodeId: 'submit-signup-email',
displayOrder: 20,
executeKey: 'submit-signup-email',
title: 'Submit signup email',
execute: async (state) => {
events.push({ type: 'execute', state });
},
},
{
flowId: 'openai',
legacyStepId: 1,
nodeId: 'open-chatgpt',
displayOrder: 10,
executeKey: 'open-chatgpt',
title: 'Open ChatGPT',
},
]);
assert.deepStrictEqual(
registry.getOrderedNodes().map((node) => node.nodeId),
['open-chatgpt', 'submit-signup-email']
);
await registry.executeNode('submit-signup-email', { activeFlowId: 'openai' });
assert.deepStrictEqual(events, [{ type: 'execute', state: { activeFlowId: 'openai' } }]);
});
+9 -31
View File
@@ -8,37 +8,10 @@ test('background imports node registry and shared workflow definitions', () => {
assert.match(source, /data\/step-definitions\.js/);
assert.match(source, /background\/workflow-engine\.js/);
assert.match(source, /MultiPageStepDefinitions\?\.getNodes/);
assert.match(source, /getStepRegistryForState\(state\)/);
assert.match(source, /buildNodeRegistry\(definitions/);
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
assert.match(source, /NORMAL_PHONE_STEP_DEFINITIONS/);
assert.match(source, /NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_PAYPAL_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GPC_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /plusPayPalStepRegistry/);
assert.match(source, /plusGoPayStepRegistry/);
assert.match(source, /normalPhoneStepRegistry/);
assert.match(source, /normalPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusPayPalPhoneStepRegistry/);
assert.match(source, /plusPayPalPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusGoPayPhoneStepRegistry/);
assert.match(source, /plusGoPayPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusGpcPhoneStepRegistry/);
assert.match(source, /plusGpcPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /const signupMethod = getSignupMethodForStepDefinitions\(state\);/);
assert.match(source, /const useBoundEmailRelogin = signupMethod === SIGNUP_METHOD_PHONE/);
assert.match(source, /useBoundEmailRelogin \? normalPhoneBoundEmailReloginStepRegistry : normalPhoneStepRegistry/);
assert.match(source, /const paymentMethod = normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\);/);
assert.match(source, /paymentMethod === PLUS_PAYMENT_METHOD_GOPAY/);
assert.match(source, /useBoundEmailRelogin \? plusGoPayPhoneBoundEmailReloginStepRegistry : plusGoPayPhoneStepRegistry/);
assert.match(source, /useBoundEmailRelogin \? plusPayPalPhoneBoundEmailReloginStepRegistry : plusPayPalPhoneStepRegistry/);
assert.match(source, /useBoundEmailRelogin \? plusGpcPhoneBoundEmailReloginStepRegistry : plusGpcPhoneStepRegistry/);
assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/);
assert.match(source, /const stepRegistryCache = new Map\(\);/);
assert.match(source, /const definitions = getNodeDefinitionsForState\(state\);/);
assert.match(source, /stepRegistryCache\.set\(cacheKey, buildStepRegistry\(definitions\)\)/);
assert.match(source, /'bind-email': \(state\) => step8Executor\.executeBindEmail\(state\)/);
assert.match(source, /'fetch-bind-email-code': \(state\) => step8Executor\.executeFetchBindEmailCode\(state\)/);
assert.match(source, /'relogin-bound-email': \(state\) => executeReloginBoundEmail\(state\)/);
@@ -51,9 +24,14 @@ test('background imports node registry and shared workflow definitions', () => {
assert.match(source, /background\/steps\/paypal-approve\.js/);
assert.match(source, /background\/steps\/gopay-approve\.js/);
assert.match(source, /background\/steps\/plus-return-confirm\.js/);
assert.match(source, /background\/steps\/kiro-device-auth\.js/);
assert.match(source, /const kiroDeviceAuthExecutor = self\.MultiPageBackgroundKiroDeviceAuth\?\.createKiroDeviceAuthExecutor\(/);
assert.match(source, /'kiro-start-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroStartDeviceLogin\(state\)/);
assert.match(source, /'kiro-await-device-login': \(state\) => kiroDeviceAuthExecutor\.executeKiroAwaitDeviceLogin\(state\)/);
assert.match(source, /'kiro-upload-credential': \(state\) => kiroDeviceAuthExecutor\.executeKiroUploadCredential\(state\)/);
assert.match(source, /'kiro-start-device-login',[\s\S]*'kiro-await-device-login',[\s\S]*'kiro-upload-credential'/);
});
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /createGoPayApproveExecutor\(\{[\s\S]*clickWithDebugger[\s\S]*requestGoPayOtpInput[\s\S]*\}\)/);
+32 -1
View File
@@ -2,11 +2,16 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8');
function loadApi() {
const scope = {};
return new Function('self', `${source}; return self.MultiPageFlowCapabilities;`)(scope);
return new Function(
'self',
`${flowRegistrySource}; ${settingsSchemaSource}; ${source}; return self.MultiPageFlowCapabilities;`
)(scope);
}
test('flow capability registry keeps OpenAI phone signup available only when runtime locks allow it', () => {
@@ -71,6 +76,32 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
assert.deepEqual(capabilityState.supportedPanelModes, []);
});
test('flow capability registry exposes Kiro as an independent flow with its own visible groups', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
const capabilityState = registry.resolveSidepanelCapabilities({
state: {
activeFlowId: 'kiro',
kiroSourceId: 'kiro-rs',
panelMode: 'sub2api',
signupMethod: 'phone',
plusModeEnabled: true,
phoneVerificationEnabled: true,
},
});
assert.equal(capabilityState.activeFlowId, 'kiro');
assert.equal(capabilityState.canShowPhoneSettings, false);
assert.equal(capabilityState.canShowPlusSettings, false);
assert.equal(capabilityState.effectiveSignupMethod, 'email');
assert.equal(capabilityState.effectiveSourceId, 'kiro-rs');
assert.deepEqual(
capabilityState.visibleGroupIds,
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy']
);
});
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry({
@@ -0,0 +1,83 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const settingsSchemaSource = fs.readFileSync('shared/settings-schema.js', 'utf8');
function loadApis() {
const scope = {};
return new Function('self', `${flowRegistrySource}; ${settingsSchemaSource}; return {
flowRegistry: self.MultiPageFlowRegistry,
settingsSchema: self.MultiPageSettingsSchema,
};`)(scope);
}
test('flow registry exposes openai and kiro with canonical source metadata', () => {
const { flowRegistry } = loadApis();
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.equal(flowRegistry.getFlowLabel('codex'), 'Codex / OpenAI');
assert.equal(flowRegistry.normalizeFlowId('codex'), 'openai');
assert.equal(flowRegistry.normalizeSourceId('openai', 'sub2api'), 'sub2api');
assert.equal(flowRegistry.normalizeSourceId('kiro', 'anything-else'), 'kiro-rs');
assert.deepEqual(
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
['kiro-runtime-status', 'kiro-source-kiro-rs', 'service-email', 'service-proxy']
);
});
test('settings schema normalizes flat input into canonical flow and service namespaces', () => {
const { settingsSchema } = loadApis();
const schema = settingsSchema.createSettingsSchema();
const normalized = schema.normalizeSettingsState({
activeFlowId: 'kiro',
panelMode: 'sub2api',
mailProvider: 'hotmail',
ipProxyEnabled: true,
ipProxyService: '711proxy',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'secret-key',
kiroRegion: 'eu-west-1',
stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 2, toStep: 9 },
kiro: { enabled: true, fromStep: 1, toStep: 3 },
},
});
assert.equal(normalized.activeFlowId, 'kiro');
assert.equal(normalized.services.email.provider, 'hotmail');
assert.equal(normalized.services.proxy.enabled, true);
assert.equal(normalized.flows.openai.source.selected, 'sub2api');
assert.equal(normalized.flows.kiro.source.selected, 'kiro-rs');
assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.source.entries['kiro-rs'].kiroRsKey, 'secret-key');
assert.equal(normalized.flows.kiro.options.kiroRegion, 'eu-west-1');
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 1,
toStep: 3,
});
});
test('settings schema can project canonical state back to legacy payload without losing flow selection', () => {
const { settingsSchema } = loadApis();
const schema = settingsSchema.createSettingsSchema();
const payload = schema.buildLegacySettingsPayload(schema.normalizeSettingsState({
activeFlowId: 'kiro',
kiroSourceId: 'kiro-rs',
kiroRsUrl: 'https://kiro.example.com/admin',
kiroRsKey: 'key-123',
kiroRegion: 'ap-southeast-1',
}));
assert.equal(payload.activeFlowId, 'kiro');
assert.equal(payload.panelMode, 'cpa');
assert.equal(payload.kiroSourceId, 'kiro-rs');
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'key-123');
assert.equal(payload.kiroRegion, 'ap-southeast-1');
assert.equal(payload.settingsSchemaVersion, 3);
assert.equal(payload.settingsState.activeFlowId, 'kiro');
});
@@ -0,0 +1,90 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('sidepanel/contribution-mode.js', 'utf8');
function createElement() {
return {
hidden: false,
disabled: false,
title: '',
textContent: '',
value: '',
classList: {
hiddenState: false,
toggle(_className, hidden) {
this.hiddenState = Boolean(hidden);
},
},
setAttribute() {},
addEventListener() {},
};
}
test('contribution mode manager does not project openai-only ui state into kiro flow', () => {
const context = {
window: {},
document: { activeElement: null },
console,
setTimeout,
clearTimeout,
};
vm.runInNewContext(source, context);
const createContributionModeManager = context.window.SidepanelContributionMode.createContributionModeManager;
const rowVpsUrl = createElement();
const dom = {
btnContributionMode: createElement(),
contributionModePanel: createElement(),
contributionModeText: createElement(),
contributionModeBadge: createElement(),
contributionOauthStatus: createElement(),
contributionCallbackStatus: createElement(),
contributionModeSummary: createElement(),
inputContributionNickname: createElement(),
inputContributionQq: createElement(),
btnStartContribution: createElement(),
btnOpenContributionUpload: createElement(),
btnExitContributionMode: createElement(),
btnOpenAccountRecords: createElement(),
selectPanelMode: createElement(),
rowVpsUrl,
};
const manager = createContributionModeManager({
state: {
getLatestState: () => ({
activeFlowId: 'kiro',
flowId: 'kiro',
contributionMode: true,
contributionSource: 'cpa',
}),
},
dom,
helpers: {
updatePanelModeUI() {},
updateAccountRunHistorySettingsUI() {},
updateConfigMenuControls() {},
closeConfigMenu() {},
closeAccountRecordsPanel() {},
isModeSwitchBlocked() {
return false;
},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {},
});
manager.render();
assert.equal(dom.contributionModePanel.hidden, true);
assert.equal(dom.selectPanelMode.disabled, false);
assert.equal(dom.btnContributionMode.disabled, true);
assert.equal(dom.btnContributionMode.title, '当前 flow 不支持贡献模式');
assert.equal(dom.btnStartContribution.disabled, true);
assert.equal(dom.btnOpenContributionUpload.disabled, true);
assert.equal(rowVpsUrl.classList.hiddenState, false);
});
@@ -0,0 +1,115 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const sidepanelHtml = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
function extractFunction(source, name) {
const asyncStart = source.indexOf(`async function ${name}`);
const normalStart = source.indexOf(`function ${name}`);
const start = asyncStart !== -1
? asyncStart
: normalStart;
if (start === -1) {
throw new Error(`Function ${name} not found`);
}
const signatureEnd = source.indexOf(')', start);
const bodyStart = source.indexOf('{', signatureEnd);
let depth = 0;
let end = bodyStart;
for (; end < source.length; end += 1) {
const char = source[end];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('sidepanel html exposes flow selector and kiro source fields', () => {
[
'id="select-flow"',
'id="label-source-selector"',
'id="row-kiro-rs-url"',
'id="row-kiro-rs-key"',
'id="row-kiro-region"',
'id="row-kiro-device-code"',
'id="row-kiro-login-url"',
'id="row-kiro-upload-status"',
].forEach((snippet) => {
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
});
test('sidepanel step definitions rerender when active flow changes even if plus/signup settings stay the same', () => {
const bundle = [
extractFunction(sidepanelSource, 'normalizeSignupMethod'),
extractFunction(sidepanelSource, 'normalizePlusPaymentMethod'),
extractFunction(sidepanelSource, 'getStepDefinitionsForMode'),
extractFunction(sidepanelSource, 'rebuildStepDefinitionState'),
extractFunction(sidepanelSource, 'syncStepDefinitionsForMode'),
].join('\n');
const api = new Function(`
const calls = [];
const window = {
MultiPageStepDefinitions: {
getSteps(options) {
calls.push({ type: 'getSteps', options });
return [{ id: options.activeFlowId === 'kiro' ? 88 : 6, order: 1, key: options.activeFlowId }];
},
},
};
let latestState = { activeFlowId: 'openai' };
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_SIGNUP_METHOD = 'email';
const DEFAULT_PLUS_PAYMENT_METHOD = 'paypal';
let stepDefinitions = [{ id: 6, key: 'openai' }];
let STEP_IDS = [6];
let STEP_DEFAULT_STATUSES = { 6: 'pending' };
let SKIPPABLE_STEPS = new Set([6]);
function renderStepsList() {
calls.push({ type: 'render', stepIds: [...STEP_IDS] });
}
${bundle}
return {
calls,
syncStepDefinitionsForMode,
getStepIds: () => [...STEP_IDS],
getCurrentFlowId: () => currentStepDefinitionFlowId,
};
`)();
api.syncStepDefinitionsForMode(false, {
activeFlowId: 'kiro',
plusPaymentMethod: 'paypal',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
});
assert.equal(api.getCurrentFlowId(), 'kiro');
assert.deepEqual(api.getStepIds(), [88]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: {
activeFlowId: 'kiro',
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
},
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
});
+9 -1
View File
@@ -4,6 +4,8 @@ const fs = require('node:fs');
test('background imports shared source registry module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /shared\/flow-registry\.js/);
assert.match(source, /shared\/settings-schema\.js/);
assert.match(source, /shared\/source-registry\.js/);
});
@@ -21,8 +23,9 @@ test('manifest loads shared source registry before content utils in static bundl
});
test('shared source registry exposes canonical source, alias, detection, and ready policies', () => {
const flowRegistrySource = fs.readFileSync('shared/flow-registry.js', 'utf8');
const source = fs.readFileSync('shared/source-registry.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageSourceRegistry;`)({});
const api = new Function('self', `${flowRegistrySource}; ${source}; return self.MultiPageSourceRegistry;`)({});
const registry = api.createSourceRegistry();
assert.equal(registry.resolveCanonicalSource('signup-page'), 'openai-auth');
@@ -50,6 +53,10 @@ test('shared source registry exposes canonical source, alias, detection, and rea
}),
'unknown-source'
);
assert.equal(registry.detectSourceFromLocation({
url: 'https://view.awsapps.com/start',
hostname: 'view.awsapps.com',
}), 'kiro-device-auth');
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
@@ -59,4 +66,5 @@ test('shared source registry exposes canonical source, alias, detection, and rea
assert.equal(registry.driverAcceptsCommand('openai-auth', 'fetch-bind-email-code'), true);
assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true);
assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false);
assert.equal(registry.driverAcceptsCommand('background/kiro-device-auth', 'kiro-start-device-login'), true);
});
+20 -1
View File
@@ -22,6 +22,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' });
assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 11);
@@ -158,10 +159,28 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 18);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('kiro'), true);
assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']);
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
assert.deepStrictEqual(
kiroSteps.map((step) => step.key),
[
'kiro-start-device-login',
'kiro-await-device-login',
'kiro-upload-credential',
]
);
assert.equal(kiroSteps.every((step) => step.flowId === 'kiro'), true);
assert.equal(kiroSteps[0].driverId, 'background/kiro-device-auth');
assert.equal(kiroSteps[2].sourceId, 'kiro-rs-admin');
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'kiro' }), [1, 2, 3]);
assert.equal(api.getLastStepId({ activeFlowId: 'kiro' }), 3);
assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'kiro' }).map((node) => node.next),
[['kiro-await-device-login'], ['kiro-upload-credential'], []]
);
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');