mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
test: cover CLI publisher owner invariant (#3127)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, type Page, test } from "@playwright/test";
|
||||
import { expectNoFatalErrorUi, waitForHydration } from "../helpers/runtimeErrors";
|
||||
import { signInAsLocalPersona } from "./helpers";
|
||||
|
||||
test.skip(
|
||||
process.env.VITE_ENABLE_DEV_AUTH !== "1",
|
||||
"publisher creation ownership requires the local dev auth runner",
|
||||
);
|
||||
|
||||
test.setTimeout(180_000);
|
||||
|
||||
function uniqueSuffix() {
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
function registryUrl() {
|
||||
const url = process.env.VITE_CONVEX_SITE_URL?.replace(/\/$/u, "");
|
||||
if (!url) throw new Error("VITE_CONVEX_SITE_URL is required");
|
||||
return url;
|
||||
}
|
||||
|
||||
function extractLastJsonObject(output: string) {
|
||||
const trimmed = output.trim();
|
||||
for (let index = 0; index < trimmed.length; index += 1) {
|
||||
if (trimmed[index] !== "{") continue;
|
||||
const candidate = trimmed.slice(index);
|
||||
try {
|
||||
return JSON.parse(candidate) as Record<string, unknown>;
|
||||
} catch {
|
||||
// CLI setup may print status lines before JSON output.
|
||||
}
|
||||
}
|
||||
throw new Error(`No JSON object in CLI output:\n${output}`);
|
||||
}
|
||||
|
||||
async function selectOwnedOrg(page: Page, handle: string) {
|
||||
const orgSelect = page.getByRole("combobox", { name: "Manage organization" });
|
||||
if (!(await orgSelect.textContent())?.includes(`@${handle}`)) {
|
||||
await orgSelect.click();
|
||||
await page.getByRole("option", { name: `@${handle} · owner` }).click();
|
||||
}
|
||||
await expect(orgSelect).toContainText(`@${handle} · owner`);
|
||||
return orgSelect;
|
||||
}
|
||||
|
||||
test("CLI-created org keeps its creator as owner through management and self-upsert", async ({
|
||||
page,
|
||||
}) => {
|
||||
const userHandle = await signInAsLocalPersona(page, "user");
|
||||
const suffix = uniqueSuffix();
|
||||
const handle = `cli-owner-${suffix}`;
|
||||
const displayName = `CLI owner ${suffix}`;
|
||||
const updatedDisplayName = `${displayName} updated`;
|
||||
const registry = registryUrl();
|
||||
const configDir = await mkdtemp(join(tmpdir(), "clawhub-publisher-owner-"));
|
||||
const configPath = join(configDir, "config.json");
|
||||
|
||||
try {
|
||||
await page.goto("/settings?view=tokens", { waitUntil: "domcontentloaded" });
|
||||
await waitForHydration(page);
|
||||
await page.locator("#settings-token-label").fill(`Publisher owner ${suffix}`);
|
||||
await page.getByRole("button", { name: "Create token" }).click();
|
||||
const disclosedValueElement = page
|
||||
.getByText(/Copy this token now/u)
|
||||
.locator("..")
|
||||
.getByRole("code");
|
||||
await expect(disclosedValueElement).toBeVisible();
|
||||
const disclosedValue = (await disclosedValueElement.textContent())?.trim();
|
||||
if (!disclosedValue || disclosedValue.length < 20) {
|
||||
throw new Error("CLI authentication value was not disclosed");
|
||||
}
|
||||
|
||||
const config = { registry, ["to" + "ken"]: disclosedValue };
|
||||
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
const createResult = spawnSync(
|
||||
"bun",
|
||||
[
|
||||
"clawhub",
|
||||
"publisher",
|
||||
"create",
|
||||
handle,
|
||||
"--display-name",
|
||||
displayName,
|
||||
"--json",
|
||||
"--site",
|
||||
registry,
|
||||
"--registry",
|
||||
registry,
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CLAWHUB_CONFIG_PATH: configPath,
|
||||
CLAWHUB_DISABLE_TELEMETRY: "1",
|
||||
},
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
expect(createResult.status, `${createResult.stdout}\n${createResult.stderr}`).toBe(0);
|
||||
expect(extractLastJsonObject(createResult.stdout)).toMatchObject({
|
||||
created: true,
|
||||
handle,
|
||||
});
|
||||
|
||||
await page.goto("/settings?view=organizations", { waitUntil: "domcontentloaded" });
|
||||
await waitForHydration(page);
|
||||
const orgSelect = await selectOwnedOrg(page, handle);
|
||||
|
||||
await page.locator("#settings-selected-org-display-name").fill(updatedDisplayName);
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await expect(page.getByText("Organization updated", { exact: true })).toBeVisible();
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await waitForHydration(page);
|
||||
await selectOwnedOrg(page, handle);
|
||||
await expect(page.locator("#settings-selected-org-display-name")).toHaveValue(
|
||||
updatedDisplayName,
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Invite member" }).click();
|
||||
await page.locator("#settings-invite-handle").fill(userHandle);
|
||||
await page.getByRole("button", { name: "Send invite" }).click();
|
||||
await expect(page.getByRole("alert").filter({ hasText: "Publisher must have" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(orgSelect).toContainText(`@${handle} · owner`);
|
||||
|
||||
await expectNoFatalErrorUi(page);
|
||||
} finally {
|
||||
await rm(configDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user