320 lines
20 KiB
JavaScript
320 lines
20 KiB
JavaScript
// Creates disposable fixtures only. No generation actions or paid model calls.
|
|||
|
|
// VERIFY_API_ORIGIN / VERIFY_FRONTEND_ORIGIN default to the public deployment.
|
||
|
|
// CANVAS_BROWSER=0 runs API checks only. PostgreSQL must back the selected API.
|
||
|
|
const assert = require('node:assert/strict')
|
||
|
|
const crypto = require('node:crypto')
|
||
|
|
const fs = require('node:fs')
|
||
|
|
const path = require('node:path')
|
||
|
|
const { execFileSync } = require('node:child_process')
|
||
|
|
|
||
|
|
const root = path.resolve(__dirname, '../..')
|
||
|
|
const directory = path.join(root, 'acceptance')
|
||
|
|
const origin = (process.env.VERIFY_API_ORIGIN || 'https://agent.pducv.cn').replace(/\/$/, '')
|
||
|
|
const frontend = (process.env.VERIFY_FRONTEND_ORIGIN || origin).replace(/\/$/, '')
|
||
|
|
const reportName = process.env.VERIFY_REPORT_NAME || 'canvas-configuration-live'
|
||
|
|
const tenant = 10059
|
||
|
|
const database = 'yawan_native_acceptance_20260915'
|
||
|
|
const prefix = `canvas-check-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`
|
||
|
|
const checks = [], failures = [], resources = []
|
||
|
|
const knowledgeKeys = ['knowledge_base_ids', 'knowledge_ids', 'knowledge_folders', 'knowledge_tags']
|
||
|
|
const browserEnabled = process.env.CANVAS_BROWSER !== '0'
|
||
|
|
let admin, member, role, user, agent, kb, browser, foreignTenant, databaseVerified = false
|
||
|
|
assert.match(reportName, /^[a-zA-Z0-9_-]+$/)
|
||
|
|
const literal = value => `'${String(value).replaceAll("'", "''")}'`
|
||
|
|
const sql = query => execFileSync('docker', ['--context', 'colima-yawan', 'exec', '-i', 'WeKnora-postgres',
|
||
|
|
'psql', '-X', '-q', '-U', 'postgres', '-d', database, '-At', '-v', 'ON_ERROR_STOP=1'],
|
||
|
|
{ input: query, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
||
|
|
function check(name, value) {
|
||
|
|
checks.push({ name, passed: !!value })
|
||
|
|
assert.ok(value, name)
|
||
|
|
console.log(`PASS ${name}`)
|
||
|
|
}
|
||
|
|
async function api(route, method = 'GET', body, identity = admin, expected = 200) {
|
||
|
|
const response = await fetch(origin + '/api/v1' + route, { method, headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
...(identity ? { Authorization: `Bearer ${identity.token}`, 'X-Tenant-ID': String(tenant) } : {}),
|
||
|
|
}, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30000) })
|
||
|
|
const data = await response.json()
|
||
|
|
assert.ok((Array.isArray(expected) ? expected : [expected]).includes(response.status),
|
||
|
|
`${method} ${route}: HTTP ${response.status}; ${data.message || data.error?.message || ''}`)
|
||
|
|
return data
|
||
|
|
}
|
||
|
|
function resourcePath(resource) {
|
||
|
|
assert.ok(resources.some(row => row.id === resource.id && row.kind === resource.kind), 'Only recorded fixtures may be changed')
|
||
|
|
return `/workbench/resources/${resource.kind}/${resource.id}`
|
||
|
|
}
|
||
|
|
async function create(kind, label, data, identity = member) {
|
||
|
|
const resource = (await api(`/workbench/resources/${kind}`, 'POST', { name: `${prefix}-${label}`, data }, identity, 201)).data
|
||
|
|
resources.push({ ...resource, identity })
|
||
|
|
assert.equal(resource.owner_id, identity.user.id)
|
||
|
|
return resource
|
||
|
|
}
|
||
|
|
function fixtureWhere(resource) {
|
||
|
|
resourcePath(resource)
|
||
|
|
assert.ok(resource.name.startsWith(prefix + '-'))
|
||
|
|
return `id=${literal(resource.id)} AND tenant_id=${Number(resource.tenant_id)} AND kind=${literal(resource.kind)} AND name LIKE ${literal(prefix + '-%')}`
|
||
|
|
}
|
||
|
|
function seed(resource, data, ownerID = resource.owner_id, status = resource.status) {
|
||
|
|
assert.ok(databaseVerified, 'API and database identity must match before seeding')
|
||
|
|
const id = sql(`UPDATE workbench_resources SET data=${literal(JSON.stringify(data))}::jsonb,
|
||
|
|
owner_id=${literal(ownerID)},status=${literal(status)} WHERE ${fixtureWhere(resource)} RETURNING id;`)
|
||
|
|
assert.equal(id, resource.id, 'Exactly the recorded fixture must be seeded')
|
||
|
|
}
|
||
|
|
async function read(resource, identity = admin) {
|
||
|
|
return (await api(resourcePath(resource), 'GET', undefined, identity)).data
|
||
|
|
}
|
||
|
|
async function absent(resource, identity = admin) {
|
||
|
|
await api(resourcePath(resource), 'GET', undefined, identity, 404)
|
||
|
|
const listed = await api(`/workbench/resources/${resource.kind}?q=${encodeURIComponent(prefix)}&page_size=100`, 'GET', undefined, identity)
|
||
|
|
assert.ok(!listed.data.some(row => row.id === resource.id), 'Deleted fixture must not reappear in a fresh list request')
|
||
|
|
}
|
||
|
|
function scopeIsEmpty(data, keys = knowledgeKeys) {
|
||
|
|
return keys.every(key => data[key] == null || Object.keys(data[key]).length === 0)
|
||
|
|
}
|
||
|
|
function manuscript(task) {
|
||
|
|
return { ...task.data, segments: [
|
||
|
|
{ id: 'accepted', title: 'Accepted fixture', agent_id: agent.id, status: 'accepted', content: 'Keep this accepted fixture prose.' },
|
||
|
|
{ id: 'skipped', title: 'Skipped fixture', agent_id: agent.id, status: 'skipped', content: 'Keep this skipped fixture prose.' },
|
||
|
|
], review: 'Keep the fixture review.', composed: 'Keep the fixture final manuscript.',
|
||
|
|
legacy_history: { fixture: prefix, opaque: { retained: true } }, custom_fixture_field: ['preserve', 123] }
|
||
|
|
}
|
||
|
|
async function checkDetached(before, actor) {
|
||
|
|
const after = await read(before)
|
||
|
|
assert.deepEqual(after.data, { ...before.data, flow_id: '' }, 'Detachment must preserve all other fields, including prose and accepted states')
|
||
|
|
assert.equal(after.owner_id, before.owner_id)
|
||
|
|
assert.equal(after.version, before.version + 1)
|
||
|
|
assert.equal(after.status, 'needs_configuration')
|
||
|
|
const revisions = JSON.parse(sql(`SELECT coalesce(json_agg(r ORDER BY version),'[]') FROM workbench_revisions r
|
||
|
|
WHERE tenant_id=${tenant} AND resource_id=${literal(before.id)} AND version IN (${before.version},${after.version});`))
|
||
|
|
const next = revisions.find(row => row.version === after.version)
|
||
|
|
assert.ok(next && next.actor_id === actor.user.id, 'Detachment revision must record the actual actor')
|
||
|
|
assert.deepEqual(next.data, after.data)
|
||
|
|
return after
|
||
|
|
}
|
||
|
|
async function browserPage(identity, resource) {
|
||
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
|
||
|
|
await context.addInitScript(({ auth, tenantID }) => {
|
||
|
|
for (const [key, value] of Object.entries({
|
||
|
|
weknora_user: JSON.stringify({ ...auth.user, tenant_id: String(auth.user.tenant_id) }),
|
||
|
|
weknora_tenant: JSON.stringify({ ...auth.active_tenant, id: String(tenantID) }),
|
||
|
|
weknora_token: auth.token, weknora_refresh_token: auth.refresh_token,
|
||
|
|
weknora_memberships: JSON.stringify(auth.memberships), 'weknora:new-user-guide-done:v1': '1',
|
||
|
|
})) localStorage.setItem(key, value)
|
||
|
|
}, { auth: identity, tenantID: tenant })
|
||
|
|
const page = await context.newPage()
|
||
|
|
page.setDefaultTimeout(20000)
|
||
|
|
page.on('pageerror', error => failures.push(`browser: ${error.message}`))
|
||
|
|
await page.route('**/api/v1/workbench/resources/**', async route => {
|
||
|
|
const request = route.request()
|
||
|
|
if (!['GET', 'HEAD'].includes(request.method())) {
|
||
|
|
const pathname = new URL(request.url()).pathname
|
||
|
|
const allowed = resources.some(row => pathname === '/api/v1' + resourcePath(row))
|
||
|
|
if (!allowed) {
|
||
|
|
failures.push(`blocked browser mutation outside recorded fixtures: ${pathname}`)
|
||
|
|
return route.abort()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return route.continue()
|
||
|
|
})
|
||
|
|
await page.goto(frontend + '/platform/workbench/flows')
|
||
|
|
const row = page.locator('.workbench-resource').filter({ hasText: resource.name })
|
||
|
|
await row.waitFor()
|
||
|
|
await row.click()
|
||
|
|
await page.getByRole('button', { name: '\u5220\u9664\u753b\u5e03', exact: true }).waitFor()
|
||
|
|
return { page, context }
|
||
|
|
}
|
||
|
|
async function deleteInBrowser(page, resource) {
|
||
|
|
const [response] = await Promise.all([
|
||
|
|
page.waitForResponse(value => value.request().method() === 'DELETE' &&
|
||
|
|
new URL(value.url()).pathname === '/api/v1' + resourcePath(resource)),
|
||
|
|
(async () => {
|
||
|
|
await page.getByRole('button', { name: '\u5220\u9664\u753b\u5e03', exact: true }).click()
|
||
|
|
await page.locator('.t-popconfirm:visible').getByRole('button', { name: /^(\u786e\u5b9a|\u786e\u8ba4)$/ }).click()
|
||
|
|
})(),
|
||
|
|
])
|
||
|
|
assert.equal(response.status(), 200)
|
||
|
|
const row = page.locator('.workbench-resource').filter({ hasText: resource.name })
|
||
|
|
await row.waitFor({ state: 'hidden' })
|
||
|
|
const [reloaded] = await Promise.all([
|
||
|
|
page.waitForResponse(value => value.request().method() === 'GET' &&
|
||
|
|
new URL(value.url()).pathname === '/api/v1/workbench/resources/flows'),
|
||
|
|
page.reload(),
|
||
|
|
])
|
||
|
|
assert.equal(reloaded.status(), 200)
|
||
|
|
await page.getByRole('heading', { name: '\u753b\u5e03\u7f16\u6392', exact: true }).waitFor()
|
||
|
|
await page.waitForLoadState('networkidle', { timeout: 30000 })
|
||
|
|
assert.equal(await row.count(), 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
|
||
|
|
admin = await api('/auth/login', 'POST', { email: credentials.email, password: credentials.password }, null)
|
||
|
|
check('same_tenant_administrator_available', (await api('/access/me')).data.administrator === true)
|
||
|
|
role = (await api('/access/roles', 'POST', { name: prefix, permissions: [
|
||
|
|
'flows.read', 'flows.write', 'content.read', 'content.write', 'agents.read', 'agents.write', 'knowledge.read', 'knowledge.write',
|
||
|
|
] })).data
|
||
|
|
check('api_uses_expected_fixture_database', sql(`SELECT count(*) FROM workbench_access_roles
|
||
|
|
WHERE tenant_id=${tenant} AND id=${literal(role.id)} AND name=${literal(prefix)}`) === '1')
|
||
|
|
databaseVerified = true
|
||
|
|
const password = crypto.randomBytes(18).toString('base64url')
|
||
|
|
const email = `${prefix}@example.test`
|
||
|
|
user = (await api('/access/users', 'POST', { username: prefix, email, password, role: 'contributor', status: 'active', custom_role_id: role.id })).data
|
||
|
|
member = await api('/auth/login', 'POST', { email, password }, null)
|
||
|
|
check('fixture_member_is_not_an_administrator', (await api('/access/me', 'GET', undefined, member)).data.administrator === false)
|
||
|
|
const source = (await api('/agents')).data.find(row => row.config?.model_id)
|
||
|
|
assert.ok(source, 'An existing configured model ID is needed for the Agent selector; it will not be invoked')
|
||
|
|
kb = (await api('/knowledge-bases', 'POST', { name: prefix + '-knowledge', type: 'document',
|
||
|
|
indexing_strategy: { vector_enabled: false, keyword_enabled: false, wiki_enabled: false, graph_enabled: false },
|
||
|
|
}, member, [200, 201])).data
|
||
|
|
agent = (await api('/agents', 'POST', { name: prefix + '-agent', config: {
|
||
|
|
role: 'production', agent_mode: 'quick-answer', model_id: source.config.model_id,
|
||
|
|
kb_selection_mode: 'selected', knowledge_bases: [kb.id], system_prompt: 'Disposable canvas fixture. No generation requested.',
|
||
|
|
fusion_agent_ids: [], instruction_skill_ids: [],
|
||
|
|
} }, member, 201)).data
|
||
|
|
const staleKB = crypto.randomUUID()
|
||
|
|
const stale = { knowledge_base_ids: [staleKB], knowledge_ids: [crypto.randomUUID()],
|
||
|
|
knowledge_folders: { [staleKB]: ['removed-folder'] }, knowledge_tags: { [staleKB]: [crypto.randomUUID()] } }
|
||
|
|
const flowData = { nodes: [{ id: 'writer', label: 'Fixture writer', agent_id: agent.id }], edges: [], ...stale }
|
||
|
|
const own = await create('flows', 'own', flowData)
|
||
|
|
check('creating_canvas_removes_all_four_legacy_knowledge_fields', knowledgeKeys.every(key => !Object.hasOwn(own.data, key)))
|
||
|
|
seed(own, { ...own.data, ...stale })
|
||
|
|
const historicalScope = (await read(own, member)).data
|
||
|
|
assert.deepEqual(Object.fromEntries(knowledgeKeys.map(key => [key, historicalScope[key]])), stale)
|
||
|
|
let ownTask = await create('content-tasks', 'own-task', { source_text: 'Disposable outline', flow_id: own.id, ...stale })
|
||
|
|
assert.deepEqual(ownTask.data.knowledge_base_ids, [kb.id])
|
||
|
|
assert.ok(scopeIsEmpty(ownTask.data, knowledgeKeys.slice(1)))
|
||
|
|
assert.ok(ownTask.data.flow_snapshot && scopeIsEmpty(ownTask.data.flow_snapshot))
|
||
|
|
assert.equal(ownTask.data.agent_id, agent.id)
|
||
|
|
check('stale_canvas_and_task_scopes_are_replaced_by_node_agent_scope', true)
|
||
|
|
seed(ownTask, manuscript(ownTask))
|
||
|
|
ownTask = await read(ownTask, member)
|
||
|
|
await api(resourcePath(own), 'DELETE', undefined, member, 409)
|
||
|
|
if (browserEnabled) {
|
||
|
|
const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||
|
|
browser = await chromium.launch({ headless: true, channel: 'chrome' })
|
||
|
|
const { page, context } = await browserPage(member, own)
|
||
|
|
check('canvas_has_no_knowledge_selector', await page.locator('.knowledge-scope').count() === 0 &&
|
||
|
|
!/\u753b\u5e03\u77e5\u8bc6\u8303\u56f4/.test(await page.locator('.workbench-detail').innerText()))
|
||
|
|
const [saved] = await Promise.all([
|
||
|
|
page.waitForResponse(value => value.request().method() === 'PUT' && new URL(value.url()).pathname === '/api/v1' + resourcePath(own)),
|
||
|
|
page.getByRole('button', { name: '\u4fdd\u5b58', exact: true }).click(),
|
||
|
|
])
|
||
|
|
assert.equal(saved.status(), 200)
|
||
|
|
const savedData = (await read(own, member)).data
|
||
|
|
check('saving_canvas_removes_all_four_legacy_knowledge_fields', knowledgeKeys.every(key => !Object.hasOwn(savedData, key)))
|
||
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
||
|
|
await page.screenshot({ path: path.join(directory, `${reportName}-mobile.png`), animations: 'disabled' })
|
||
|
|
check('canvas_mobile_has_no_horizontal_overflow', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2))
|
||
|
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
||
|
|
await deleteInBrowser(page, own)
|
||
|
|
check('owner_delete_survives_browser_reload', true)
|
||
|
|
await context.close()
|
||
|
|
} else {
|
||
|
|
await api(resourcePath(own) + '?detach=true', 'DELETE', undefined, member)
|
||
|
|
}
|
||
|
|
await absent(own, member)
|
||
|
|
await checkDetached(ownTask, member)
|
||
|
|
check('owner_delete_detaches_safely_and_does_not_reappear', true)
|
||
|
|
|
||
|
|
const old = await create('flows', 'historical', flowData)
|
||
|
|
let oldTask = await create('content-tasks', 'historical-task', { source_text: 'Disposable historical outline', flow_id: old.id })
|
||
|
|
const blocker = await create('content-tasks', 'other-owner-reference', { source_text: 'Disposable blocker', flow_id: old.id })
|
||
|
|
seed(blocker, blocker.data, admin.user.id)
|
||
|
|
const blockerBefore = await read(blocker)
|
||
|
|
assert.equal(blockerBefore.owner_id, admin.user.id)
|
||
|
|
const orphanOwner = crypto.randomUUID()
|
||
|
|
assert.equal(sql(`SELECT count(*) FROM users WHERE id=${literal(orphanOwner)}`), '0')
|
||
|
|
seed(old, { ...old.data, legacy_import: { read_only: true, fixture: prefix } }, orphanOwner, 'needs_configuration')
|
||
|
|
seed(oldTask, manuscript(oldTask), orphanOwner, 'completed')
|
||
|
|
const historical = await read(old)
|
||
|
|
oldTask = await read(oldTask)
|
||
|
|
await api(resourcePath(old), 'PUT', { name: old.name + '-forbidden', data: historical.data, version: historical.version }, admin, [403, 404])
|
||
|
|
await api(resourcePath(old), 'DELETE', undefined, member, [403, 404])
|
||
|
|
await api(resourcePath(oldTask), 'DELETE', undefined, admin, [403, 404])
|
||
|
|
assert.deepEqual(await read(old), historical)
|
||
|
|
check('administrator_delete_permission_does_not_allow_editing_or_task_deletion', true)
|
||
|
|
|
||
|
|
await api(resourcePath(old) + '?detach=true', 'DELETE', undefined, admin, 409)
|
||
|
|
assert.deepEqual(await read(oldTask), oldTask)
|
||
|
|
assert.deepEqual(await read(blocker), blockerBefore)
|
||
|
|
check('another_owners_reference_blocks_deletion_without_changing_tasks', true)
|
||
|
|
await api(resourcePath(blocker), 'DELETE')
|
||
|
|
await api(resourcePath(old), 'DELETE', undefined, admin, 409)
|
||
|
|
if (browserEnabled) {
|
||
|
|
const { page, context } = await browserPage(admin, old)
|
||
|
|
check('administrator_sees_delete_but_not_save_for_historical_canvas',
|
||
|
|
await page.getByRole('button', { name: '\u5220\u9664\u753b\u5e03', exact: true }).isEnabled() &&
|
||
|
|
await page.getByRole('button', { name: '\u4fdd\u5b58', exact: true }).count() === 0)
|
||
|
|
await page.screenshot({ path: path.join(directory, `${reportName}-admin.png`), animations: 'disabled' })
|
||
|
|
await deleteInBrowser(page, old)
|
||
|
|
await context.close()
|
||
|
|
} else {
|
||
|
|
await api(resourcePath(old) + '?detach=true', 'DELETE')
|
||
|
|
}
|
||
|
|
await absent(old)
|
||
|
|
await checkDetached(oldTask, admin)
|
||
|
|
assert.equal(sql(`SELECT count(*) FROM workbench_events WHERE tenant_id=${tenant} AND resource_id=${literal(old.id)}
|
||
|
|
AND action='delete' AND owner_id=${literal(admin.user.id)}`), '1')
|
||
|
|
check('administrator_deletes_orphaned_canvas_preserving_manuscript_and_audit_actor', true)
|
||
|
|
|
||
|
|
foreignTenant = Number(sql(`INSERT INTO tenants(name,business,status)
|
||
|
|
VALUES (${literal(prefix)},'canvas-fixture','active') RETURNING id;`))
|
||
|
|
assert.ok(Number.isSafeInteger(foreignTenant) && foreignTenant > 0 && foreignTenant !== tenant)
|
||
|
|
const foreign = { id: crypto.randomUUID(), tenant_id: foreignTenant, owner_id: admin.user.id, kind: 'flows', name: prefix + '-foreign' }
|
||
|
|
resources.push(foreign)
|
||
|
|
sql(`INSERT INTO workbench_resources(id,tenant_id,owner_id,owner_name,kind,name,status,version,data,created_at,updated_at)
|
||
|
|
VALUES(${literal(foreign.id)},${foreignTenant},${literal(admin.user.id)},${literal(prefix)},'flows',${literal(foreign.name)},'draft',1,'{}',now(),now());`)
|
||
|
|
await api(resourcePath(foreign), 'GET', undefined, admin, 404)
|
||
|
|
await api(resourcePath(foreign) + '?detach=true', 'DELETE', undefined, admin, 404)
|
||
|
|
check('administrator_cannot_delete_same_owner_canvas_in_another_tenant',
|
||
|
|
sql(`SELECT count(*) FROM workbench_resources WHERE ${fixtureWhere(foreign)}`) === '1')
|
||
|
|
check('no_generation_jobs_or_model_usage', sql(`SELECT
|
||
|
|
(SELECT count(*) FROM workbench_jobs WHERE resource_id IN (${resources.map(row => literal(row.id)).join(',')})) +
|
||
|
|
(SELECT count(*) FROM workbench_usage WHERE tenant_id=${tenant} AND agent_id=${literal(agent.id)})`) === '0')
|
||
|
|
}
|
||
|
|
|
||
|
|
async function cleanup() {
|
||
|
|
async function attempt(label, action) {
|
||
|
|
try { await action() } catch (error) { failures.push(`cleanup ${label}: ${error.message}`) }
|
||
|
|
}
|
||
|
|
await attempt('browser', async () => { await browser?.close() })
|
||
|
|
if (databaseVerified) {
|
||
|
|
for (const resource of [...resources].reverse()) await attempt(resource.id, async () => {
|
||
|
|
assert.equal(sql(`SELECT count(*) FROM workbench_jobs WHERE resource_id=${literal(resource.id)} AND status IN ('queued','running')`), '0')
|
||
|
|
sql(`BEGIN;
|
||
|
|
DELETE FROM workbench_events WHERE tenant_id=${Number(resource.tenant_id)} AND resource_id=${literal(resource.id)};
|
||
|
|
DELETE FROM workbench_revisions WHERE tenant_id=${Number(resource.tenant_id)} AND resource_id=${literal(resource.id)};
|
||
|
|
DELETE FROM workbench_jobs WHERE tenant_id=${Number(resource.tenant_id)} AND resource_id=${literal(resource.id)};
|
||
|
|
DELETE FROM workbench_resources WHERE ${fixtureWhere(resource)};
|
||
|
|
COMMIT;`)
|
||
|
|
})
|
||
|
|
if (foreignTenant) await attempt('foreign tenant', async () => {
|
||
|
|
assert.equal(sql(`SELECT count(*) FROM workbench_resources WHERE tenant_id=${foreignTenant}`), '0')
|
||
|
|
sql(`DELETE FROM tenants WHERE id=${foreignTenant} AND name=${literal(prefix)} AND business='canvas-fixture';`)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if (agent) await attempt('agent', () => api(`/agents/${agent.id}?detach=true`, 'DELETE', undefined, member, [200, 404]))
|
||
|
|
if (kb) await attempt('knowledge', () => api(`/knowledge-bases/${kb.id}`, 'DELETE', undefined, member, [200, 202, 404]))
|
||
|
|
if (databaseVerified) await attempt('fixture publications', async () => {
|
||
|
|
const ids = [...resources.map(row => row.id), agent?.id, kb?.id].filter(Boolean)
|
||
|
|
if (ids.length) for (const table of ['workbench_access_grants', 'workbench_access_publications']) {
|
||
|
|
sql(`DELETE FROM ${table} WHERE tenant_id=${tenant} AND resource_id IN (${ids.map(literal).join(',')});`)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
if (user) await attempt('user', () => api(`/access/users/${user.id}`, 'DELETE', undefined, admin, [200, 404]))
|
||
|
|
if (role) await attempt('role', () => api(`/access/roles/${role.id}`, 'DELETE', undefined, admin, [200, 404]))
|
||
|
|
if (databaseVerified && resources.length) await attempt('verify resources removed', async () => {
|
||
|
|
check('all_recorded_canvas_and_task_fixtures_removed', sql(`SELECT count(*) FROM workbench_resources
|
||
|
|
WHERE id IN (${resources.map(row => literal(row.id)).join(',')})`) === '0')
|
||
|
|
})
|
||
|
|
}
|
||
|
|
main().catch(error => { failures.push(error.message) }).finally(async () => {
|
||
|
|
await cleanup()
|
||
|
|
const report = { checked_at: new Date().toISOString(), origin, frontend, prefix, browser_enabled: browserEnabled,
|
||
|
|
checks, failures, passed: failures.length === 0, fixtures: { role_id: role?.id, user_id: user?.id, agent_id: agent?.id,
|
||
|
|
knowledge_base_id: kb?.id, foreign_tenant: foreignTenant, resources: resources.map(({ id, kind, tenant_id }) => ({ id, kind, tenant_id })) } }
|
||
|
|
fs.writeFileSync(path.join(directory, `${reportName}.json`), JSON.stringify(report, null, 2), { mode: 0o600 })
|
||
|
|
console.log(JSON.stringify(report, null, 2))
|
||
|
|
if (failures.length) process.exitCode = 1
|
||
|
|
})
|