@iblai/iblai-js 1.8.4 → 1.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data-layer/playwright/claw-sandbox-helpers.d.ts +266 -0
- package/dist/data-layer/playwright/index.d.ts +2 -0
- package/dist/playwright/index.cjs +865 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.ts +267 -2
- package/dist/playwright/index.esm.js +816 -1
- package/dist/playwright/index.esm.js.map +1 -1
- package/dist/playwright/playwright/claw-sandbox-helpers.d.ts +266 -0
- package/dist/playwright/playwright/index.d.ts +2 -0
- package/dist/web-containers/playwright/claw-sandbox-helpers.d.ts +266 -0
- package/dist/web-containers/playwright/index.d.ts +2 -0
- package/dist/web-containers/source/index.esm.js +943 -125
- package/dist/web-containers/source/next/index.esm.js +87 -95
- package/dist/web-utils/playwright/claw-sandbox-helpers.d.ts +266 -0
- package/dist/web-utils/playwright/index.d.ts +2 -0
- package/package.json +5 -4
|
@@ -1713,6 +1713,821 @@ async function verifyMemoryNotExists(page, content) {
|
|
|
1713
1713
|
logger.info(`Verified memory removed: "${content}"`);
|
|
1714
1714
|
}
|
|
1715
1715
|
|
|
1716
|
+
// ============================
|
|
1717
|
+
// Navigation Helpers
|
|
1718
|
+
// ============================
|
|
1719
|
+
/**
|
|
1720
|
+
* Check if the Sandbox tab is visible in the edit-mentor modal.
|
|
1721
|
+
* Returns true if the tab exists and is visible (i.e. mentor has is_claw_enabled === true).
|
|
1722
|
+
*/
|
|
1723
|
+
async function isSandboxTabVisible(page) {
|
|
1724
|
+
const tab = page.getByRole('tab', { name: 'Sandbox', exact: true });
|
|
1725
|
+
try {
|
|
1726
|
+
await test$1.expect(tab).toBeVisible({ timeout: 5000 });
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
catch (_a) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Switch to the Sandbox tab inside the edit-mentor modal.
|
|
1735
|
+
* Assumes the modal is already open and the user has permission to see the tab.
|
|
1736
|
+
*/
|
|
1737
|
+
async function switchToSandboxTab(page) {
|
|
1738
|
+
const tab = page.getByRole('tab', { name: 'Sandbox', exact: true });
|
|
1739
|
+
await test$1.expect(tab).toBeVisible({ timeout: 10000 });
|
|
1740
|
+
await tab.click();
|
|
1741
|
+
await test$1.expect(page.getByRole('heading', { name: 'Sandbox' }).or(page.getByText('Sandbox'))).toBeVisible({ timeout: 10000 });
|
|
1742
|
+
logger.info('Switched to Sandbox tab');
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Switch to the Skills tab inside the edit-mentor modal.
|
|
1746
|
+
*/
|
|
1747
|
+
async function switchToSkillsTab(page) {
|
|
1748
|
+
const tab = page.getByRole('tab', { name: 'Skills', exact: true });
|
|
1749
|
+
await test$1.expect(tab).toBeVisible({ timeout: 10000 });
|
|
1750
|
+
await tab.click();
|
|
1751
|
+
logger.info('Switched to Skills tab');
|
|
1752
|
+
}
|
|
1753
|
+
// ============================
|
|
1754
|
+
// Instance Table Helpers (not-connected state)
|
|
1755
|
+
// ============================
|
|
1756
|
+
/**
|
|
1757
|
+
* Verify the instance table is visible with all expected column headers.
|
|
1758
|
+
* Should be called when the mentor has no connected sandbox instance.
|
|
1759
|
+
*/
|
|
1760
|
+
async function verifyInstanceTableVisible(page) {
|
|
1761
|
+
const table = page.locator('table');
|
|
1762
|
+
await test$1.expect(table).toBeVisible({ timeout: 15000 });
|
|
1763
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'NAME' })).toBeVisible();
|
|
1764
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'URL' })).toBeVisible();
|
|
1765
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'TYPE' })).toBeVisible();
|
|
1766
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'STATUS' })).toBeVisible();
|
|
1767
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'HEALTH' })).toBeVisible();
|
|
1768
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'VERSION' })).toBeVisible();
|
|
1769
|
+
await test$1.expect(table.locator('th').filter({ hasText: 'LAST CHECK' })).toBeVisible();
|
|
1770
|
+
logger.info('Instance table is visible with correct headers');
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Get the number of rows currently displayed in the instance table (excluding empty-state rows).
|
|
1774
|
+
*/
|
|
1775
|
+
async function getInstanceRowCount(page) {
|
|
1776
|
+
const rows = page.locator('table tbody tr').filter({ hasNot: page.locator('td[colspan]') });
|
|
1777
|
+
const count = await rows.count();
|
|
1778
|
+
logger.info(`Instance table has ${count} rows`);
|
|
1779
|
+
return count;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Verify the empty state is shown when there are no instances.
|
|
1783
|
+
*/
|
|
1784
|
+
async function verifyInstanceTableEmpty(page) {
|
|
1785
|
+
await test$1.expect(page.getByText(/No instances available/i)).toBeVisible({ timeout: 10000 });
|
|
1786
|
+
logger.info('Instance table empty state is displayed');
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Filter the instance table by search query (name or URL).
|
|
1790
|
+
*/
|
|
1791
|
+
async function searchInstances(page, query) {
|
|
1792
|
+
const searchInput = page.getByPlaceholder('Search instances...');
|
|
1793
|
+
await test$1.expect(searchInput).toBeVisible({ timeout: 10000 });
|
|
1794
|
+
await searchInput.fill(query);
|
|
1795
|
+
// Let the filter apply
|
|
1796
|
+
await page.waitForTimeout(300);
|
|
1797
|
+
logger.info(`Searched instances with query: "${query}"`);
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Clear the instance table search query.
|
|
1801
|
+
*/
|
|
1802
|
+
async function clearInstanceSearch(page) {
|
|
1803
|
+
const searchInput = page.getByPlaceholder('Search instances...');
|
|
1804
|
+
await searchInput.fill('');
|
|
1805
|
+
await page.waitForTimeout(300);
|
|
1806
|
+
logger.info('Cleared instance search');
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Open the Actions dropdown menu for a specific instance row.
|
|
1810
|
+
* Returns the dropdown menu locator.
|
|
1811
|
+
*/
|
|
1812
|
+
async function openInstanceActionsMenu(page, instanceName) {
|
|
1813
|
+
const row = page.locator('table tbody tr').filter({ hasText: instanceName });
|
|
1814
|
+
await test$1.expect(row).toBeVisible({ timeout: 10000 });
|
|
1815
|
+
const actionsButton = row.getByRole('button', { name: 'Actions' });
|
|
1816
|
+
await test$1.expect(actionsButton).toBeVisible({ timeout: 5000 });
|
|
1817
|
+
await actionsButton.click();
|
|
1818
|
+
const menu = page.getByRole('menu');
|
|
1819
|
+
await test$1.expect(menu).toBeVisible({ timeout: 5000 });
|
|
1820
|
+
return menu;
|
|
1821
|
+
}
|
|
1822
|
+
// ============================
|
|
1823
|
+
// Create Instance Helpers
|
|
1824
|
+
// ============================
|
|
1825
|
+
/**
|
|
1826
|
+
* Open the "New Instance" dialog by clicking the Add Instance button.
|
|
1827
|
+
* Returns the dialog locator.
|
|
1828
|
+
*/
|
|
1829
|
+
async function openNewInstanceDialog(page) {
|
|
1830
|
+
const addButton = page.getByRole('button', { name: /Add Instance/i });
|
|
1831
|
+
await test$1.expect(addButton).toBeVisible({ timeout: 10000 });
|
|
1832
|
+
await addButton.click();
|
|
1833
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'New Instance' });
|
|
1834
|
+
return waitForDialogReady(page, dialog);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Fill the New Instance form and submit it.
|
|
1838
|
+
* @param name - Display name for the instance
|
|
1839
|
+
* @param serverUrl - Fully qualified https URL
|
|
1840
|
+
* @param clawType - 'openclaw' (default) or 'ironclaw'
|
|
1841
|
+
* @param gatewayToken - Optional auth token
|
|
1842
|
+
*/
|
|
1843
|
+
async function createInstance(page, { name, serverUrl, clawType = 'openclaw', gatewayToken, }) {
|
|
1844
|
+
await openNewInstanceDialog(page);
|
|
1845
|
+
await page.getByLabel('Name').fill(name);
|
|
1846
|
+
await page.getByLabel('Server URL').fill(serverUrl);
|
|
1847
|
+
if (clawType !== 'openclaw') {
|
|
1848
|
+
const typeSelect = page.getByLabel('Type');
|
|
1849
|
+
await typeSelect.click();
|
|
1850
|
+
const option = page.getByRole('option', { name: new RegExp(clawType, 'i') });
|
|
1851
|
+
await test$1.expect(option).toBeVisible({ timeout: 5000 });
|
|
1852
|
+
await option.click();
|
|
1853
|
+
}
|
|
1854
|
+
if (gatewayToken) {
|
|
1855
|
+
await page.getByLabel('Gateway Token').fill(gatewayToken);
|
|
1856
|
+
}
|
|
1857
|
+
const createButton = page.getByRole('button', { name: /^Create$/i });
|
|
1858
|
+
await test$1.expect(createButton).toBeEnabled({ timeout: 5000 });
|
|
1859
|
+
await createButton.click();
|
|
1860
|
+
await test$1.expect(page.getByText('Instance created', { exact: false })).toBeVisible({
|
|
1861
|
+
timeout: 10000,
|
|
1862
|
+
});
|
|
1863
|
+
logger.info(`Created instance "${name}" at ${serverUrl}`);
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Cancel the New Instance dialog without submitting.
|
|
1867
|
+
*/
|
|
1868
|
+
async function cancelNewInstanceDialog(page) {
|
|
1869
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'New Instance' });
|
|
1870
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
1871
|
+
await page.keyboard.press('Escape');
|
|
1872
|
+
await test$1.expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
1873
|
+
logger.info('Cancelled New Instance dialog');
|
|
1874
|
+
}
|
|
1875
|
+
// ============================
|
|
1876
|
+
// Edit Instance Helpers
|
|
1877
|
+
// ============================
|
|
1878
|
+
/**
|
|
1879
|
+
* Open the Edit dialog for an instance from its Actions menu.
|
|
1880
|
+
* Returns the edit dialog locator.
|
|
1881
|
+
*/
|
|
1882
|
+
async function openEditInstanceDialog(page, instanceName) {
|
|
1883
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
1884
|
+
const editItem = page.getByRole('menuitem', { name: /Edit/i });
|
|
1885
|
+
await test$1.expect(editItem).toBeVisible({ timeout: 5000 });
|
|
1886
|
+
await editItem.click();
|
|
1887
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Edit Instance' });
|
|
1888
|
+
return waitForDialogReady(page, dialog);
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Edit an existing instance with new values.
|
|
1892
|
+
* @param instanceName - The current name of the instance to edit
|
|
1893
|
+
* @param updates - Fields to update
|
|
1894
|
+
*/
|
|
1895
|
+
async function editInstance(page, instanceName, updates) {
|
|
1896
|
+
await openEditInstanceDialog(page, instanceName);
|
|
1897
|
+
if (updates.name !== undefined) {
|
|
1898
|
+
const nameInput = page.getByLabel('Name');
|
|
1899
|
+
await nameInput.fill(updates.name);
|
|
1900
|
+
}
|
|
1901
|
+
if (updates.serverUrl !== undefined) {
|
|
1902
|
+
const urlInput = page.getByLabel('Server URL');
|
|
1903
|
+
await urlInput.fill(updates.serverUrl);
|
|
1904
|
+
}
|
|
1905
|
+
if (updates.clawType !== undefined) {
|
|
1906
|
+
const typeSelect = page.getByLabel('Type');
|
|
1907
|
+
await typeSelect.click();
|
|
1908
|
+
const option = page.getByRole('option', { name: new RegExp(updates.clawType, 'i') });
|
|
1909
|
+
await test$1.expect(option).toBeVisible({ timeout: 5000 });
|
|
1910
|
+
await option.click();
|
|
1911
|
+
}
|
|
1912
|
+
const saveButton = page.getByRole('button', { name: /^Save$/i });
|
|
1913
|
+
await test$1.expect(saveButton).toBeEnabled({ timeout: 5000 });
|
|
1914
|
+
await saveButton.click();
|
|
1915
|
+
await test$1.expect(page.getByText('Instance updated', { exact: false })).toBeVisible({
|
|
1916
|
+
timeout: 10000,
|
|
1917
|
+
});
|
|
1918
|
+
logger.info(`Edited instance "${instanceName}" with updates: ${JSON.stringify(updates)}`);
|
|
1919
|
+
}
|
|
1920
|
+
// ============================
|
|
1921
|
+
// Delete Instance Helpers
|
|
1922
|
+
// ============================
|
|
1923
|
+
/**
|
|
1924
|
+
* Delete an instance via the Actions menu, confirming the deletion dialog.
|
|
1925
|
+
*/
|
|
1926
|
+
async function deleteInstance(page, instanceName) {
|
|
1927
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
1928
|
+
const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
|
|
1929
|
+
await test$1.expect(deleteItem).toBeVisible({ timeout: 5000 });
|
|
1930
|
+
await deleteItem.click();
|
|
1931
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Instance' });
|
|
1932
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
1933
|
+
const confirmButton = dialog.getByRole('button', { name: /^Delete$/i });
|
|
1934
|
+
await test$1.expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
1935
|
+
await confirmButton.click();
|
|
1936
|
+
await test$1.expect(page.getByText('Instance deleted', { exact: false })).toBeVisible({
|
|
1937
|
+
timeout: 10000,
|
|
1938
|
+
});
|
|
1939
|
+
logger.info(`Deleted instance "${instanceName}"`);
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Open the delete confirmation dialog and cancel it without deleting.
|
|
1943
|
+
*/
|
|
1944
|
+
async function cancelDeleteInstance(page, instanceName) {
|
|
1945
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
1946
|
+
const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
|
|
1947
|
+
await deleteItem.click();
|
|
1948
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Instance' });
|
|
1949
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
1950
|
+
const cancelButton = dialog.getByRole('button', { name: /Cancel/i });
|
|
1951
|
+
await cancelButton.click();
|
|
1952
|
+
await test$1.expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
1953
|
+
logger.info(`Cancelled delete of instance "${instanceName}"`);
|
|
1954
|
+
}
|
|
1955
|
+
// ============================
|
|
1956
|
+
// Connect / Disconnect Helpers
|
|
1957
|
+
// ============================
|
|
1958
|
+
/**
|
|
1959
|
+
* Connect the current mentor to an instance via the Actions menu.
|
|
1960
|
+
*/
|
|
1961
|
+
async function connectToInstance(page, instanceName) {
|
|
1962
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
1963
|
+
const connectItem = page.getByRole('menuitem', { name: /Connect/i });
|
|
1964
|
+
await test$1.expect(connectItem).toBeVisible({ timeout: 5000 });
|
|
1965
|
+
await connectItem.click();
|
|
1966
|
+
await test$1.expect(page.getByText('Instance connected', { exact: false })).toBeVisible({
|
|
1967
|
+
timeout: 10000,
|
|
1968
|
+
});
|
|
1969
|
+
// After connect, the UI should switch to show the Connected Instance card
|
|
1970
|
+
await test$1.expect(page.getByText('Connected Instance')).toBeVisible({ timeout: 10000 });
|
|
1971
|
+
logger.info(`Connected mentor to instance "${instanceName}"`);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Disconnect the current mentor from its connected instance,
|
|
1975
|
+
* confirming the disconnect dialog.
|
|
1976
|
+
*/
|
|
1977
|
+
async function disconnectInstance(page) {
|
|
1978
|
+
const disconnectButton = page.getByRole('button', { name: /^Disconnect$/i });
|
|
1979
|
+
await test$1.expect(disconnectButton).toBeVisible({ timeout: 10000 });
|
|
1980
|
+
await disconnectButton.click();
|
|
1981
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Disconnect Instance' });
|
|
1982
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
1983
|
+
const confirmButton = dialog.getByRole('button', { name: /^Disconnect$/i });
|
|
1984
|
+
await confirmButton.click();
|
|
1985
|
+
await test$1.expect(page.getByText('Instance disconnected', { exact: false })).toBeVisible({
|
|
1986
|
+
timeout: 10000,
|
|
1987
|
+
});
|
|
1988
|
+
logger.info('Disconnected mentor from instance');
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Open the disconnect confirmation dialog and cancel it.
|
|
1992
|
+
*/
|
|
1993
|
+
async function cancelDisconnectInstance(page) {
|
|
1994
|
+
const disconnectButton = page.getByRole('button', { name: /^Disconnect$/i });
|
|
1995
|
+
await disconnectButton.click();
|
|
1996
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Disconnect Instance' });
|
|
1997
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
1998
|
+
const cancelButton = dialog.getByRole('button', { name: /Cancel/i });
|
|
1999
|
+
await cancelButton.click();
|
|
2000
|
+
await test$1.expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
2001
|
+
logger.info('Cancelled disconnect');
|
|
2002
|
+
}
|
|
2003
|
+
// ============================
|
|
2004
|
+
// Health-check / Connectivity Helpers
|
|
2005
|
+
// ============================
|
|
2006
|
+
/**
|
|
2007
|
+
* Open an instance's actions menu and click "Run checks". Resolves once the
|
|
2008
|
+
* health-check + connectivity-test toasts have surfaced (success or error).
|
|
2009
|
+
*/
|
|
2010
|
+
async function runInstanceChecks(page, instanceName) {
|
|
2011
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
2012
|
+
const runChecks = page.getByRole('menuitem', { name: /Run checks/i });
|
|
2013
|
+
await test$1.expect(runChecks).toBeVisible({ timeout: 5000 });
|
|
2014
|
+
await runChecks.click();
|
|
2015
|
+
// Each action toasts independently. We don't assert success vs. error here —
|
|
2016
|
+
// callers can inspect the row's status/health afterwards via `getInstanceHealthLabel`.
|
|
2017
|
+
await test$1.expect(page.getByText(/Health check (passed|failed)|Connectivity test (passed|failed)/i).first()).toBeVisible({ timeout: 15000 });
|
|
2018
|
+
logger.info(`Ran checks for instance "${instanceName}"`);
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Trigger the "Run checks" button on the connected-instance card. Used for the
|
|
2022
|
+
* connected-state flow rather than the table dropdown.
|
|
2023
|
+
*/
|
|
2024
|
+
async function runConnectedInstanceChecks(page) {
|
|
2025
|
+
await test$1.expect(page.getByText('Connected Instance')).toBeVisible({ timeout: 10000 });
|
|
2026
|
+
const runChecks = page.getByRole('button', { name: /Run checks/i });
|
|
2027
|
+
await test$1.expect(runChecks).toBeVisible({ timeout: 5000 });
|
|
2028
|
+
await test$1.expect(runChecks).toBeEnabled();
|
|
2029
|
+
await runChecks.click();
|
|
2030
|
+
await test$1.expect(page.getByText(/Health check (passed|failed)|Connectivity test (passed|failed)/i).first()).toBeVisible({ timeout: 15000 });
|
|
2031
|
+
logger.info('Ran checks on connected instance');
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Read the canonical health label rendered by `StatusDot` for the given row.
|
|
2035
|
+
* Returns "Healthy" / "Unhealthy" / null (em-dash for unknown).
|
|
2036
|
+
*/
|
|
2037
|
+
async function getInstanceHealthLabel(page, instanceName) {
|
|
2038
|
+
const row = page.locator('table tbody tr').filter({ hasText: instanceName });
|
|
2039
|
+
await test$1.expect(row).toBeVisible({ timeout: 10000 });
|
|
2040
|
+
// Health is the 5th data cell (Name, URL, Type, Status, Health, …).
|
|
2041
|
+
const healthCell = row.locator('td').nth(4);
|
|
2042
|
+
const text = (await healthCell.innerText()).trim();
|
|
2043
|
+
if (text === 'Healthy' || text === 'Unhealthy')
|
|
2044
|
+
return text;
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Read the canonical status label rendered by `StatusDot` for the given row.
|
|
2049
|
+
* Returns "Active" / "Error" / null (em-dash for unknown).
|
|
2050
|
+
*/
|
|
2051
|
+
async function getInstanceStatusLabel(page, instanceName) {
|
|
2052
|
+
const row = page.locator('table tbody tr').filter({ hasText: instanceName });
|
|
2053
|
+
await test$1.expect(row).toBeVisible({ timeout: 10000 });
|
|
2054
|
+
// Status is the 4th data cell (Name, URL, Type, Status, …).
|
|
2055
|
+
const statusCell = row.locator('td').nth(3);
|
|
2056
|
+
const text = (await statusCell.innerText()).trim();
|
|
2057
|
+
if (text === 'Active' || text === 'Error')
|
|
2058
|
+
return text;
|
|
2059
|
+
return null;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Verify that the Connect dropdown item is blocked (dimmed + cursor-not-allowed)
|
|
2063
|
+
* because the instance status is unhealthy. Hovering should reveal the unhealthy
|
|
2064
|
+
* tooltip explaining why.
|
|
2065
|
+
*/
|
|
2066
|
+
async function verifyConnectDisabledForUnhealthy(page, instanceName) {
|
|
2067
|
+
await openInstanceActionsMenu(page, instanceName);
|
|
2068
|
+
const connectItem = page.getByRole('menuitem', { name: /^Connect$/i });
|
|
2069
|
+
await test$1.expect(connectItem).toBeVisible({ timeout: 5000 });
|
|
2070
|
+
await test$1.expect(connectItem).toHaveClass(/opacity-50/);
|
|
2071
|
+
await test$1.expect(connectItem).toHaveClass(/cursor-not-allowed/);
|
|
2072
|
+
// Close the menu without firing Connect.
|
|
2073
|
+
await page.keyboard.press('Escape');
|
|
2074
|
+
logger.info(`Verified Connect is blocked for unhealthy instance "${instanceName}"`);
|
|
2075
|
+
}
|
|
2076
|
+
// ============================
|
|
2077
|
+
// Connected-State Helpers
|
|
2078
|
+
// ============================
|
|
2079
|
+
/**
|
|
2080
|
+
* Verify the Connected Instance card shows the expected fields.
|
|
2081
|
+
* The URL field shows the full server_url; other fields are status/health/last-check.
|
|
2082
|
+
*/
|
|
2083
|
+
async function verifyConnectedInstanceCard(page) {
|
|
2084
|
+
await test$1.expect(page.getByText('Connected Instance')).toBeVisible({ timeout: 10000 });
|
|
2085
|
+
// Labels inside the card
|
|
2086
|
+
await test$1.expect(page.getByText('Name', { exact: true })).toBeVisible();
|
|
2087
|
+
await test$1.expect(page.getByText('URL', { exact: true })).toBeVisible();
|
|
2088
|
+
await test$1.expect(page.getByText('Status', { exact: true })).toBeVisible();
|
|
2089
|
+
await test$1.expect(page.getByText('Health', { exact: true })).toBeVisible();
|
|
2090
|
+
await test$1.expect(page.getByText('Last Check', { exact: true })).toBeVisible();
|
|
2091
|
+
logger.info('Connected instance card is displayed with all expected fields');
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Toggle the Auto Push on Save switch.
|
|
2095
|
+
* Returns the new checked state.
|
|
2096
|
+
*/
|
|
2097
|
+
async function toggleAutoPush(page) {
|
|
2098
|
+
const switchEl = page
|
|
2099
|
+
.locator('div')
|
|
2100
|
+
.filter({ hasText: /^Auto Push on Save/ })
|
|
2101
|
+
.getByRole('switch')
|
|
2102
|
+
.first();
|
|
2103
|
+
await test$1.expect(switchEl).toBeVisible({ timeout: 10000 });
|
|
2104
|
+
const wasChecked = await switchEl.isChecked();
|
|
2105
|
+
await switchEl.click();
|
|
2106
|
+
await test$1.expect(page.getByText(/Configuration updated/i)).toBeVisible({ timeout: 10000 });
|
|
2107
|
+
const isNowChecked = await switchEl.isChecked();
|
|
2108
|
+
logger.info(`Toggled auto-push from ${wasChecked} to ${isNowChecked}`);
|
|
2109
|
+
return isNowChecked;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Click the Push button to push the current configuration to the worker.
|
|
2113
|
+
*/
|
|
2114
|
+
async function pushConfiguration(page) {
|
|
2115
|
+
const pushButton = page.getByRole('button', { name: /^Push$/i });
|
|
2116
|
+
await test$1.expect(pushButton).toBeVisible({ timeout: 10000 });
|
|
2117
|
+
await pushButton.click();
|
|
2118
|
+
await test$1.expect(page.getByText(/Configuration push queued/i)).toBeVisible({ timeout: 10000 });
|
|
2119
|
+
logger.info('Pushed configuration to worker');
|
|
2120
|
+
}
|
|
2121
|
+
// ============================
|
|
2122
|
+
// LLM Model Selection Helpers
|
|
2123
|
+
// ============================
|
|
2124
|
+
/**
|
|
2125
|
+
* Open the LLM provider picker by clicking Change (or Select Model) in the Model row.
|
|
2126
|
+
* Returns the picker dialog locator.
|
|
2127
|
+
*/
|
|
2128
|
+
async function openLLMProviderPicker(page) {
|
|
2129
|
+
const triggerButton = page.getByRole('button', { name: /^Change$|^Select Model$/i });
|
|
2130
|
+
await test$1.expect(triggerButton).toBeVisible({ timeout: 10000 });
|
|
2131
|
+
await triggerButton.click();
|
|
2132
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Select Provider' });
|
|
2133
|
+
return waitForDialogReady(page, dialog);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Pick an LLM provider + model combination via the provider picker flow.
|
|
2137
|
+
* @param providerName - The provider key (e.g. 'anthropic', 'openai')
|
|
2138
|
+
* @param modelName - The model name (e.g. 'claude-sonnet-4-5-20250929')
|
|
2139
|
+
*/
|
|
2140
|
+
async function selectLLMModel(page, providerName, modelName) {
|
|
2141
|
+
await openLLMProviderPicker(page);
|
|
2142
|
+
// Click the provider card
|
|
2143
|
+
const providerCard = page.getByRole('dialog').getByText(new RegExp(`^${providerName}$`, 'i'));
|
|
2144
|
+
await test$1.expect(providerCard).toBeVisible({ timeout: 10000 });
|
|
2145
|
+
await providerCard.click();
|
|
2146
|
+
// Model selection modal ("LLM Selection")
|
|
2147
|
+
const modelDialog = page.getByRole('dialog').filter({ hasText: 'LLM Selection' });
|
|
2148
|
+
await test$1.expect(modelDialog).toBeVisible({ timeout: 10000 });
|
|
2149
|
+
const modelButton = modelDialog.getByRole('button', { name: new RegExp(modelName, 'i') });
|
|
2150
|
+
await test$1.expect(modelButton).toBeVisible({ timeout: 10000 });
|
|
2151
|
+
await modelButton.click();
|
|
2152
|
+
await test$1.expect(page.getByText('Model updated', { exact: false })).toBeVisible({
|
|
2153
|
+
timeout: 10000,
|
|
2154
|
+
});
|
|
2155
|
+
logger.info(`Selected model ${providerName}/${modelName}`);
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Get the currently-selected model identifier shown next to the Model row.
|
|
2159
|
+
* Returns null if no model is set (button reads "Select Model").
|
|
2160
|
+
*/
|
|
2161
|
+
async function getCurrentModel(page) {
|
|
2162
|
+
var _a;
|
|
2163
|
+
const selectButton = page.getByRole('button', { name: /^Select Model$/i });
|
|
2164
|
+
const hasSelectButton = await selectButton.isVisible().catch(() => false);
|
|
2165
|
+
if (hasSelectButton)
|
|
2166
|
+
return null;
|
|
2167
|
+
// When a model is set, the row shows: Model <tooltip> <model-text> [Change]
|
|
2168
|
+
const modelRow = page
|
|
2169
|
+
.locator('div.rounded-lg.border')
|
|
2170
|
+
.filter({ has: page.getByRole('button', { name: /^Change$/i }) });
|
|
2171
|
+
const text = await modelRow.textContent();
|
|
2172
|
+
// Extract the model identifier — it's the text before "Change"
|
|
2173
|
+
const cleaned = (_a = text === null || text === void 0 ? void 0 : text.replace(/Change$/, '').replace(/^Model/, '').trim()) !== null && _a !== void 0 ? _a : null;
|
|
2174
|
+
logger.info(`Current model: ${cleaned}`);
|
|
2175
|
+
return cleaned;
|
|
2176
|
+
}
|
|
2177
|
+
// ============================
|
|
2178
|
+
// Agent Config Prompts Helpers (Prompts tab)
|
|
2179
|
+
// ============================
|
|
2180
|
+
const AGENT_PROMPT_LABELS = [
|
|
2181
|
+
'Identity',
|
|
2182
|
+
'Soul',
|
|
2183
|
+
'User Context',
|
|
2184
|
+
'Tools',
|
|
2185
|
+
'Agents',
|
|
2186
|
+
'Bootstrap',
|
|
2187
|
+
'Heartbeat',
|
|
2188
|
+
'Memory',
|
|
2189
|
+
];
|
|
2190
|
+
/**
|
|
2191
|
+
* Verify that all 8 agent config prompt fields are visible in the Prompts tab.
|
|
2192
|
+
* Only renders when the mentor has enable_claw === true on its settings.
|
|
2193
|
+
*
|
|
2194
|
+
* Note: PATCH on the agent-config endpoint is upsert — there's no separate
|
|
2195
|
+
* "Create Agent Config" button anymore. The first edit on any field bootstraps
|
|
2196
|
+
* the row.
|
|
2197
|
+
*/
|
|
2198
|
+
async function verifyAgentConfigPromptsVisible(page) {
|
|
2199
|
+
for (const label of AGENT_PROMPT_LABELS) {
|
|
2200
|
+
await test$1.expect(page.getByText(label, { exact: true })).toBeVisible({ timeout: 10000 });
|
|
2201
|
+
}
|
|
2202
|
+
logger.info('All agent config prompt fields are visible');
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Open the edit modal for a specific agent prompt field (e.g. Identity, Soul).
|
|
2206
|
+
*/
|
|
2207
|
+
async function openAgentPromptEditModal(page, field) {
|
|
2208
|
+
const row = page
|
|
2209
|
+
.locator('div.rounded-lg.border.p-6')
|
|
2210
|
+
.filter({ hasText: new RegExp(`^${field}$`) });
|
|
2211
|
+
const editButton = row.getByRole('button', { name: /Edit/i });
|
|
2212
|
+
await test$1.expect(editButton).toBeVisible({ timeout: 10000 });
|
|
2213
|
+
await editButton.click();
|
|
2214
|
+
const dialog = page.getByRole('dialog').filter({ hasText: `Edit ${field}` });
|
|
2215
|
+
return waitForDialogReady(page, dialog);
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Edit an agent prompt field (opens the modal, types content, saves).
|
|
2219
|
+
*/
|
|
2220
|
+
async function editAgentPrompt(page, field, content) {
|
|
2221
|
+
await openAgentPromptEditModal(page, field);
|
|
2222
|
+
// The RichTextEditor renders into a contenteditable element; fall back to textarea if simpler.
|
|
2223
|
+
const editorRegion = page.locator('[contenteditable="true"]').first();
|
|
2224
|
+
const textarea = page.locator('textarea').first();
|
|
2225
|
+
const hasEditor = await editorRegion.isVisible().catch(() => false);
|
|
2226
|
+
if (hasEditor) {
|
|
2227
|
+
await editorRegion.click();
|
|
2228
|
+
await page.keyboard.press('ControlOrMeta+A');
|
|
2229
|
+
await page.keyboard.press('Delete');
|
|
2230
|
+
await page.keyboard.type(content);
|
|
2231
|
+
}
|
|
2232
|
+
else {
|
|
2233
|
+
await textarea.fill(content);
|
|
2234
|
+
}
|
|
2235
|
+
const saveButton = page.getByRole('button', { name: /^Save$/i });
|
|
2236
|
+
await test$1.expect(saveButton).toBeEnabled({ timeout: 5000 });
|
|
2237
|
+
await saveButton.click();
|
|
2238
|
+
await test$1.expect(page.getByText(`${field} updated successfully`, { exact: false })).toBeVisible({
|
|
2239
|
+
timeout: 10000,
|
|
2240
|
+
});
|
|
2241
|
+
logger.info(`Edited agent prompt "${field}" with ${content.length} chars`);
|
|
2242
|
+
}
|
|
2243
|
+
// ============================
|
|
2244
|
+
// Skills Tab Helpers
|
|
2245
|
+
// ============================
|
|
2246
|
+
/**
|
|
2247
|
+
* Verify the Skills tab content is loaded (either showing skill rows or the empty state).
|
|
2248
|
+
*/
|
|
2249
|
+
async function verifySkillsTabVisible(page) {
|
|
2250
|
+
// Either at least one skill toggle row is visible, or the empty message is shown
|
|
2251
|
+
const hasSkills = await page
|
|
2252
|
+
.getByRole('switch')
|
|
2253
|
+
.first()
|
|
2254
|
+
.isVisible()
|
|
2255
|
+
.catch(() => false);
|
|
2256
|
+
if (hasSkills) {
|
|
2257
|
+
logger.info('Skills tab is visible with skills rendered');
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
await test$1.expect(page.getByText(/No skills available for this platform/i)).toBeVisible({
|
|
2261
|
+
timeout: 10000,
|
|
2262
|
+
});
|
|
2263
|
+
logger.info('Skills tab is visible with empty state');
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Verify that the "No skills available" empty state is shown.
|
|
2267
|
+
*/
|
|
2268
|
+
async function verifySkillsEmptyState(page) {
|
|
2269
|
+
await test$1.expect(page.getByText(/No skills available for this platform/i)).toBeVisible({
|
|
2270
|
+
timeout: 10000,
|
|
2271
|
+
});
|
|
2272
|
+
logger.info('Skills empty state is displayed');
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Count the number of skill rows rendered in the Skills tab.
|
|
2276
|
+
* Each row represents an available (enabled) platform skill.
|
|
2277
|
+
*/
|
|
2278
|
+
async function getSkillRowCount(page) {
|
|
2279
|
+
const rows = page.locator('div.rounded-lg.border.p-6').filter({ has: page.getByRole('switch') });
|
|
2280
|
+
const count = await rows.count();
|
|
2281
|
+
logger.info(`${count} skill row(s) displayed`);
|
|
2282
|
+
return count;
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Verify that a specific skill row is visible in the Skills tab.
|
|
2286
|
+
*/
|
|
2287
|
+
async function verifySkillVisible(page, skillName) {
|
|
2288
|
+
const row = page
|
|
2289
|
+
.locator('div.rounded-lg.border.p-6')
|
|
2290
|
+
.filter({ hasText: skillName })
|
|
2291
|
+
.filter({ has: page.getByRole('switch') });
|
|
2292
|
+
await test$1.expect(row).toBeVisible({ timeout: 10000 });
|
|
2293
|
+
logger.info(`Skill "${skillName}" is visible`);
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Check if a specific skill is currently enabled for the mentor.
|
|
2297
|
+
*/
|
|
2298
|
+
async function isSkillEnabled(page, skillName) {
|
|
2299
|
+
const switchEl = page.getByLabel(new RegExp(`^${skillName} (enabled|disabled)$`));
|
|
2300
|
+
await test$1.expect(switchEl).toBeVisible({ timeout: 10000 });
|
|
2301
|
+
const enabled = await switchEl.isChecked();
|
|
2302
|
+
logger.info(`Skill "${skillName}" is ${enabled ? 'enabled' : 'disabled'}`);
|
|
2303
|
+
return enabled;
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Enable a skill for the current mentor by toggling it on.
|
|
2307
|
+
* No-op if already enabled. Waits for the success toast.
|
|
2308
|
+
*/
|
|
2309
|
+
async function enableSkill(page, skillName) {
|
|
2310
|
+
const disabledSwitch = page.getByLabel(`${skillName} disabled`);
|
|
2311
|
+
const isDisabled = await disabledSwitch.isVisible().catch(() => false);
|
|
2312
|
+
if (!isDisabled) {
|
|
2313
|
+
logger.info(`Skill "${skillName}" already enabled`);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
await disabledSwitch.click();
|
|
2317
|
+
await test$1.expect(page.getByText(`${skillName} enabled`, { exact: false })).toBeVisible({
|
|
2318
|
+
timeout: 10000,
|
|
2319
|
+
});
|
|
2320
|
+
logger.info(`Enabled skill "${skillName}"`);
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Disable a skill for the current mentor by toggling it off.
|
|
2324
|
+
* No-op if already disabled. Waits for the success toast.
|
|
2325
|
+
*/
|
|
2326
|
+
async function disableSkill(page, skillName) {
|
|
2327
|
+
const enabledSwitch = page.getByLabel(`${skillName} enabled`);
|
|
2328
|
+
const isEnabled = await enabledSwitch.isVisible().catch(() => false);
|
|
2329
|
+
if (!isEnabled) {
|
|
2330
|
+
logger.info(`Skill "${skillName}" already disabled`);
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
await enabledSwitch.click();
|
|
2334
|
+
await test$1.expect(page.getByText(`${skillName} disabled`, { exact: false })).toBeVisible({
|
|
2335
|
+
timeout: 10000,
|
|
2336
|
+
});
|
|
2337
|
+
logger.info(`Disabled skill "${skillName}"`);
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Toggle a skill's enabled state (flips whatever the current state is).
|
|
2341
|
+
* Returns the new checked state.
|
|
2342
|
+
*/
|
|
2343
|
+
async function toggleSkill(page, skillName) {
|
|
2344
|
+
const wasEnabled = await isSkillEnabled(page, skillName);
|
|
2345
|
+
if (wasEnabled) {
|
|
2346
|
+
await disableSkill(page, skillName);
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
await enableSkill(page, skillName);
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Open the "New Skill" dialog by clicking the New Skill button in the Skills tab.
|
|
2354
|
+
* Returns the dialog locator.
|
|
2355
|
+
*/
|
|
2356
|
+
async function openNewSkillDialog(page) {
|
|
2357
|
+
const newButton = page.getByRole('button', { name: /^New Skill$/i });
|
|
2358
|
+
await test$1.expect(newButton).toBeVisible({ timeout: 10000 });
|
|
2359
|
+
await newButton.click();
|
|
2360
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'New Skill' });
|
|
2361
|
+
return waitForDialogReady(page, dialog);
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Type content into the RichTextEditor Instruction field.
|
|
2365
|
+
* The editor renders as a contenteditable element.
|
|
2366
|
+
*/
|
|
2367
|
+
async function fillInstructionEditor(page, content) {
|
|
2368
|
+
// RichTextEditor uses a contenteditable region
|
|
2369
|
+
const editor = page.locator('[contenteditable="true"]').first();
|
|
2370
|
+
const hasEditor = await editor.isVisible().catch(() => false);
|
|
2371
|
+
if (hasEditor) {
|
|
2372
|
+
await editor.click();
|
|
2373
|
+
await page.keyboard.press('ControlOrMeta+A');
|
|
2374
|
+
await page.keyboard.press('Delete');
|
|
2375
|
+
if (content) {
|
|
2376
|
+
await page.keyboard.type(content);
|
|
2377
|
+
}
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
// Fallback: a plain labelled input/textarea named "Instruction"
|
|
2381
|
+
const textarea = page.getByLabel('Instruction');
|
|
2382
|
+
await textarea.fill(content);
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Fill the skill form fields (name, slug, description, version, instruction).
|
|
2386
|
+
* The form is used by both the New and Edit dialogs.
|
|
2387
|
+
* The Instruction field is a rich-text (markdown) editor.
|
|
2388
|
+
*/
|
|
2389
|
+
async function fillSkillForm(page, values) {
|
|
2390
|
+
await page.getByLabel('Name').fill(values.name);
|
|
2391
|
+
await page.getByLabel('Slug').fill(values.slug);
|
|
2392
|
+
if (values.version !== undefined) {
|
|
2393
|
+
await page.getByLabel('Version').fill(values.version);
|
|
2394
|
+
}
|
|
2395
|
+
if (values.description !== undefined) {
|
|
2396
|
+
await page.getByLabel('Description').fill(values.description);
|
|
2397
|
+
}
|
|
2398
|
+
if (values.instruction !== undefined) {
|
|
2399
|
+
await fillInstructionEditor(page, values.instruction);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Create a new platform-level skill via the Skills tab.
|
|
2404
|
+
*/
|
|
2405
|
+
async function createSkill(page, values) {
|
|
2406
|
+
await openNewSkillDialog(page);
|
|
2407
|
+
await fillSkillForm(page, values);
|
|
2408
|
+
const createButton = page.getByRole('button', { name: /^Create$/i });
|
|
2409
|
+
await test$1.expect(createButton).toBeEnabled({ timeout: 5000 });
|
|
2410
|
+
await createButton.click();
|
|
2411
|
+
await test$1.expect(page.getByText('Skill created', { exact: false })).toBeVisible({
|
|
2412
|
+
timeout: 10000,
|
|
2413
|
+
});
|
|
2414
|
+
logger.info(`Created skill "${values.name}"`);
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Open the Actions dropdown for a specific skill row.
|
|
2418
|
+
* Returns the dropdown menu locator.
|
|
2419
|
+
*/
|
|
2420
|
+
async function openSkillActionsMenu(page, skillName) {
|
|
2421
|
+
const actionsButton = page.getByRole('button', { name: `${skillName} actions` });
|
|
2422
|
+
await test$1.expect(actionsButton).toBeVisible({ timeout: 10000 });
|
|
2423
|
+
await actionsButton.click();
|
|
2424
|
+
const menu = page.getByRole('menu');
|
|
2425
|
+
await test$1.expect(menu).toBeVisible({ timeout: 5000 });
|
|
2426
|
+
return menu;
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Open the Edit Skill dialog from the actions menu.
|
|
2430
|
+
* Returns the dialog locator.
|
|
2431
|
+
*/
|
|
2432
|
+
async function openEditSkillDialog(page, skillName) {
|
|
2433
|
+
await openSkillActionsMenu(page, skillName);
|
|
2434
|
+
const editItem = page.getByRole('menuitem', { name: /Edit/i });
|
|
2435
|
+
await test$1.expect(editItem).toBeVisible({ timeout: 5000 });
|
|
2436
|
+
await editItem.click();
|
|
2437
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Edit Skill' });
|
|
2438
|
+
return waitForDialogReady(page, dialog);
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Edit an existing platform-level skill.
|
|
2442
|
+
* @param skillName - The current name of the skill to edit
|
|
2443
|
+
* @param updates - Fields to update on the skill
|
|
2444
|
+
*/
|
|
2445
|
+
async function editSkill(page, skillName, updates) {
|
|
2446
|
+
await openEditSkillDialog(page, skillName);
|
|
2447
|
+
if (updates.name !== undefined) {
|
|
2448
|
+
await page.getByLabel('Name').fill(updates.name);
|
|
2449
|
+
}
|
|
2450
|
+
if (updates.slug !== undefined) {
|
|
2451
|
+
await page.getByLabel('Slug').fill(updates.slug);
|
|
2452
|
+
}
|
|
2453
|
+
if (updates.version !== undefined) {
|
|
2454
|
+
await page.getByLabel('Version').fill(updates.version);
|
|
2455
|
+
}
|
|
2456
|
+
if (updates.description !== undefined) {
|
|
2457
|
+
await page.getByLabel('Description').fill(updates.description);
|
|
2458
|
+
}
|
|
2459
|
+
if (updates.instruction !== undefined) {
|
|
2460
|
+
await fillInstructionEditor(page, updates.instruction);
|
|
2461
|
+
}
|
|
2462
|
+
const saveButton = page.getByRole('button', { name: /^Save$/i });
|
|
2463
|
+
await test$1.expect(saveButton).toBeEnabled({ timeout: 5000 });
|
|
2464
|
+
await saveButton.click();
|
|
2465
|
+
await test$1.expect(page.getByText('Skill updated', { exact: false })).toBeVisible({
|
|
2466
|
+
timeout: 10000,
|
|
2467
|
+
});
|
|
2468
|
+
logger.info(`Edited skill "${skillName}" with updates: ${JSON.stringify(updates)}`);
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Delete a platform-level skill, confirming the deletion dialog.
|
|
2472
|
+
*/
|
|
2473
|
+
async function deleteSkill(page, skillName) {
|
|
2474
|
+
await openSkillActionsMenu(page, skillName);
|
|
2475
|
+
const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
|
|
2476
|
+
await test$1.expect(deleteItem).toBeVisible({ timeout: 5000 });
|
|
2477
|
+
await deleteItem.click();
|
|
2478
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Skill' });
|
|
2479
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
2480
|
+
const confirmButton = dialog.getByRole('button', { name: /^Delete$/i });
|
|
2481
|
+
await test$1.expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
2482
|
+
await confirmButton.click();
|
|
2483
|
+
await test$1.expect(page.getByText('Skill deleted', { exact: false })).toBeVisible({
|
|
2484
|
+
timeout: 10000,
|
|
2485
|
+
});
|
|
2486
|
+
logger.info(`Deleted skill "${skillName}"`);
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Open the delete confirmation for a skill and cancel it (no deletion).
|
|
2490
|
+
*/
|
|
2491
|
+
async function cancelDeleteSkill(page, skillName) {
|
|
2492
|
+
await openSkillActionsMenu(page, skillName);
|
|
2493
|
+
const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
|
|
2494
|
+
await deleteItem.click();
|
|
2495
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Skill' });
|
|
2496
|
+
await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
|
|
2497
|
+
const cancelButton = dialog.getByRole('button', { name: /Cancel/i });
|
|
2498
|
+
await cancelButton.click();
|
|
2499
|
+
await test$1.expect(dialog).not.toBeVisible({ timeout: 5000 });
|
|
2500
|
+
logger.info(`Cancelled delete of skill "${skillName}"`);
|
|
2501
|
+
}
|
|
2502
|
+
// ============================
|
|
2503
|
+
// Combined Workflow Helpers
|
|
2504
|
+
// ============================
|
|
2505
|
+
/**
|
|
2506
|
+
* Full workflow: open Sandbox tab, create a new instance, connect the mentor to it.
|
|
2507
|
+
* Returns when the mentor is successfully connected.
|
|
2508
|
+
*/
|
|
2509
|
+
async function setupSandboxInstance(page, instance) {
|
|
2510
|
+
await switchToSandboxTab(page);
|
|
2511
|
+
await createInstance(page, instance);
|
|
2512
|
+
await connectToInstance(page, instance.name);
|
|
2513
|
+
await verifyConnectedInstanceCard(page);
|
|
2514
|
+
logger.info(`Sandbox setup complete for "${instance.name}"`);
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Full workflow: open Sandbox tab, disconnect the current instance (if any),
|
|
2518
|
+
* then delete it from the table.
|
|
2519
|
+
*/
|
|
2520
|
+
async function teardownSandboxInstance(page, instanceName) {
|
|
2521
|
+
await switchToSandboxTab(page);
|
|
2522
|
+
// If a connected instance is present, disconnect first
|
|
2523
|
+
const disconnectButton = page.getByRole('button', { name: /^Disconnect$/i });
|
|
2524
|
+
if (await disconnectButton.isVisible().catch(() => false)) {
|
|
2525
|
+
await disconnectInstance(page);
|
|
2526
|
+
}
|
|
2527
|
+
await deleteInstance(page, instanceName);
|
|
2528
|
+
logger.info(`Sandbox teardown complete for "${instanceName}"`);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
1716
2531
|
// ============================
|
|
1717
2532
|
// Navigation Helpers
|
|
1718
2533
|
// ============================
|
|
@@ -2625,8 +3440,13 @@ exports.billingCreditsSection = billingCreditsSection;
|
|
|
2625
3440
|
exports.billingPlanSection = billingPlanSection;
|
|
2626
3441
|
exports.buildReportUrl = buildReportUrl;
|
|
2627
3442
|
exports.canChatWithEmbedMentor = canChatWithEmbedMentor;
|
|
3443
|
+
exports.cancelDeleteInstance = cancelDeleteInstance;
|
|
3444
|
+
exports.cancelDeleteSkill = cancelDeleteSkill;
|
|
3445
|
+
exports.cancelDisconnectInstance = cancelDisconnectInstance;
|
|
3446
|
+
exports.cancelNewInstanceDialog = cancelNewInstanceDialog;
|
|
2628
3447
|
exports.checkAdminStatus = checkAdminStatus;
|
|
2629
3448
|
exports.clearDateRangeFilter = clearDateRangeFilter;
|
|
3449
|
+
exports.clearInstanceSearch = clearInstanceSearch;
|
|
2630
3450
|
exports.clickBackHome = clickBackHome;
|
|
2631
3451
|
exports.clickBillingAddCredits = clickBillingAddCredits;
|
|
2632
3452
|
exports.clickBillingManageBilling = clickBillingManageBilling;
|
|
@@ -2636,14 +3456,25 @@ exports.clickDownloadAgain = clickDownloadAgain;
|
|
|
2636
3456
|
exports.clickManualDownloadLink = clickManualDownloadLink;
|
|
2637
3457
|
exports.closeCreditBalanceDropdown = closeCreditBalanceDropdown;
|
|
2638
3458
|
exports.closeWithEsc = closeWithEsc;
|
|
3459
|
+
exports.connectToInstance = connectToInstance;
|
|
2639
3460
|
exports.createAuthSetup = createAuthSetup;
|
|
2640
3461
|
exports.createEnvConfig = createEnvConfig;
|
|
3462
|
+
exports.createInstance = createInstance;
|
|
2641
3463
|
exports.createPlaywrightConfig = createPlaywrightConfig;
|
|
3464
|
+
exports.createSkill = createSkill;
|
|
2642
3465
|
exports.creditBalancePanel = creditBalancePanel;
|
|
2643
3466
|
exports.creditBalancePlanBadge = creditBalancePlanBadge;
|
|
2644
3467
|
exports.creditBalanceTrigger = creditBalanceTrigger;
|
|
2645
3468
|
exports.deleteFirstMemory = deleteFirstMemory;
|
|
3469
|
+
exports.deleteInstance = deleteInstance;
|
|
2646
3470
|
exports.deleteMemoryByContent = deleteMemoryByContent;
|
|
3471
|
+
exports.deleteSkill = deleteSkill;
|
|
3472
|
+
exports.disableSkill = disableSkill;
|
|
3473
|
+
exports.disconnectInstance = disconnectInstance;
|
|
3474
|
+
exports.editAgentPrompt = editAgentPrompt;
|
|
3475
|
+
exports.editInstance = editInstance;
|
|
3476
|
+
exports.editSkill = editSkill;
|
|
3477
|
+
exports.enableSkill = enableSkill;
|
|
2647
3478
|
exports.expectBillingAutoRechargeSection = expectBillingAutoRechargeSection;
|
|
2648
3479
|
exports.expectBillingCreditsSection = expectBillingCreditsSection;
|
|
2649
3480
|
exports.expectBillingPlanSection = expectBillingPlanSection;
|
|
@@ -2672,10 +3503,15 @@ exports.getBillingPlanLabel = getBillingPlanLabel;
|
|
|
2672
3503
|
exports.getBrowserKey = getBrowserKey;
|
|
2673
3504
|
exports.getCreditBalancePlanLabel = getCreditBalancePlanLabel;
|
|
2674
3505
|
exports.getCreditBalanceRemaining = getCreditBalanceRemaining;
|
|
3506
|
+
exports.getCurrentModel = getCurrentModel;
|
|
2675
3507
|
exports.getCurrentTenantShowPaywall = getCurrentTenantShowPaywall;
|
|
3508
|
+
exports.getInstanceHealthLabel = getInstanceHealthLabel;
|
|
3509
|
+
exports.getInstanceRowCount = getInstanceRowCount;
|
|
3510
|
+
exports.getInstanceStatusLabel = getInstanceStatusLabel;
|
|
2676
3511
|
exports.getMemoryCount = getMemoryCount;
|
|
2677
3512
|
exports.getMentorIdFromUrl = getMentorIdFromUrl;
|
|
2678
3513
|
exports.getPaginationInfo = getPaginationInfo;
|
|
3514
|
+
exports.getSkillRowCount = getSkillRowCount;
|
|
2679
3515
|
exports.goToFirstPage = goToFirstPage;
|
|
2680
3516
|
exports.goToLastPage = goToLastPage;
|
|
2681
3517
|
exports.goToNextPage = goToNextPage;
|
|
@@ -2687,6 +3523,8 @@ exports.isJSON = isJSON;
|
|
|
2687
3523
|
exports.isMemoryTabVisible = isMemoryTabVisible;
|
|
2688
3524
|
exports.isOnFirstPage = isOnFirstPage;
|
|
2689
3525
|
exports.isOnLastPage = isOnLastPage;
|
|
3526
|
+
exports.isSandboxTabVisible = isSandboxTabVisible;
|
|
3527
|
+
exports.isSkillEnabled = isSkillEnabled;
|
|
2690
3528
|
exports.logger = logger;
|
|
2691
3529
|
exports.loginWithEmailAndPassword = loginWithEmailAndPassword;
|
|
2692
3530
|
exports.loginWithMicrosoftIdp = loginWithMicrosoftIdp;
|
|
@@ -2696,13 +3534,27 @@ exports.navigateToAuditLogAndWaitForData = navigateToAuditLogAndWaitForData;
|
|
|
2696
3534
|
exports.navigateToDataReports = navigateToDataReports;
|
|
2697
3535
|
exports.navigateToReportDownload = navigateToReportDownload;
|
|
2698
3536
|
exports.openAddMemoryDialog = openAddMemoryDialog;
|
|
3537
|
+
exports.openAgentPromptEditModal = openAgentPromptEditModal;
|
|
2699
3538
|
exports.openCreditBalanceDropdown = openCreditBalanceDropdown;
|
|
3539
|
+
exports.openEditInstanceDialog = openEditInstanceDialog;
|
|
3540
|
+
exports.openEditSkillDialog = openEditSkillDialog;
|
|
3541
|
+
exports.openInstanceActionsMenu = openInstanceActionsMenu;
|
|
3542
|
+
exports.openLLMProviderPicker = openLLMProviderPicker;
|
|
3543
|
+
exports.openNewInstanceDialog = openNewInstanceDialog;
|
|
3544
|
+
exports.openNewSkillDialog = openNewSkillDialog;
|
|
3545
|
+
exports.openSkillActionsMenu = openSkillActionsMenu;
|
|
2700
3546
|
exports.parseReportUrlParams = parseReportUrlParams;
|
|
3547
|
+
exports.pushConfiguration = pushConfiguration;
|
|
2701
3548
|
exports.reliableClick = reliableClick;
|
|
2702
3549
|
exports.reliableFill = reliableFill;
|
|
2703
3550
|
exports.retry = retry;
|
|
3551
|
+
exports.runConnectedInstanceChecks = runConnectedInstanceChecks;
|
|
3552
|
+
exports.runInstanceChecks = runInstanceChecks;
|
|
2704
3553
|
exports.safeWaitForURL = safeWaitForURL;
|
|
3554
|
+
exports.searchInstances = searchInstances;
|
|
2705
3555
|
exports.selectDateFromCalendar = selectDateFromCalendar;
|
|
3556
|
+
exports.selectLLMModel = selectLLMModel;
|
|
3557
|
+
exports.setupSandboxInstance = setupSandboxInstance;
|
|
2706
3558
|
exports.shouldAddNewRowWhenClickingAddRowButton = shouldAddNewRowWhenClickingAddRowButton;
|
|
2707
3559
|
exports.shouldAllowEditingCellValuesInCSVEditor = shouldAllowEditingCellValuesInCSVEditor;
|
|
2708
3560
|
exports.shouldCancelCombiningReports = shouldCancelCombiningReports;
|
|
@@ -2721,23 +3573,36 @@ exports.shouldShowCombiningReportsDialog = shouldShowCombiningReportsDialog;
|
|
|
2721
3573
|
exports.shouldVerifyCSVEditorDialogAccessibility = shouldVerifyCSVEditorDialogAccessibility;
|
|
2722
3574
|
exports.signUpWithEmailAndPassword = signUpWithEmailAndPassword;
|
|
2723
3575
|
exports.switchToMemoryTab = switchToMemoryTab;
|
|
3576
|
+
exports.switchToSandboxTab = switchToSandboxTab;
|
|
3577
|
+
exports.switchToSkillsTab = switchToSkillsTab;
|
|
3578
|
+
exports.teardownSandboxInstance = teardownSandboxInstance;
|
|
2724
3579
|
exports.test = test;
|
|
3580
|
+
exports.toggleAutoPush = toggleAutoPush;
|
|
2725
3581
|
exports.toggleMemorySwitch = toggleMemorySwitch;
|
|
3582
|
+
exports.toggleSkill = toggleSkill;
|
|
3583
|
+
exports.verifyAgentConfigPromptsVisible = verifyAgentConfigPromptsVisible;
|
|
2726
3584
|
exports.verifyAuditLogEmptyState = verifyAuditLogEmptyState;
|
|
2727
3585
|
exports.verifyAuditLogEntryStructure = verifyAuditLogEntryStructure;
|
|
2728
3586
|
exports.verifyAuditLogGenericError = verifyAuditLogGenericError;
|
|
2729
3587
|
exports.verifyAuditLogLoading = verifyAuditLogLoading;
|
|
2730
3588
|
exports.verifyAuditLogPermissionError = verifyAuditLogPermissionError;
|
|
2731
3589
|
exports.verifyAuditLogTableVisible = verifyAuditLogTableVisible;
|
|
3590
|
+
exports.verifyConnectDisabledForUnhealthy = verifyConnectDisabledForUnhealthy;
|
|
3591
|
+
exports.verifyConnectedInstanceCard = verifyConnectedInstanceCard;
|
|
2732
3592
|
exports.verifyCurrentPage = verifyCurrentPage;
|
|
2733
3593
|
exports.verifyDonePhase = verifyDonePhase;
|
|
2734
3594
|
exports.verifyDownloadingPhase = verifyDownloadingPhase;
|
|
2735
3595
|
exports.verifyErrorPhase = verifyErrorPhase;
|
|
3596
|
+
exports.verifyInstanceTableEmpty = verifyInstanceTableEmpty;
|
|
3597
|
+
exports.verifyInstanceTableVisible = verifyInstanceTableVisible;
|
|
2736
3598
|
exports.verifyMemoryExists = verifyMemoryExists;
|
|
2737
3599
|
exports.verifyMemoryNotExists = verifyMemoryNotExists;
|
|
2738
3600
|
exports.verifyMemoryTabMemoriesList = verifyMemoryTabMemoriesList;
|
|
2739
3601
|
exports.verifyMemoryTabSettings = verifyMemoryTabSettings;
|
|
2740
3602
|
exports.verifyPreparingPhase = verifyPreparingPhase;
|
|
3603
|
+
exports.verifySkillVisible = verifySkillVisible;
|
|
3604
|
+
exports.verifySkillsEmptyState = verifySkillsEmptyState;
|
|
3605
|
+
exports.verifySkillsTabVisible = verifySkillsTabVisible;
|
|
2741
3606
|
exports.waitForAuditLogDataLoaded = waitForAuditLogDataLoaded;
|
|
2742
3607
|
exports.waitForBillingTabReady = waitForBillingTabReady;
|
|
2743
3608
|
exports.waitForCreditBalanceLoaded = waitForCreditBalanceLoaded;
|