重构 Grok SSO 上传到 webchat2api

This commit is contained in:
QLHazyCoder
2026-05-23 20:41:22 +08:00
parent f856910e5b
commit c649d43ee6
20 changed files with 957 additions and 97 deletions
@@ -0,0 +1,274 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadPublisherApi() {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const publisherSource = fs.readFileSync('flows/grok/background/publisher-webchat2api.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${publisherSource}; return self;`)(globalScope);
return globalScope.MultiPageBackgroundGrokPublisherWebchat2Api;
}
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
statusText: status >= 200 && status < 300 ? 'OK' : 'Error',
text: async () => JSON.stringify(payload),
};
}
function getGrokRuntime(state = {}) {
return state?.runtimeState?.flowState?.grok || {};
}
test('grok webchat2api publisher exposes helpers and normalizes to origin inject endpoint', () => {
const api = loadPublisherApi();
assert.equal(typeof api?.createGrokWebchat2ApiPublisher, 'function');
assert.equal(
api.buildWebchat2ApiInjectUrl('https://remote.example.com/admin/deep/path'),
'https://remote.example.com/api/remote-account/inject'
);
assert.equal(
api.buildWebchat2ApiInjectUrl('remote.example.com/admin'),
'http://remote.example.com/api/remote-account/inject'
);
});
test('grok webchat2api publisher requires URL, admin key, and SSO cookie', async () => {
const api = loadPublisherApi();
assert.throws(
() => api.buildWebchat2ApiInjectUrl(''),
/缺少 webchat2api 地址/
);
assert.throws(
() => api.buildGrokSsoInjectPayload(''),
/缺少 Grok SSO Cookie/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api('http://remote.example.com', '', 'sso-cookie', async () => createJsonResponse({})),
/缺少 webchat2api 管理密钥/
);
});
test('grok webchat2api publisher posts Grok SSO payload with bearer admin key', async () => {
const api = loadPublisherApi();
const requests = [];
const result = await api.uploadGrokSsoToWebchat2Api(
'https://remote.example.com/admin/deep/path',
' admin-secret ',
'sso-cookie-001',
async (url, options = {}) => {
requests.push({
url,
method: options.method,
authorization: options.headers?.Authorization,
contentType: options.headers?.['Content-Type'],
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, data: { total: 1 }, message: 'ok' });
}
);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].method, 'POST');
assert.equal(requests[0].authorization, 'Bearer admin-secret');
assert.equal(requests[0].contentType, 'application/json');
assert.deepEqual(requests[0].body, {
accounts: [{
token: 'sso-cookie-001',
provider: 'grok',
type: 'sso',
}],
strategy: 'merge',
source_id: 'flowpilot-grok-sso',
source_name: 'FlowPilot Grok SSO',
provider: 'grok',
});
assert.equal(result.endpointUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(result.message, 'ok');
});
test('grok webchat2api publisher accepts code-zero envelopes and rejects HTTP/API errors', async () => {
const api = loadPublisherApi();
const success = await api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 0, data: { added: 1 } })
);
assert.equal(success.message, '上传成功');
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ error: 'invalid admin key' }, 403)
),
/webchat2api SSO 上传失败:invalid admin key/
);
await assert.rejects(
() => api.uploadGrokSsoToWebchat2Api(
'http://remote.example.com',
'admin-secret',
'sso-cookie',
async () => createJsonResponse({ code: 4001, message: 'bad token' })
),
/webchat2api SSO 上传失败:bad token/
);
});
test('grok webchat2api executor reads latest state and writes upload runtime without leaking secrets to logs', async () => {
const api = loadPublisherApi();
const requests = [];
const logs = [];
const completed = [];
let liveState = {
grokWebchat2ApiUrl: '',
grokWebchat2ApiAdminKey: '',
grokSsoCookie: '',
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'live-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'live-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async (url, options = {}) => {
requests.push({
url,
authorization: options.headers?.Authorization,
body: JSON.parse(options.body),
});
return createJsonResponse({ code: 0, message: 'uploaded' });
},
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await publisher.executeGrokUploadSsoToWebchat2Api({
nodeId: 'grok-upload-sso-to-webchat2api',
grokWebchat2ApiAdminKey: 'stale-key',
grokSsoCookie: 'stale-sso-cookie',
});
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://remote.example.com/api/remote-account/inject');
assert.equal(requests[0].authorization, 'Bearer live-admin-key');
assert.equal(requests[0].body.accounts[0].token, 'live-sso-cookie');
assert.equal(completed.length, 1);
assert.equal(completed[0].nodeId, 'grok-upload-sso-to-webchat2api');
assert.equal(getGrokRuntime(completed[0].payload).upload.status, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.message, 'uploaded');
assert.equal(getGrokRuntime(completed[0].payload).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(typeof getGrokRuntime(completed[0].payload).upload.uploadedAt, 'number');
assert.equal(logs.some(({ message }) => message.includes('live-sso-cookie') || message.includes('live-admin-key')), false);
});
test('grok webchat2api executor persists failure state without completing or leaking secrets', async () => {
const api = loadPublisherApi();
const logs = [];
const completed = [];
let liveState = {
settingsState: {
flows: {
grok: {
targets: {
webchat2api: {
baseUrl: 'https://remote.example.com/admin',
apiKey: 'secret-admin-key',
},
},
},
},
},
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'secret-sso-cookie',
},
},
},
},
};
const publisher = api.createGrokWebchat2ApiPublisher({
addLog: async (message, level) => {
logs.push({ message, level });
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
fetchImpl: async () => createJsonResponse({ error: 'invalid admin key' }, 403),
getState: async () => ({ ...liveState }),
setState: async (updates = {}) => {
liveState = {
...liveState,
...updates,
runtimeState: {
...(liveState.runtimeState || {}),
...(updates.runtimeState || {}),
flowState: {
...(liveState.runtimeState?.flowState || {}),
...(updates.runtimeState?.flowState || {}),
},
},
};
},
});
await assert.rejects(
() => publisher.executeGrokUploadSsoToWebchat2Api({ nodeId: 'grok-upload-sso-to-webchat2api' }),
/webchat2api SSO 上传失败:invalid admin key/
);
assert.equal(completed.length, 0);
assert.equal(getGrokRuntime(liveState).upload.status, 'error');
assert.equal(getGrokRuntime(liveState).upload.uploadedAt, 0);
assert.equal(getGrokRuntime(liveState).upload.message, 'webchat2api SSO 上传失败:invalid admin key');
assert.equal(getGrokRuntime(liveState).upload.targetUrl, 'https://remote.example.com/api/remote-account/inject');
assert.equal(logs.some(({ message }) => message.includes('secret-sso-cookie') || message.includes('secret-admin-key')), false);
});
+59 -5
View File
@@ -37,6 +37,12 @@ test('grok state view projects canonical runtime state into legacy flat read fie
cookies: ['cookie-a', 'cookie-b', 'cookie-a'],
extractedAt: 3000,
},
upload: {
status: 'uploaded',
uploadedAt: 4000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
@@ -55,9 +61,14 @@ test('grok state view projects canonical runtime state into legacy flat read fie
assert.equal(view.grokSsoCookie, 'cookie-a');
assert.deepEqual(view.grokSsoCookies, ['cookie-a', 'cookie-b']);
assert.equal(view.grokSsoExtractedAt, 3000);
assert.equal(view.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(view.grokWebchat2ApiUploadedAt, 4000);
assert.equal(view.grokWebchat2ApiUploadMessage, 'ok');
assert.equal(view.grokWebchat2ApiTargetUrl, 'https://remote.example.com/api/remote-account/inject');
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.flowState.grok.upload.status, 'uploaded');
assert.equal(view.flows.grok.sso.cookies.length, 2);
});
@@ -69,6 +80,10 @@ test('grok completion payloads update canonical runtime state and flat compatibi
grokSsoCookie: 'cookie-z',
grokSsoCookies: ['cookie-z', 'cookie-z', 'cookie-y'],
grokCompletedAt: 456,
grokWebchat2ApiUploadStatus: 'uploaded',
grokWebchat2ApiUploadedAt: 789,
grokWebchat2ApiUploadMessage: '上传成功',
grokWebchat2ApiTargetUrl: 'http://remote.example.com/api/remote-account/inject',
});
assert.equal(patch.grokEmail, 'grok@example.com');
@@ -77,11 +92,39 @@ test('grok completion payloads update canonical runtime state and flat compatibi
assert.deepEqual(patch.grokSsoCookies, ['cookie-z', 'cookie-y']);
assert.equal(patch.grokCompletedAt, 456);
assert.equal(patch.grokSsoExtractedAt, 456);
assert.equal(patch.grokWebchat2ApiUploadStatus, 'uploaded');
assert.equal(patch.grokWebchat2ApiUploadedAt, 789);
assert.equal(patch.grokWebchat2ApiUploadMessage, '上传成功');
assert.equal(patch.grokWebchat2ApiTargetUrl, 'http://remote.example.com/api/remote-account/inject');
assert.equal(patch.runtimeState.flowState.grok.register.email, 'grok@example.com');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-z');
assert.equal(patch.runtimeState.flowState.grok.upload.status, 'uploaded');
});
test('grok fresh keep-state reset preserves SSO cookies but clears registration runtime', () => {
test('grok state keeps canonical runtime values when flat compatibility fields are empty', () => {
const api = loadGrokStateApi();
const runtimeState = api.ensureRuntimeState({
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: 'canonical-cookie',
cookies: ['canonical-cookie'],
extractedAt: 1234,
},
},
},
},
});
assert.equal(runtimeState.sso.currentCookie, 'canonical-cookie');
assert.deepEqual(runtimeState.sso.cookies, ['canonical-cookie']);
assert.equal(runtimeState.sso.extractedAt, 1234);
});
test('grok fresh keep-state reset clears registration, SSO, and upload runtime', () => {
const api = loadGrokStateApi();
const patch = api.buildFreshKeepState({
runtimeState: {
@@ -101,6 +144,12 @@ test('grok fresh keep-state reset preserves SSO cookies but clears registration
cookies: ['cookie-a', 'cookie-b'],
extractedAt: 2000,
},
upload: {
status: 'uploaded',
uploadedAt: 3000,
message: 'ok',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
@@ -111,11 +160,16 @@ test('grok fresh keep-state reset preserves SSO cookies but clears registration
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.grokSsoCookie, '');
assert.deepEqual(patch.grokSsoCookies, []);
assert.equal(patch.grokSsoExtractedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadStatus, '');
assert.equal(patch.grokWebchat2ApiUploadedAt, 0);
assert.equal(patch.grokWebchat2ApiUploadMessage, '');
assert.equal(patch.grokWebchat2ApiTargetUrl, '');
assert.equal(patch.runtimeState.flowState.grok.register.email, '');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, 'cookie-a');
assert.equal(patch.runtimeState.flowState.grok.sso.currentCookie, '');
assert.equal(patch.runtimeState.flowState.grok.upload.status, '');
});
test('grok downstream reset clears only the state owned by the restarted tail', () => {
+4 -1
View File
@@ -19,12 +19,14 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
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.match(source, /flows\/grok\/background\/publisher-webchat2api\.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, /const grokWebchat2ApiPublisher = self\.MultiPageBackgroundGrokPublisherWebchat2Api\?\.createGrokWebchat2ApiPublisher\(/);
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
@@ -40,6 +42,7 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
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, /'grok-upload-sso-to-webchat2api': \(state\) => grokWebchat2ApiPublisher\.executeGrokUploadSsoToWebchat2Api\(state\)/);
assert.match(
source,
@@ -47,7 +50,7 @@ test('background imports node registry and wires the rebuilt Kiro executors', ()
);
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'/
/'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',[\s\S]*'grok-upload-sso-to-webchat2api'/
);
});
+1 -1
View File
@@ -153,7 +153,7 @@ test('settings schema can project canonical state into a read view without legac
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
enabled: false,
fromStep: 1,
toStep: 5,
toStep: 6,
});
});
+14 -5
View File
@@ -110,7 +110,7 @@ test('grok verification runner polls by flow node and submits normalized code',
assert.equal(getGrokRuntime(completedPayload).register.status, 'verified');
});
test('grok SSO extraction accumulates unique cookies without logging the secret value', async () => {
test('grok SSO extraction stores only the current cookie without logging the secret value', async () => {
const api = loadGrokRunnerApi();
const logs = [];
let completedPayload = null;
@@ -118,7 +118,7 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
let currentState = {
activeFlowId: 'grok',
grokRegisterTabId: 202,
grokSsoCookies: ['existing-cookie', 'new-cookie'],
grokSsoCookies: ['old-cookie'],
runtimeState: {
flowState: {
grok: {
@@ -126,7 +126,13 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
registerTabId: 202,
},
sso: {
cookies: ['existing-cookie', 'new-cookie'],
cookies: ['old-cookie'],
},
upload: {
status: 'uploaded',
uploadedAt: 1000,
message: 'old upload',
targetUrl: 'https://old.example.com/api/remote-account/inject',
},
},
},
@@ -169,9 +175,12 @@ test('grok SSO extraction accumulates unique cookies without logging the secret
await runner.executeGrokExtractSsoCookie({ nodeId: 'grok-extract-sso-cookie', ...currentState });
assert.equal(completedPayload.grokSsoCookie, 'new-cookie');
assert.deepEqual(completedPayload.grokSsoCookies, ['existing-cookie', 'new-cookie']);
assert.deepEqual(completedPayload.grokSsoCookies, ['new-cookie']);
assert.equal(completedPayload.grokWebchat2ApiUploadStatus, '');
assert.equal(getGrokRuntime(completedPayload).sso.currentCookie, 'new-cookie');
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['existing-cookie', 'new-cookie']);
assert.deepEqual(getGrokRuntime(completedPayload).sso.cookies, ['new-cookie']);
assert.equal(getGrokRuntime(completedPayload).upload.status, '');
assert.equal(getGrokRuntime(completedPayload).upload.targetUrl, '');
assert.equal(markUsedPayload.grokSsoCookie, 'new-cookie');
assert.equal(logs.some(({ message }) => message.includes('new-cookie')), false);
});
+14 -4
View File
@@ -49,15 +49,17 @@ test('sidepanel html exposes flow selector and kiro source fields', () => {
'id="row-kiro-upload-status"',
'id="row-grok-register-status"',
'id="row-grok-sso-status"',
'id="row-grok-webchat2api-upload-status"',
'id="display-grok-webchat2api-upload-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.doesNotMatch(sidepanelHtml, /id="btn-export-grok-sso"/);
assert.ok(
sidepanelHtml.indexOf('<script src="../flows/kiro/workflow.js"></script>')
< sidepanelHtml.indexOf('<script src="../flows/grok/index.js"></script>')
@@ -86,6 +88,7 @@ test('sidepanel renders Grok SSO status from canonical runtime state', () => {
extractFunction(sidepanelSource, 'getGrokRuntimeState'),
extractFunction(sidepanelSource, 'normalizeGrokSsoCookies'),
extractFunction(sidepanelSource, 'getGrokRegisterStatusLabel'),
extractFunction(sidepanelSource, 'getGrokWebchat2ApiUploadStatusLabel'),
extractFunction(sidepanelSource, 'renderGrokRuntimeState'),
].join('\n');
@@ -94,17 +97,17 @@ let latestState = {};
const displayGrokRegisterStatus = { textContent: '' };
const displayGrokSsoStatus = { textContent: '' };
const displayGrokSsoCookie = { textContent: '', title: '' };
const displayGrokWebchat2ApiUploadStatus = { textContent: '', title: '' };
const buttons = [];
const btnCopyGrokSso = { disabled: false };
const btnExportGrokSso = { disabled: false };
const btnClearGrokSso = { disabled: false };
${bundle}
return {
displayGrokRegisterStatus,
displayGrokSsoStatus,
displayGrokSsoCookie,
displayGrokWebchat2ApiUploadStatus,
btnCopyGrokSso,
btnExportGrokSso,
btnClearGrokSso,
renderGrokRuntimeState,
};
@@ -120,6 +123,12 @@ return {
cookies: ['1234567890abcdef', 'second-cookie'],
extractedAt: 0,
},
upload: {
status: 'uploaded',
uploadedAt: 0,
message: '上传成功',
targetUrl: 'https://remote.example.com/api/remote-account/inject',
},
},
},
},
@@ -128,8 +137,9 @@ return {
assert.equal(api.displayGrokRegisterStatus.textContent, '已完成');
assert.match(api.displayGrokSsoStatus.textContent, /^已提取 2 条/);
assert.equal(api.displayGrokSsoCookie.textContent, '12345678...abcdef');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.textContent, '已上传:上传成功');
assert.equal(api.displayGrokWebchat2ApiUploadStatus.title, 'https://remote.example.com/api/remote-account/inject');
assert.equal(api.btnCopyGrokSso.disabled, false);
assert.equal(api.btnExportGrokSso.disabled, false);
assert.equal(api.btnClearGrokSso.disabled, false);
});
+1
View File
@@ -197,4 +197,5 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
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);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/publisher-webchat2api', 'grok-upload-sso-to-webchat2api'), true);
});
+5 -3
View File
@@ -220,6 +220,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'grok-submit-verification-code',
'grok-submit-profile',
'grok-extract-sso-cookie',
'grok-upload-sso-to-webchat2api',
]
);
assert.equal(grokSteps.every((step) => step.flowId === 'grok'), true);
@@ -228,10 +229,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(grokSteps[2].mailRuleId, 'grok-submit-verification-code');
assert.deepStrictEqual(
grokSteps.map((step) => step.title),
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie']
['打开 Grok 注册页', '获取邮箱并继续', '获取验证码并继续', '填写资料并继续', '提取 SSO Cookie', '上传 SSO 到 webchat2api']
);
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5]);
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 5);
assert.deepStrictEqual(api.getStepIds({ activeFlowId: 'grok' }), [1, 2, 3, 4, 5, 6]);
assert.equal(api.getLastStepId({ activeFlowId: 'grok' }), 6);
assert.deepStrictEqual(
api.getNodes({ activeFlowId: 'grok' }).map((node) => node.next),
[
@@ -239,6 +240,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
['grok-submit-verification-code'],
['grok-submit-profile'],
['grok-extract-sso-cookie'],
['grok-upload-sso-to-webchat2api'],
[],
]
);