mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
feat: auto-reload qa lab fast refresh
This commit is contained in:
@@ -49,7 +49,8 @@ pnpm qa:lab:watch
|
||||
|
||||
`qa:lab:up:fast` keeps the Docker services on a prebuilt image and bind-mounts
|
||||
`extensions/qa-lab/web/dist` into the `qa-lab` container. `qa:lab:watch`
|
||||
rebuilds that bundle on change; refresh the browser after each rebuild.
|
||||
rebuilds that bundle on change, and the browser auto-reloads when the QA Lab
|
||||
asset hash changes.
|
||||
|
||||
## Repo-backed seeds
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ Fast UI refresh:
|
||||
- \`pnpm qa:lab:up --use-prebuilt-image --bind-ui-dist --skip-ui-build\`
|
||||
- In another shell, rebuild the QA Lab bundle on change:
|
||||
- \`pnpm qa:lab:watch\`
|
||||
- Refresh the browser after each rebuild to pick up the new bundle.
|
||||
- The browser auto-reloads when the QA Lab asset hash changes.
|
||||
|
||||
Gateway:
|
||||
|
||||
|
||||
@@ -206,6 +206,23 @@ describe("qa-lab server", () => {
|
||||
const html = await rootResponse.text();
|
||||
expect(html).not.toContain("QA Lab UI not built");
|
||||
expect(html).toContain("<title>");
|
||||
|
||||
const version1 = (await (await fetch(`${lab.baseUrl}/api/ui-version`)).json()) as {
|
||||
version: string | null;
|
||||
};
|
||||
expect(version1.version).toMatch(/^[0-9a-f]{12}$/);
|
||||
|
||||
await writeFile(
|
||||
path.join(uiDistDir, "index.html"),
|
||||
"<!doctype html><html><head><title>QA Lab Updated</title></head><body><div id='app'></div></body></html>",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const version2 = (await (await fetch(`${lab.baseUrl}/api/ui-version`)).json()) as {
|
||||
version: string | null;
|
||||
};
|
||||
expect(version2.version).toMatch(/^[0-9a-f]{12}$/);
|
||||
expect(version2.version).not.toBe(version1.version);
|
||||
});
|
||||
|
||||
it("can disable the embedded echo gateway for real-suite runs", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
createServer,
|
||||
@@ -180,6 +181,45 @@ function resolveUiDistDir(overrideDir?: string | null) {
|
||||
);
|
||||
}
|
||||
|
||||
function listUiAssetFiles(rootDir: string, currentDir = rootDir): string[] {
|
||||
const entries = fs
|
||||
.readdirSync(currentDir, { withFileTypes: true })
|
||||
.toSorted((left, right) => left.name.localeCompare(right.name));
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const resolved = path.join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...listUiAssetFiles(rootDir, resolved));
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
files.push(path.relative(rootDir, resolved));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function resolveUiAssetVersion(overrideDir?: string | null): string | null {
|
||||
try {
|
||||
const distDir = resolveUiDistDir(overrideDir);
|
||||
const indexPath = path.join(distDir, "index.html");
|
||||
if (!fs.existsSync(indexPath) || !fs.statSync(indexPath).isFile()) {
|
||||
return null;
|
||||
}
|
||||
const hash = createHash("sha1");
|
||||
for (const relativeFile of listUiAssetFiles(distDir)) {
|
||||
hash.update(relativeFile);
|
||||
hash.update("\0");
|
||||
hash.update(fs.readFileSync(path.join(distDir, relativeFile)));
|
||||
hash.update("\0");
|
||||
}
|
||||
return hash.digest("hex").slice(0, 12);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAdvertisedBaseUrl(params: {
|
||||
bindHost?: string;
|
||||
bindPort: number;
|
||||
@@ -536,6 +576,14 @@ export async function startQaLabServer(params?: {
|
||||
writeJson(res, 200, { report: latestReport });
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname === "/api/ui-version") {
|
||||
res.writeHead(200, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": "no-store",
|
||||
});
|
||||
res.end(JSON.stringify({ version: resolveUiAssetVersion(params?.uiDistDir) }));
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname === "/api/outcomes") {
|
||||
writeJson(res, 200, { run: latestScenarioRun });
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,14 @@ async function getJson<T>(path: string): Promise<T> {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function getJsonNoStore<T>(path: string): Promise<T> {
|
||||
const response = await fetch(path, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function postJson<T>(path: string, body: unknown): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
method: "POST",
|
||||
@@ -91,6 +99,7 @@ export async function createQaLabApp(root: HTMLDivElement) {
|
||||
let lastFingerprint = "";
|
||||
let renderDeferred = false;
|
||||
let previousRunnerStatus: string | null = null;
|
||||
let currentUiVersion: string | null = null;
|
||||
|
||||
function stateFingerprint(): string {
|
||||
const msgs = state.snapshot?.messages;
|
||||
@@ -182,6 +191,24 @@ export async function createQaLabApp(root: HTMLDivElement) {
|
||||
}
|
||||
}
|
||||
|
||||
async function pollUiVersion() {
|
||||
if (document.visibilityState === "hidden") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = await getJsonNoStore<{ version: string | null }>("/api/ui-version");
|
||||
if (!currentUiVersion) {
|
||||
currentUiVersion = payload.version;
|
||||
return;
|
||||
}
|
||||
if (payload.version && payload.version !== currentUiVersion) {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient rebuild windows while the dist dir is being rewritten.
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Draft mutations ---------- */
|
||||
|
||||
function updateRunnerDraft(mutator: (draft: RunnerSelection) => RunnerSelection) {
|
||||
@@ -590,5 +617,7 @@ export async function createQaLabApp(root: HTMLDivElement) {
|
||||
|
||||
render();
|
||||
await refresh();
|
||||
void pollUiVersion();
|
||||
setInterval(() => void refresh(), 1_000);
|
||||
setInterval(() => void pollUiVersion(), 1_000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user