71c7672545
* feat(skill-hub): add Skill Hub catalog, publish flow, and lite materialize pipeline Introduce Skill Hub for browsing, importing, publishing, and installing skills, with lite instance package materialization and runtime sync support. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): remove token-governance hooks from Skill Hub PR Strip validateManagedRuntimeEnvironmentOverrides, network lock policy sync, and egress proxy audit wiring that belong to the upcoming token-usage work, so the Skill Hub branch compiles independently. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): update migration number in materialize docs Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): make RuntimeAgentClient test stub and hub tests compile-safe Add ResyncInstanceSkills to the runtime pool handler fake client, and harden skill hub payload helpers/tests against nil storage/instance repos so go test passes. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add Skill Hub hardening with session usage tracking and egress governance Unify Skill Hub runtime sync improvements with session-token observability, egress network policy, and admin/instance usage reporting for reopenable PR. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): repair CI tests and nested skill install * fix(ci): restore release deployment configuration --------- Co-authored-by: heshengran <heshengran@ieisystem.com> Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
6.7 KiB
TypeScript
168 lines
6.7 KiB
TypeScript
import { expect, test } from "../../fixtures/test.js";
|
|
import { env } from "../../fixtures/env.js";
|
|
import {
|
|
gatewayChatCompletion,
|
|
getInstanceSessionUsage,
|
|
listGatewayModels,
|
|
listInstances,
|
|
login,
|
|
} from "../../fixtures/apiClient.js";
|
|
import { getInstanceGatewayToken } from "../../fixtures/dbClient.js";
|
|
import { users } from "../../fixtures/users.js";
|
|
|
|
function firstOpenClawInstance(instances: Awaited<ReturnType<typeof listInstances>>) {
|
|
return instances.instances.find(
|
|
(instance) =>
|
|
instance.type === "openclaw" &&
|
|
instance.status !== "deleting",
|
|
);
|
|
}
|
|
|
|
async function fetchSessionUsage(
|
|
request: Parameters<typeof login>[0],
|
|
accessToken: string,
|
|
instanceId: number,
|
|
) {
|
|
return getInstanceSessionUsage(request, accessToken, instanceId, { page: 1, limit: 50 });
|
|
}
|
|
|
|
function isGatewaySuccessStatus(status: number): boolean {
|
|
return status === 200 || status === 201;
|
|
}
|
|
|
|
test("@p1 session usage endpoint returns structured payload for openclaw instance", async ({
|
|
request,
|
|
}) => {
|
|
const accessToken = (await login(request, users.admin)).access_token;
|
|
const instances = await listInstances(request, accessToken, { limit: 100 });
|
|
const instance = firstOpenClawInstance(instances);
|
|
test.skip(!instance, "No openclaw instance available for session usage test");
|
|
|
|
const data = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
expect(data.summary).toBeTruthy();
|
|
expect(Array.isArray(data.items)).toBe(true);
|
|
expect(typeof data.total).toBe("number");
|
|
expect(typeof data.compliance.recent_fallback_audit_count).toBe("number");
|
|
});
|
|
|
|
test("@p1 session usage detail requires session_id", async ({ request }) => {
|
|
const accessToken = (await login(request, users.admin)).access_token;
|
|
const instances = await listInstances(request, accessToken, { limit: 100 });
|
|
const instance = firstOpenClawInstance(instances);
|
|
test.skip(!instance, "No openclaw instance available for session usage detail test");
|
|
|
|
const response = await request.get(
|
|
`${env.backendUrl}/instances/${instance!.id}/session-usage/detail`,
|
|
{
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
},
|
|
);
|
|
expect(response.status()).toBe(400);
|
|
});
|
|
|
|
test("@p0 gateway calls aggregate tokens by openclaw session key", async ({ request }) => {
|
|
const accessToken = (await login(request, users.admin)).access_token;
|
|
const instances = await listInstances(request, accessToken, { limit: 100 });
|
|
const instance = firstOpenClawInstance(instances);
|
|
test.skip(!instance, "No openclaw instance available for gateway aggregation test");
|
|
|
|
const models = await listGatewayModels(request, accessToken);
|
|
test.skip(models.length === 0, "No gateway models configured for session aggregation test");
|
|
|
|
const baseline = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
const baselineMain = baseline.items.find((item) => item.session_key === "main");
|
|
let successfulCalls = 0;
|
|
|
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
const response = await gatewayChatCompletion(request, accessToken, {
|
|
model: "auto",
|
|
instance_id: instance!.id,
|
|
messages: [{ role: "user", content: `session aggregation probe ${Date.now()}-${attempt}` }],
|
|
}, {
|
|
"x-openclaw-session-key": "main",
|
|
"x-openclaw-run-id": `e2e-session-${Date.now()}-${attempt}`,
|
|
});
|
|
if (isGatewaySuccessStatus(response.status())) {
|
|
successfulCalls += 1;
|
|
}
|
|
}
|
|
test.skip(successfulCalls === 0, "Gateway upstream unavailable; skipping token aggregation assertion");
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const latest = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
const main = latest.items.find((item) => item.session_key === "main");
|
|
return main?.invocation_count ?? 0;
|
|
}, { timeout: 20_000 })
|
|
.toBeGreaterThan(baselineMain?.invocation_count ?? 0);
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const latest = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
const main = latest.items.find((item) => item.session_key === "main");
|
|
return main?.total_tokens ?? 0;
|
|
}, { timeout: 20_000 })
|
|
.toBeGreaterThan(baselineMain?.total_tokens ?? 0);
|
|
});
|
|
|
|
test("@p0 gateway missing session key surfaces fallback compliance", async ({ request }) => {
|
|
const accessToken = (await login(request, users.admin)).access_token;
|
|
const instances = await listInstances(request, accessToken, { limit: 100 });
|
|
const instance = firstOpenClawInstance(instances);
|
|
test.skip(!instance, "No openclaw instance available for fallback compliance test");
|
|
|
|
const models = await listGatewayModels(request, accessToken);
|
|
test.skip(models.length === 0, "No gateway models configured for fallback compliance test");
|
|
|
|
const baseline = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
const response = await gatewayChatCompletion(request, accessToken, {
|
|
model: "auto",
|
|
instance_id: instance!.id,
|
|
messages: [{ role: "user", content: `fallback probe ${Date.now()}` }],
|
|
}, {
|
|
"x-openclaw-run-id": `e2e-fallback-${Date.now()}`,
|
|
});
|
|
test.skip(!isGatewaySuccessStatus(response.status()), "Gateway upstream unavailable; skipping fallback assertion");
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const latest = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
return latest.compliance.has_fallback_sessions;
|
|
}, { timeout: 20_000 })
|
|
.toBe(true);
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const latest = await fetchSessionUsage(request, accessToken, instance!.id);
|
|
return latest.compliance.recent_fallback_audit_count;
|
|
}, { timeout: 20_000 })
|
|
.toBeGreaterThan(baseline.compliance.recent_fallback_audit_count);
|
|
});
|
|
|
|
test("@p0 instance gateway token can call chat completions", async ({ request }) => {
|
|
const accessToken = (await login(request, users.admin)).access_token;
|
|
const instances = await listInstances(request, accessToken, { limit: 100 });
|
|
const instance = firstOpenClawInstance(instances);
|
|
test.skip(!instance, "No openclaw instance available for instance gateway token test");
|
|
|
|
let gatewayToken: string | null = null;
|
|
try {
|
|
gatewayToken = await getInstanceGatewayToken(instance!.id);
|
|
} catch {
|
|
test.skip(true, "E2E database unavailable for instance gateway token lookup");
|
|
}
|
|
test.skip(!gatewayToken, "Instance gateway token not provisioned");
|
|
|
|
const models = await listGatewayModels(request, gatewayToken);
|
|
test.skip(models.length === 0, "No gateway models configured for instance token test");
|
|
|
|
const response = await gatewayChatCompletion(request, gatewayToken, {
|
|
model: "auto",
|
|
messages: [{ role: "user", content: `instance token probe ${Date.now()}` }],
|
|
}, {
|
|
"x-openclaw-session-key": "main",
|
|
"x-openclaw-run-id": `e2e-instance-token-${Date.now()}`,
|
|
});
|
|
expect([200, 201]).toContain(response.status());
|
|
});
|