diff --git a/.github/scripts/actions-efficiency.test.mjs b/.github/scripts/actions-efficiency.test.mjs
index 0a18dc636f..90523b506b 100644
--- a/.github/scripts/actions-efficiency.test.mjs
+++ b/.github/scripts/actions-efficiency.test.mjs
@@ -25,14 +25,21 @@ function evaluateCondition(content, github) {
const expression = (folded?.[1] ?? content.match(/^ if: (.+)$/m)?.[1])?.trim();
assert.ok(expression, "missing job condition");
const evaluate = new Function("github", "contains", `return (${expression.replace(/^\$\{\{\s*|\s*\}\}$/g, "")});`);
- return evaluate(github, (search, item) => String(search ?? "").toLowerCase().includes(String(item).toLowerCase()));
+ return evaluate(github, (search, item) =>
+ String(search ?? "")
+ .toLowerCase()
+ .includes(String(item).toLowerCase()),
+ );
}
function commentEvent(body, { userType = "User", login = "contributor", pullRequest = false } = {}) {
- return { repository_owner: "t8y2", event: {
- issue: { pull_request: pullRequest ? { url: "https://api.github.com/repos/t8y2/dbx/pulls/123" } : undefined },
- comment: { body, user: { type: userType, login } },
- } };
+ return {
+ repository_owner: "t8y2",
+ event: {
+ issue: { pull_request: pullRequest ? { url: "https://api.github.com/repos/t8y2/dbx/pulls/123" } : undefined },
+ comment: { body, user: { type: userType, login } },
+ },
+ };
}
test("issue command prefilter admits every supported command and skips unrelated comments", async () => {
@@ -56,10 +63,15 @@ test("spam prefilter retains all existing malicious-link cases and never bypasse
const content = job(workflow("spam-comment-guard"), "delete-malicious-link");
const pattern = content.match(/grep -qiP '([^']+)'/)[1];
const matcher = new RegExp(pattern.replaceAll("[:space:]", "\\s"), "i");
- for (const body of ["https://evil.example/update.zip", "https://evil.example/update.ZIP?download=1",
- "[update.zip](//evil.example/download)", "[update.ZIP](https://evil%2Eexample/download)",
- "[update.7z](https://evil.example/download)", "[update.exe](https://evil.example/download)",
- "[update.SCR](//evil.example/download)"]) {
+ for (const body of [
+ "https://evil.example/update.zip",
+ "https://evil.example/update.ZIP?download=1",
+ "[update.zip](//evil.example/download)",
+ "[update.ZIP](https://evil%2Eexample/download)",
+ "[update.7z](https://evil.example/download)",
+ "[update.exe](https://evil.example/download)",
+ "[update.SCR](//evil.example/download)",
+ ]) {
assert.equal(matcher.test(body), true, body);
assert.equal(evaluateCondition(content, commentEvent(body)), true, body);
assert.equal(evaluateCondition(content, commentEvent(body, { userType: "Bot" })), false);
@@ -69,9 +81,7 @@ test("spam prefilter retains all existing malicious-link cases and never bypasse
for (const body of ["Thanks!", "https://example.com/docs", "", null]) {
assert.equal(evaluateCondition(content, commentEvent(body)), false);
}
- for (const body of ["Please upload a .zip", "https://github.com/t8y2/dbx/archive/main.zip",
- "https://user-images.githubusercontent.com/files/logs.zip", "[logs.exe](https://github.com/user-attachments/files/123/logs.exe)",
- "[logs.7z](//raw.githubusercontent.com/t8y2/dbx/main/logs.7z)"]) {
+ for (const body of ["Please upload a .zip", "https://github.com/t8y2/dbx/archive/main.zip", "https://user-images.githubusercontent.com/files/logs.zip", "[logs.exe](https://github.com/user-attachments/files/123/logs.exe)", "[logs.7z](//raw.githubusercontent.com/t8y2/dbx/main/logs.7z)"]) {
assert.equal(evaluateCondition(content, commentEvent(body)), true, body);
assert.equal(matcher.test(body), false, body);
}
@@ -98,8 +108,7 @@ test("plugin checks run once before unchanged cross-platform verification", () =
assert.match(tests, /needs: checks/);
assert.match(tests, /os: \[ubuntu-24\.04, macos-15, windows-2022\]/);
assert.doesNotMatch(tests, /npm run (?:lint|fmt:check)/);
- for (const command of ["npm test --prefix plugins/sdk/dev-host", "cargo nextest run", "cargo test --doc",
- "npm test --prefix packages/plugin-cli", "node scripts/verify-plugin-cli-package.mjs", "DBX_PLUGIN_CLI_VERIFY_NATIVE: '1'"]) {
+ for (const command of ["npm test --prefix plugins/sdk/dev-host", "cargo nextest run", "cargo test --doc", "npm test --prefix packages/plugin-cli", "node scripts/verify-plugin-cli-package.mjs", "DBX_PLUGIN_CLI_VERIFY_NATIVE: '1'"]) {
assert.ok(tests.includes(command), command);
}
assert.match(content, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.ref \}\}\s+cancel-in-progress: true/);
@@ -118,11 +127,16 @@ test("GitHub script selection excludes planner tests only when fast checks own t
const files = readdirSync(new URL(".github/scripts/", repository)).filter((file) => file.endsWith(".test.mjs"));
for (const fast of ["true", "false", ""]) {
const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", `node() { printf '%s\\n' "$@"; }\n${script}`], {
- cwd: repository, encoding: "utf8", env: { ...process.env, FAST_CHECKS: fast },
+ cwd: repository,
+ encoding: "utf8",
+ env: { ...process.env, FAST_CHECKS: fast },
});
assert.equal(result.status, 0, result.stderr);
const selected = result.stdout.trim().split("\n").slice(1).sort();
- const expected = files.filter((file) => fast !== "true" || !file.startsWith("ci-")).map((file) => `.github/scripts/${file}`).sort();
+ const expected = files
+ .filter((file) => fast !== "true" || !file.startsWith("ci-"))
+ .map((file) => `.github/scripts/${file}`)
+ .sort();
assert.deepEqual(selected, expected);
assert.ok(selected.includes(".github/scripts/actions-efficiency.test.mjs"));
}
@@ -135,8 +149,7 @@ async function snapshotFixture(context) {
}
function snapshot() {
- return { repository: "t8y2/dbx", generatedAt: "2026-10-01T18:00:00.000Z", stars: 10,
- contributors: [{ login: "contributor", commits: 3, mergedPullRequests: 1 }] };
+ return { repository: "t8y2/dbx", generatedAt: "2026-10-01T18:00:00.000Z", stars: 10, contributors: [{ login: "contributor", commits: 3, mergedPullRequests: 1 }] };
}
test("a missing contributor snapshot is created and timestamp-only refreshes preserve exact bytes", async (context) => {
@@ -154,9 +167,12 @@ test("a missing contributor snapshot is created and timestamp-only refreshes pre
test("actual contributor, star and repository changes update the snapshot", async (context) => {
const outputPath = await snapshotFixture(context);
const original = snapshot();
- for (const updated of [{ ...original, stars: 11 }, { ...original, repository: "other/dbx" },
+ for (const updated of [
+ { ...original, stars: 11 },
+ { ...original, repository: "other/dbx" },
{ ...original, contributors: [{ ...original.contributors[0], commits: 4 }] },
- { ...original, contributors: [] }]) {
+ { ...original, contributors: [] },
+ ]) {
await writeContributorSnapshot(outputPath, original);
const refreshed = { ...updated, generatedAt: "2026-10-08T18:00:00.000Z" };
assert.equal(await writeContributorSnapshot(outputPath, refreshed), true);
diff --git a/.github/scripts/ai-issue-priority.mjs b/.github/scripts/ai-issue-priority.mjs
index 1a60a110e5..d3b74390d5 100644
--- a/.github/scripts/ai-issue-priority.mjs
+++ b/.github/scripts/ai-issue-priority.mjs
@@ -52,26 +52,30 @@ If input is truncated, assess only visible evidence and express uncertainty abou
class PriorityError extends Error {}
function labelNames(issue) {
- return (issue.labels || []).map((label) => typeof label === "string" ? label : label.name).filter(Boolean);
+ return (issue.labels || []).map((label) => (typeof label === "string" ? label : label.name)).filter(Boolean);
}
function withoutUserPriority(body) {
let inPrioritySection = false;
- return String(body || "").split(/\r?\n/).filter((line) => {
- const heading = line.match(/^#{1,6}\s+(.+)/);
- if (heading) inPrioritySection = /优先级|priority|urgency/i.test(heading[1]);
- return !inPrioritySection && !/^\s*\*\*[^*]*(?:优先级|priority|urgency)[^*]*\*\*\s*[::]/i.test(line);
- }).join("\n").trim();
+ return String(body || "")
+ .split(/\r?\n/)
+ .filter((line) => {
+ const heading = line.match(/^#{1,6}\s+(.+)/);
+ if (heading) inPrioritySection = /优先级|priority|urgency/i.test(heading[1]);
+ return !inPrioritySection && !/^\s*\*\*[^*]*(?:优先级|priority|urgency)[^*]*\*\*\s*[::]/i.test(line);
+ })
+ .join("\n")
+ .trim();
}
export function prepareIssue(issue) {
const body = withoutUserPriority(issue.body);
return {
title: String(issue.title || "").slice(0, 500),
- body: body.length > MAX_BODY_LENGTH
- ? `${body.slice(0, 10000)}\n[TRUNCATED]\n${body.slice(-4000)}`
- : body,
- labels: labelNames(issue).filter((label) => /^(db\/|bug$|enhancement$|feature$|question$)/.test(label)).sort(),
+ body: body.length > MAX_BODY_LENGTH ? `${body.slice(0, 10000)}\n[TRUNCATED]\n${body.slice(-4000)}` : body,
+ labels: labelNames(issue)
+ .filter((label) => /^(db\/|bug$|enhancement$|feature$|question$)/.test(label))
+ .sort(),
truncated: body.length > MAX_BODY_LENGTH,
};
}
@@ -83,21 +87,26 @@ export function parseAssessment(content, issue) {
} catch {
throw new PriorityError("AtlasCloud returned invalid priority JSON");
}
- const validTextList = (value, maxLength) => Array.isArray(value) && value.length <= 3
- && value.every((entry) => typeof entry === "string" && entry.trim() && entry.length <= maxLength);
- if (!result || typeof result.priority !== "string" || !Object.hasOwn(PRIORITY_LABELS, result.priority)
- || !["high", "medium", "low"].includes(result.confidence)
- || !["security", "data-loss", "core-blocked", "functional", "feature", "cosmetic", "unknown"].includes(result.risk)
- || typeof result.reason !== "string" || !result.reason.trim() || result.reason.length > 600
- || !validTextList(result.evidence, 300) || !validTextList(result.missing_information, 250)) {
+ const validTextList = (value, maxLength) => Array.isArray(value) && value.length <= 3 && value.every((entry) => typeof entry === "string" && entry.trim() && entry.length <= maxLength);
+ if (
+ !result ||
+ typeof result.priority !== "string" ||
+ !Object.hasOwn(PRIORITY_LABELS, result.priority) ||
+ !["high", "medium", "low"].includes(result.confidence) ||
+ !["security", "data-loss", "core-blocked", "functional", "feature", "cosmetic", "unknown"].includes(result.risk) ||
+ typeof result.reason !== "string" ||
+ !result.reason.trim() ||
+ result.reason.length > 600 ||
+ !validTextList(result.evidence, 300) ||
+ !validTextList(result.missing_information, 250)
+ ) {
throw new PriorityError("AtlasCloud returned an invalid priority assessment");
}
if (!result.evidence.every((quote) => issue.title.includes(quote) || issue.body.includes(quote))) {
throw new PriorityError("AtlasCloud priority evidence does not match the issue text");
}
const insufficientEvidence = result.confidence === "low" || result.evidence.length === 0 || result.risk === "unknown";
- const unsupportedCriticalPriority = result.priority === "P0" && (result.confidence !== "high"
- || !["security", "data-loss", "core-blocked"].includes(result.risk));
+ const unsupportedCriticalPriority = result.priority === "P0" && (result.confidence !== "high" || !["security", "data-loss", "core-blocked"].includes(result.risk));
return {
priority: insufficientEvidence || unsupportedCriticalPriority ? "needs-info" : result.priority,
confidence: result.confidence,
@@ -219,8 +228,7 @@ export async function run({ event, apiKey, github, model = DEFAULT_MODEL, fetchI
await github("POST", `${issuePath}/labels`, { labels: [targetLabel] });
}
for (const label of existingLabels) {
- if (label !== targetLabel && label.startsWith(LABEL_PREFIX)
- && Object.hasOwn(PRIORITY_LABELS, label.slice(LABEL_PREFIX.length))) {
+ if (label !== targetLabel && label.startsWith(LABEL_PREFIX) && Object.hasOwn(PRIORITY_LABELS, label.slice(LABEL_PREFIX.length))) {
try {
await github("DELETE", `${issuePath}/labels/${encodeURIComponent(label)}`);
} catch (error) {
@@ -234,10 +242,12 @@ export async function run({ event, apiKey, github, model = DEFAULT_MODEL, fetchI
export function formatSummary(result) {
const escape = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
if (result.skipped) return `### AI issue priority\n\nSkipped: ${escape(result.skipped)}\n`;
- return `### AI issue priority${result.dryRun ? " (dry run)" : ""}\n\n`
- + "AI suggestion based on issue text only, not a verified diagnosis or release commitment. "
- + "User-reported and maintainer priority labels are unchanged.\n\n"
- + `
${escape(JSON.stringify({ model: result.model, ...result.assessment }, null, 2))} \n`;
+ return (
+ `### AI issue priority${result.dryRun ? " (dry run)" : ""}\n\n` +
+ "AI suggestion based on issue text only, not a verified diagnosis or release commitment. " +
+ "User-reported and maintainer priority labels are unchanged.\n\n" +
+ `${escape(JSON.stringify({ model: result.model, ...result.assessment }, null, 2))} \n`
+ );
}
async function main() {
diff --git a/.github/scripts/ai-issue-priority.test.mjs b/.github/scripts/ai-issue-priority.test.mjs
index fb191a08a3..2648de4415 100644
--- a/.github/scripts/ai-issue-priority.test.mjs
+++ b/.github/scripts/ai-issue-priority.test.mjs
@@ -1,22 +1,13 @@
import assert from "node:assert/strict";
import test from "node:test";
-import {
- assessIssue,
- createGitHubClient,
- DEFAULT_MODEL,
- formatSummary,
- parseAssessment,
- prepareIssue,
- run,
-} from "./ai-issue-priority.mjs";
+import { assessIssue, createGitHubClient, DEFAULT_MODEL, formatSummary, parseAssessment, prepareIssue, run } from "./ai-issue-priority.mjs";
const baseIssue = {
number: 42,
state: "open",
title: "[Bug] Grid column alignment is inconsistent",
- body: "### Description\n\nOnly the column header is misaligned; queries and editing work.\n\n"
- + "### Priority (urgency)\n\nP0 Must fix next release\n\n### Additional information\n\nDBX on Linux.",
+ body: "### Description\n\nOnly the column header is misaligned; queries and editing work.\n\n" + "### Priority (urgency)\n\nP0 Must fix next release\n\n### Additional information\n\nDBX on Linux.",
labels: [{ name: "bug" }, { name: "db/mysql" }, { name: "user-priority/P0" }],
};
@@ -51,7 +42,9 @@ function fixture({ issue = baseIssue, latest = issue, result = assessment(), act
if (method === "GET") return ++reads === 1 ? issue : latest;
return {};
},
- fetchImpl: modelFetch(result, () => { modelCalls += 1; }),
+ fetchImpl: modelFetch(result, () => {
+ modelCalls += 1;
+ }),
};
return { options, calls, modelCalls: () => modelCalls };
}
@@ -114,14 +107,7 @@ test("validates every priority and normalizes low-confidence or ungrounded decis
for (const priority of ["P1", "P2", "P3", "needs-info"]) {
assert.equal(parseAssessment(JSON.stringify(assessment({ priority })), prepared).priority, priority);
}
- for (const override of [
- { confidence: "low" },
- { evidence: [] },
- { risk: "unknown" },
- { priority: "P0", risk: "cosmetic" },
- { priority: "P0", risk: "feature" },
- { priority: "P0", risk: "data-loss", confidence: "medium" },
- ]) {
+ for (const override of [{ confidence: "low" }, { evidence: [] }, { risk: "unknown" }, { priority: "P0", risk: "cosmetic" }, { priority: "P0", risk: "feature" }, { priority: "P0", risk: "data-loss", confidence: "medium" }]) {
assert.equal(parseAssessment(JSON.stringify(assessment(override)), prepared).priority, "needs-info");
}
const criticalIssue = prepareIssue({ title: "Data loss", body: "Saving one row overwrites every row in the table." });
@@ -132,13 +118,25 @@ test("validates every priority and normalizes low-confidence or ungrounded decis
test("rejects malformed, oversized, unknown and fabricated model results", () => {
const prepared = prepareIssue(baseIssue);
for (const invalid of [
- "not JSON", "null", "[]", "```json\n{}\n```",
+ "not JSON",
+ "null",
+ "[]",
+ "```json\n{}\n```",
...[
- { priority: "P9" }, { priority: "__proto__" }, { priority: "constructor" }, { priority: ["P0"] },
- { confidence: "certain" }, { risk: "urgent" }, { reason: " " }, { reason: "x".repeat(601) },
- { evidence: ["Invented widespread data loss"] }, { evidence: ["P0 Must fix next release"] },
- { evidence: [""] }, { evidence: "not an array" },
- { missing_information: ["x".repeat(251)] }, { missing_information: ["a", "b", "c", "d"] },
+ { priority: "P9" },
+ { priority: "__proto__" },
+ { priority: "constructor" },
+ { priority: ["P0"] },
+ { confidence: "certain" },
+ { risk: "urgent" },
+ { reason: " " },
+ { reason: "x".repeat(601) },
+ { evidence: ["Invented widespread data loss"] },
+ { evidence: ["P0 Must fix next release"] },
+ { evidence: [""] },
+ { evidence: "not an array" },
+ { missing_information: ["x".repeat(251)] },
+ { missing_information: ["a", "b", "c", "d"] },
].map((override) => JSON.stringify(assessment(override))),
]) {
assert.throws(() => parseAssessment(invalid, prepared));
@@ -151,14 +149,22 @@ test("labels independently of reporter and maintainer priority, removing only ob
const result = await run(state.options);
assert.equal(result.label, "ai-priority/P3");
assert.equal(state.modelCalls(), 1);
- assert.deepEqual(state.calls.filter(({ method }) => method !== "GET"), [
- { method: "POST", path: "/labels", body: {
- name: "ai-priority/P3", color: "0e8a16",
- description: "AI: low repair/implementation priority; minor or optional improvement",
- } },
- { method: "POST", path: "/issues/42/labels", body: { labels: ["ai-priority/P3"] } },
- { method: "DELETE", path: "/issues/42/labels/ai-priority%2FP1", body: undefined },
- ]);
+ assert.deepEqual(
+ state.calls.filter(({ method }) => method !== "GET"),
+ [
+ {
+ method: "POST",
+ path: "/labels",
+ body: {
+ name: "ai-priority/P3",
+ color: "0e8a16",
+ description: "AI: low repair/implementation priority; minor or optional improvement",
+ },
+ },
+ { method: "POST", path: "/issues/42/labels", body: { labels: ["ai-priority/P3"] } },
+ { method: "DELETE", path: "/issues/42/labels/ai-priority%2FP1", body: undefined },
+ ],
+ );
});
test("does not rewrite an unchanged AI label", async () => {
@@ -187,8 +193,7 @@ test("skips missing key, unrelated events, PRs, closed and opted-out issues befo
test("edits reconcile priority even when a preceding workflow may have been canceled", async () => {
for (const config of [
{ action: "edited", changes: { body: { from: baseIssue.body.replace("P0", "P3") } } },
- { action: "edited", issue: { ...baseIssue, labels: [...baseIssue.labels, "ai-priority/P1"] },
- changes: { body: { from: baseIssue.body.replace("P0", "P3") } } },
+ { action: "edited", issue: { ...baseIssue, labels: [...baseIssue.labels, "ai-priority/P1"] }, changes: { body: { from: baseIssue.body.replace("P0", "P3") } } },
{ action: "reopened" },
{ action: "edited", changes: { title: { from: "Old title" } } },
{ action: "edited", changes: { body: { from: "Old body" } } },
@@ -211,10 +216,7 @@ test("uses fresh issue content and rechecks eligibility before paid assessment",
});
test("does not publish stale results after changes, closure or maintainer opt-out", async () => {
- for (const override of [
- { title: "New title" }, { body: "New evidence" }, { labels: ["db/postgres"] },
- { state: "closed" }, { labels: ["ai-priority/skip"] },
- ]) {
+ for (const override of [{ title: "New title" }, { body: "New evidence" }, { labels: ["db/postgres"] }, { state: "closed" }, { labels: ["ai-priority/skip"] }]) {
const state = fixture({ latest: { ...baseIssue, ...override } });
assert.equal((await run(state.options)).skipped, "issue changed during assessment");
assert.equal(state.modelCalls(), 1);
@@ -247,7 +249,9 @@ test("provider failures and invalid outputs preserve all existing labels and do
async () => Response.json({ error: "secret-response-body" }, { status: 401 }),
async () => Response.json({ error: "secret-response-body" }, { status: 429 }),
async () => Response.json({ error: "secret-response-body" }, { status: 500 }),
- async () => { throw new Error("secret-response-body"); },
+ async () => {
+ throw new Error("secret-response-body");
+ },
async () => new Response("secret-response-body"),
async () => Response.json({ choices: [] }),
async () => Response.json({ choices: [{ finish_reason: "length", message: { content: "secret-response-body" } }] }),
@@ -282,7 +286,10 @@ test("failed label addition never removes the previous AI priority", async () =>
return original(method, path, body);
};
await assert.rejects(run(state.options), /GitHub unavailable/);
- assert.equal(state.calls.some(({ method }) => method === "DELETE"), false);
+ assert.equal(
+ state.calls.some(({ method }) => method === "DELETE"),
+ false,
+ );
});
test("invalid issue numbers are rejected before requests", async () => {
@@ -299,7 +306,8 @@ test("GitHub client keeps credentials separate, uses timeouts and never includes
assert.throws(() => createGitHubClient({ token: "fixture-token", repository: "https://other.example" }));
const calls = [];
const github = createGitHubClient({
- token: "fixture-token", repository: "t8y2/dbx",
+ token: "fixture-token",
+ repository: "t8y2/dbx",
fetchImpl: async (url, options) => {
calls.push({ url, options });
return options.method === "DELETE" ? new Response(null, { status: 204 }) : Response.json(baseIssue);
@@ -311,7 +319,8 @@ test("GitHub client keeps credentials separate, uses timeouts and never includes
assert.ok(calls[0].options.signal instanceof AbortSignal);
assert.equal(await github("DELETE", "/issues/42/labels/test"), null);
const failing = createGitHubClient({
- token: "fixture-token", repository: "t8y2/dbx",
+ token: "fixture-token",
+ repository: "t8y2/dbx",
fetchImpl: async () => new Response("secret-response-body", { status: 403 }),
});
await assert.rejects(failing("GET", "/issues/42"), (error) => {
diff --git a/.github/scripts/backfill-similar-issues.mjs b/.github/scripts/backfill-similar-issues.mjs
index 26679bc73c..0e4434e834 100644
--- a/.github/scripts/backfill-similar-issues.mjs
+++ b/.github/scripts/backfill-similar-issues.mjs
@@ -33,23 +33,14 @@ export function selectChunk(issues, { chunkIndex, chunkCount, maxIssues = 0 }) {
export async function listOpenUnassignedIssues(client) {
const issues = [];
for (let page = 1; ; page += 1) {
- const items = await client.request(
- "GET",
- `/repos/${client.repository}/issues?state=open&sort=created&direction=desc&per_page=100&page=${page}`,
- );
+ const items = await client.request("GET", `/repos/${client.repository}/issues?state=open&sort=created&direction=desc&per_page=100&page=${page}`);
issues.push(...items.filter((issue) => !issue.pull_request && (issue.assignees || []).length === 0));
if (items.length < 100) break;
}
return issues;
}
-export async function backfill({
- client,
- chunkIndex,
- chunkCount,
- maxIssues = 0,
- searchDelayMs = DEFAULT_SEARCH_DELAY_MS,
-}) {
+export async function backfill({ client, chunkIndex, chunkCount, maxIssues = 0, searchDelayMs = DEFAULT_SEARCH_DELAY_MS }) {
const issues = selectChunk(await listOpenUnassignedIssues(client), { chunkIndex, chunkCount, maxIssues });
const originalSearchIssues = client.searchIssues.bind(client);
let lastSearchStartedAt = 0;
@@ -98,10 +89,6 @@ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url)
chunkIndex: integerValue(process.env.BACKFILL_CHUNK_INDEX, 0, "BACKFILL_CHUNK_INDEX"),
chunkCount: integerValue(process.env.BACKFILL_CHUNK_COUNT, 1, "BACKFILL_CHUNK_COUNT"),
maxIssues: integerValue(process.env.BACKFILL_MAX_ISSUES, 0, "BACKFILL_MAX_ISSUES"),
- searchDelayMs: integerValue(
- process.env.BACKFILL_SEARCH_DELAY_MS,
- DEFAULT_SEARCH_DELAY_MS,
- "BACKFILL_SEARCH_DELAY_MS",
- ),
+ searchDelayMs: integerValue(process.env.BACKFILL_SEARCH_DELAY_MS, DEFAULT_SEARCH_DELAY_MS, "BACKFILL_SEARCH_DELAY_MS"),
});
}
diff --git a/.github/scripts/backfill-similar-issues.test.mjs b/.github/scripts/backfill-similar-issues.test.mjs
index 4c5aeefcd1..2f96dd7092 100644
--- a/.github/scripts/backfill-similar-issues.test.mjs
+++ b/.github/scripts/backfill-similar-issues.test.mjs
@@ -1,17 +1,19 @@
import assert from "node:assert/strict";
import test from "node:test";
-import {
- backfill,
- listOpenUnassignedIssues,
- selectChunk,
-} from "./backfill-similar-issues.mjs";
+import { backfill, listOpenUnassignedIssues, selectChunk } from "./backfill-similar-issues.mjs";
test("selectChunk partitions issue numbers deterministically", () => {
const issues = [1, 2, 3, 4, 5, 6].map((number) => ({ number }));
- assert.deepEqual(selectChunk(issues, { chunkIndex: 0, chunkCount: 3 }).map(({ number }) => number), [6, 3]);
- assert.deepEqual(selectChunk(issues, { chunkIndex: 1, chunkCount: 3 }).map(({ number }) => number), [4, 1]);
+ assert.deepEqual(
+ selectChunk(issues, { chunkIndex: 0, chunkCount: 3 }).map(({ number }) => number),
+ [6, 3],
+ );
+ assert.deepEqual(
+ selectChunk(issues, { chunkIndex: 1, chunkCount: 3 }).map(({ number }) => number),
+ [4, 1],
+ );
assert.deepEqual(
selectChunk(issues, { chunkIndex: 2, chunkCount: 3, maxIssues: 1 }).map(({ number }) => number),
[5],
@@ -34,8 +36,14 @@ test("listOpenUnassignedIssues filters pull requests and assigned issues across
const issues = await listOpenUnassignedIssues(client);
assert.equal(issues.length, 99);
- assert.equal(issues.some(({ number }) => number === 1), false);
- assert.equal(issues.some(({ number }) => number === 2), false);
+ assert.equal(
+ issues.some(({ number }) => number === 1),
+ false,
+ );
+ assert.equal(
+ issues.some(({ number }) => number === 2),
+ false,
+ );
assert.equal(issues.at(-1).number, 101);
});
diff --git a/.github/scripts/bump-agent-versions.mjs b/.github/scripts/bump-agent-versions.mjs
index 58779afcf1..36c17d7146 100644
--- a/.github/scripts/bump-agent-versions.mjs
+++ b/.github/scripts/bump-agent-versions.mjs
@@ -5,10 +5,7 @@ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs
const VERSIONS_PATH = "agents/versions.json";
const VERSION_SYNC_SUBJECT = "chore: bump module versions [skip ci]";
const JRE_BUILD_PATHS = new Set([".github/workflows/agents-release.yml"]);
-const NATIVE_RELEASE_PACKAGING_PATHS = new Set([
- ".github/scripts/reuse-agent-release-assets.mjs",
- "agents/scripts/version_agent_artifacts.py",
-]);
+const NATIVE_RELEASE_PACKAGING_PATHS = new Set([".github/scripts/reuse-agent-release-assets.mjs", "agents/scripts/version_agent_artifacts.py"]);
function bumpPatchVersion(version) {
const match = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(version);
@@ -35,11 +32,7 @@ export function parseLegacyStandaloneProjects(buildGradle) {
const match = /legacyStandaloneProjects\s*=\s*\[([^\]]*)\]/m.exec(buildGradle);
if (!match) return new Set();
- return new Set(
- [...match[1].matchAll(/['"]([^'"]+)['"]/g)]
- .map((entry) => entry[1])
- .filter(Boolean),
- );
+ return new Set([...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((entry) => entry[1]).filter(Boolean));
}
function fileContainsCommonDependency(path, moduleExists, readModuleFile) {
@@ -72,23 +65,10 @@ const crateNativeDriverDirectories = {
};
const nativeDriverModules = new Set(["cassandra", "duckdb", "hive", "argo", "oracle", "oracle-oci", "xugu", "kingbase", "iotdb", "neo4j", "nebula", "vastbase", "rabbitmq", "rocketmq", "zookeeper", "tdengine", "etcd", "etcd2", "sqlite-worker"]);
const nativeDriverSharedPaths = {
- hive: [
- "agents/go-common/go-gssapi",
- "agents/go-common/gohive",
- "agents/go-common/gosasl",
- ],
- argo: [
- "agents/go-common/go-gssapi",
- "agents/go-common/gohive",
- "agents/go-common/gosasl",
- ],
- zookeeper: [
- "agents/go-common/go-gssapi",
- "agents/go-common/gosasl",
- ],
- etcd: [
- "agents/go-common/go-semver",
- ],
+ hive: ["agents/go-common/go-gssapi", "agents/go-common/gohive", "agents/go-common/gosasl"],
+ argo: ["agents/go-common/go-gssapi", "agents/go-common/gohive", "agents/go-common/gosasl"],
+ zookeeper: ["agents/go-common/go-gssapi", "agents/go-common/gosasl"],
+ etcd: ["agents/go-common/go-semver"],
};
function resolveAgentModule(moduleName, { legacyStandaloneModules, moduleExists, readModuleFile }) {
@@ -137,16 +117,7 @@ function classifyModules(versions, options) {
.filter(({ module }) => module);
}
-export function evaluateAgentVersionBump({
- versions,
- prevVersions = versions,
- changedFiles,
- legacyStandaloneModules = new Set(),
- moduleExists = existsSync,
- readModuleFile = (path) => readFileSync(path, "utf8"),
- skipBump = false,
- manualVersionsChanged = changedFiles.includes(VERSIONS_PATH),
-}) {
+export function evaluateAgentVersionBump({ versions, prevVersions = versions, changedFiles, legacyStandaloneModules = new Set(), moduleExists = existsSync, readModuleFile = (path) => readFileSync(path, "utf8"), skipBump = false, manualVersionsChanged = changedFiles.includes(VERSIONS_PATH) }) {
const nextVersions = { ...versions };
const logs = [];
let changed = false;
@@ -180,8 +151,7 @@ export function evaluateAgentVersionBump({
}
for (const { moduleName, module } of resolvedModules) {
- const moduleChanged = [module.modulePath, ...module.sharedPaths]
- .some((path) => pathChanged(changedFiles, path));
+ const moduleChanged = [module.modulePath, ...module.sharedPaths].some((path) => pathChanged(changedFiles, path));
// Only modules that package agents/common need installer-visible updates
// for shared Java runtime changes; native and standalone agents do not.
const commonAffectsModule = commonChanged && module.commonDependent;
@@ -233,22 +203,15 @@ function git(args) {
}
function lines(value) {
- return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
+ return value
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean);
}
export function resolveAgentReleaseBaseline({ prevTag, headRef = "HEAD", gitOutput = git }) {
const allChangedFiles = lines(gitOutput(["diff", "--name-only", `${prevTag}..${headRef}`]));
- const versionCommits = lines(
- gitOutput([
- "log",
- "--reverse",
- "--ancestry-path",
- "--format=%H%x09%s",
- `${prevTag}..${headRef}`,
- "--",
- VERSIONS_PATH,
- ]),
- );
+ const versionCommits = lines(gitOutput(["log", "--reverse", "--ancestry-path", "--format=%H%x09%s", `${prevTag}..${headRef}`, "--", VERSIONS_PATH]));
let syncCommit = "";
for (const entry of versionCommits) {
@@ -266,12 +229,8 @@ export function resolveAgentReleaseBaseline({ prevTag, headRef = "HEAD", gitOutp
const versionsRef = syncCommit || prevTag;
const versions = JSON.parse(gitOutput(["show", `${versionsRef}:${VERSIONS_PATH}`]));
- const versionsChangedAfterSync = syncCommit
- ? lines(gitOutput(["log", "--format=%H", `${syncCommit}..${headRef}`, "--", VERSIONS_PATH])).length > 0
- : false;
- const changedFiles = syncCommit && !versionsChangedAfterSync
- ? allChangedFiles.filter((file) => file !== VERSIONS_PATH)
- : allChangedFiles;
+ const versionsChangedAfterSync = syncCommit ? lines(gitOutput(["log", "--format=%H", `${syncCommit}..${headRef}`, "--", VERSIONS_PATH])).length > 0 : false;
+ const changedFiles = syncCommit && !versionsChangedAfterSync ? allChangedFiles.filter((file) => file !== VERSIONS_PATH) : allChangedFiles;
return {
prevTag,
diff --git a/.github/scripts/bump-agent-versions.test.mjs b/.github/scripts/bump-agent-versions.test.mjs
index 577e7ebc6f..eb26f8a5e9 100644
--- a/.github/scripts/bump-agent-versions.test.mjs
+++ b/.github/scripts/bump-agent-versions.test.mjs
@@ -118,11 +118,7 @@ test("bumps the native RocketMQ agent from its Go directory", () => {
});
test("bumps ZooKeeper from native and shared SASL source directories", () => {
- for (const changedFile of [
- "agents/drivers/zookeeper/main.go",
- "agents/go-common/gosasl/sasl.go",
- "agents/go-common/go-gssapi/krb5/krb5.go",
- ]) {
+ for (const changedFile of ["agents/drivers/zookeeper/main.go", "agents/go-common/gosasl/sasl.go", "agents/go-common/go-gssapi/krb5/krb5.go"]) {
const result = evaluateAgentVersionBump({
versions: { zookeeper: "0.1.0" },
changedFiles: [changedFile],
@@ -161,12 +157,7 @@ test("bumps Cassandra from its native Go source directory", () => {
});
test("bumps Hive from native and shared Kerberos source directories", () => {
- for (const changedFile of [
- "agents/drivers/hive-go/main.go",
- "agents/go-common/gohive/driver.go",
- "agents/go-common/gosasl/gssapi.go",
- "agents/go-common/go-gssapi/krb5/krb5.go",
- ]) {
+ for (const changedFile of ["agents/drivers/hive-go/main.go", "agents/go-common/gohive/driver.go", "agents/go-common/gosasl/gssapi.go", "agents/go-common/go-gssapi/krb5/krb5.go"]) {
const result = evaluateAgentVersionBump({
versions: { hive: "0.1.43" },
changedFiles: [changedFile],
@@ -207,12 +198,7 @@ test("bumps IoTDB from its native Go source directory", () => {
});
test("rebuilds native modules when shared native packaging changes", () => {
- const existing = new Set([
- "agents/drivers/access",
- "agents/drivers/access/build.gradle",
- "agents/drivers/duckdb",
- "agents/drivers/neo4j-go",
- ]);
+ const existing = new Set(["agents/drivers/access", "agents/drivers/access/build.gradle", "agents/drivers/duckdb", "agents/drivers/neo4j-go"]);
const result = evaluateAgentVersionBump({
versions: { access: "0.1.37", duckdb: "0.1.3", neo4j: "0.1.40" },
changedFiles: ["agents/scripts/version_agent_artifacts.py"],
@@ -255,12 +241,7 @@ test("bumps DuckDB when its Cargo target configuration changes", () => {
});
test("builds only common-dependent Java modules for a shared runtime change", () => {
- const existing = new Set([
- "agents/drivers/access",
- "agents/drivers/access/build.gradle",
- "agents/drivers/mongodb",
- "agents/drivers/mongodb/build.gradle",
- ]);
+ const existing = new Set(["agents/drivers/access", "agents/drivers/access/build.gradle", "agents/drivers/mongodb", "agents/drivers/mongodb/build.gradle"]);
const result = evaluateAgentVersionBump({
versions: { access: "0.1.0", mongodb: "0.1.0" },
changedFiles: ["agents/common/src/main/java/com/dbx/Agent.java"],
@@ -341,7 +322,7 @@ function createRepository(versions) {
mkdirSync(join(repository, "agents/drivers/duckdb"), { recursive: true });
writeVersions(repository, versions);
writeFileSync(join(repository, "agents/drivers/kingbase-go/kingbase_metadata.go"), "package main\n");
- writeFileSync(join(repository, "agents/drivers/duckdb/Cargo.toml"), "[package]\nname = \"duckdb-test\"\n");
+ writeFileSync(join(repository, "agents/drivers/duckdb/Cargo.toml"), '[package]\nname = "duckdb-test"\n');
commitAll(repository, "feat(agents): initial release state");
return repository;
}
diff --git a/.github/scripts/ci-change-base.mjs b/.github/scripts/ci-change-base.mjs
index 9e9914fb62..62720a3822 100644
--- a/.github/scripts/ci-change-base.mjs
+++ b/.github/scripts/ci-change-base.mjs
@@ -5,8 +5,12 @@ import { pathToFileURL } from "node:url";
export function changeBase({ eventName, before, root = process.cwd() }) {
if (eventName === "pull_request") {
const commits = execFileSync("git", ["rev-list", "--parents", "-n", "1", "HEAD"], {
- cwd: root, encoding: "utf8", stdio: "pipe",
- }).trim().split(/\s+/);
+ cwd: root,
+ encoding: "utf8",
+ stdio: "pipe",
+ })
+ .trim()
+ .split(/\s+/);
if (commits.length !== 3) throw new Error("PR change detection requires the checked-out merge commit and both parents");
return commits[1];
}
diff --git a/.github/scripts/ci-change-base.test.mjs b/.github/scripts/ci-change-base.test.mjs
index e262c83e6d..1d02ba8aa1 100644
--- a/.github/scripts/ci-change-base.test.mjs
+++ b/.github/scripts/ci-change-base.test.mjs
@@ -52,10 +52,7 @@ test("PR routing uses the exact test merge first parent, excluding unrelated mai
assert.equal(base, fixture.actualBase);
assert.notEqual(base, fixture.pullRequestHead);
const files = changedPaths(base, fixture.root);
- assert.deepEqual(files.toSorted(), [
- "crates/dbx-core/tests/old.rs", "crates/dbx-core/tests/deleted.rs", "crates/dbx-core/tests/renamed\nfile.rs",
- "crates/dbx-core/tests/runtime_diagnostic_history.rs", "crates/dbx-web/src/routes/transfer.rs",
- ].toSorted());
+ assert.deepEqual(files.toSorted(), ["crates/dbx-core/tests/old.rs", "crates/dbx-core/tests/deleted.rs", "crates/dbx-core/tests/renamed\nfile.rs", "crates/dbx-core/tests/runtime_diagnostic_history.rs", "crates/dbx-web/src/routes/transfer.rs"].toSorted());
const metadata = {
workspace_members: ["dbx-core", "dbx-web", "dbx"],
packages: [
@@ -66,7 +63,10 @@ test("PR routing uses the exact test merge first parent, excluding unrelated mai
};
const corrected = planCi({ files, metadata, root: fixture.root });
assert.equal(corrected.rust_full, false);
- assert.deepEqual(corrected.rust_matrix.include.map((entry) => entry.group), ["application"]);
+ assert.deepEqual(
+ corrected.rust_matrix.include.map((entry) => entry.group),
+ ["application"],
+ );
assert.deepEqual(corrected.agent_rust.include, [{ driver: "duckdb" }]);
assert.equal(corrected.agent_java, false);
assert.deepEqual(corrected.agent_go.include, []);
@@ -90,7 +90,9 @@ test("a depth-two checkout preserves the PR parents and supports the base CLI",
assert.equal(changeBase({ eventName: "pull_request", root: checkout }), fixture.actualBase);
assert.deepEqual(changedPaths(fixture.actualBase, checkout), changedPaths(fixture.actualBase, fixture.root));
const output = execFileSync(process.execPath, [path.join(import.meta.dirname, "ci-change-base.mjs")], {
- cwd: checkout, encoding: "utf8", env: { ...process.env, GITHUB_EVENT_NAME: "pull_request", BEFORE_SHA: fixture.staleBase },
+ cwd: checkout,
+ encoding: "utf8",
+ env: { ...process.env, GITHUB_EVENT_NAME: "pull_request", BEFORE_SHA: fixture.staleBase },
});
assert.equal(output.trim(), fixture.actualBase);
});
diff --git a/.github/scripts/ci-config.mjs b/.github/scripts/ci-config.mjs
index 8d49cf1dea..145da17d5e 100644
--- a/.github/scripts/ci-config.mjs
+++ b/.github/scripts/ci-config.mjs
@@ -1,13 +1,6 @@
export const rustGroups = {
- foundation: [
- "dbx-types", "dbx-platform", "dbx-sql-core", "dbx-sql-dialect", "dbx-sql-data", "dbx-sql-schema", "dbx-sql",
- "dbx-formats", "dbx-ai-provider", "dbx-plugin-runtime",
- ],
- drivers: [
- "dbx-driver-support", "dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb",
- "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-drivers",
- "dbx-sqlite-worker",
- ],
+ foundation: ["dbx-types", "dbx-platform", "dbx-sql-core", "dbx-sql-dialect", "dbx-sql-data", "dbx-sql-schema", "dbx-sql", "dbx-formats", "dbx-ai-provider", "dbx-plugin-runtime"],
+ drivers: ["dbx-driver-support", "dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb", "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-drivers", "dbx-sqlite-worker"],
application: ["dbx", "dbx-core", "dbx-web", "dbx-cli", "dbx-mcp", "dbx-tauri-consul", "dbx-tauri-schema"],
};
@@ -32,7 +25,9 @@ export const integrationCases = [
{ driver: "zookeeper", scenario: "zookeeper-sasl", version: "3.7.0" },
...["4.9.8", "5.3.1"].map((version) => ({ driver: "rocketmq", scenario: "rocketmq", version })),
...["2.4.0.14", "2.6.0.34", "3.0.7.1", "3.3.6.13", "3.4.2.2"].map((version) => ({
- driver: "tdengine", scenario: "tdengine", version,
+ driver: "tdengine",
+ scenario: "tdengine",
+ version,
image: `tdengine/${version === "3.4.2.2" ? "tsdb" : "tdengine"}:${version}`,
})),
...["3.11.19", "5.0.6"].map((version) => ({ driver: "cassandra-go", scenario: "cassandra", version })),
diff --git a/.github/scripts/ci-execution.test.mjs b/.github/scripts/ci-execution.test.mjs
index bc503d6f1a..4cf57ae424 100644
--- a/.github/scripts/ci-execution.test.mjs
+++ b/.github/scripts/ci-execution.test.mjs
@@ -9,10 +9,24 @@ import { goAgents, integrationCases } from "./ci-config.mjs";
import { lockfileManifests } from "./ci-lockfiles.mjs";
const root = fileURLToPath(new URL("../../", import.meta.url));
-const environmentKeys = ["CGO_ENABLED", "GOOS", "GOARCH", "GONOSUMDB", "DBX_ZOOKEEPER_TEST_CONNECT_STRING",
- "DBX_ZOOKEEPER_TEST_AUTH_SCHEME", "DBX_ZOOKEEPER_TEST_USERNAME", "DBX_ZOOKEEPER_TEST_PASSWORD", "TDENGINE_INTEGRATION",
- "TDENGINE_TEST_HOST", "TDENGINE_TEST_PORT", "CASSANDRA_TEST_HOST", "CASSANDRA_TEST_PORT", "RABBITMQ_INTEGRATION",
- "RABBITMQ_USERNAME", "RABBITMQ_PASSWORD"];
+const environmentKeys = [
+ "CGO_ENABLED",
+ "GOOS",
+ "GOARCH",
+ "GONOSUMDB",
+ "DBX_ZOOKEEPER_TEST_CONNECT_STRING",
+ "DBX_ZOOKEEPER_TEST_AUTH_SCHEME",
+ "DBX_ZOOKEEPER_TEST_USERNAME",
+ "DBX_ZOOKEEPER_TEST_PASSWORD",
+ "TDENGINE_INTEGRATION",
+ "TDENGINE_TEST_HOST",
+ "TDENGINE_TEST_PORT",
+ "CASSANDRA_TEST_HOST",
+ "CASSANDRA_TEST_PORT",
+ "RABBITMQ_INTEGRATION",
+ "RABBITMQ_USERNAME",
+ "RABBITMQ_PASSWORD",
+];
function withToolStubs(run, extraEnv = {}) {
const directory = mkdtempSync(path.join(tmpdir(), "dbx-ci-execution-"));
@@ -20,7 +34,9 @@ function withToolStubs(run, extraEnv = {}) {
mkdirSync(binaries);
const log = path.join(directory, "commands.jsonl");
const stub = path.join(binaries, "stub.cjs");
- writeFileSync(stub, `#!/usr/bin/env node
+ writeFileSync(
+ stub,
+ `#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const tool = path.basename(process.argv[1]);
@@ -29,100 +45,131 @@ const env = Object.fromEntries(${JSON.stringify(environmentKeys)}.filter(name =>
fs.appendFileSync(process.env.CI_STUB_LOG, JSON.stringify({tool,args,cwd:process.cwd(),env})+'\\n');
if (process.env.CI_STUB_FAIL === tool) process.exit(23);
if (tool === 'docker' && args.includes('taos')) console.log('| ready |');
-`, { mode: 0o755 });
+`,
+ { mode: 0o755 },
+ );
for (const name of ["docker", "go", "cargo", "timeout", "curl", "sleep", "bash"]) symlinkSync(stub, path.join(binaries, name));
try {
const env = { ...process.env, ...extraEnv, TMPDIR: directory, CI_STUB_LOG: log, PATH: `${binaries}${path.delimiter}${process.env.PATH}` };
- return run(env, () => readFileSync(log, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)));
- } finally { rmSync(directory, { recursive: true, force: true }); }
+ return run(env, () =>
+ readFileSync(log, "utf8")
+ .trim()
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => JSON.parse(line)),
+ );
+ } finally {
+ rmSync(directory, { recursive: true, force: true });
+ }
}
for (const agent of goAgents) {
- test(`Go job preserves ${agent.driver} tests and target builds without executing compilers`, () => withToolStubs((env, commands) => {
- const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-go.sh", agent.driver, agent.binary, String(agent.race)], { cwd: root, env, encoding: "utf8" });
- assert.equal(result.status, 0, result.stderr);
- const invocations = commands();
- assert.deepEqual(invocations[0].args, agent.race ? ["test", "-race", "./..."] : ["test", "./..."]);
- assert.ok(invocations.every((entry) => entry.cwd === path.join(root, "agents/drivers", agent.driver)));
- const builds = invocations.slice(1);
- assert.equal(builds.length, agent.driver === "zookeeper" ? 6 : 1);
- for (const build of builds) {
- assert.deepEqual(build.args.slice(0, 3), ["build", "-trimpath", "-ldflags=-s -w"]);
- assert.equal(build.env.CGO_ENABLED, "0");
- }
- if (agent.driver === "zookeeper") assert.deepEqual(builds.map((entry) => `${entry.env.GOOS}/${entry.env.GOARCH}`),
- ["darwin/arm64", "darwin/amd64", "linux/arm64", "linux/amd64", "windows/arm64", "windows/amd64"]);
- else assert.ok(builds[0].args.includes(`/tmp/dbx-agent-${agent.binary}-linux-x64`));
- if (agent.driver === "xugu") assert.ok(invocations.every((entry) => entry.env.GONOSUMDB === "gitee.com/XuguDB/go-xugu-driver"));
- }));
+ test(`Go job preserves ${agent.driver} tests and target builds without executing compilers`, () =>
+ withToolStubs((env, commands) => {
+ const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-go.sh", agent.driver, agent.binary, String(agent.race)], { cwd: root, env, encoding: "utf8" });
+ assert.equal(result.status, 0, result.stderr);
+ const invocations = commands();
+ assert.deepEqual(invocations[0].args, agent.race ? ["test", "-race", "./..."] : ["test", "./..."]);
+ assert.ok(invocations.every((entry) => entry.cwd === path.join(root, "agents/drivers", agent.driver)));
+ const builds = invocations.slice(1);
+ assert.equal(builds.length, agent.driver === "zookeeper" ? 6 : 1);
+ for (const build of builds) {
+ assert.deepEqual(build.args.slice(0, 3), ["build", "-trimpath", "-ldflags=-s -w"]);
+ assert.equal(build.env.CGO_ENABLED, "0");
+ }
+ if (agent.driver === "zookeeper")
+ assert.deepEqual(
+ builds.map((entry) => `${entry.env.GOOS}/${entry.env.GOARCH}`),
+ ["darwin/arm64", "darwin/amd64", "linux/arm64", "linux/amd64", "windows/arm64", "windows/amd64"],
+ );
+ else assert.ok(builds[0].args.includes(`/tmp/dbx-agent-${agent.binary}-linux-x64`));
+ if (agent.driver === "xugu") assert.ok(invocations.every((entry) => entry.env.GONOSUMDB === "gitee.com/XuguDB/go-xugu-driver"));
+ }));
}
for (const entry of integrationCases) {
- test(`live matrix retains ${entry.scenario}/${entry.version} commands without starting any container`, () => withToolStubs((env, commands) => {
- const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", entry.scenario, entry.version, entry.image ?? ""], { cwd: root, env, encoding: "utf8" });
- assert.equal(result.status, 0, result.stderr);
- const invocations = commands();
- if (entry.scenario === "rocketmq") {
- assert.ok(invocations.some((call) => call.tool === "bash" && call.args.at(-1) === entry.version));
- return;
- }
- const start = invocations.find((call) => call.tool === "docker" && call.args[0] === "run");
- assert.ok(start, "starts the selected container");
- const expectedImage = entry.image ?? (entry.scenario.startsWith("zookeeper") ? `zookeeper:${entry.version}`
- : entry.scenario === "rabbitmq" ? `rabbitmq:${entry.version}-management` : `cassandra:${entry.version}`);
- assert.equal(start.args.at(-1), expectedImage);
- const last = invocations.at(-1);
- assert.equal(last.tool, "docker");
- assert.deepEqual(last.args.slice(0, 2), ["rm", "-fv"]);
- const tests = invocations.filter((call) => ["go", "cargo"].includes(call.tool));
- assert.ok(tests.length);
- if (entry.scenario === "zookeeper") {
- assert.equal(tests.length, entry.version === "3.7.0" ? 2 : 1);
- assert.ok(tests[0].args.includes("^TestZooKeeperIntegration$"));
- if (entry.version === "3.7.0") assert.ok(tests[1].args.includes("^TestZooKeeperLargeChildrenIntegration$"));
- } else if (entry.scenario === "zookeeper-sasl") {
- assert.equal(tests[0].env.DBX_ZOOKEEPER_TEST_AUTH_SCHEME, "sasl_digest");
- assert.equal(tests[0].env.DBX_ZOOKEEPER_TEST_USERNAME, "dbx");
- assert.ok(start.args.some((arg) => arg.includes("sessionRequireClientSASLAuth=true")));
- } else if (entry.scenario === "tdengine") {
- assert.deepEqual(tests[0].args, ["nextest", "run", "--manifest-path", "drivers/tdengine/Cargo.toml", "--locked", "--test", "live", "--no-capture", "--no-fail-fast"]);
- assert.equal(tests[0].env.TDENGINE_INTEGRATION, "1");
- assert.equal(tests[0].env.TDENGINE_TEST_PORT, "6041");
- } else if (entry.scenario === "cassandra") assert.ok(tests[0].args.includes("^TestCassandraIntegration$"));
- else {
- assert.ok(tests[0].args.includes("^TestRabbitMQIntegration$"));
- assert.equal(tests[0].env.RABBITMQ_INTEGRATION, "1");
- assert.ok(start.args.includes("999:999"));
- assert.ok(start.args.includes("/var/lib/rabbitmq:rw,exec,uid=999,gid=999,mode=700"));
- }
- }));
+ test(`live matrix retains ${entry.scenario}/${entry.version} commands without starting any container`, () =>
+ withToolStubs((env, commands) => {
+ const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", entry.scenario, entry.version, entry.image ?? ""], { cwd: root, env, encoding: "utf8" });
+ assert.equal(result.status, 0, result.stderr);
+ const invocations = commands();
+ if (entry.scenario === "rocketmq") {
+ assert.ok(invocations.some((call) => call.tool === "bash" && call.args.at(-1) === entry.version));
+ return;
+ }
+ const start = invocations.find((call) => call.tool === "docker" && call.args[0] === "run");
+ assert.ok(start, "starts the selected container");
+ const expectedImage = entry.image ?? (entry.scenario.startsWith("zookeeper") ? `zookeeper:${entry.version}` : entry.scenario === "rabbitmq" ? `rabbitmq:${entry.version}-management` : `cassandra:${entry.version}`);
+ assert.equal(start.args.at(-1), expectedImage);
+ const last = invocations.at(-1);
+ assert.equal(last.tool, "docker");
+ assert.deepEqual(last.args.slice(0, 2), ["rm", "-fv"]);
+ const tests = invocations.filter((call) => ["go", "cargo"].includes(call.tool));
+ assert.ok(tests.length);
+ if (entry.scenario === "zookeeper") {
+ assert.equal(tests.length, entry.version === "3.7.0" ? 2 : 1);
+ assert.ok(tests[0].args.includes("^TestZooKeeperIntegration$"));
+ if (entry.version === "3.7.0") assert.ok(tests[1].args.includes("^TestZooKeeperLargeChildrenIntegration$"));
+ } else if (entry.scenario === "zookeeper-sasl") {
+ assert.equal(tests[0].env.DBX_ZOOKEEPER_TEST_AUTH_SCHEME, "sasl_digest");
+ assert.equal(tests[0].env.DBX_ZOOKEEPER_TEST_USERNAME, "dbx");
+ assert.ok(start.args.some((arg) => arg.includes("sessionRequireClientSASLAuth=true")));
+ } else if (entry.scenario === "tdengine") {
+ assert.deepEqual(tests[0].args, ["nextest", "run", "--manifest-path", "drivers/tdengine/Cargo.toml", "--locked", "--test", "live", "--no-capture", "--no-fail-fast"]);
+ assert.equal(tests[0].env.TDENGINE_INTEGRATION, "1");
+ assert.equal(tests[0].env.TDENGINE_TEST_PORT, "6041");
+ } else if (entry.scenario === "cassandra") assert.ok(tests[0].args.includes("^TestCassandraIntegration$"));
+ else {
+ assert.ok(tests[0].args.includes("^TestRabbitMQIntegration$"));
+ assert.equal(tests[0].env.RABBITMQ_INTEGRATION, "1");
+ assert.ok(start.args.includes("999:999"));
+ assert.ok(start.args.includes("/var/lib/rabbitmq:rw,exec,uid=999,gid=999,mode=700"));
+ }
+ }));
}
-test("failed live tests stay failed and still clean their container", () => withToolStubs((env, commands) => {
- const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", "cassandra", "5.0.6"], { cwd: root, env, encoding: "utf8" });
- assert.equal(result.status, 23);
- assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
-}, { CI_STUB_FAIL: "go" }));
+test("failed live tests stay failed and still clean their container", () =>
+ withToolStubs(
+ (env, commands) => {
+ const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", "cassandra", "5.0.6"], { cwd: root, env, encoding: "utf8" });
+ assert.equal(result.status, 23);
+ assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
+ },
+ { CI_STUB_FAIL: "go" },
+ ));
-test("failed nextest live tests propagate failure and still clean their container", () => withToolStubs((env, commands) => {
- const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", "tdengine", "3.4.2.2"], { cwd: root, env, encoding: "utf8" });
- assert.equal(result.status, 23);
- assert.deepEqual(commands().find((command) => command.tool === "cargo").args.slice(0, 2), ["nextest", "run"]);
- assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
-}, { CI_STUB_FAIL: "cargo" }));
+test("failed nextest live tests propagate failure and still clean their container", () =>
+ withToolStubs(
+ (env, commands) => {
+ const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", "tdengine", "3.4.2.2"], { cwd: root, env, encoding: "utf8" });
+ assert.equal(result.status, 23);
+ assert.deepEqual(
+ commands()
+ .find((command) => command.tool === "cargo")
+ .args.slice(0, 2),
+ ["nextest", "run"],
+ );
+ assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
+ },
+ { CI_STUB_FAIL: "cargo" },
+ ));
test("lock preflight resolves full graphs, keeps --locked and propagates failures", () => {
const plan = { rust: true, agent_rust: { include: [{ driver: "duckdb" }, { driver: "tdengine" }] } };
assert.deepEqual(lockfileManifests(plan), ["Cargo.toml", "agents/drivers/duckdb/Cargo.toml", "agents/drivers/tdengine/Cargo.toml"]);
assert.throws(() => lockfileManifests({ rust: false, agent_rust: { include: [{ driver: "../../unknown" }] } }));
- for (const failure of ["", "cargo"]) withToolStubs((env, commands) => {
- const result = spawnSync(process.execPath, [".github/scripts/ci-lockfiles.mjs"], { cwd: root, env, encoding: "utf8" });
- assert.equal(result.status, failure ? 23 : 0, result.stderr);
- assert.equal(commands().length, failure ? 1 : 3);
- for (const command of commands()) {
- assert.equal(command.args[0], "metadata");
- assert.ok(command.args.includes("--locked"));
- assert.ok(!command.args.includes("--no-deps"));
- }
- }, { CI_PLAN: JSON.stringify(plan), CI_STUB_FAIL: failure });
+ for (const failure of ["", "cargo"])
+ withToolStubs(
+ (env, commands) => {
+ const result = spawnSync(process.execPath, [".github/scripts/ci-lockfiles.mjs"], { cwd: root, env, encoding: "utf8" });
+ assert.equal(result.status, failure ? 23 : 0, result.stderr);
+ assert.equal(commands().length, failure ? 1 : 3);
+ for (const command of commands()) {
+ assert.equal(command.args[0], "metadata");
+ assert.ok(command.args.includes("--locked"));
+ assert.ok(!command.args.includes("--no-deps"));
+ }
+ },
+ { CI_PLAN: JSON.stringify(plan), CI_STUB_FAIL: failure },
+ );
});
diff --git a/.github/scripts/ci-gate.mjs b/.github/scripts/ci-gate.mjs
index e20f083383..56e8b4f72f 100644
--- a/.github/scripts/ci-gate.mjs
+++ b/.github/scripts/ci-gate.mjs
@@ -4,33 +4,61 @@ import { pathToFileURL } from "node:url";
export function gateFailures(needs, mode) {
if (needs.changes?.result !== "success") return ["changes did not succeed"];
let plan;
- try { plan = JSON.parse(needs.changes.outputs.plan); } catch { return ["missing or invalid CI plan"]; }
+ try {
+ plan = JSON.parse(needs.changes.outputs.plan);
+ } catch {
+ return ["missing or invalid CI plan"];
+ }
if (!plan || typeof plan !== "object" || Array.isArray(plan)) return ["incomplete CI plan"];
const flags = ["rust", "agents", "fast", "agent_java", "agent_go_changed", "agent_rust_changed", "agent_integration_changed"];
if (!flags.every((flag) => typeof plan[flag] === "boolean")) return ["incomplete CI plan"];
- const routedJobs = { frontend: "frontend", packages: "packages", "github-scripts": "github_scripts",
- "windows-standard-check": "windows_standard", "windows-win7-bundle": "windows_win7_bundle",
- "duckdb-windows-driver": "duckdb_windows", jdbc: "jdbc", "offline-jdbc-release": "offline_jdbc", "nix-packaging": "nix" };
+ const routedJobs = {
+ frontend: "frontend",
+ packages: "packages",
+ "github-scripts": "github_scripts",
+ "windows-standard-check": "windows_standard",
+ "windows-win7-bundle": "windows_win7_bundle",
+ "duckdb-windows-driver": "duckdb_windows",
+ jdbc: "jdbc",
+ "offline-jdbc-release": "offline_jdbc",
+ "nix-packaging": "nix",
+ };
if (mode === "all" && !Object.values(routedJobs).every((output) => ["true", "false"].includes(needs.changes.outputs[output]))) {
return ["missing or invalid job selection outputs"];
}
if (mode === "frontend" && !["true", "false"].includes(needs.changes.outputs.frontend)) {
return ["missing or invalid frontend selection output"];
}
- const expected = mode === "rust" ? {
- "fast-checks": plan.fast, "rust-fmt-clippy": plan.rust, "rust-test": plan.rust,
- } : mode === "agents" ? {
- "fast-checks": plan.fast, "agent-checks": plan.agents, "agent-java": plan.agent_java, "agent-go": plan.agent_go_changed,
- "agent-rust": plan.agent_rust_changed, "agent-integration": plan.agent_integration_changed,
- } : mode === "frontend" ? {
- "frontend-checks": needs.changes.outputs.frontend === "true",
- "frontend-typecheck": needs.changes.outputs.frontend === "true",
- "frontend-test": needs.changes.outputs.frontend === "true",
- } : mode === "all" ? {
- rust: true, agents: true, "fast-checks": plan.fast,
- ...Object.fromEntries(Object.entries(routedJobs)
- .map(([job, output]) => [job, needs.changes.outputs[output] === "true"])),
- } : null;
+ const expected =
+ mode === "rust"
+ ? {
+ "fast-checks": plan.fast,
+ "rust-fmt-clippy": plan.rust,
+ "rust-test": plan.rust,
+ }
+ : mode === "agents"
+ ? {
+ "fast-checks": plan.fast,
+ "agent-checks": plan.agents,
+ "agent-java": plan.agent_java,
+ "agent-go": plan.agent_go_changed,
+ "agent-rust": plan.agent_rust_changed,
+ "agent-integration": plan.agent_integration_changed,
+ }
+ : mode === "frontend"
+ ? {
+ "frontend-checks": needs.changes.outputs.frontend === "true",
+ "frontend-typecheck": needs.changes.outputs.frontend === "true",
+ "frontend-test": needs.changes.outputs.frontend === "true",
+ }
+ : mode === "all"
+ ? {
+ rust: true,
+ agents: true,
+ "fast-checks": plan.fast,
+ ...Object.fromEntries(Object.entries(routedJobs).map(([job, output]) => [job, needs.changes.outputs[output] === "true"])),
+ }
+ : null;
if (!expected) return [`unknown gate: ${mode}`];
return Object.entries(expected).flatMap(([job, required]) => {
const result = needs[job]?.result;
diff --git a/.github/scripts/ci-lockfiles.mjs b/.github/scripts/ci-lockfiles.mjs
index b15b786aa4..74d0cefd14 100644
--- a/.github/scripts/ci-lockfiles.mjs
+++ b/.github/scripts/ci-lockfiles.mjs
@@ -3,11 +3,13 @@ import path from "node:path";
import { pathToFileURL } from "node:url";
export function lockfileManifests(plan) {
- return [...(plan.rust ? ["Cargo.toml"] : []),
+ return [
+ ...(plan.rust ? ["Cargo.toml"] : []),
...plan.agent_rust.include.map(({ driver }) => {
if (!["duckdb", "tdengine"].includes(driver)) throw new Error(`Unknown native Rust driver: ${driver}`);
return `agents/drivers/${driver}/Cargo.toml`;
- })];
+ }),
+ ];
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
diff --git a/.github/scripts/ci-plan.mjs b/.github/scripts/ci-plan.mjs
index 6cd81cba04..86c3ae134e 100644
--- a/.github/scripts/ci-plan.mjs
+++ b/.github/scripts/ci-plan.mjs
@@ -16,16 +16,7 @@ const win7InfrastructureFiles = new Set([
"src-tauri/tauri.webview2-win7-fixed.conf.json",
"src-tauri/build.rs",
]);
-const win7InfrastructurePrefixes = [
- ".cargo/",
- "rust-toolchain",
- ".github/fixtures/win7-",
- "src-tauri/windows/nsis/",
- "src-tauri/src/commands/update",
- "crates/dbx-core/src/host/update",
- "crates/dbx-platform/",
- "vendor/",
-];
+const win7InfrastructurePrefixes = [".cargo/", "rust-toolchain", ".github/fixtures/win7-", "src-tauri/windows/nsis/", "src-tauri/src/commands/update", "crates/dbx-core/src/host/update", "crates/dbx-platform/", "vendor/"];
function expandAffectedPackages(packages, members, affected) {
let expanded;
@@ -44,17 +35,19 @@ export function changedPaths(base, root) {
if (!base || /^0+$/.test(base)) return null;
execFileSync("git", ["rev-parse", "--verify", `${base}^{commit}`], { cwd: root, stdio: "pipe" });
return execFileSync("git", ["diff", "--no-renames", "--name-only", "-z", base, "HEAD", "--"], {
- cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024,
- }).split("\0").filter(Boolean);
+ cwd: root,
+ encoding: "utf8",
+ maxBuffer: 16 * 1024 * 1024,
+ })
+ .split("\0")
+ .filter(Boolean);
}
export function knownJavaDrivers(root, nativeDrivers) {
try {
const driversRoot = path.join(root, "agents/drivers");
return readdirSync(driversRoot, { withFileTypes: true })
- .filter((entry) => entry.isDirectory()
- && existsSync(path.join(driversRoot, entry.name, "build.gradle"))
- && !nativeDrivers.includes(entry.name))
+ .filter((entry) => entry.isDirectory() && existsSync(path.join(driversRoot, entry.name, "build.gradle")) && !nativeDrivers.includes(entry.name))
.map((entry) => entry.name)
.sort();
} catch {
@@ -63,9 +56,13 @@ export function knownJavaDrivers(root, nativeDrivers) {
}
export function cargoMetadata(root, execute = execFileSync) {
- return JSON.parse(execute("cargo", ["+stable", "metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"], {
- cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024,
- }));
+ return JSON.parse(
+ execute("cargo", ["+stable", "metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"], {
+ cwd: root,
+ encoding: "utf8",
+ maxBuffer: 16 * 1024 * 1024,
+ }),
+ );
}
export function planCi({ files, metadata, root, eventName = "pull_request", rustChanged = false, agentsChanged = false, javaDrivers }) {
@@ -75,8 +72,7 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
const members = new Map(packages.map((pkg) => [pkg.name, pkg]));
const packagePaths = packages.map((pkg) => [pkg.name, `${path.relative(root, path.dirname(pkg.manifest_path)).split(path.sep).join("/")}/`]);
const ciChanged = unknownDiff || files.some((file) => /^\.github\/(?:workflows\/ci(?:[.-]|\/)|scripts\/ci-|actions\/ci-)/.test(file));
- const sharedRust = ciChanged || files.some((file) => /^(?:Cargo\.(?:toml|lock)$|rust-toolchain|\.cargo\/|vendor\/)/.test(file)
- || /^(?:crates\/[^/]+|src-tauri)\/Cargo\.toml$/.test(file));
+ const sharedRust = ciChanged || files.some((file) => /^(?:Cargo\.(?:toml|lock)$|rust-toolchain|\.cargo\/|vendor\/)/.test(file) || /^(?:crates\/[^/]+|src-tauri)\/Cargo\.toml$/.test(file));
const affected = new Set();
let unknownRust = false;
for (const file of files) {
@@ -94,11 +90,8 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
expandAffectedPackages(packages, members, win7Affected);
if (full) for (const pkg of packages) affected.add(pkg.name);
expandAffectedPackages(packages, members, affected);
- const win7InfrastructureChanged = unknownDiff || files.some((file) => win7InfrastructureFiles.has(file)
- || win7InfrastructurePrefixes.some((prefix) => file.startsWith(prefix))
- || /^src-tauri\/tauri.*\.json$/.test(file));
- const win7DependencyChanged = files.includes("Cargo.toml") || files.includes("Cargo.lock")
- || files.some((file) => /^(?:crates\/[^/]+|src-tauri)\/Cargo\.toml$/.test(file));
+ const win7InfrastructureChanged = unknownDiff || files.some((file) => win7InfrastructureFiles.has(file) || win7InfrastructurePrefixes.some((prefix) => file.startsWith(prefix)) || /^src-tauri\/tauri.*\.json$/.test(file));
+ const win7DependencyChanged = files.includes("Cargo.toml") || files.includes("Cargo.lock") || files.some((file) => /^(?:crates\/[^/]+|src-tauri)\/Cargo\.toml$/.test(file));
const windowsWin7Reasons = {
infrastructure: win7InfrastructureChanged,
dependency_input: win7DependencyChanged,
@@ -107,20 +100,24 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
unknown_member: rust && unknownMember,
};
const windowsWin7Candidate = Object.values(windowsWin7Reasons).some(Boolean);
- const rustMatrix = !rust ? [] : full ? [{ group: "workspace" }] : Object.entries(rustGroups)
- .filter(([, names]) => names.some((name) => affected.has(name)))
- .map(([group]) => ({ group }));
+ const rustMatrix = !rust
+ ? []
+ : full
+ ? [{ group: "workspace" }]
+ : Object.entries(rustGroups)
+ .filter(([, names]) => names.some((name) => affected.has(name)))
+ .map(([group]) => ({ group }));
const nativeDrivers = [...goAgents.map((agent) => agent.driver), ...rustAgents];
const jdbcDrivers = javaDrivers ?? knownJavaDrivers(root, nativeDrivers);
const nativeChanges = new Set(nativeDrivers.filter((driver) => files.some((file) => file.startsWith(`agents/drivers/${driver}/`))));
const javaDriverChanges = jdbcDrivers.some((driver) => files.some((file) => file.startsWith(`agents/drivers/${driver}/`)));
// Driver-scoped JDBC/Go/Rust trees are not "shared Agent inputs". Unknown
// agents/drivers// paths still fail open to full coverage below.
- const sharedAgents = ciChanged || sharedRust || files.some((file) => file.startsWith("agents/")
- && !nativeDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`))
- && !jdbcDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`)))
- || files.some((file) => file.startsWith(".github/scripts/bump-agent-versions.") || file === ".github/workflows/agents-release.yml"
- || file === "crates/dbx-driver-agent/assets/agent-protocol-v2.json");
+ const sharedAgents =
+ ciChanged ||
+ sharedRust ||
+ files.some((file) => file.startsWith("agents/") && !nativeDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`)) && !jdbcDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`))) ||
+ files.some((file) => file.startsWith(".github/scripts/bump-agent-versions.") || file === ".github/workflows/agents-release.yml" || file === "crates/dbx-driver-agent/assets/agent-protocol-v2.json");
const allAgents = sharedAgents || (agentsChanged && nativeChanges.size === 0 && !javaDriverChanges);
if (allAgents) {
for (const driver of nativeDrivers) nativeChanges.add(driver);
@@ -133,11 +130,19 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
const agents = agentsChanged || sharedAgents || nativeChanges.size > 0 || javaDriverChanges;
return {
contributor_snapshot_only: files.length > 0 && files.every((file) => file === "docs/data/contributors.json"),
- rust, rust_full: full, rust_matrix: { include: rustMatrix }, rust_groups_known: !unknownMember,
+ rust,
+ rust_full: full,
+ rust_matrix: { include: rustMatrix },
+ rust_groups_known: !unknownMember,
affected_packages: [...affected].sort(),
- agents, agent_java: java, agent_go: { include: goMatrix }, agent_rust: { include: nativeRustMatrix },
- agent_integration: { include: liveMatrix }, agent_go_changed: goMatrix.length > 0,
- agent_rust_changed: nativeRustMatrix.length > 0, agent_integration_changed: liveMatrix.length > 0,
+ agents,
+ agent_java: java,
+ agent_go: { include: goMatrix },
+ agent_rust: { include: nativeRustMatrix },
+ agent_integration: { include: liveMatrix },
+ agent_go_changed: goMatrix.length > 0,
+ agent_rust_changed: nativeRustMatrix.length > 0,
+ agent_integration_changed: liveMatrix.length > 0,
duckdb_changed: nativeChanges.has("duckdb"),
duckdb_windows: sharedRust || files.some((file) => file.startsWith("agents/drivers/duckdb/")),
windows_standard: windowsWin7Candidate,
@@ -153,12 +158,21 @@ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.ar
const root = process.cwd();
const metadata = cargoMetadata(root);
const plan = planCi({
- files: changedPaths(process.env.BASE_SHA, root), metadata, root, eventName: process.env.GITHUB_EVENT_NAME,
- rustChanged: process.env.RUST_CHANGED === "true", agentsChanged: process.env.AGENTS_CHANGED === "true",
+ files: changedPaths(process.env.BASE_SHA, root),
+ metadata,
+ root,
+ eventName: process.env.GITHUB_EVENT_NAME,
+ rustChanged: process.env.RUST_CHANGED === "true",
+ agentsChanged: process.env.AGENTS_CHANGED === "true",
});
if (process.env.GITHUB_OUTPUT) {
const outputs = { ...plan, plan };
- appendFileSync(process.env.GITHUB_OUTPUT, Object.entries(outputs).map(([name, value]) => `${name}=${JSON.stringify(value)}\n`).join(""));
+ appendFileSync(
+ process.env.GITHUB_OUTPUT,
+ Object.entries(outputs)
+ .map(([name, value]) => `${name}=${JSON.stringify(value)}\n`)
+ .join(""),
+ );
}
console.log(JSON.stringify(plan, null, 2));
}
diff --git a/.github/scripts/ci-plan.test.mjs b/.github/scripts/ci-plan.test.mjs
index 1c6fc37404..c72810fcb0 100644
--- a/.github/scripts/ci-plan.test.mjs
+++ b/.github/scripts/ci-plan.test.mjs
@@ -12,11 +12,16 @@ import { assertCoverage, parseCoverage } from "./ci-rust-coverage.mjs";
const root = "/fixture";
const graph = {
- "dbx-types": [], "dbx-platform": [], "dbx-formats": [], "dbx-sql-core": ["dbx-types"],
- "dbx-sql-dialect": ["dbx-types"], "dbx-sql-data": ["dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
+ "dbx-types": [],
+ "dbx-platform": [],
+ "dbx-formats": [],
+ "dbx-sql-core": ["dbx-types"],
+ "dbx-sql-dialect": ["dbx-types"],
+ "dbx-sql-data": ["dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
"dbx-sql-schema": ["dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
"dbx-sql": ["dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-sql-schema", "dbx-types"],
- "dbx-ai-provider": ["dbx-platform"], "dbx-plugin-runtime": ["dbx-platform", "dbx-types"],
+ "dbx-ai-provider": ["dbx-platform"],
+ "dbx-plugin-runtime": ["dbx-platform", "dbx-types"],
"dbx-driver-support": ["dbx-platform", "dbx-types"],
"dbx-driver-agent": ["dbx-platform", "dbx-sql-core", "dbx-types"],
"dbx-driver-elasticsearch": ["dbx-driver-support", "dbx-types"],
@@ -25,17 +30,22 @@ const graph = {
"dbx-driver-postgres": ["dbx-driver-support", "dbx-sql-core", "dbx-types"],
"dbx-driver-redis": ["dbx-driver-support", "dbx-types"],
"dbx-driver-sqlserver": ["dbx-driver-support", "dbx-sql-core", "dbx-sql-data", "dbx-types"],
- "dbx-sqlite-worker": [], "dbx-drivers": ["dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb",
- "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-driver-support",
- "dbx-platform", "dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-types", "dbx-sqlite-worker"],
+ "dbx-sqlite-worker": [],
+ "dbx-drivers": ["dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb", "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-driver-support", "dbx-platform", "dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-types", "dbx-sqlite-worker"],
"dbx-core": ["dbx-drivers", "dbx-sql", "dbx-types", "dbx-platform", "dbx-ai-provider", "dbx-plugin-runtime", "dbx-formats"],
- "dbx-mcp": ["dbx-core"], "dbx-cli": ["dbx-core", "dbx-mcp"], "dbx-web": ["dbx-core", "dbx-mcp"], "dbx": ["dbx-core", "dbx-mcp", "dbx-tauri-consul", "dbx-tauri-schema"],
- "dbx-tauri-consul": ["dbx-core"], "dbx-tauri-schema": ["dbx-core"],
+ "dbx-mcp": ["dbx-core"],
+ "dbx-cli": ["dbx-core", "dbx-mcp"],
+ "dbx-web": ["dbx-core", "dbx-mcp"],
+ dbx: ["dbx-core", "dbx-mcp", "dbx-tauri-consul", "dbx-tauri-schema"],
+ "dbx-tauri-consul": ["dbx-core"],
+ "dbx-tauri-schema": ["dbx-core"],
};
const metadata = {
workspace_members: Object.keys(graph),
packages: Object.entries(graph).map(([name, dependencies]) => ({
- id: name, name, manifest_path: `${root}/${name === "dbx" ? "src-tauri" : `crates/${name}`}/Cargo.toml`,
+ id: name,
+ name,
+ manifest_path: `${root}/${name === "dbx" ? "src-tauri" : `crates/${name}`}/Cargo.toml`,
dependencies: dependencies.map((dependency) => ({ name: dependency })),
})),
};
@@ -53,17 +63,22 @@ test("the planner uses the runner stable Cargo only for metadata", () => {
assert.deepEqual(invocation.args, ["+stable", "metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"]);
assert.equal(invocation.options.cwd, "/workspace");
});
-const routedJobs = { frontend: "frontend", packages: "packages", "github-scripts": "github_scripts",
- "windows-standard-check": "windows_standard", "windows-win7-bundle": "windows_win7_bundle",
- "duckdb-windows-driver": "duckdb_windows", jdbc: "jdbc", "offline-jdbc-release": "offline_jdbc", "nix-packaging": "nix" };
+const routedJobs = {
+ frontend: "frontend",
+ packages: "packages",
+ "github-scripts": "github_scripts",
+ "windows-standard-check": "windows_standard",
+ "windows-win7-bundle": "windows_win7_bundle",
+ "duckdb-windows-driver": "duckdb_windows",
+ jdbc: "jdbc",
+ "offline-jdbc-release": "offline_jdbc",
+ "nix-packaging": "nix",
+};
test("only an exact contributor snapshot diff skips desktop frontend validation", () => {
for (const eventName of ["pull_request", "push"]) {
assert.equal(plan(["docs/data/contributors.json"], { eventName }).contributor_snapshot_only, true);
- for (const files of [null, [], ["docs/lib/contributorActivity.test.ts"],
- ["docs/data/contributors.json", "apps/desktop/src/main.ts"],
- ["docs/data/contributors.json", "docs/lib/contributorActivity.ts"],
- ["docs/data/contributors.json", "Cargo.lock"]]) {
+ for (const files of [null, [], ["docs/lib/contributorActivity.test.ts"], ["docs/data/contributors.json", "apps/desktop/src/main.ts"], ["docs/data/contributors.json", "docs/lib/contributorActivity.ts"], ["docs/data/contributors.json", "Cargo.lock"]]) {
assert.equal(plan(files, { eventName }).contributor_snapshot_only, false);
}
}
@@ -97,9 +112,16 @@ test("Win7 candidate follows the desktop package dependency graph", () => {
});
test("ordinary desktop Rust changes keep Windows compilation without full Win7 packaging", () => {
- for (const file of ["crates/dbx-core/src/query/mod.rs", "crates/dbx-core/tests/runtime_diagnostic_history.rs",
- "crates/dbx-drivers/src/lib.rs", "crates/dbx-driver-postgres/src/postgres.rs", "crates/dbx-driver-mysql/tests/query.rs",
- "src-tauri/src/commands/query.rs", "crates/dbx-tauri-consul/src/lib.rs", "crates/dbx-tauri-schema/src/lib.rs"]) {
+ for (const file of [
+ "crates/dbx-core/src/query/mod.rs",
+ "crates/dbx-core/tests/runtime_diagnostic_history.rs",
+ "crates/dbx-drivers/src/lib.rs",
+ "crates/dbx-driver-postgres/src/postgres.rs",
+ "crates/dbx-driver-mysql/tests/query.rs",
+ "src-tauri/src/commands/query.rs",
+ "crates/dbx-tauri-consul/src/lib.rs",
+ "crates/dbx-tauri-schema/src/lib.rs",
+ ]) {
const result = plan([file]);
assert.equal(result.windows_standard, true, file);
assert.equal(result.windows_win7_bundle, false, file);
@@ -112,12 +134,7 @@ test("ordinary desktop Rust changes keep Windows compilation without full Win7 p
});
test("old Win7 Rust paths remain candidates through the dependency graph", () => {
- for (const file of [
- "src-tauri/src/commands/update.rs",
- "crates/dbx-core/src/host/update.rs",
- "crates/dbx-driver-postgres/src/postgres.rs",
- "crates/dbx-platform/src/lib.rs",
- ]) {
+ for (const file of ["src-tauri/src/commands/update.rs", "crates/dbx-core/src/host/update.rs", "crates/dbx-driver-postgres/src/postgres.rs", "crates/dbx-platform/src/lib.rs"]) {
assert.equal(plan([file]).windows_win7_candidate, true, file);
}
});
@@ -215,8 +232,7 @@ test("code generation and test-only dependencies participate in impact analysis"
assert.ok(plan(["crates/dbx-formats/src/lib.rs"], { metadata: fixture }).affected_packages.includes("dbx-types"));
});
-for (const file of ["Cargo.toml", "Cargo.lock", ".cargo/config.toml", "rust-toolchain.toml", "vendor/wry/src/lib.rs",
- "crates/dbx-sql/Cargo.toml", ".github/workflows/ci.yml", ".github/scripts/ci-plan.mjs", ".github/scripts/ci-gate.test.mjs"]) {
+for (const file of ["Cargo.toml", "Cargo.lock", ".cargo/config.toml", "rust-toolchain.toml", "vendor/wry/src/lib.rs", "crates/dbx-sql/Cargo.toml", ".github/workflows/ci.yml", ".github/scripts/ci-plan.mjs", ".github/scripts/ci-gate.test.mjs"]) {
test(`shared input ${file} retains full workspace and Agent coverage`, () => {
const result = plan([file]);
assert.deepEqual(groups(result), ["workspace"]);
@@ -275,8 +291,7 @@ test("known JDBC driver changes only select Java agent tests", () => {
});
test("shared Agent inputs and unknown native modules never silently lose coverage", () => {
- for (const file of ["agents/common/src/main/java/Protocol.java", "agents/scripts/validate_agents.py", "agents/build.gradle",
- "agents/drivers/new-driver/main.go", "crates/dbx-driver-agent/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
+ for (const file of ["agents/common/src/main/java/Protocol.java", "agents/scripts/validate_agents.py", "agents/build.gradle", "agents/drivers/new-driver/main.go", "crates/dbx-driver-agent/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
const result = plan([file]);
assert.equal(result.agent_go.include.length, goAgents.length, file);
assert.equal(result.agent_integration.include.length, 16, file);
@@ -289,13 +304,32 @@ test("shared Agent inputs and unknown native modules never silently lose coverag
});
test("the independent live matrix preserves every old version and authentication case", () => {
- assert.deepEqual(integrationCases.map((entry) => `${entry.scenario}:${entry.version}`), [
- "zookeeper:3.4.14", "zookeeper:3.5.5", "zookeeper:3.7.0", "zookeeper:3.9.5", "zookeeper-sasl:3.7.0",
- "rocketmq:4.9.8", "rocketmq:5.3.1", "tdengine:2.4.0.14", "tdengine:2.6.0.34", "tdengine:3.0.7.1",
- "tdengine:3.3.6.13", "tdengine:3.4.2.2", "cassandra:3.11.19", "cassandra:5.0.6", "rabbitmq:3.13", "rabbitmq:4.3",
- ]);
+ assert.deepEqual(
+ integrationCases.map((entry) => `${entry.scenario}:${entry.version}`),
+ [
+ "zookeeper:3.4.14",
+ "zookeeper:3.5.5",
+ "zookeeper:3.7.0",
+ "zookeeper:3.9.5",
+ "zookeeper-sasl:3.7.0",
+ "rocketmq:4.9.8",
+ "rocketmq:5.3.1",
+ "tdengine:2.4.0.14",
+ "tdengine:2.6.0.34",
+ "tdengine:3.0.7.1",
+ "tdengine:3.3.6.13",
+ "tdengine:3.4.2.2",
+ "cassandra:3.11.19",
+ "cassandra:5.0.6",
+ "rabbitmq:3.13",
+ "rabbitmq:4.3",
+ ],
+ );
assert.equal(integrationCases.find((entry) => entry.version === "3.4.2.2").image, "tdengine/tsdb:3.4.2.2");
- assert.deepEqual(goAgents.filter((entry) => entry.race).map((entry) => entry.driver), ["rocketmq", "zookeeper"]);
+ assert.deepEqual(
+ goAgents.filter((entry) => entry.race).map((entry) => entry.driver),
+ ["rocketmq", "zookeeper"],
+ );
});
test("test groups partition every current workspace package exactly once", () => {
@@ -309,8 +343,7 @@ test("Rust groups cover the actual workspace metadata, including extracted Tauri
const actual = cargoMetadata(repository);
const members = actual.packages.filter((pkg) => actual.workspace_members.includes(pkg.id)).map((pkg) => pkg.name);
assert.deepEqual(Object.values(rustGroups).flat().toSorted(), members.toSorted());
- const result = planCi({ files: ["crates/dbx-core/tests/runtime_diagnostic_history.rs", "crates/dbx-web/src/routes/transfer.rs"],
- metadata: actual, root: repository });
+ const result = planCi({ files: ["crates/dbx-core/tests/runtime_diagnostic_history.rs", "crates/dbx-web/src/routes/transfer.rs"], metadata: actual, root: repository });
assert.equal(result.rust_groups_known, true);
assert.equal(result.rust_full, false);
assert.deepEqual(groups(result), ["application"]);
@@ -361,13 +394,17 @@ test("nextest, doctests and coverage select identical packages and features in e
function results(files) {
const selected = plan(files);
- return { changes: { result: "success", outputs: { plan: JSON.stringify(selected) } },
+ return {
+ changes: { result: "success", outputs: { plan: JSON.stringify(selected) } },
"fast-checks": { result: selected.fast ? "success" : "skipped" },
- "rust-fmt-clippy": { result: selected.rust ? "success" : "skipped" }, "rust-test": { result: selected.rust ? "success" : "skipped" },
- "agent-checks": { result: selected.agents ? "success" : "skipped" }, "agent-java": { result: selected.agent_java ? "success" : "skipped" },
+ "rust-fmt-clippy": { result: selected.rust ? "success" : "skipped" },
+ "rust-test": { result: selected.rust ? "success" : "skipped" },
+ "agent-checks": { result: selected.agents ? "success" : "skipped" },
+ "agent-java": { result: selected.agent_java ? "success" : "skipped" },
"agent-go": { result: selected.agent_go_changed ? "success" : "skipped" },
"agent-rust": { result: selected.agent_rust_changed ? "success" : "skipped" },
- "agent-integration": { result: selected.agent_integration_changed ? "success" : "skipped" } };
+ "agent-integration": { result: selected.agent_integration_changed ? "success" : "skipped" },
+ };
}
test("gates accept only successful selected jobs and intentionally skipped unselected jobs", () => {
@@ -410,17 +447,23 @@ test("git diff routing includes both sides of renames, deleted files, and unusua
const directory = mkdtempSync(path.join(tmpdir(), "dbx-ci-plan-"));
const git = (...args) => execFileSync("git", args, { cwd: directory, encoding: "utf8", stdio: "pipe" }).trim();
try {
- git("init"); git("config", "user.email", "fixture@example.invalid"); git("config", "user.name", "CI Fixture");
+ git("init");
+ git("config", "user.email", "fixture@example.invalid");
+ git("config", "user.name", "CI Fixture");
writeFileSync(path.join(directory, "old.rs"), "unchanged source\n");
writeFileSync(path.join(directory, "deleted.rs"), "gone\n");
- git("add", "."); git("-c", "core.hooksPath=/dev/null", "commit", "-m", "fixture baseline");
+ git("add", ".");
+ git("-c", "core.hooksPath=/dev/null", "commit", "-m", "fixture baseline");
const base = git("rev-parse", "HEAD");
- git("mv", "old.rs", "renamed\nfile.rs"); git("rm", "deleted.rs");
+ git("mv", "old.rs", "renamed\nfile.rs");
+ git("rm", "deleted.rs");
git("-c", "core.hooksPath=/dev/null", "commit", "-m", "fixture change");
assert.deepEqual(changedPaths(base, directory).toSorted(), ["old.rs", "deleted.rs", "renamed\nfile.rs"].toSorted());
assert.equal(changedPaths("0".repeat(40), directory), null);
assert.throws(() => changedPaths("not-a-ref", directory));
- } finally { rmSync(directory, { recursive: true, force: true }); }
+ } finally {
+ rmSync(directory, { recursive: true, force: true });
+ }
});
test("the final gate rejects absent routing outputs and skipped selected jobs", () => {
@@ -483,8 +526,7 @@ test("the final gate independently requires standard Windows compilation without
});
test("coverage audit rejects lost features, omitted packages, and overlapping group selection", () => {
- const coverage = { workspace: Object.fromEntries(Object.keys(graph).map((name) => [name, ["fixture"]])),
- ...Object.fromEntries(Object.entries(rustGroups).map(([group, packages]) => [group, Object.fromEntries(packages.map((name) => [name, ["fixture"]]))])) };
+ const coverage = { workspace: Object.fromEntries(Object.keys(graph).map((name) => [name, ["fixture"]])), ...Object.fromEntries(Object.entries(rustGroups).map(([group, packages]) => [group, Object.fromEntries(packages.map((name) => [name, ["fixture"]]))])) };
assertCoverage(coverage);
const lost = structuredClone(coverage);
lost.foundation["dbx-types"] = [];
diff --git a/.github/scripts/ci-rust-coverage.mjs b/.github/scripts/ci-rust-coverage.mjs
index 9ce356e499..828c1d8b6f 100644
--- a/.github/scripts/ci-rust-coverage.mjs
+++ b/.github/scripts/ci-rust-coverage.mjs
@@ -6,11 +6,17 @@ import { rustGroups } from "./ci-config.mjs";
import { rustCommand } from "./ci-rust.mjs";
export function parseCoverage(output) {
- return Object.fromEntries(output.trim().split("\n").filter(Boolean).map((line) => {
- const [name, features] = line.split("|");
- assert.ok(features !== undefined, `Invalid Cargo tree output: ${line}`);
- return [name.split(" ")[0], features.split(",").filter(Boolean)];
- }));
+ return Object.fromEntries(
+ output
+ .trim()
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => {
+ const [name, features] = line.split("|");
+ assert.ok(features !== undefined, `Invalid Cargo tree output: ${line}`);
+ return [name.split(" ")[0], features.split(",").filter(Boolean)];
+ }),
+ );
}
export function assertCoverage(coverage) {
diff --git a/.github/scripts/ci-rust.mjs b/.github/scripts/ci-rust.mjs
index 6b633e98f2..5e072ae597 100644
--- a/.github/scripts/ci-rust.mjs
+++ b/.github/scripts/ci-rust.mjs
@@ -4,26 +4,43 @@ import { pathToFileURL } from "node:url";
import { rustGroups } from "./ci-config.mjs";
export function rustCommand(action, group, mode) {
- if (!["test", "doctest", "clippy", "tree"].includes(action) || !["full", "fast"].includes(mode)
- || (group !== "workspace" && !Object.hasOwn(rustGroups, group)) || (action === "clippy" && group !== "workspace")) {
+ if (!["test", "doctest", "clippy", "tree"].includes(action) || !["full", "fast"].includes(mode) || (group !== "workspace" && !Object.hasOwn(rustGroups, group)) || (action === "clippy" && group !== "workspace")) {
throw new Error(`Unknown Rust CI configuration: ${action}/${group}/${mode}`);
}
const capabilityFeatures = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "sqlserver-native-attention"];
if (mode === "full") capabilityFeatures.push("system-fonts");
const appFeatures = ["dbx", "dbx-core", "dbx-web"].flatMap((name) => capabilityFeatures.map((feature) => `${name}/${feature}`));
- const foundationFeatures = ["dbx-types/mq-admin", "dbx-types/openapi", "dbx-sql/duckdb-sidecar", "dbx-sql/openapi",
- "dbx-sql-data/openapi", "dbx-sql-schema/duckdb-sidecar",
- "dbx-platform/downloads", "dbx-platform/host-prompts", "dbx-platform/test-support", "dbx-plugin-runtime/default", "dbx-plugin-runtime/test-support"];
- const features = group === "foundation" ? foundationFeatures : group === "drivers" ? [
- ...["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-bundled", "sqlite-sqlcipher", "sqlserver-native-attention", "test-support"].map((feature) => `dbx-drivers/${feature}`),
- "dbx-driver-agent/test-support", "dbx-driver-mysql/test-support", "dbx-driver-redis/test-support",
- "dbx-driver-support/test-support",
- "dbx-sqlite-worker/runtime", "dbx-types/openapi", "dbx-sql-data/openapi",
- ] : appFeatures;
+ const foundationFeatures = [
+ "dbx-types/mq-admin",
+ "dbx-types/openapi",
+ "dbx-sql/duckdb-sidecar",
+ "dbx-sql/openapi",
+ "dbx-sql-data/openapi",
+ "dbx-sql-schema/duckdb-sidecar",
+ "dbx-platform/downloads",
+ "dbx-platform/host-prompts",
+ "dbx-platform/test-support",
+ "dbx-plugin-runtime/default",
+ "dbx-plugin-runtime/test-support",
+ ];
+ const features =
+ group === "foundation"
+ ? foundationFeatures
+ : group === "drivers"
+ ? [
+ ...["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-bundled", "sqlite-sqlcipher", "sqlserver-native-attention", "test-support"].map((feature) => `dbx-drivers/${feature}`),
+ "dbx-driver-agent/test-support",
+ "dbx-driver-mysql/test-support",
+ "dbx-driver-redis/test-support",
+ "dbx-driver-support/test-support",
+ "dbx-sqlite-worker/runtime",
+ "dbx-types/openapi",
+ "dbx-sql-data/openapi",
+ ]
+ : appFeatures;
const packages = group === "workspace" ? ["--workspace"] : rustGroups[group].flatMap((name) => ["--package", name]);
const command = action === "test" ? ["nextest", "run", "--no-fail-fast"] : action === "doctest" ? ["test", "--doc"] : [action];
- return [...command, ...packages, "--locked", ...(["test", "clippy"].includes(action) ? ["--timings"] : []), ...(action === "clippy" ? ["--all-targets"] : []),
- "--no-default-features", "--features", features.join(","), ...(action === "clippy" ? ["--", "-D", "warnings"] : [])];
+ return [...command, ...packages, "--locked", ...(["test", "clippy"].includes(action) ? ["--timings"] : []), ...(action === "clippy" ? ["--all-targets"] : []), "--no-default-features", "--features", features.join(","), ...(action === "clippy" ? ["--", "-D", "warnings"] : [])];
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
diff --git a/.github/scripts/ci-vitest-file-timing-reporter.test.mjs b/.github/scripts/ci-vitest-file-timing-reporter.test.mjs
index 734d3b9b48..3d5bb787b4 100644
--- a/.github/scripts/ci-vitest-file-timing-reporter.test.mjs
+++ b/.github/scripts/ci-vitest-file-timing-reporter.test.mjs
@@ -29,10 +29,13 @@ test("file timing reports sort total work and keep timing phases", () => {
}),
]);
- assert.deepEqual(rows.map(({ file, totalMs, testsMs, collectMs, setupMs }) => ({ file, totalMs, testsMs, collectMs, setupMs })), [
- { file: "slow.spec.ts", totalMs: 200, testsMs: 60, collectMs: 50, setupMs: 90 },
- { file: "fast.spec.ts", totalMs: 100, testsMs: 100, collectMs: 0, setupMs: 0 },
- ]);
+ assert.deepEqual(
+ rows.map(({ file, totalMs, testsMs, collectMs, setupMs }) => ({ file, totalMs, testsMs, collectMs, setupMs })),
+ [
+ { file: "slow.spec.ts", totalMs: 200, testsMs: 60, collectMs: 50, setupMs: 90 },
+ { file: "fast.spec.ts", totalMs: 100, testsMs: 100, collectMs: 0, setupMs: 0 },
+ ],
+ );
assert.match(consoleReport(rows, 1), /0\.20s slow\.spec\.ts/);
assert.doesNotMatch(consoleReport(rows, 1), /fast\.spec\.ts/);
assert.match(markdownReport(rows, "1\/2"), /Vitest file timings \(1\/2\)/);
diff --git a/.github/scripts/ci-workflow.test.mjs b/.github/scripts/ci-workflow.test.mjs
index 454c10aeb9..2e3e0b689c 100644
--- a/.github/scripts/ci-workflow.test.mjs
+++ b/.github/scripts/ci-workflow.test.mjs
@@ -20,26 +20,27 @@ function concurrencySettings(github) {
assert.ok(block, "missing workflow concurrency");
const format = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, index) => String(values[Number(index)]));
const endsWith = (search, value) => String(search).toLowerCase().endsWith(String(value).toLowerCase());
- return Object.fromEntries(block.trim().split("\n").map((line) => {
- const [, setting, value] = line.match(/^\s*([\w-]+):\s*(.+)$/);
- const evaluate = (expression) =>
- new Function("github", "format", "endsWith", `return (${expression});`)(github, format, endsWith);
- const expressions = [...value.matchAll(/\$\{\{\s*(.*?)\s*\}\}/g)];
- return [setting, expressions.length === 1 && expressions[0][0] === value
- ? evaluate(expressions[0][1]) : value.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_, expression) => evaluate(expression))];
- }));
+ return Object.fromEntries(
+ block
+ .trim()
+ .split("\n")
+ .map((line) => {
+ const [, setting, value] = line.match(/^\s*([\w-]+):\s*(.+)$/);
+ const evaluate = (expression) => new Function("github", "format", "endsWith", `return (${expression});`)(github, format, endsWith);
+ const expressions = [...value.matchAll(/\$\{\{\s*(.*?)\s*\}\}/g)];
+ return [setting, expressions.length === 1 && expressions[0][0] === value ? evaluate(expressions[0][1]) : value.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_, expression) => evaluate(expression))];
+ }),
+ );
}
function concurrencyContext({ authorId = 101, author = "contributor", actor = author, number = 100, ref = `refs/pull/${number}/merge`, eventName = "pull_request" } = {}) {
- return { workflow: "CI", repository_owner: "t8y2", actor, ref, event_name: eventName,
- event: eventName === "pull_request" ? { pull_request: { number, user: { id: authorId, login: author } } } : {} };
+ return { workflow: "CI", repository_owner: "t8y2", actor, ref, event_name: eventName, event: eventName === "pull_request" ? { pull_request: { number, user: { id: authorId, login: author } } } : {} };
}
test("a contributor's PRs share two main CI queues split by PR number parity, regardless of trigger actor", () => {
const even = concurrencySettings(concurrencyContext());
assert.deepEqual(even, { group: "CI-pr-author-101-even", "cancel-in-progress": false, queue: "max" });
- for (const context of [concurrencyContext({ number: 202 }), concurrencyContext({ actor: "t8y2" }),
- concurrencyContext({ actor: "github-actions[bot]" }), concurrencyContext({ author: "renamed-contributor" })]) {
+ for (const context of [concurrencyContext({ number: 202 }), concurrencyContext({ actor: "t8y2" }), concurrencyContext({ actor: "github-actions[bot]" }), concurrencyContext({ author: "renamed-contributor" })]) {
assert.deepEqual(concurrencySettings(context), even);
}
for (const number of [101, 11849, 11853, 7]) {
@@ -64,8 +65,7 @@ test("main pushes retain their independent cancellation group without PR context
});
test("expanded author queues never combine max queuing with in-progress cancellation", () => {
- for (const context of [concurrencyContext(), concurrencyContext({ author: "t8y2" }),
- concurrencyContext({ eventName: "push", ref: "refs/heads/main" })]) {
+ for (const context of [concurrencyContext(), concurrencyContext({ author: "t8y2" }), concurrencyContext({ eventName: "push", ref: "refs/heads/main" })]) {
const settings = concurrencySettings(context);
assert.ok(settings.queue === "single" || settings.queue === "max");
assert.equal(settings.queue === "max" && settings["cancel-in-progress"], false);
@@ -202,7 +202,11 @@ test("S3 and secretless Rust jobs export identical compiler wrappers", (context)
const setup = job(name).match(/- name: Configure sccache\n([\s\S]*?)(?=\n - name: Rust cache)/)?.[1];
assert.ok(setup, name);
assert.doesNotMatch(setup, /^\s+if:/m);
- const script = setup.match(/ run: \|\n([\s\S]*)/)[1].split("\n").map((line) => line.slice(10)).join("\n");
+ const script = setup
+ .match(/ run: \|\n([\s\S]*)/)[1]
+ .split("\n")
+ .map((line) => line.slice(10))
+ .join("\n");
for (const remote of [true, false]) {
const output = path.join(directory, `${name}-${remote}.env`);
const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", script], {
@@ -221,16 +225,25 @@ test("S3 and secretless Rust jobs export identical compiler wrappers", (context)
});
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "");
- const exported = Object.fromEntries(readFileSync(output, "utf8").trim().split("\n").map((line) => {
- const separator = line.indexOf("=");
- return [line.slice(0, separator), line.slice(separator + 1)];
- }));
- assert.deepEqual(exported, {
- ...(remote ? backend : {}),
- SCCACHE_IDLE_TIMEOUT: "0",
- CC: `${compiler} cc`,
- CXX: `${compiler} c++`,
- }, `${name}: ${remote ? "S3" : "local"}`);
+ const exported = Object.fromEntries(
+ readFileSync(output, "utf8")
+ .trim()
+ .split("\n")
+ .map((line) => {
+ const separator = line.indexOf("=");
+ return [line.slice(0, separator), line.slice(separator + 1)];
+ }),
+ );
+ assert.deepEqual(
+ exported,
+ {
+ ...(remote ? backend : {}),
+ SCCACHE_IDLE_TIMEOUT: "0",
+ CC: `${compiler} cc`,
+ CXX: `${compiler} c++`,
+ },
+ `${name}: ${remote ? "S3" : "local"}`,
+ );
}
}
});
@@ -323,7 +336,7 @@ test("Windows compatibility jobs cache Rust compilation without wrapping C or C+
assert.ok(win7.includes("--config .github/fixtures/win7-host-repro-config.toml"));
const hostConfig = readFileSync(new URL("../fixtures/win7-host-repro-config.toml", import.meta.url), "utf8");
assert.ok(hostConfig.includes("target-applies-to-host = false"));
- assert.ok(hostConfig.includes('[host.x86_64-pc-windows-msvc]'));
+ assert.ok(hostConfig.includes("[host.x86_64-pc-windows-msvc]"));
assert.ok(hostConfig.includes('rustflags = ["-Clink-arg=/Brepro"]'));
assert.ok(win7.includes("--timings"));
assert.ok(win7.includes("name: DBX-win7-cargo-timings"));
@@ -366,8 +379,7 @@ test("path filtering and the planner use the exact test merge first parent", ()
assert.ok(changes.includes("node .github/scripts/ci-plan.mjs"));
assert.doesNotMatch(changes, /fetch-depth:\s*0/);
assert.doesNotMatch(changes, /dtolnay\/rust-toolchain/);
- for (const flag of ["rust", "rust_full", "rust_matrix", "agents", "agent_go", "agent_rust", "agent_integration",
- "windows_standard", "windows_win7_bundle", "windows_win7_candidate", "windows_win7_affected_packages", "windows_win7_reasons", "plan"]) {
+ for (const flag of ["rust", "rust_full", "rust_matrix", "agents", "agent_go", "agent_rust", "agent_integration", "windows_standard", "windows_win7_bundle", "windows_win7_candidate", "windows_win7_affected_packages", "windows_win7_reasons", "plan"]) {
assert.ok(changes.includes(`steps.plan.outputs.${flag}`));
}
assert.doesNotMatch(changes, /steps\.filter\.outputs\.windows_win7_bundle|github\.event\.pull_request\.base\.sha|WIN7_CURRENT/);
diff --git a/.github/scripts/cleanup-cnb-releases.mjs b/.github/scripts/cleanup-cnb-releases.mjs
index f4d40d66e8..bab458998e 100644
--- a/.github/scripts/cleanup-cnb-releases.mjs
+++ b/.github/scripts/cleanup-cnb-releases.mjs
@@ -33,9 +33,7 @@ function parseArgs(argv) {
}
if (!args.token) throw new Error("CNB_TOKEN is required.");
if (!args.currentTag || !args.tagPattern || !Number.isInteger(args.retain) || args.retain < 1) {
- throw new Error(
- "Usage: cleanup-cnb-releases.mjs --current-tag --tag-pattern --retain [--apply]",
- );
+ throw new Error("Usage: cleanup-cnb-releases.mjs --current-tag --tag-pattern --retain [--apply]");
}
try {
new RegExp(args.tagPattern);
diff --git a/.github/scripts/cleanup-cnb-releases.test.mjs b/.github/scripts/cleanup-cnb-releases.test.mjs
index 202527f0f3..bac2758cc3 100644
--- a/.github/scripts/cleanup-cnb-releases.test.mjs
+++ b/.github/scripts/cleanup-cnb-releases.test.mjs
@@ -17,18 +17,11 @@ const appReleases = [
];
test("retention only removes old releases from the selected family", () => {
- const plan = planReleaseRetention(
- [
- release("agents-latest", "agents-latest", "2026-01-01T00:00:00Z"),
- release("agent", "agents-v0.2.67", "2026-07-29T05:35:19Z"),
- ...appReleases.toReversed(),
- ],
- {
- currentTag: "v0.5.69",
- tagPattern: "^v[0-9]+[.][0-9]+[.][0-9]+$",
- retain: 5,
- },
- );
+ const plan = planReleaseRetention([release("agents-latest", "agents-latest", "2026-01-01T00:00:00Z"), release("agent", "agents-v0.2.67", "2026-07-29T05:35:19Z"), ...appReleases.toReversed()], {
+ currentTag: "v0.5.69",
+ tagPattern: "^v[0-9]+[.][0-9]+[.][0-9]+$",
+ retain: 5,
+ });
assert.equal(plan.skipped, "");
assert.deepEqual(
diff --git a/.github/scripts/database-issue-catalog.mjs b/.github/scripts/database-issue-catalog.mjs
index 9d923b7562..625610fd64 100644
--- a/.github/scripts/database-issue-catalog.mjs
+++ b/.github/scripts/database-issue-catalog.mjs
@@ -1,36 +1,8 @@
import fs from "node:fs";
const extraAliases = {
- mysql: [
- "mariadb",
- "percona",
- "tidb",
- "greatsql",
- "oceanbase mysql",
- "oceanbase mysql mode",
- "oceanbase mysql 模式",
- "tdsql mysql",
- "tdsql-mysql",
- "polardb mysql",
- "polardb for mysql",
- "polardb-x",
- "polardbx",
- ],
- postgres: [
- "postgresql",
- "postgres",
- "pgsql",
- "pg",
- "hologres",
- "cloudberry",
- "apache cloudberry",
- "cockroachdb",
- "cockroach db",
- "polardb postgresql",
- "polardb for postgresql",
- "polardb postgres",
- "polardb for postgres",
- ],
+ mysql: ["mariadb", "percona", "tidb", "greatsql", "oceanbase mysql", "oceanbase mysql mode", "oceanbase mysql 模式", "tdsql mysql", "tdsql-mysql", "polardb mysql", "polardb for mysql", "polardb-x", "polardbx"],
+ postgres: ["postgresql", "postgres", "pgsql", "pg", "hologres", "cloudberry", "apache cloudberry", "cockroachdb", "cockroach db", "polardb postgresql", "polardb for postgresql", "polardb postgres", "polardb for postgres"],
sqlite: ["sqlite3", "sql lite"],
clickhouse: ["click house", "ch"],
sqlserver: ["sql server", "mssql", "microsoft sql server", "sqlservice"],
@@ -104,9 +76,7 @@ const catalogEntries = [
}),
...supplementalDrivers.filter((driver) => !manifestDbTypes.has(driver.dbType)),
];
-const duplicateDbTypes = catalogEntries
- .map((driver) => driver.dbType)
- .filter((dbType, index, values) => values.indexOf(dbType) !== index);
+const duplicateDbTypes = catalogEntries.map((driver) => driver.dbType).filter((dbType, index, values) => values.indexOf(dbType) !== index);
if (duplicateDbTypes.length > 0) {
throw new Error(`Duplicate database issue catalog entries: ${[...new Set(duplicateDbTypes)].join(", ")}`);
@@ -115,12 +85,5 @@ if (duplicateDbTypes.length > 0) {
export const databaseIssueDrivers = catalogEntries.map((driver) => ({
dbType: driver.dbType,
label: driver.label,
- aliases: [
- ...new Set([
- driver.dbType,
- driver.label,
- ...(driver.aliases || []),
- ...(extraAliases[driver.dbType] || []),
- ]),
- ],
+ aliases: [...new Set([driver.dbType, driver.label, ...(driver.aliases || []), ...(extraAliases[driver.dbType] || [])])],
}));
diff --git a/.github/scripts/install-mcp.test.mjs b/.github/scripts/install-mcp.test.mjs
index a18dc7179d..07e537873a 100644
--- a/.github/scripts/install-mcp.test.mjs
+++ b/.github/scripts/install-mcp.test.mjs
@@ -21,20 +21,32 @@ function fixture(context, platform = "darwin-arm64") {
const binary = join(source, "package/bin/dbx-mcp");
writeFileSync(binary, "#!/bin/sh\nprintf 'dbx-mcp 0.4.96\\n'\n", { mode: 0o755 });
copyFileSync(binary, join(source, "dbx-mcp"));
- for (const [archive, member] of [["npm.tgz", "package/bin/dbx-mcp"], ["github.tar.gz", "dbx-mcp"]]) {
+ for (const [archive, member] of [
+ ["npm.tgz", "package/bin/dbx-mcp"],
+ ["github.tar.gz", "dbx-mcp"],
+ ]) {
const result = spawnSync("tar", ["-czf", join(root, archive), "-C", source, member]);
assert.equal(result.status, 0, result.stderr.toString());
}
- const integrity = "sha512-" + createHash("sha512").update(readFileSync(join(root, "npm.tgz"))).digest("base64");
- const checksum = createHash("sha256").update(readFileSync(join(root, "github.tar.gz"))).digest("hex");
+ const integrity =
+ "sha512-" +
+ createHash("sha512")
+ .update(readFileSync(join(root, "npm.tgz")))
+ .digest("base64");
+ const checksum = createHash("sha256")
+ .update(readFileSync(join(root, "github.tar.gz")))
+ .digest("hex");
writeFileSync(join(root, "latest.json"), JSON.stringify({ description: 'ignore "version":"99.0.0"', version: "0.4.96" }));
writeFileSync(join(root, "metadata.json"), JSON.stringify({ name: "test", version: "0.4.96", dist: { integrity } }));
- writeFileSync(join(root, "refs.json"), JSON.stringify([
- { ref: "refs/tags/packages-v0.4.9", object: {} },
- { ref: "refs/tags/packages-v0.4.96", object: {} },
- { ref: "refs/tags/packages-v1.0.0-beta.1", object: {} },
- { ref: "refs/tags/packages-v0.4.10", object: {} },
- ]));
+ writeFileSync(
+ join(root, "refs.json"),
+ JSON.stringify([
+ { ref: "refs/tags/packages-v0.4.9", object: {} },
+ { ref: "refs/tags/packages-v0.4.96", object: {} },
+ { ref: "refs/tags/packages-v1.0.0-beta.1", object: {} },
+ { ref: "refs/tags/packages-v0.4.10", object: {} },
+ ]),
+ );
writeFileSync(join(root, "SHA256SUMS"), `${checksum} dbx-mcp-${platform}.tar.gz\n`);
const routes = {};
for (const registry of ["https://registry.npmjs.org", "https://registry.npmmirror.com"]) {
@@ -46,7 +58,9 @@ function fixture(context, platform = "darwin-arm64") {
routes["https://github.com/t8y2/dbx/releases/download/packages-v0.4.96/SHA256SUMS"] = "SHA256SUMS";
routes[`https://github.com/t8y2/dbx/releases/download/packages-v0.4.96/dbx-mcp-${platform}.tar.gz`] = "github.tar.gz";
writeFileSync(join(root, "routes.json"), JSON.stringify(routes));
- writeFileSync(join(mocks, "curl"), `#!${process.execPath}
+ writeFileSync(
+ join(mocks, "curl"),
+ `#!${process.execPath}
const fs = require('node:fs');
const path = require('node:path');
const args = process.argv.slice(2);
@@ -57,9 +71,13 @@ if (process.env.FAIL_URL && new RegExp(process.env.FAIL_URL).test(url)) process.
const routes = JSON.parse(fs.readFileSync(path.join(root, 'routes.json')));
if (!routes[url]) process.exit(22);
fs.copyFileSync(path.join(root, routes[url]), args[args.indexOf('-o') + 1]);
-`, { mode: 0o755 });
+`,
+ { mode: 0o755 },
+ );
writeFileSync(join(mocks, "uname"), `#!/bin/sh\ncase "$1" in -s) echo '${platform.startsWith("darwin") ? "Darwin" : "Linux"}';; -m) echo '${platform.includes("arm64") ? "aarch64" : "x86_64"}';; esac\n`, { mode: 0o755 });
- writeFileSync(join(mocks, "codesign"), `#!${process.execPath}
+ writeFileSync(
+ join(mocks, "codesign"),
+ `#!${process.execPath}
const args = process.argv.slice(2);
const mode = process.env.SIGNATURE_MODE || 'valid';
const requirement = 'identifier "com.dbx.app.mcp" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "TVDM965TDL"';
@@ -73,13 +91,17 @@ console.error('Identifier=com.dbx.app.mcp');
console.error('TeamIdentifier=TVDM965TDL');
console.error('Timestamp=' + (mode === 'no-timestamp' ? 'none' : 'Oct 3, 2026'));
console.error('designated => ' + (mode === 'hash-bound' ? 'cdhash H"' + 'ab'.repeat(20) + '"' : requirement.replaceAll('exists', '/* exists */')));
-`, { mode: 0o755 });
+`,
+ { mode: 0o755 },
+ );
const installedBinary = join(home, ".dbx/bin/dbx-mcp");
const marker = join(home, ".dbx/bin/.dbx-mcp-version");
- const run = (extra = {}) => spawnSync("sh", [installer], {
- env: { ...process.env, HOME: home, SHELL: "/bin/zsh", ZDOTDIR: home, PATH: `${mocks}:${process.env.PATH}`, FIXTURE: root, FIXTURE_PLATFORM: platform, ...extra },
- encoding: "utf8", timeout: 30_000,
- });
+ const run = (extra = {}) =>
+ spawnSync("sh", [installer], {
+ env: { ...process.env, HOME: home, SHELL: "/bin/zsh", ZDOTDIR: home, PATH: `${mocks}:${process.env.PATH}`, FIXTURE: root, FIXTURE_PLATFORM: platform, ...extra },
+ encoding: "utf8",
+ timeout: 30_000,
+ });
return { root, home, mocks, binary: installedBinary, marker, run };
}
@@ -92,7 +114,10 @@ for (const platform of platforms) {
assert.equal(readFileSync(setup.marker, "utf8"), "0.4.96\n");
const json = JSON.parse(result.stdout.split("\n").find((line) => line.startsWith('{"mcpServers"')));
assert.equal(json.mcpServers.dbx.command, setup.binary);
- const tomlCommand = result.stdout.split("\n").find((line) => line.startsWith("command = ")).slice(10);
+ const tomlCommand = result.stdout
+ .split("\n")
+ .find((line) => line.startsWith("command = "))
+ .slice(10);
assert.equal(JSON.parse(tomlCommand), setup.binary);
assert.deepEqual(readdirSync(join(setup.home, ".dbx/bin")).sort(), [".dbx-mcp-version", "dbx-mcp"]);
});
diff --git a/.github/scripts/issue-commands.mjs b/.github/scripts/issue-commands.mjs
index d61c638703..56100539c9 100644
--- a/.github/scripts/issue-commands.mjs
+++ b/.github/scripts/issue-commands.mjs
@@ -63,13 +63,7 @@ export function detectIssueCommand(commentBody) {
export function findEligibleMergedPullRequest({ context, commentUser, repository }) {
return context.pullRequests
- .filter(
- (pullRequest) =>
- pullRequest.state === "MERGED" &&
- pullRequest.baseRefName === context.defaultBranch &&
- pullRequest.repository?.nameWithOwner === repository &&
- sameLogin(pullRequest.author?.login, commentUser),
- )
+ .filter((pullRequest) => pullRequest.state === "MERGED" && pullRequest.baseRefName === context.defaultBranch && pullRequest.repository?.nameWithOwner === repository && sameLogin(pullRequest.author?.login, commentUser))
.sort((left, right) => (right.mergedAt || "").localeCompare(left.mergedAt || ""))[0];
}
@@ -92,10 +86,7 @@ async function handleUnclaim({ issueNumber, commentUser, client }) {
const assignee = context.assignees.find(({ login }) => sameLogin(login, commentUser));
if (!assignee) {
- await client.commentIssue(
- issueNumber,
- `❌ @${commentUser} 你当前没有认领 #${issueNumber},无法使用 \`/unclaim\`。`,
- );
+ await client.commentIssue(issueNumber, `❌ @${commentUser} 你当前没有认领 #${issueNumber},无法使用 \`/unclaim\`。`);
return { status: "not-assignee" };
}
@@ -116,28 +107,15 @@ async function handleClose({ issueNumber, commentUser, repository, client }) {
const pullRequest = findEligibleMergedPullRequest({ context, commentUser, repository });
if (!pullRequest) {
- await client.commentIssue(
- issueNumber,
- `❌ @${commentUser} 暂不能关闭 #${issueNumber}:未找到由你提交、已合并到 \`${context.defaultBranch}\` 且关联此 Issue 的 PR。`,
- );
+ await client.commentIssue(issueNumber, `❌ @${commentUser} 暂不能关闭 #${issueNumber}:未找到由你提交、已合并到 \`${context.defaultBranch}\` 且关联此 Issue 的 PR。`);
return { status: "no-merged-pull-request" };
}
- await client.closeIssue(
- issueNumber,
- `✅ @${commentUser} 的关联 PR #${pullRequest.number} 已合并,关闭 #${issueNumber}。`,
- );
+ await client.closeIssue(issueNumber, `✅ @${commentUser} 的关联 PR #${pullRequest.number} 已合并,关闭 #${issueNumber}。`);
return { status: "closed", pullRequest };
}
-export async function handleIssueCommand({
- issueNumber,
- commentBody,
- commentUser,
- commentUserType,
- repository,
- client,
-}) {
+export async function handleIssueCommand({ issueNumber, commentBody, commentUser, commentUserType, repository, client }) {
const command = detectIssueCommand(commentBody);
if (!command) return { status: "ignored" };
if (commentUserType === "Bot") return { status: "ignored-bot" };
@@ -158,17 +136,7 @@ export function createGitHubClient({ repository, runGh = gh }) {
return {
async getIssueContext(issueNumber) {
- return JSON.parse(
- await runGh([
- "issue",
- "view",
- String(issueNumber),
- "--repo",
- repository,
- "--json",
- "state,assignees",
- ]),
- );
+ return JSON.parse(await runGh(["issue", "view", String(issueNumber), "--repo", repository, "--json", "state,assignees"]));
},
async getIssueCloseContext(issueNumber) {
@@ -176,18 +144,7 @@ export function createGitHubClient({ repository, runGh = gh }) {
let context;
do {
- const args = [
- "api",
- "graphql",
- "-f",
- `owner=${owner}`,
- "-f",
- `name=${name}`,
- "-F",
- `number=${issueNumber}`,
- "-f",
- `query=${ISSUE_CLOSE_CONTEXT_QUERY}`,
- ];
+ const args = ["api", "graphql", "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${issueNumber}`, "-f", `query=${ISSUE_CLOSE_CONTEXT_QUERY}`];
if (endCursor) args.push("-f", `endCursor=${endCursor}`);
const response = JSON.parse(await runGh(args));
@@ -205,11 +162,7 @@ export function createGitHubClient({ repository, runGh = gh }) {
if (!context.defaultBranch) throw new Error(`Default branch was not found for ${repository}`);
}
- context.pullRequests.push(
- ...issue.timelineItems.nodes
- .map((node) => node.source)
- .filter((source) => source?.__typename === "PullRequest"),
- );
+ context.pullRequests.push(...issue.timelineItems.nodes.map((node) => node.source).filter((source) => source?.__typename === "PullRequest"));
const { hasNextPage, endCursor: nextCursor } = issue.timelineItems.pageInfo;
endCursor = hasNextPage ? nextCursor : undefined;
@@ -231,17 +184,7 @@ export function createGitHubClient({ repository, runGh = gh }) {
},
async closeIssue(issueNumber, comment) {
- await runGh([
- "issue",
- "close",
- String(issueNumber),
- "--repo",
- repository,
- "--reason",
- "completed",
- "--comment",
- comment,
- ]);
+ await runGh(["issue", "close", String(issueNumber), "--repo", repository, "--reason", "completed", "--comment", comment]);
},
};
}
diff --git a/.github/scripts/issue-commands.test.mjs b/.github/scripts/issue-commands.test.mjs
index b03e3ff719..99aec4ee72 100644
--- a/.github/scripts/issue-commands.test.mjs
+++ b/.github/scripts/issue-commands.test.mjs
@@ -1,12 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
-import {
- createGitHubClient,
- detectIssueCommand,
- findEligibleMergedPullRequest,
- handleIssueCommand,
-} from "./issue-commands.mjs";
+import { createGitHubClient, detectIssueCommand, findEligibleMergedPullRequest, handleIssueCommand } from "./issue-commands.mjs";
const repository = "t8y2/dbx";
@@ -117,13 +112,7 @@ test("findEligibleMergedPullRequest requires the assignee's merged PR on the def
repository: { nameWithOwner: repository },
};
const context = closeContext({
- pullRequests: [
- { ...eligible, number: 101, state: "OPEN" },
- { ...eligible, number: 102, baseRefName: "release" },
- { ...eligible, number: 103, author: { login: "someone-else" } },
- { ...eligible, number: 104, repository: { nameWithOwner: "someone/dbx" } },
- eligible,
- ],
+ pullRequests: [{ ...eligible, number: 101, state: "OPEN" }, { ...eligible, number: 102, baseRefName: "release" }, { ...eligible, number: 103, author: { login: "someone-else" } }, { ...eligible, number: 104, repository: { nameWithOwner: "someone/dbx" } }, eligible],
});
assert.equal(findEligibleMergedPullRequest({ context, commentUser: "contributor", repository }), eligible);
@@ -175,9 +164,7 @@ test("close succeeds after the assignee's linked PR is merged", async () => {
test("commands ignore bots and closed unclaim requests", async () => {
const botClient = recordingClient();
- const botResult = await handleIssueCommand(
- commandOptions("/claim", botClient, { commentUserType: "Bot" }),
- );
+ const botResult = await handleIssueCommand(commandOptions("/claim", botClient, { commentUserType: "Bot" }));
assert.equal(botResult.status, "ignored-bot");
assert.deepEqual(botClient.calls, []);
@@ -213,10 +200,7 @@ test("GitHub client aggregates paginated pull request cross references", async (
assignees: { nodes: [{ login: "contributor" }] },
timelineItems: {
pageInfo: { hasNextPage: false, endCursor: "done" },
- nodes: [
- { source: { __typename: "Issue", number: 11 } },
- { source: { __typename: "PullRequest", number: 12 } },
- ],
+ nodes: [{ source: { __typename: "Issue", number: 11 } }, { source: { __typename: "PullRequest", number: 12 } }],
},
},
},
@@ -233,7 +217,10 @@ test("GitHub client aggregates paginated pull request cross references", async (
});
const context = await client.getIssueCloseContext("123");
- assert.deepEqual(context.pullRequests.map(({ number }) => number), [10, 12]);
+ assert.deepEqual(
+ context.pullRequests.map(({ number }) => number),
+ [10, 12],
+ );
assert.equal(args.length, 2);
assert.equal(args[1].includes("endCursor=next-page"), true);
});
diff --git a/.github/scripts/jdbc-plugin-version.test.mjs b/.github/scripts/jdbc-plugin-version.test.mjs
index 7005cd3ba4..ac0017e99c 100644
--- a/.github/scripts/jdbc-plugin-version.test.mjs
+++ b/.github/scripts/jdbc-plugin-version.test.mjs
@@ -17,14 +17,8 @@ test("reads the JDBC plugin version from Gradle", () => {
});
test("checks Gradle and manifest versions together", () => {
- assert.deepEqual(
- evaluateJdbcPluginVersionChange({ headGradleVersion: "0.1.26", headManifestVersion: "0.1.26" }),
- [],
- );
- assert.match(
- evaluateJdbcPluginVersionChange({ headGradleVersion: "0.1.27", headManifestVersion: "0.1.26" })[0],
- /build\.gradle is 0\.1\.27/,
- );
+ assert.deepEqual(evaluateJdbcPluginVersionChange({ headGradleVersion: "0.1.26", headManifestVersion: "0.1.26" }), []);
+ assert.match(evaluateJdbcPluginVersionChange({ headGradleVersion: "0.1.27", headManifestVersion: "0.1.26" })[0], /build\.gradle is 0\.1\.27/);
});
test("bumps Gradle and manifest versions for JDBC source changes", () => {
diff --git a/.github/scripts/label-issue-database.mjs b/.github/scripts/label-issue-database.mjs
index f28aa38448..64e86d6a01 100644
--- a/.github/scripts/label-issue-database.mjs
+++ b/.github/scripts/label-issue-database.mjs
@@ -17,52 +17,14 @@ const USER_PRIORITY_LABELS = {
P3: { color: "bfd4f2", description: "User-reported priority: low" },
};
-const LABEL_PALETTE = [
- "0e8a16",
- "1d76db",
- "5319e7",
- "c2e0c6",
- "bfd4f2",
- "d4c5f9",
- "fef2c0",
- "fbca04",
- "f9d0c4",
- "e99695",
- "f29513",
- "c5def5",
-];
+const LABEL_PALETTE = ["0e8a16", "1d76db", "5319e7", "c2e0c6", "bfd4f2", "d4c5f9", "fef2c0", "fbca04", "f9d0c4", "e99695", "f29513", "c5def5"];
-const globallyAmbiguousAliases = new Set([
- "access",
- "ch",
- "h2",
- "iris",
- "jdbc",
- "mq",
- "pg",
- "spark",
-]);
+const globallyAmbiguousAliases = new Set(["access", "ch", "h2", "iris", "jdbc", "mq", "pg", "spark"]);
-const genericDatabaseValues = [
- "general",
- "generic",
- "global",
- "all",
- "any",
- "none",
- "n/a",
- "na",
- "通用",
- "全部",
- "所有",
- "无",
- "不适用",
-];
+const genericDatabaseValues = ["general", "generic", "global", "all", "any", "none", "n/a", "na", "通用", "全部", "所有", "无", "不适用"];
const drivers = databaseIssueDrivers;
-const exactAsciiAliases = new Set(
- drivers.flatMap((driver) => driver.aliases.map((alias) => compactAscii(alias)).filter((alias) => alias.length >= 5)),
-);
+const exactAsciiAliases = new Set(drivers.flatMap((driver) => driver.aliases.map((alias) => compactAscii(alias)).filter((alias) => alias.length >= 5)));
function loadIssue() {
if (process.env.GITHUB_EVENT_PATH && fs.existsSync(process.env.GITHUB_EVENT_PATH)) {
@@ -113,20 +75,12 @@ function extractInlineField(body, predicate) {
}
function extractDatabaseField(body) {
- const matchesDatabaseHeading = (heading) => (
- heading.includes("数据库类型") ||
- heading.toLowerCase().includes("database type") ||
- /^database$/i.test(heading)
- );
- return extractIssueFormSection(body, matchesDatabaseHeading)
- || extractInlineField(body, matchesDatabaseHeading);
+ const matchesDatabaseHeading = (heading) => heading.includes("数据库类型") || heading.toLowerCase().includes("database type") || /^database$/i.test(heading);
+ return extractIssueFormSection(body, matchesDatabaseHeading) || extractInlineField(body, matchesDatabaseHeading);
}
function extractPriorityField(body) {
- return extractIssueFormSection(body, (heading) => (
- heading.includes("优先级") ||
- heading.toLowerCase().includes("priority")
- ));
+ return extractIssueFormSection(body, (heading) => heading.includes("优先级") || heading.toLowerCase().includes("priority"));
}
function extractTitleSummaryField(body) {
@@ -146,21 +100,24 @@ function extractTitleSummaryField(body) {
}
function matchPriorityLabel(value) {
- const match = String(value || "").normalize("NFKC").match(/\bP([0-3])\b/i);
+ const match = String(value || "")
+ .normalize("NFKC")
+ .match(/\bP([0-3])\b/i);
return match ? `P${match[1]}` : null;
}
function genericIssueTitlePrefix(title) {
- const normalized = String(title || "").normalize("NFKC").trim().replace(/\s+/g, " ");
+ const normalized = String(title || "")
+ .normalize("NFKC")
+ .trim()
+ .replace(/\s+/g, " ");
const bracketMatch = normalized.match(/^([\[【][^\]】]*(?:bug|feature|feat|compatibility|question)[^\]】]*[\]】])$/i);
if (bracketMatch) return bracketMatch[1];
const bareMatch = normalized.match(/^(bug|feature|feat|compatibility|question):?$/i);
if (!bareMatch) return null;
- const normalizedPrefix = bareMatch[1].toLowerCase() === "feat" ? "Feat" : (
- bareMatch[1].charAt(0).toUpperCase() + bareMatch[1].slice(1).toLowerCase()
- );
+ const normalizedPrefix = bareMatch[1].toLowerCase() === "feat" ? "Feat" : bareMatch[1].charAt(0).toUpperCase() + bareMatch[1].slice(1).toLowerCase();
return `[${normalizedPrefix}]`;
}
@@ -179,14 +136,7 @@ function cleanTitleSummaryLine(line) {
}
function isSkippableTitleSummaryLine(line) {
- return (
- /^DBX debug log$/i.test(line) ||
- /^(Exported|User agent|Platform|Timezone):/i.test(line) ||
- /^(Native|Tauri) log dir:/i.test(line) ||
- /^=+.*logs?.*=+$/i.test(line) ||
- /^\[\d{4}-\d{2}-\d{2}(?:T|\])/.test(line) ||
- /^\[(DEBUG|INFO|WARN|ERROR|LOG|STARTUP)\]/i.test(line)
- );
+ return /^DBX debug log$/i.test(line) || /^(Exported|User agent|Platform|Timezone):/i.test(line) || /^(Native|Tauri) log dir:/i.test(line) || /^=+.*logs?.*=+$/i.test(line) || /^\[\d{4}-\d{2}-\d{2}(?:T|\])/.test(line) || /^\[(DEBUG|INFO|WARN|ERROR|LOG|STARTUP)\]/i.test(line);
}
function firstSentence(value) {
@@ -199,9 +149,7 @@ function firstSentence(value) {
for (const line of lines) {
const sentenceMatch = line.match(/^(.{4,120}?[。!?!?]|.{12,120}?\.(?=\s|$))/u);
- const sentence = (sentenceMatch ? sentenceMatch[1] : line)
- .replace(/[。!?!?.]+$/u, "")
- .trim();
+ const sentence = (sentenceMatch ? sentenceMatch[1] : line).replace(/[。!?!?.]+$/u, "").trim();
if (sentence.length >= 4) {
return sentence.length > 90 ? `${sentence.slice(0, 89)}…` : sentence;
}
@@ -251,11 +199,7 @@ function levenshteinDistance(left, right) {
const current = [leftIndex + 1];
for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
- current[rightIndex + 1] = Math.min(
- current[rightIndex] + 1,
- previous[rightIndex + 1] + 1,
- previous[rightIndex] + substitutionCost,
- );
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
}
previous = current;
}
@@ -318,9 +262,7 @@ function aliasMatches(text, alias) {
.filter(Boolean)
.map(escapeRegExp)
.join("[\\s._/-]*");
- const trailingBoundary = /[0-9]$/.test(normalizedAlias)
- ? "(?:[^a-z0-9]|$)"
- : "(?:[^a-z0-9]|$|(?=v?\\d))";
+ const trailingBoundary = /[0-9]$/.test(normalizedAlias) ? "(?:[^a-z0-9]|$)" : "(?:[^a-z0-9]|$|(?=v?\\d))";
const pattern = new RegExp(`(^|[^a-z0-9])${flexibleAlias}${trailingBoundary}`, "i");
return pattern.test(normalizedText);
}
@@ -332,10 +274,7 @@ function isGenericDatabaseValue(value) {
.map((part) => part.trim())
.filter(Boolean);
- return (
- parts.length > 0 &&
- parts.every((part) => genericDatabaseValues.some((generic) => aliasMatches(part, generic)))
- );
+ return parts.length > 0 && parts.every((part) => genericDatabaseValues.some((generic) => aliasMatches(part, generic)));
}
function matchDrivers(text, { allowAmbiguous }) {
@@ -456,24 +395,18 @@ export function triageIssue(issue) {
const hasDatabaseField = databaseField.length > 0;
const fieldIsGeneric = hasDatabaseField && isGenericDatabaseValue(databaseField);
const sourceText = hasDatabaseField ? databaseField : issue.title || "";
- const matchedDrivers = isGenericDatabaseValue(sourceText)
- ? []
- : uniqueByDbType(matchDrivers(sourceText, { allowAmbiguous: hasDatabaseField }));
+ const matchedDrivers = isGenericDatabaseValue(sourceText) ? [] : uniqueByDbType(matchDrivers(sourceText, { allowAmbiguous: hasDatabaseField }));
const titleMatchedDrivers = uniqueByDbType(matchDrivers(issue.title || "", { allowAmbiguous: false }));
if (hasDatabaseField && titleMatchedDrivers.length > 0) {
matchedDrivers.push(...titleMatchedDrivers);
}
if (hasDatabaseField && matchedDrivers.length === 0 && !fieldIsGeneric) {
- matchedDrivers.push(
- ...uniqueByDbType(matchDrivers(`${issue.title || ""}\n${databaseField}`, { allowAmbiguous: false })),
- );
+ matchedDrivers.push(...uniqueByDbType(matchDrivers(`${issue.title || ""}\n${databaseField}`, { allowAmbiguous: false })));
}
const uniqueMatchedDrivers = uniqueByDbType(matchedDrivers);
const targetLabels = uniqueMatchedDrivers.map((driver) => `${LABEL_PREFIX}${driver.dbType}`);
- const targetLabelColors = Object.fromEntries(
- uniqueMatchedDrivers.map((driver) => [`${LABEL_PREFIX}${driver.dbType}`, stablePaletteColor(driver.dbType)]),
- );
+ const targetLabelColors = Object.fromEntries(uniqueMatchedDrivers.map((driver) => [`${LABEL_PREFIX}${driver.dbType}`, stablePaletteColor(driver.dbType)]));
const priorityField = extractPriorityField(issue.body || "");
const targetPriorityValue = matchPriorityLabel(priorityField);
const targetPriorityLabel = targetPriorityValue ? `${USER_PRIORITY_PREFIX}${targetPriorityValue}` : null;
@@ -484,22 +417,11 @@ export function triageIssue(issue) {
const existingLabels = labelNames(issue.labels);
const titleToUpdate = inferIssueTitle(issue.title || "", issue.body || "");
const existingDatabaseLabels = existingLabels.filter((name) => name.startsWith(LABEL_PREFIX));
- const staleDatabaseLabels = hasDatabaseField
- ? existingDatabaseLabels.filter((name) => !targetLabels.includes(name))
- : [];
+ const staleDatabaseLabels = hasDatabaseField ? existingDatabaseLabels.filter((name) => !targetLabels.includes(name)) : [];
const existingPriorityLabels = existingLabels.filter((name) => name.startsWith(USER_PRIORITY_PREFIX));
- const stalePriorityLabels = targetPriorityLabel
- ? existingPriorityLabels.filter((name) => name !== targetPriorityLabel)
- : [];
- const labelsToAdd = [...targetLabels, targetPriorityLabel]
- .filter(Boolean)
- .filter((name) => !existingLabels.includes(name));
- const labelSpecs = [
- ...uniqueMatchedDrivers.map(databaseLabelSpec),
- ...(targetPriorityValue
- ? [{ name: targetPriorityLabel, ...USER_PRIORITY_LABELS[targetPriorityValue] }]
- : []),
- ];
+ const stalePriorityLabels = targetPriorityLabel ? existingPriorityLabels.filter((name) => name !== targetPriorityLabel) : [];
+ const labelsToAdd = [...targetLabels, targetPriorityLabel].filter(Boolean).filter((name) => !existingLabels.includes(name));
+ const labelSpecs = [...uniqueMatchedDrivers.map(databaseLabelSpec), ...(targetPriorityValue ? [{ name: targetPriorityLabel, ...USER_PRIORITY_LABELS[targetPriorityValue] }] : [])];
const summary = {
issue: issue.number,
databaseField: hasDatabaseField ? databaseField : null,
diff --git a/.github/scripts/label-issue-database.test.mjs b/.github/scripts/label-issue-database.test.mjs
index b7a9fa7088..993c574b2c 100644
--- a/.github/scripts/label-issue-database.test.mjs
+++ b/.github/scripts/label-issue-database.test.mjs
@@ -21,8 +21,8 @@ function issueWithDatabase(database, title = "[Bug]") {
}
function matchedLabels(database, title) {
- return triageIssue(issueWithDatabase(database, title)).summary.labelsToAdd
- .filter((label) => label.startsWith("db/"))
+ return triageIssue(issueWithDatabase(database, title))
+ .summary.labelsToAdd.filter((label) => label.startsWith("db/"))
.sort();
}
@@ -35,10 +35,7 @@ test("database issue catalog covers every DatabaseType", () => {
test("database label synchronization includes every catalog entry once", () => {
const labelNames = allDatabaseLabelSpecs().map((label) => label.name);
assert.equal(new Set(labelNames).size, databaseIssueDrivers.length);
- assert.deepEqual(
- labelNames.sort(),
- databaseIssueDrivers.map((driver) => `db/${driver.dbType}`).sort(),
- );
+ assert.deepEqual(labelNames.sort(), databaseIssueDrivers.map((driver) => `db/${driver.dbType}`).sort());
});
test("Kingbase aliases retain historical and current company names", () => {
diff --git a/.github/scripts/label-pull-request.mjs b/.github/scripts/label-pull-request.mjs
index 223b3afbdd..63ce79fef9 100644
--- a/.github/scripts/label-pull-request.mjs
+++ b/.github/scripts/label-pull-request.mjs
@@ -33,20 +33,7 @@ export const STATIC_LABEL_SPECS = {
const CORE_CRATE_PATH = /^crates\/dbx-(?:core|driver(?:s|-[^/]+)|sql(?:-[^/]+)?|formats|types|platform|ai-provider|plugin-runtime)\//;
const TYPE_LABELS = new Set(["bug", "enhancement", "documentation", "maintenance"]);
-const LABEL_PALETTE = [
- "0e8a16",
- "1d76db",
- "5319e7",
- "c2e0c6",
- "bfd4f2",
- "d4c5f9",
- "fef2c0",
- "fbca04",
- "f9d0c4",
- "e99695",
- "f29513",
- "c5def5",
-];
+const LABEL_PALETTE = ["0e8a16", "1d76db", "5319e7", "c2e0c6", "bfd4f2", "d4c5f9", "fef2c0", "fbca04", "f9d0c4", "e99695", "f29513", "c5def5"];
const DRIVER_DATABASE_ALIASES = {
"cassandra-go": "cassandra",
@@ -86,12 +73,7 @@ const DATABASE_PATH_TOKENS = [
[/sqlserver/i, "sqlserver"],
];
-const PACKAGE_DEPENDENCY_SECTIONS = [
- "dependencies",
- "devDependencies",
- "optionalDependencies",
- "peerDependencies",
-];
+const PACKAGE_DEPENDENCY_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
function stablePaletteColor(value) {
let hash = 0;
@@ -106,14 +88,14 @@ function normalizeChangedFiles(changedFiles) {
}
function conventionalType(title) {
- const normalized = String(title || "").trim().replace(/^[^\p{L}\p{N}]+/u, "");
+ const normalized = String(title || "")
+ .trim()
+ .replace(/^[^\p{L}\p{N}]+/u, "");
return /^([a-z]+)(?:\([^)]*\))?!?[::]/i.exec(normalized)?.[1]?.toLowerCase() || null;
}
export function isDocumentationFile(file) {
- return file.startsWith("docs/")
- || /(^|\/)README(?:\.[^/]+)?\.md$/i.test(file)
- || /(^|\/)(SECURITY|CONTRIBUTING|CODE_OF_CONDUCT)\.md$/i.test(file);
+ return file.startsWith("docs/") || /(^|\/)README(?:\.[^/]+)?\.md$/i.test(file) || /(^|\/)(SECURITY|CONTRIBUTING|CODE_OF_CONDUCT)\.md$/i.test(file);
}
export function inferTypeLabel(title, changedFiles) {
@@ -130,10 +112,7 @@ export function inferAreaLabels(changedFiles) {
const labels = new Set();
const has = (predicate) => changedFiles.some(predicate);
- if (has((file) => file === "package.json"
- || file.startsWith("apps/desktop/")
- || file.startsWith("packages/app-tests/")
- || file.startsWith("src-tauri/"))) {
+ if (has((file) => file === "package.json" || file.startsWith("apps/desktop/") || file.startsWith("packages/app-tests/") || file.startsWith("src-tauri/"))) {
labels.add("area/desktop");
}
if (has((file) => CORE_CRATE_PATH.test(file) || file.startsWith("packages/node-core/"))) {
@@ -143,27 +122,16 @@ export function inferAreaLabels(changedFiles) {
labels.add("area/web");
}
if (has((file) => file.startsWith("agents/"))) labels.add("area/agents");
- if (has((file) => file.startsWith("crates/dbx-mcp/")
- || file.startsWith("packages/mcp-")
- || file.startsWith("packages/mcp-server/")
- || file.startsWith("examples/mcp/"))) {
+ if (has((file) => file.startsWith("crates/dbx-mcp/") || file.startsWith("packages/mcp-") || file.startsWith("packages/mcp-server/") || file.startsWith("examples/mcp/"))) {
labels.add("area/mcp");
}
- if (has((file) => file.startsWith("crates/dbx-cli/")
- || file.startsWith("packages/cli-")
- || file.startsWith("packages/cli/")
- || file.startsWith("skills/dbx/")
- || file.startsWith("examples/cli/"))) {
+ if (has((file) => file.startsWith("crates/dbx-cli/") || file.startsWith("packages/cli-") || file.startsWith("packages/cli/") || file.startsWith("skills/dbx/") || file.startsWith("examples/cli/"))) {
labels.add("area/cli");
}
if (has((file) => file.startsWith("plugins/"))) labels.add("area/plugins");
if (has(isDocumentationFile)) labels.add("area/docs");
if (has((file) => file.startsWith(".github/"))) labels.add("area/ci");
- if (has((file) => file.startsWith("deploy/")
- || file === "flake.nix"
- || file === "flake.lock"
- || file === "Makefile"
- || /^packages\/(?:cli|mcp)-(?:darwin|linux|win32)-/.test(file))) {
+ if (has((file) => file.startsWith("deploy/") || file === "flake.nix" || file === "flake.lock" || file === "Makefile" || /^packages\/(?:cli|mcp)-(?:darwin|linux|win32)-/.test(file))) {
labels.add("area/deploy");
}
@@ -178,29 +146,20 @@ export function isUiChangeFile(file) {
if (isTestFile(file)) return false;
if (file.startsWith("apps/desktop/")) {
- return /\.(?:vue|css|less|sass|scss|styl)$/i.test(file)
- || file.startsWith("apps/desktop/src/components/")
- || file.startsWith("apps/desktop/src/i18n/")
- || file.startsWith("apps/desktop/src/styles/")
- || file.startsWith("apps/desktop/public/")
- || isVisualAsset(file);
+ return /\.(?:vue|css|less|sass|scss|styl)$/i.test(file) || file.startsWith("apps/desktop/src/components/") || file.startsWith("apps/desktop/src/i18n/") || file.startsWith("apps/desktop/src/styles/") || file.startsWith("apps/desktop/public/") || isVisualAsset(file);
}
if (file.startsWith("src-tauri/icons/")) return true;
if (file.startsWith("docs/")) {
- return /\.(?:css|jsx|tsx)$/i.test(file)
- || file.startsWith("docs/components/")
- || (file.startsWith("docs/public/") && isVisualAsset(file));
+ return /\.(?:css|jsx|tsx)$/i.test(file) || file.startsWith("docs/components/") || (file.startsWith("docs/public/") && isVisualAsset(file));
}
return false;
}
export function isTestFile(file) {
- return /(^|\/)(?:__tests__|fixtures|snapshots|test|tests|test-support)(\/|$)/.test(file)
- || /\.(?:spec|test)\.[^/]+$/i.test(file)
- || /(^|\/)packages\/app-tests\//.test(file);
+ return /(^|\/)(?:__tests__|fixtures|snapshots|test|tests|test-support)(\/|$)/.test(file) || /\.(?:spec|test)\.[^/]+$/i.test(file) || /(^|\/)packages\/app-tests\//.test(file);
}
function normalizeDatabaseType(value, aliases) {
@@ -248,8 +207,7 @@ export function parsePackageDependencyNames(source) {
function unquoteTomlKey(value) {
const trimmed = String(value || "").trim();
- if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
- || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1);
}
return trimmed;
@@ -340,17 +298,10 @@ function dependencySide(file) {
if (file === "package.json" || file === "docs/package.json" || file === "packages/mongo-shell/package.json") {
return "frontend";
}
- if (/^packages\/(?:cli(?:-|\/)|mcp(?:-|server\/)).*\/package\.json$/.test(file)
- || file === "packages/cli/package.json"
- || file === "packages/mcp-server/package.json") {
+ if (/^packages\/(?:cli(?:-|\/)|mcp(?:-|server\/)).*\/package\.json$/.test(file) || file === "packages/cli/package.json" || file === "packages/mcp-server/package.json") {
return "backend";
}
- if (file === "Cargo.toml"
- || file.endsWith("/Cargo.toml")
- || file.endsWith("/build.gradle")
- || file.endsWith("/build.gradle.kts")
- || file.endsWith("/go.mod")
- || file.endsWith("/pom.xml")) {
+ if (file === "Cargo.toml" || file.endsWith("/Cargo.toml") || file.endsWith("/build.gradle") || file.endsWith("/build.gradle.kts") || file.endsWith("/go.mod") || file.endsWith("/pom.xml")) {
return "backend";
}
return null;
@@ -382,13 +333,7 @@ export function inferAddedDependencies(changedFiles, readBaseFile, readHeadFile)
return added;
}
-export function evaluatePullRequestLabels({
- title,
- changedFiles,
- knownDatabaseTypes,
- readBaseFile = () => null,
- readHeadFile = () => null,
-}) {
+export function evaluatePullRequestLabels({ title, changedFiles, knownDatabaseTypes, readBaseFile = () => null, readHeadFile = () => null }) {
const files = normalizeChangedFiles(changedFiles);
const areaLabels = inferAreaLabels(files);
const labels = new Set(areaLabels.length > 3 ? ["area/multiple"] : areaLabels);
@@ -510,12 +455,7 @@ async function removeLabel(pullRequestNumber, name) {
}
function isManagedLabel(name) {
- return TYPE_LABELS.has(name)
- || name.startsWith("area/")
- || name.startsWith(DATABASE_LABEL_PREFIX)
- || name.startsWith("dependencies/")
- || name === "tests-only"
- || name === "ui-change";
+ return TYPE_LABELS.has(name) || name.startsWith("area/") || name.startsWith(DATABASE_LABEL_PREFIX) || name.startsWith("dependencies/") || name === "tests-only" || name === "ui-change";
}
async function main() {
@@ -547,14 +487,20 @@ async function main() {
};
}
- console.log(JSON.stringify({
- pullRequest: pullRequest.number,
- title: pullRequest.title,
- ...result,
- existingLabels,
- labelsToAdd,
- labelsToRemove,
- }, null, 2));
+ console.log(
+ JSON.stringify(
+ {
+ pullRequest: pullRequest.number,
+ title: pullRequest.title,
+ ...result,
+ existingLabels,
+ labelsToAdd,
+ labelsToRemove,
+ },
+ null,
+ 2,
+ ),
+ );
if (dryRun) {
console.log("Dry run enabled; no GitHub API calls were made");
diff --git a/.github/scripts/label-pull-request.test.mjs b/.github/scripts/label-pull-request.test.mjs
index 1779fadea5..222e3cc60c 100644
--- a/.github/scripts/label-pull-request.test.mjs
+++ b/.github/scripts/label-pull-request.test.mjs
@@ -1,42 +1,14 @@
import assert from "node:assert/strict";
import test from "node:test";
-import {
- evaluatePullRequestLabels,
- inferAddedDependencies,
- inferDatabaseTypes,
- parseCargoDependencyNames,
- parseGoDependencyNames,
- parseGradleDependencyNames,
- parseMavenDependencyNames,
- parsePackageDependencyNames,
-} from "./label-pull-request.mjs";
+import { evaluatePullRequestLabels, inferAddedDependencies, inferDatabaseTypes, parseCargoDependencyNames, parseGoDependencyNames, parseGradleDependencyNames, parseMavenDependencyNames, parsePackageDependencyNames } from "./label-pull-request.mjs";
-const knownDatabaseTypes = new Set([
- "access",
- "cassandra",
- "doris",
- "jdbc",
- "mongodb",
- "mq",
- "mysql",
- "neo4j",
- "oceanbase-oracle",
- "oracle",
- "postgres",
- "redis",
- "sqlite",
- "sqlserver",
- "vastbase",
-]);
+const knownDatabaseTypes = new Set(["access", "cassandra", "doris", "jdbc", "mongodb", "mq", "mysql", "neo4j", "oceanbase-oracle", "oracle", "postgres", "redis", "sqlite", "sqlserver", "vastbase"]);
test("labels a desktop MySQL UI fix", () => {
const result = evaluatePullRequestLabels({
title: "fix(mysql): improve connection dialog",
- changedFiles: [
- "apps/desktop/src/components/connection/MySqlConnectionDialog.vue",
- "apps/desktop/src/i18n/en.ts",
- ],
+ changedFiles: ["apps/desktop/src/components/connection/MySqlConnectionDialog.vue", "apps/desktop/src/i18n/en.ts"],
knownDatabaseTypes,
});
@@ -65,15 +37,7 @@ test("labels the bundled DBX skill as CLI work", () => {
test("maps agent and dialect paths to existing database types", () => {
assert.deepEqual(
- inferDatabaseTypes([
- "agents/drivers/oracle-go/go.mod",
- "agents/drivers/cassandra-go/go.mod",
- "agents/drivers/neo4j-go/go.mod",
- "agents/drivers/vastbase-go/go.mod",
- "agents/drivers/kafka/build.gradle",
- "plugins/dialects/postgresql.yaml",
- "plugins/dialects/oceanbase.yaml",
- ], knownDatabaseTypes),
+ inferDatabaseTypes(["agents/drivers/oracle-go/go.mod", "agents/drivers/cassandra-go/go.mod", "agents/drivers/neo4j-go/go.mod", "agents/drivers/vastbase-go/go.mod", "agents/drivers/kafka/build.gradle", "plugins/dialects/postgresql.yaml", "plugins/dialects/oceanbase.yaml"], knownDatabaseTypes),
["cassandra", "mq", "neo4j", "oceanbase-oracle", "oracle", "postgres", "vastbase"],
);
});
@@ -81,10 +45,7 @@ test("maps agent and dialect paths to existing database types", () => {
test("labels tests-only changes", () => {
const result = evaluatePullRequestLabels({
title: "test(core): cover transaction recovery",
- changedFiles: [
- "crates/dbx-core/tests/transaction_recovery.rs",
- "apps/desktop/src/lib/__tests__/transactionRecovery.spec.ts",
- ],
+ changedFiles: ["crates/dbx-core/tests/transaction_recovery.rs", "apps/desktop/src/lib/__tests__/transactionRecovery.spec.ts"],
knownDatabaseTypes,
});
@@ -94,10 +55,7 @@ test("labels tests-only changes", () => {
test("does not treat component tests or MCP server code as UI or deploy changes", () => {
const result = evaluatePullRequestLabels({
title: "🐛 fix(mcp): cover server routing",
- changedFiles: [
- "apps/desktop/src/components/mcp/McpPanel.spec.ts",
- "packages/mcp-server/src/index.ts",
- ],
+ changedFiles: ["apps/desktop/src/components/mcp/McpPanel.spec.ts", "packages/mcp-server/src/index.ts"],
knownDatabaseTypes,
});
@@ -117,15 +75,7 @@ test("recognizes conventional titles with a full-width colon", () => {
test("recognizes extracted core crates and their database sources", () => {
const result = evaluatePullRequestLabels({
title: "fix(sql): preserve PostgreSQL query contracts",
- changedFiles: [
- "crates/dbx-driver-postgres/src/postgres.rs",
- "crates/dbx-sql-core/src/sql.rs",
- "crates/dbx-types/src/types.rs",
- "crates/dbx-platform/src/process.rs",
- "crates/dbx-ai-provider/src/ai.rs",
- "crates/dbx-formats/src/csv_export.rs",
- "crates/dbx-plugin-runtime/src/plugins.rs",
- ],
+ changedFiles: ["crates/dbx-driver-postgres/src/postgres.rs", "crates/dbx-sql-core/src/sql.rs", "crates/dbx-types/src/types.rs", "crates/dbx-platform/src/process.rs", "crates/dbx-ai-provider/src/ai.rs", "crates/dbx-formats/src/csv_export.rs", "crates/dbx-plugin-runtime/src/plugins.rs"],
knownDatabaseTypes,
});
@@ -168,11 +118,11 @@ test("detects frontend and backend additions in the same pull request", () => {
const manifests = {
base: {
"package.json": JSON.stringify({ dependencies: { vue: "3.5.0" } }),
- "crates/dbx-core/Cargo.toml": "[dependencies]\ntokio = \"1\"\n",
+ "crates/dbx-core/Cargo.toml": '[dependencies]\ntokio = "1"\n',
},
head: {
"package.json": JSON.stringify({ dependencies: { vue: "3.5.0", zod: "4" } }),
- "crates/dbx-core/Cargo.toml": "[dependencies]\ntokio = \"1\"\nserde = \"1\"\n",
+ "crates/dbx-core/Cargo.toml": '[dependencies]\ntokio = "1"\nserde = "1"\n',
},
};
const result = evaluatePullRequestLabels({
@@ -183,13 +133,7 @@ test("detects frontend and backend additions in the same pull request", () => {
readHeadFile: (file) => manifests.head[file],
});
- assert.deepEqual(result.labels, [
- "area/core",
- "area/desktop",
- "dependencies/backend",
- "dependencies/frontend",
- "enhancement",
- ]);
+ assert.deepEqual(result.labels, ["area/core", "area/desktop", "dependencies/backend", "dependencies/frontend", "enhancement"]);
});
test("ignores lockfile-only dependency changes", () => {
@@ -204,7 +148,8 @@ test("ignores lockfile-only dependency changes", () => {
test("parses dependency names from supported backend manifests", () => {
assert.deepEqual(
- [...parseCargoDependencyNames(`
+ [
+ ...parseCargoDependencyNames(`
[workspace.dependencies]
serde = "1"
@@ -213,39 +158,50 @@ windows-sys = "0.61"
[dependencies.mysql_async]
git = "https://example.com/mysql_async"
-`)].sort(),
+`),
+ ].sort(),
["mysql_async", "serde", "windows-sys"],
);
assert.deepEqual(
- [...parseGradleDependencyNames(`
+ [
+ ...parseGradleDependencyNames(`
implementation 'com.zaxxer:HikariCP:7.0.2'
testImplementation project(':test-support')
runtimeOnly libs.slf4j.nop
-`)].sort(),
+`),
+ ].sort(),
["catalog:libs.slf4j.nop", "com.zaxxer:HikariCP", "project::test-support"],
);
assert.deepEqual(
- [...parseGoDependencyNames(`
+ [
+ ...parseGoDependencyNames(`
require github.com/example/direct v1.0.0
require (
github.com/example/block v2.0.0
)
-`)].sort(),
+`),
+ ].sort(),
["github.com/example/block", "github.com/example/direct"],
);
assert.deepEqual(
- [...parseMavenDependencyNames(`
+ [
+ ...parseMavenDependencyNames(`
org.example client 1
-`)],
+`),
+ ],
["org.example:client"],
);
assert.deepEqual(
- [...parsePackageDependencyNames(JSON.stringify({
- dependencies: { vue: "3" },
- devDependencies: { vitest: "4" },
- }))].sort(),
+ [
+ ...parsePackageDependencyNames(
+ JSON.stringify({
+ dependencies: { vue: "3" },
+ devDependencies: { vitest: "4" },
+ }),
+ ),
+ ].sort(),
["vitest", "vue"],
);
});
diff --git a/.github/scripts/reuse-agent-release-assets.mjs b/.github/scripts/reuse-agent-release-assets.mjs
index d5ab3d32f5..722403bfc2 100644
--- a/.github/scripts/reuse-agent-release-assets.mjs
+++ b/.github/scripts/reuse-agent-release-assets.mjs
@@ -1,16 +1,7 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
-import {
- copyFileSync,
- existsSync,
- mkdirSync,
- mkdtempSync,
- readFileSync,
- readdirSync,
- rmSync,
- statSync,
-} from "node:fs";
+import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
import { basename, join } from "node:path";
import { tmpdir } from "node:os";
@@ -23,14 +14,7 @@ const NATIVE_MODULES = new Set(["duckdb", "oracle", "oracle-oci", "xugu", "kingb
const NATIVE_MODULE_PLATFORMS = {
"oracle-oci": ["windows-x64"],
};
-const PLATFORMS = [
- "macos-aarch64",
- "macos-x64",
- "linux-aarch64",
- "linux-x64",
- "windows-aarch64",
- "windows-x64",
-];
+const PLATFORMS = ["macos-aarch64", "macos-x64", "linux-aarch64", "linux-x64", "windows-aarch64", "windows-x64"];
function expectedNativePlatforms(moduleName) {
return [...(NATIVE_MODULE_PLATFORMS[moduleName] ?? PLATFORMS)].sort();
@@ -93,9 +77,7 @@ export function collectReusableAssetPlan({ registry, release, versions, modules,
const missingPlatforms = expectedPlatforms.filter((platform) => !nativePlatforms.includes(platform));
const extraPlatforms = nativePlatforms.filter((platform) => !expectedPlatforms.includes(platform));
if (missingPlatforms.length > 0 || extraPlatforms.length > 0) {
- throw new Error(
- `Previous native artifacts are incomplete for ${moduleName}: missing=${missingPlatforms.join(",") || "none"}, extra=${extraPlatforms.join(",") || "none"}`,
- );
+ throw new Error(`Previous native artifacts are incomplete for ${moduleName}: missing=${missingPlatforms.join(",") || "none"}, extra=${extraPlatforms.join(",") || "none"}`);
}
}
@@ -119,9 +101,7 @@ export function collectReusableAssetPlan({ registry, release, versions, modules,
const missingPlatforms = PLATFORMS.filter((platform) => !platforms.includes(platform));
const extraPlatforms = platforms.filter((platform) => !PLATFORMS.includes(platform));
if (missingPlatforms.length > 0 || extraPlatforms.length > 0) {
- throw new Error(
- `Previous JRE ${jreKey} artifacts are incomplete: missing=${missingPlatforms.join(",") || "none"}, extra=${extraPlatforms.join(",") || "none"}`,
- );
+ throw new Error(`Previous JRE ${jreKey} artifacts are incomplete: missing=${missingPlatforms.join(",") || "none"}, extra=${extraPlatforms.join(",") || "none"}`);
}
for (const platform of platforms) {
jreAssets.push({
@@ -210,13 +190,17 @@ export function extractReusableDriverPackages({ packagesDir, outputDir, versions
const javaName = `dbx-agent-${moduleName}-${releaseVersion}.tar.zst`;
if (filenames.has(javaName)) {
- extractRawDriver(join(packagesDir, javaName), {
- name: javaName,
- moduleName,
- kind: "jar",
- platform: "",
- releaseVersion,
- }, outputDir);
+ extractRawDriver(
+ join(packagesDir, javaName),
+ {
+ name: javaName,
+ moduleName,
+ kind: "jar",
+ platform: "",
+ releaseVersion,
+ },
+ outputDir,
+ );
extracted += 1;
}
@@ -225,13 +209,17 @@ export function extractReusableDriverPackages({ packagesDir, outputDir, versions
const nativeName = `dbx-agent-${moduleName}-${releaseVersion}-${platform}.tar.zst`;
if (!filenames.has(nativeName)) continue;
nativePlatforms.push(platform);
- extractRawDriver(join(packagesDir, nativeName), {
- name: nativeName,
- moduleName,
- kind: "native",
- platform,
- releaseVersion,
- }, outputDir);
+ extractRawDriver(
+ join(packagesDir, nativeName),
+ {
+ name: nativeName,
+ moduleName,
+ kind: "native",
+ platform,
+ releaseVersion,
+ },
+ outputDir,
+ );
extracted += 1;
}
@@ -253,23 +241,7 @@ function gh(args, options = {}) {
function downloadReleaseAssets(assets, downloadDir) {
if (assets.length === 0) return;
- const args = [
- "--fail",
- "--location",
- "--silent",
- "--show-error",
- "--retry",
- "5",
- "--retry-all-errors",
- "--retry-delay",
- "2",
- "--connect-timeout",
- "30",
- "--parallel",
- "--parallel-immediate",
- "--parallel-max",
- "6",
- ];
+ const args = ["--fail", "--location", "--silent", "--show-error", "--retry", "5", "--retry-all-errors", "--retry-delay", "2", "--connect-timeout", "30", "--parallel", "--parallel-immediate", "--parallel-max", "6"];
for (const asset of assets) {
if (!asset.browser_download_url) {
throw new Error(`GitHub release asset is missing browser_download_url: ${asset.name}`);
@@ -359,7 +331,10 @@ function main() {
}));
if (plannedAssets.length > 0) {
- downloadReleaseAssets(plannedAssets.map((asset) => asset.releaseAsset), downloadDir);
+ downloadReleaseAssets(
+ plannedAssets.map((asset) => asset.releaseAsset),
+ downloadDir,
+ );
}
for (const asset of plannedAssets) {
diff --git a/.github/scripts/reuse-agent-release-assets.test.mjs b/.github/scripts/reuse-agent-release-assets.test.mjs
index 9a2cf099b3..9d548b24e5 100644
--- a/.github/scripts/reuse-agent-release-assets.test.mjs
+++ b/.github/scripts/reuse-agent-release-assets.test.mjs
@@ -3,23 +3,12 @@ import test from "node:test";
import { collectReusableAssetPlan } from "./reuse-agent-release-assets.mjs";
-const platforms = [
- "macos-aarch64",
- "macos-x64",
- "linux-aarch64",
- "linux-x64",
- "windows-aarch64",
- "windows-x64",
-];
+const platforms = ["macos-aarch64", "macos-x64", "linux-aarch64", "linux-x64", "windows-aarch64", "windows-x64"];
test("collects complete reusable Java, native, and JRE assets", () => {
const access = artifact("dbx-agent-access-0.1.34.tar.zst", "a");
- const kingbase = Object.fromEntries(
- platforms.map((platform, index) => [platform, artifact(`dbx-agent-kingbase-0.1.40-${platform}.tar.zst`, String(index + 1))]),
- );
- const jre = Object.fromEntries(
- platforms.map((platform, index) => [platform, artifact(`dbx-jre-21-${platform}.tar.zst`, String(index + 7))]),
- );
+ const kingbase = Object.fromEntries(platforms.map((platform, index) => [platform, artifact(`dbx-agent-kingbase-0.1.40-${platform}.tar.zst`, String(index + 1))]));
+ const jre = Object.fromEntries(platforms.map((platform, index) => [platform, artifact(`dbx-jre-21-${platform}.tar.zst`, String(index + 7))]));
const registry = {
drivers: {
access: { version: "0.1.34", jar: access },
@@ -39,95 +28,93 @@ test("collects complete reusable Java, native, and JRE assets", () => {
assert.equal(plan.driverAssets.length, 7);
assert.equal(plan.jreAssets.length, 6);
- assert.deepEqual(plan.driverAssets.map((asset) => asset.moduleName), ["access", ...Array(6).fill("kingbase")]);
+ assert.deepEqual(
+ plan.driverAssets.map((asset) => asset.moduleName),
+ ["access", ...Array(6).fill("kingbase")],
+ );
});
test("rejects an incomplete reusable native platform set", () => {
- const native = Object.fromEntries(
- platforms.slice(1).map((platform, index) => [platform, artifact(`dbx-agent-vastbase-0.1.38-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.slice(1).map((platform, index) => [platform, artifact(`dbx-agent-vastbase-0.1.38-${platform}.tar.zst`, String(index + 1))]));
const registry = { drivers: { vastbase: { version: "0.1.38", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { vastbase: "0.1.38" },
- modules: ["vastbase"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { vastbase: "0.1.38" },
+ modules: ["vastbase"],
+ reuseJre: false,
+ }),
/missing=macos-aarch64/,
);
});
test("requires all TDengine native platforms when reusing a release", () => {
- const native = Object.fromEntries(
- platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-tdengine-0.1.40-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-tdengine-0.1.40-${platform}.tar.zst`, String(index + 1))]));
const registry = { drivers: { tdengine: { version: "0.1.40", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { tdengine: "0.1.40" },
- modules: ["tdengine"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { tdengine: "0.1.40" },
+ modules: ["tdengine"],
+ reuseJre: false,
+ }),
/missing=windows-x64/,
);
});
test("requires all Neo4j native platforms when reusing a release", () => {
- const native = Object.fromEntries(
- platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-neo4j-0.1.40-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-neo4j-0.1.40-${platform}.tar.zst`, String(index + 1))]));
const registry = { drivers: { neo4j: { version: "0.1.40", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { neo4j: "0.1.40" },
- modules: ["neo4j"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { neo4j: "0.1.40" },
+ modules: ["neo4j"],
+ reuseJre: false,
+ }),
/missing=windows-x64/,
);
});
test("requires all IoTDB native platforms when reusing a release", () => {
- const native = Object.fromEntries(
- platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-iotdb-0.1.30-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-iotdb-0.1.30-${platform}.tar.zst`, String(index + 1))]));
const registry = { drivers: { iotdb: { version: "0.1.30", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { iotdb: "0.1.30" },
- modules: ["iotdb"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { iotdb: "0.1.30" },
+ modules: ["iotdb"],
+ reuseJre: false,
+ }),
/missing=windows-x64/,
);
});
test("requires all RocketMQ native platforms when reusing a release", () => {
- const native = Object.fromEntries(
- platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-rocketmq-0.1.40-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-rocketmq-0.1.40-${platform}.tar.zst`, String(index + 1))]));
const registry = { drivers: { rocketmq: { version: "0.1.40", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { rocketmq: "0.1.40" },
- modules: ["rocketmq"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { rocketmq: "0.1.40" },
+ modules: ["rocketmq"],
+ reuseJre: false,
+ }),
/missing=windows-x64/,
);
});
@@ -146,7 +133,10 @@ test("reuses the Windows-only Oracle OCI native package", () => {
// Windows-only is the complete platform set for this module, so requiring all
// six platforms would reject a correct release.
- assert.deepEqual(plan.driverAssets.map((asset) => asset.platform), ["windows-x64"]);
+ assert.deepEqual(
+ plan.driverAssets.map((asset) => asset.platform),
+ ["windows-x64"],
+ );
});
test("rejects an Oracle OCI release that lost its Windows native package", () => {
@@ -154,21 +144,20 @@ test("rejects an Oracle OCI release that lost its Windows native package", () =>
const registry = { drivers: { "oracle-oci": { version: "0.1.0", native } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor(Object.values(native)),
- versions: { "oracle-oci": "0.1.0" },
- modules: ["oracle-oci"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor(Object.values(native)),
+ versions: { "oracle-oci": "0.1.0" },
+ modules: ["oracle-oci"],
+ reuseJre: false,
+ }),
/missing=windows-x64, extra=linux-x64/,
);
});
test("ignores zero-size legacy JAR placeholders for native-only modules", () => {
- const native = Object.fromEntries(
- platforms.map((platform, index) => [platform, artifact(`dbx-agent-duckdb-0.1.2-${platform}.tar.zst`, String(index + 1))]),
- );
+ const native = Object.fromEntries(platforms.map((platform, index) => [platform, artifact(`dbx-agent-duckdb-0.1.2-${platform}.tar.zst`, String(index + 1))]));
const registry = {
drivers: {
duckdb: {
@@ -193,7 +182,10 @@ test("ignores zero-size legacy JAR placeholders for native-only modules", () =>
});
assert.equal(plan.driverAssets.length, 6);
- assert.equal(plan.driverAssets.some((asset) => asset.kind === "jar"), false);
+ assert.equal(
+ plan.driverAssets.some((asset) => asset.kind === "jar"),
+ false,
+ );
});
test("rejects a registry version that differs from the effective baseline", () => {
@@ -201,13 +193,14 @@ test("rejects a registry version that differs from the effective baseline", () =
const registry = { drivers: { access: { version: "0.1.33", jar: access } }, jres: {} };
assert.throws(
- () => collectReusableAssetPlan({
- registry,
- release: releaseFor([access]),
- versions: { access: "0.1.34" },
- modules: ["access"],
- reuseJre: false,
- }),
+ () =>
+ collectReusableAssetPlan({
+ registry,
+ release: releaseFor([access]),
+ versions: { access: "0.1.34" },
+ modules: ["access"],
+ reuseJre: false,
+ }),
/registry=0\.1\.33, expected=0\.1\.34/,
);
});
diff --git a/.github/scripts/sign-mcp-macos.mjs b/.github/scripts/sign-mcp-macos.mjs
index 5e595e349c..00641f3fec 100644
--- a/.github/scripts/sign-mcp-macos.mjs
+++ b/.github/scripts/sign-mcp-macos.mjs
@@ -22,12 +22,18 @@ async function command(executable, args, signal) {
return new Promise((resolveCommand, reject) => {
const child = spawn(executable, args, { stdio: ["ignore", "pipe", "pipe"], signal });
let output = "";
- child.stdout.on("data", (data) => { output += data; });
- child.stderr.on("data", (data) => { output += data; });
+ child.stdout.on("data", (data) => {
+ output += data;
+ });
+ child.stderr.on("data", (data) => {
+ output += data;
+ });
const timer = setTimeout(() => child.kill("SIGKILL"), 120000);
const fail = () => reject(new Error(`${executable.split("/").at(-1)} ${args[0]} failed.`));
let failed = false;
- child.on("error", () => { failed = true; });
+ child.on("error", () => {
+ failed = true;
+ });
child.on("close", (code) => {
clearTimeout(timer);
if (code === 0 && !failed) resolveCommand(output);
@@ -43,10 +49,7 @@ export async function verifyMcp(binary, options = {}) {
if (!statSync(binary).isFile()) throw new Error("MCP binary must be a regular file.");
await command(ctx.codesign, ["--verify", "--strict", "--all-architectures", "-R", `=${ctx.requirement}`, binary], ctx.signal);
const info = await command(ctx.codesign, ["--display", "--verbose=4", "--requirements", "-", binary], ctx.signal);
- if (!info.split(/\r?\n/).includes(`Identifier=${mcpIdentifier}`)
- || !info.split(/\r?\n/).includes(`TeamIdentifier=${ctx.env.APPLE_TEAM_ID}`)
- || !/^Timestamp=(?!none$).+/m.test(info)
- || /Signature=adhoc/.test(info)) {
+ if (!info.split(/\r?\n/).includes(`Identifier=${mcpIdentifier}`) || !info.split(/\r?\n/).includes(`TeamIdentifier=${ctx.env.APPLE_TEAM_ID}`) || !/^Timestamp=(?!none$).+/m.test(info) || /Signature=adhoc/.test(info)) {
throw new Error("MCP signature must have the expected identifier, team and secure timestamp.");
}
const designated = info.match(/^designated => (.+)$/m)?.[1];
@@ -66,7 +69,10 @@ export async function signMcp(binary, options = {}) {
throw new Error("APPLE_CERTIFICATE must be a base64 PKCS12 bundle.");
}
const originalList = await command(ctx.security, ["list-keychains", "-d", "user"], ctx.signal);
- const originalKeychains = originalList.split(/\r?\n/).filter((line) => line.trim()).map((line) => JSON.parse(line.trim()));
+ const originalKeychains = originalList
+ .split(/\r?\n/)
+ .filter((line) => line.trim())
+ .map((line) => JSON.parse(line.trim()));
if (!originalKeychains.every((keychain) => typeof keychain === "string")) throw new Error("Invalid keychain search list.");
const directory = mkdtempSync(join(ctx.env.RUNNER_TEMP || ctx.env.TMPDIR || tmpdir(), "dbx-mcp-signing-"));
chmodSync(directory, 0o700);
@@ -84,22 +90,33 @@ export async function signMcp(binary, options = {}) {
await command(ctx.security, ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychain], ctx.signal);
await command(ctx.security, ["list-keychains", "-d", "user", "-s", keychain, ...originalKeychains], ctx.signal);
const identities = await command(ctx.security, ["find-identity", "-v", "-p", "codesigning", keychain], ctx.signal);
- const candidates = [...identities.matchAll(/\b([A-Fa-f0-9]{40}) "(Developer ID Application: [^"\r\n]+)"/g)]
- .filter(([, hash, name]) => name.endsWith(` (${ctx.env.APPLE_TEAM_ID})`)
- && (name === ctx.env.APPLE_SIGNING_IDENTITY || hash.toUpperCase() === ctx.env.APPLE_SIGNING_IDENTITY.toUpperCase()));
+ const candidates = [...identities.matchAll(/\b([A-Fa-f0-9]{40}) "(Developer ID Application: [^"\r\n]+)"/g)].filter(
+ ([, hash, name]) => name.endsWith(` (${ctx.env.APPLE_TEAM_ID})`) && (name === ctx.env.APPLE_SIGNING_IDENTITY || hash.toUpperCase() === ctx.env.APPLE_SIGNING_IDENTITY.toUpperCase()),
+ );
if (candidates.length !== 1) throw new Error("Expected exactly one valid configured Developer ID Application identity in the temporary keychain.");
- await command(ctx.codesign, ["--force", "--sign", candidates[0][1], "--keychain", keychain, "--identifier", mcpIdentifier,
- "--requirements", `=designated => ${ctx.requirement}`, "--timestamp", binary], ctx.signal);
+ await command(ctx.codesign, ["--force", "--sign", candidates[0][1], "--keychain", keychain, "--identifier", mcpIdentifier, "--requirements", `=designated => ${ctx.requirement}`, "--timestamp", binary], ctx.signal);
await verifyMcp(binary, options);
} catch (failure) {
error = failure;
} finally {
const failures = [];
- try { await command(ctx.security, ["list-keychains", "-d", "user", "-s", ...originalKeychains]); } catch (failure) { failures.push(failure); }
+ try {
+ await command(ctx.security, ["list-keychains", "-d", "user", "-s", ...originalKeychains]);
+ } catch (failure) {
+ failures.push(failure);
+ }
if (existsSync(keychain)) {
- try { await command(ctx.security, ["delete-keychain", keychain]); } catch (failure) { failures.push(failure); }
+ try {
+ await command(ctx.security, ["delete-keychain", keychain]);
+ } catch (failure) {
+ failures.push(failure);
+ }
+ }
+ try {
+ rmSync(directory, { recursive: true, force: true });
+ } catch (failure) {
+ failures.push(failure);
}
- try { rmSync(directory, { recursive: true, force: true }); } catch (failure) { failures.push(failure); }
if (failures.length) error = new Error("MCP signing keychain cleanup failed; refusing publication.", { cause: error });
}
if (error) throw error;
diff --git a/.github/scripts/suggest-similar-issues.mjs b/.github/scripts/suggest-similar-issues.mjs
index 1415ddc8b9..8b79371df0 100644
--- a/.github/scripts/suggest-similar-issues.mjs
+++ b/.github/scripts/suggest-similar-issues.mjs
@@ -10,81 +10,12 @@ const MAX_QUERY_LENGTH = 480;
const MAX_CANDIDATES = 3;
const SEARCH_RESULT_LIMIT = 20;
-const ignoredSectionPatterns = [
- /^来源$/i,
- /^source$/i,
- /数据库类型/i,
- /database type/i,
- /支持信息/i,
- /support info/i,
- /优先级/i,
- /priority/i,
- /补充信息/i,
- /additional context/i,
- /环境信息/i,
- /environment/i,
-];
+const ignoredSectionPatterns = [/^来源$/i, /^source$/i, /数据库类型/i, /database type/i, /支持信息/i, /support info/i, /优先级/i, /priority/i, /补充信息/i, /additional context/i, /环境信息/i, /environment/i];
-const genericLatinTokens = new Set([
- "ai",
- "alter",
- "bug",
- "datagrip",
- "dbeaver",
- "dbx",
- "delete",
- "desktop",
- "feature",
- "from",
- "insert",
- "issue",
- "navicat",
- "question",
- "request",
- "select",
- "sql",
- "support",
- "table",
- "update",
- "version",
- "web",
- "where",
- "windows",
-]);
+const genericLatinTokens = new Set(["ai", "alter", "bug", "datagrip", "dbeaver", "dbx", "delete", "desktop", "feature", "from", "insert", "issue", "navicat", "question", "request", "select", "sql", "support", "table", "update", "version", "web", "where", "windows"]);
-const genericCjkTokens = new Set([
- "一个",
- "以及",
- "使用",
- "功能",
- "可以",
- "当前",
- "支持",
- "数据库",
- "新增",
- "增加",
- "希望",
- "异常",
- "操作",
- "所在",
- "数据",
- "显示",
- "没有",
- "现在",
- "设置",
- "问题",
- "进行",
- "错误",
- "需要",
-]);
-const identifierQueryNoiseTokens = new Set([
- "失效",
- "弹出",
- "快捷",
- "提醒",
- "时候",
- "选择",
-]);
+const genericCjkTokens = new Set(["一个", "以及", "使用", "功能", "可以", "当前", "支持", "数据库", "新增", "增加", "希望", "异常", "操作", "所在", "数据", "显示", "没有", "现在", "设置", "问题", "进行", "错误", "需要"]);
+const identifierQueryNoiseTokens = new Set(["失效", "弹出", "快捷", "提醒", "时候", "选择"]);
const cjkSegmenter = new Intl.Segmenter("zh-CN", { granularity: "word" });
const databaseDrivers = databaseIssueDrivers;
@@ -107,7 +38,9 @@ function labelNames(labels) {
}
function stripIssuePrefix(title) {
- return String(title || "").replace(/^\s*\[[^\]]+\]\s*/u, "").trim();
+ return String(title || "")
+ .replace(/^\s*\[[^\]]+\]\s*/u, "")
+ .trim();
}
function stripMarkdown(value) {
@@ -143,9 +76,7 @@ function databaseField(body) {
}
function relevantBody(body) {
- const sections = parseIssueSections(body).filter(
- ({ heading }) => !ignoredSectionPatterns.some((pattern) => pattern.test(heading)),
- );
+ const sections = parseIssueSections(body).filter(({ heading }) => !ignoredSectionPatterns.some((pattern) => pattern.test(heading)));
return stripMarkdown(sections.map(({ content }) => content).join("\n")).slice(0, 2400);
}
@@ -197,9 +128,7 @@ function semanticTokens(value) {
export function searchTitleTerms(issue) {
const identifiers = technicalIdentifiers(issue);
- const tokens = [...semanticTokens(stripIssuePrefix(issue.title))].filter(
- (token) => identifiers.size === 0 || !identifierQueryNoiseTokens.has(token),
- );
+ const tokens = [...semanticTokens(stripIssuePrefix(issue.title))].filter((token) => identifiers.size === 0 || !identifierQueryNoiseTokens.has(token));
return tokens.slice(0, 8).join(" ").slice(0, MAX_QUERY_LENGTH);
}
@@ -207,11 +136,7 @@ function latinTokens(value) {
return new Set(
normalizeText(value)
.match(/[a-z0-9][a-z0-9+._-]{1,}/gu)
- ?.filter((token) => (
- !genericLatinTokens.has(token)
- && !/^v?\d+(?:[._-]\d+)*$/u.test(token)
- && !/^(.)\1{2,}$/u.test(token)
- )) || [],
+ ?.filter((token) => !genericLatinTokens.has(token) && !/^v?\d+(?:[._-]\d+)*$/u.test(token) && !/^(.)\1{2,}$/u.test(token)) || [],
);
}
@@ -334,26 +259,15 @@ export function buildCorpusContext(issue, candidates) {
const frequencies = new Map();
for (const token of queryTitleTokens) {
- const documentFrequency = candidateTitleTokens.reduce(
- (count, tokens) => count + (tokens.has(token) ? 1 : 0),
- 0,
- );
+ const documentFrequency = candidateTitleTokens.reduce((count, tokens) => count + (tokens.has(token) ? 1 : 0), 0);
frequencies.set(token, documentFrequency);
const queryFrequencyBoost = 1 + Math.log(Math.max(1, tokenOccurrenceCount(issueTitle, token)));
weights.set(token, inverseDocumentFrequency(candidates.length, documentFrequency) * queryFrequencyBoost);
}
- const observedWeights = [...queryTitleTokens]
- .filter((token) => (frequencies.get(token) || 0) > 0)
- .map((token) => weights.get(token) || 0);
+ const observedWeights = [...queryTitleTokens].filter((token) => (frequencies.get(token) || 0) > 0).map((token) => weights.get(token) || 0);
const maximumWeight = Math.max(0, ...observedWeights);
- const anchorTokens = new Set(
- [...queryTitleTokens].filter((token) => (
- maximumWeight > 0
- && (frequencies.get(token) || 0) > 0
- && (weights.get(token) || 0) >= maximumWeight * 0.9
- )),
- );
+ const anchorTokens = new Set([...queryTitleTokens].filter((token) => maximumWeight > 0 && (frequencies.get(token) || 0) > 0 && (weights.get(token) || 0) >= maximumWeight * 0.9));
return { queryTitleTokens, candidateTitleTokens, weights, anchorTokens };
}
@@ -369,29 +283,18 @@ export function scoreCandidate(issue, candidate, rank = 0, corpusContext) {
const candidateTitle = stripIssuePrefix(candidate.title);
const issueBody = relevantBody(issue.body).slice(0, 1200);
const candidateBody = relevantBody(candidate.body).slice(0, 1200);
- const titleSimilarity = diceCoefficient(
- characterNgrams(contentTitleText(issue)),
- characterNgrams(contentTitleText(candidate)),
- );
+ const titleSimilarity = diceCoefficient(characterNgrams(contentTitleText(issue)), characterNgrams(contentTitleText(candidate)));
const bodySimilarity = diceCoefficient(characterNgrams(issueBody), characterNgrams(candidateBody));
const context = corpusContext || buildCorpusContext(issue, [candidate]);
- const candidateTitleTokens = corpusContext
- ? context.candidateTitleTokens[rank]
- : context.candidateTitleTokens[0];
+ const candidateTitleTokens = corpusContext ? context.candidateTitleTokens[rank] : context.candidateTitleTokens[0];
const titleCoverage = weightedCoverage(context.queryTitleTokens, candidateTitleTokens, context.weights);
const anchorHit = setsOverlap(candidateTitleTokens, context.anchorTokens);
const issueIdentifiers = technicalIdentifiers(issue);
const candidateIdentifiers = technicalIdentifiers(candidate);
- const identifierCoverage = issueIdentifiers.size === 0
- ? 0
- : intersectionSize(issueIdentifiers, candidateIdentifiers) / issueIdentifiers.size;
+ const identifierCoverage = issueIdentifiers.size === 0 ? 0 : intersectionSize(issueIdentifiers, candidateIdentifiers) / issueIdentifiers.size;
const rankPrior = 1 / Math.log2(rank + 2);
- let score = titleCoverage * 0.45
- + titleSimilarity * 0.25
- + bodySimilarity * 0.1
- + identifierCoverage * 0.15
- + rankPrior * 0.05;
+ let score = titleCoverage * 0.45 + titleSimilarity * 0.25 + bodySimilarity * 0.1 + identifierCoverage * 0.15 + rankPrior * 0.05;
const issueTypes = typeLabels(issue.labels);
const candidateTypes = typeLabels(candidate.labels);
if (issueTypes.size > 0 && candidateTypes.size > 0) {
@@ -401,10 +304,7 @@ export function scoreCandidate(issue, candidate, rank = 0, corpusContext) {
// Like Discourse, retrieval is deliberately broad and public suggestions
// require a separate threshold. Rare title terms act as BM25F-style anchors.
- const accepted = score >= 0.34
- || (anchorHit && titleCoverage >= 0.38 && score >= 0.2)
- || (identifierCoverage >= 0.5 && titleCoverage >= 0.2 && score >= 0.28)
- || (titleSimilarity + bodySimilarity >= 0.42 && score >= 0.28);
+ const accepted = score >= 0.34 || (anchorHit && titleCoverage >= 0.38 && score >= 0.2) || (identifierCoverage >= 0.5 && titleCoverage >= 0.2 && score >= 0.28) || (titleSimilarity + bodySimilarity >= 0.42 && score >= 0.28);
return {
accepted,
@@ -421,9 +321,7 @@ export function scoreCandidate(issue, candidate, rank = 0, corpusContext) {
}
export function rankCandidates(issue, items) {
- const candidates = items.filter(
- (candidate) => !candidate.pull_request && Number(candidate.number) !== Number(issue.number),
- );
+ const candidates = items.filter((candidate) => !candidate.pull_request && Number(candidate.number) !== Number(issue.number));
const context = buildCorpusContext(issue, candidates);
return candidates
.map((candidate, rank) => ({ candidate, ...scoreCandidate(issue, candidate, rank, context) }))
@@ -510,11 +408,13 @@ export async function run({ issue = loadIssue(), client } = {}) {
return [];
}
- const github = client || new GitHubClient({
- token: process.env.GITHUB_TOKEN,
- repository: process.env.GITHUB_REPOSITORY,
- apiBase: process.env.GITHUB_API_URL,
- });
+ const github =
+ client ||
+ new GitHubClient({
+ token: process.env.GITHUB_TOKEN,
+ repository: process.env.GITHUB_REPOSITORY,
+ apiBase: process.env.GITHUB_API_URL,
+ });
if (await github.hasExistingComment(issue.number)) {
console.log("Similar issue comment already exists");
return [];
diff --git a/.github/scripts/suggest-similar-issues.test.mjs b/.github/scripts/suggest-similar-issues.test.mjs
index 8d7e6e0697..8481d77232 100644
--- a/.github/scripts/suggest-similar-issues.test.mjs
+++ b/.github/scripts/suggest-similar-issues.test.mjs
@@ -2,15 +2,7 @@ import assert from "node:assert/strict";
import { createServer } from "node:http";
import test from "node:test";
-import {
- GitHubClient,
- formatComment,
- rankCandidates,
- run,
- scoreCandidate,
- searchTerms,
- searchTitleTerms,
-} from "./suggest-similar-issues.mjs";
+import { GitHubClient, formatComment, rankCandidates, run, scoreCandidate, searchTerms, searchTitleTerms } from "./suggest-similar-issues.mjs";
const baseIssue = {
number: 4043,
@@ -213,7 +205,10 @@ test("uses corpus rarity to reject generic real-world matches", () => {
];
const ranked = rankCandidates(issue, candidates);
- assert.deepEqual(ranked.map(({ candidate }) => candidate.number), [2201]);
+ assert.deepEqual(
+ ranked.map(({ candidate }) => candidate.number),
+ [2201],
+ );
assert.equal(ranked[0].signals.anchorHit, true);
});
@@ -247,9 +242,7 @@ test("ranks candidates, excludes the current issue and limits output", () => {
});
test("formats a cautious localized comment with concise issue references", () => {
- const comment = formatComment(baseIssue, [
- { candidate: { number: 82, title: "[Bug] [快捷键](https://example.com)", state: "closed" } },
- ]);
+ const comment = formatComment(baseIssue, [{ candidate: { number: 82, title: "[Bug] [快捷键](https://example.com)", state: "closed" } }]);
assert.match(comment, /^/u);
assert.match(comment, /\n- #82\n/u);
@@ -272,16 +265,20 @@ test("run searches and posts one idempotent comment", async (t) => {
return;
}
if (request.method === "GET" && request.url.startsWith("/search/issues?")) {
- response.end(JSON.stringify({
- search_type: "hybrid",
- items: [{
- number: 82,
- title: "Ctrl+Enter 执行当前 SQL 语句",
- body: "Ctrl+Enter 直接执行光标所在语句,不显示选择框。",
- state: "open",
- labels: [{ name: "bug" }, { name: "db/mysql" }],
- }],
- }));
+ response.end(
+ JSON.stringify({
+ search_type: "hybrid",
+ items: [
+ {
+ number: 82,
+ title: "Ctrl+Enter 执行当前 SQL 语句",
+ body: "Ctrl+Enter 直接执行光标所在语句,不显示选择框。",
+ state: "open",
+ labels: [{ name: "bug" }, { name: "db/mysql" }],
+ },
+ ],
+ }),
+ );
return;
}
if (request.method === "POST" && request.url.endsWith("/comments")) {
diff --git a/.github/scripts/sync-changelog.mjs b/.github/scripts/sync-changelog.mjs
index b3ec839f59..afc535dc45 100644
--- a/.github/scripts/sync-changelog.mjs
+++ b/.github/scripts/sync-changelog.mjs
@@ -59,13 +59,15 @@ export function stripDownloadSection(body) {
// 剥离 release notes 里的 issue 引用 (closes #xxx),保留贡献者署名 (contributed by @xxx)。
// 同时处理纯 closes 括号和混在 contributed by 括号里的 closes 片段。
function stripIssueRefs(text) {
- return text
- // 纯 closes 括号整体去掉,如 (closes #123) 或 (closes #123, closes #456)
- .replace(/\s*\(closes #\d+(?:,\s*closes #\d+)*\)/g, "")
- // closes 在前、contributed by 在后,如 (closes #88, contributed by @xxx) → (contributed by @xxx)
- .replace(/\(closes #\d+,\s*/g, "(")
- // contributed by 在前、closes 在后,如 (contributed by @xxx, closes #123) → (contributed by @xxx)
- .replace(/,\s*closes #\d+/g, "");
+ return (
+ text
+ // 纯 closes 括号整体去掉,如 (closes #123) 或 (closes #123, closes #456)
+ .replace(/\s*\(closes #\d+(?:,\s*closes #\d+)*\)/g, "")
+ // closes 在前、contributed by 在后,如 (closes #88, contributed by @xxx) → (contributed by @xxx)
+ .replace(/\(closes #\d+,\s*/g, "(")
+ // contributed by 在前、closes 在后,如 (contributed by @xxx, closes #123) → (contributed by @xxx)
+ .replace(/,\s*closes #\d+/g, "")
+ );
}
// 对整份 releases JSON 统一剥离 closes # 引用。cache 复用的英文翻译来自 R2,
@@ -187,9 +189,7 @@ function buildLatestEnNotes(enReleasesJson) {
}
export function buildLatestReleaseNotes(releases) {
- const latest = releases
- .filter(isAppRelease)
- .sort((a, b) => new Date(b.published_at) - new Date(a.published_at))[0];
+ const latest = releases.filter(isAppRelease).sort((a, b) => new Date(b.published_at) - new Date(a.published_at))[0];
if (!latest) return null;
return { version: latest.tag_name, notes: latest.body || "" };
}
diff --git a/.github/scripts/sync-cnb-release.mjs b/.github/scripts/sync-cnb-release.mjs
index 6b94f2933f..e877d7de2d 100644
--- a/.github/scripts/sync-cnb-release.mjs
+++ b/.github/scripts/sync-cnb-release.mjs
@@ -40,9 +40,7 @@ async function main() {
});
console.log(`Uploading ${assets.length} CNB asset(s) with concurrency ${args.concurrency}.`);
- await mapWithConcurrency(assets, args.concurrency, (assetPath) =>
- uploadWithRetry(client, release.id, assetPath, args.overwriteExisting),
- );
+ await mapWithConcurrency(assets, args.concurrency, (assetPath) => uploadWithRetry(client, release.id, assetPath, args.overwriteExisting));
}
function parseArgs(argv) {
@@ -70,9 +68,7 @@ function parseArgs(argv) {
}
if (!args.token) throw new Error("CNB_TOKEN is required.");
if (!args.githubReleasePath || (!args.metadataOnly && !args.assetsDir)) {
- throw new Error(
- "Usage: sync-cnb-release.mjs --github-release (--assets-dir | --metadata-only) [--prune-assets] [--make-latest]",
- );
+ throw new Error("Usage: sync-cnb-release.mjs --github-release (--assets-dir | --metadata-only) [--prune-assets] [--make-latest]");
}
if (!Number.isInteger(args.concurrency) || args.concurrency < 1) {
throw new Error("CNB_UPLOAD_CONCURRENCY must be a positive integer.");
@@ -162,19 +158,13 @@ export class CnbClient {
}
async deleteAsset(releaseId, assetId) {
- await this.request(
- "DELETE",
- `/${this.repository}/-/releases/${encodeURIComponent(releaseId)}/assets/${encodeURIComponent(assetId)}`,
- );
+ await this.request("DELETE", `/${this.repository}/-/releases/${encodeURIComponent(releaseId)}/assets/${encodeURIComponent(assetId)}`);
}
async listReleases(pageSize = 100) {
const releases = [];
for (let page = 1; ; page++) {
- const batch = await this.request(
- "GET",
- `/${this.repository}/-/releases?page=${page}&page_size=${pageSize}`,
- );
+ const batch = await this.request("GET", `/${this.repository}/-/releases?page=${page}&page_size=${pageSize}`);
if (!Array.isArray(batch)) throw new Error("CNB release list response must be an array.");
releases.push(...batch);
if (batch.length < pageSize) return releases;
diff --git a/.github/scripts/sync-cnb-release.test.mjs b/.github/scripts/sync-cnb-release.test.mjs
index 80d94a7757..e26071c733 100644
--- a/.github/scripts/sync-cnb-release.test.mjs
+++ b/.github/scripts/sync-cnb-release.test.mjs
@@ -103,11 +103,7 @@ test("ensureRelease explicitly marks a promoted release as latest", async (t) =>
token: "test-token",
});
- await client.ensureRelease(
- "v1.2.3",
- { name: "DBX v1.2.3", body: "Stable", isPrerelease: false },
- { makeLatest: true },
- );
+ await client.ensureRelease("v1.2.3", { name: "DBX v1.2.3", body: "Stable", isPrerelease: false }, { makeLatest: true });
assert.deepEqual(requests, [
{ method: "GET", url: "/dbxio.com/dbx/-/releases/tags/v1.2.3", body: null },
@@ -157,26 +153,19 @@ test("metadata-only sync can mark the release as latest without reading an asset
const tempDir = await mkdtemp(join(tmpdir(), "sync-cnb-release-"));
t.after(() => rm(tempDir, { recursive: true, force: true }));
const releasePath = join(tempDir, "release.json");
- await writeFile(
- releasePath,
- JSON.stringify({ tagName: "v1.2.3", name: "DBX v1.2.3", body: "Stable", isPrerelease: false }),
- );
+ await writeFile(releasePath, JSON.stringify({ tagName: "v1.2.3", name: "DBX v1.2.3", body: "Stable", isPrerelease: false }));
const address = server.address();
assert.ok(address && typeof address !== "string");
const result = await new Promise((resolve) => {
- const child = spawn(
- process.execPath,
- [scriptPath, "--github-release", releasePath, "--metadata-only", "--make-latest"],
- {
- env: {
- ...process.env,
- CNB_API_BASE: `http://127.0.0.1:${address.port}`,
- CNB_REPOSITORY: "dbxio.com/dbx",
- CNB_TOKEN: "test-token",
- },
+ const child = spawn(process.execPath, [scriptPath, "--github-release", releasePath, "--metadata-only", "--make-latest"], {
+ env: {
+ ...process.env,
+ CNB_API_BASE: `http://127.0.0.1:${address.port}`,
+ CNB_REPOSITORY: "dbxio.com/dbx",
+ CNB_TOKEN: "test-token",
},
- );
+ });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => (stdout += chunk));
diff --git a/.github/ui-preview/publish.mjs b/.github/ui-preview/publish.mjs
index a278cd4517..f7f49467bb 100755
--- a/.github/ui-preview/publish.mjs
+++ b/.github/ui-preview/publish.mjs
@@ -55,7 +55,8 @@ async function files() {
// the description with the block in place of the old one, or at its foot
function withBlock(body, block) {
body = body || "";
- const a = body.indexOf(BEGIN), b = body.indexOf(END);
+ const a = body.indexOf(BEGIN),
+ b = body.indexOf(END);
if (a >= 0 && b > a) return body.slice(0, a) + block + body.slice(b + END.length);
return body.replace(/\s*$/, "") + "\n\n" + block;
}
@@ -102,7 +103,9 @@ async function pushMedia(sha, dir) {
git(["fetch", "-q", "--depth=1", remote, BRANCH], work);
base = git(["rev-parse", "FETCH_HEAD"], work);
git(["checkout", "-q", "-f", "--detach", "FETCH_HEAD"], work);
- } catch { /* the first one */ }
+ } catch {
+ /* the first one */
+ }
// this PR's latest run only, and no PR that's been closed
for (const d of await fs.readdir(work).catch(() => [])) {
const m = /^pr-(\d+)$/.exec(d);
@@ -153,8 +156,12 @@ ${shots.map((x) => ` s.shots.length) && !m.video)) {
- const why = m?.leak ? "录制时屏幕上出现了密钥,已全部丢弃,没有发布任何截图"
- : m?.skipped === "no visible UI change found in the diff" ? "看了改动,没有找到用户能看到的界面变化,所以没有录制"
- : `这次没录成(${result})`;
+ const why = m?.leak ? "录制时屏幕上出现了密钥,已全部丢弃,没有发布任何截图" : m?.skipped === "no visible UI change found in the diff" ? "看了改动,没有找到用户能看到的界面变化,所以没有录制" : `这次没录成(${result})`;
const errs = m?.errors?.length ? `\n\n细节 \n\n${m.errors.map((e) => "- " + e).join("\n")}\n ` : "";
await setBlock(`${head(sha)}\n${m?.summary ? `**改动**(按代码):${m.summary}\n\n` : ""}${m?.mismatch ? `> [!WARNING]\n> **描述与代码不符**:${m.mismatch}\n\n` : ""}${why}。${runURL ? `详见[运行记录](${runURL})。` : ""}${errs}`);
return;
@@ -195,5 +202,7 @@ async function publish() {
}
const cmd = process.argv[2];
-(cmd === "detect" ? detect() : cmd === "publish" ? publish() : Promise.reject(new Error("detect | publish")))
- .catch((e) => { console.error(e); process.exit(1); });
+(cmd === "detect" ? detect() : cmd === "publish" ? publish() : Promise.reject(new Error("detect | publish"))).catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/.github/ui-preview/record.mjs b/.github/ui-preview/record.mjs
index c834aba907..0a2f4c5497 100755
--- a/.github/ui-preview/record.mjs
+++ b/.github/ui-preview/record.mjs
@@ -34,7 +34,10 @@ const MODEL = env("PLAN_MODEL", "deepseek-flash");
const VIEW = { width: 1440, height: 900 };
// never pressed: they delete the sandbox's own data or its setup
const FORBIDDEN = /删除|清空|移除|卸载|重置|格式化|delete|remove|clear all|drop\b|truncate\b/i;
-const secrets = env("SECRETS", "").split("\n").map((s) => s.trim()).filter((s) => s.length >= 8);
+const secrets = env("SECRETS", "")
+ .split("\n")
+ .map((s) => s.trim())
+ .filter((s) => s.length >= 8);
const log = (...a) => console.log("[ui-preview]", ...a);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -46,7 +49,13 @@ const manifest = { summary: "", scenes: [], video: null, poster: null, errors: [
function outline() {
const lines = [];
const MAX = 420;
- const own = (e) => [...e.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent.trim()).join(" ").replace(/\s+/g, " ").slice(0, 70);
+ const own = (e) =>
+ [...e.childNodes]
+ .filter((n) => n.nodeType === 3)
+ .map((n) => n.textContent.trim())
+ .join(" ")
+ .replace(/\s+/g, " ")
+ .slice(0, 70);
const shown = (e) => {
const r = e.getBoundingClientRect();
if (!r.width || !r.height) return false;
@@ -58,9 +67,19 @@ function outline() {
if (lines.length >= MAX || e.hasAttribute?.("data-ui-preview")) return;
const tag = e.tagName.toLowerCase();
if (["script", "style", "svg", "link", "meta", "noscript", "template"].includes(tag) || !shown(e)) return;
- const cls = [...e.classList].slice(0, 4).map((c) => "." + c).join("");
- const data = [...e.attributes].filter((a) => a.name.startsWith("data-") && a.value.length < 40).slice(0, 3).map((a) => `[${a.name}="${a.value}"]`).join("");
- const aria = ["role", "aria-label", "title", "placeholder", "type"].map((k) => e.getAttribute(k) ? `${k}="${e.getAttribute(k).slice(0, 40)}"` : "").filter(Boolean).join(" ");
+ const cls = [...e.classList]
+ .slice(0, 4)
+ .map((c) => "." + c)
+ .join("");
+ const data = [...e.attributes]
+ .filter((a) => a.name.startsWith("data-") && a.value.length < 40)
+ .slice(0, 3)
+ .map((a) => `[${a.name}="${a.value}"]`)
+ .join("");
+ const aria = ["role", "aria-label", "title", "placeholder", "type"]
+ .map((k) => (e.getAttribute(k) ? `${k}="${e.getAttribute(k).slice(0, 40)}"` : ""))
+ .filter(Boolean)
+ .join(" ");
const text = own(e);
const r = e.getBoundingClientRect();
const away = r.top >= fold ? " (below the fold)" : "";
@@ -75,8 +94,7 @@ function outline() {
}
async function screenText(page) {
- return page.evaluate(() => document.documentElement.innerText + "\n" +
- [...document.querySelectorAll("input, textarea")].map((e) => e.value).join("\n")).catch(() => "");
+ return page.evaluate(() => document.documentElement.innerText + "\n" + [...document.querySelectorAll("input, textarea")].map((e) => e.value).join("\n")).catch(() => "");
}
async function checkLeak(page, where) {
if (!secrets.length) return;
@@ -109,8 +127,12 @@ async function askPlanner(messages) {
if (!res.ok) throw new Error(`planner API ${res.status}: ${(await res.text()).slice(0, 300)}`);
const j = await res.json();
const text = j.choices?.[0]?.message?.content ?? "";
- try { return JSON.parse(text.replace(/^```(?:json)?\s*|\s+```$/g, "")); }
- catch (e) { last = new Error(`reply not JSON (${j.choices?.[0]?.finish_reason}): ${e.message}`); log(last.message, "— asking again"); }
+ try {
+ return JSON.parse(text.replace(/^```(?:json)?\s*|\s+```$/g, ""));
+ } catch (e) {
+ last = new Error(`reply not JSON (${j.choices?.[0]?.finish_reason}): ${e.message}`);
+ log(last.message, "— asking again");
+ }
}
throw last;
}
@@ -141,16 +163,22 @@ async function readTree(root) {
async function codeContext(diff, src) {
if (!src) return "";
- const changed = [...new Set([...diff.matchAll(/^diff --git a\/(\S+) b\/\1$/gm)].map((m) => m[1]))]
- .filter((f) => /^apps\/desktop\/src\//.test(f) && !SKIP.test(f.slice("apps/desktop/src/".length - 1)) && !SKIP.test(f));
+ const changed = [...new Set([...diff.matchAll(/^diff --git a\/(\S+) b\/\1$/gm)].map((m) => m[1]))].filter((f) => /^apps\/desktop\/src\//.test(f) && !SKIP.test(f.slice("apps/desktop/src/".length - 1)) && !SKIP.test(f));
// 1. the changed files
let out = "";
for (const f of changed.slice(0, 12)) {
let body;
- try { body = await fs.readFile(path.join(src, f), "utf8"); } catch { continue; }
+ try {
+ body = await fs.readFile(path.join(src, f), "utf8");
+ } catch {
+ continue;
+ }
const chunk = `--- ${f}\n${body.length > 8000 ? body.slice(0, 8000) + "\n… (cut)" : body}\n`;
- if (out.length + chunk.length > 20e3) { out += "… (cut)\n"; break; }
+ if (out.length + chunk.length > 20e3) {
+ out += "… (cut)\n";
+ break;
+ }
out += chunk;
}
@@ -170,16 +198,14 @@ async function codeContext(diff, src) {
let entries = "";
for (const name of components.slice(0, 6)) {
const kebab = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
- const re = new RegExp(
- "import\\s+" + name + "\\b" +
- "|from\\s+[\"'][^\"']*/" + name + "(\\.vue)?[\"']" +
- "|import\\(\\s*[\"'][^\"']*/" + name + "(\\.vue)?[\"']\\s*\\)" +
- "|<" + kebab + "[\\s>/]|<" + name + "[\\s>/]");
+ const re = new RegExp("import\\s+" + name + "\\b" + "|from\\s+[\"'][^\"']*/" + name + "(\\.vue)?[\"']" + "|import\\(\\s*[\"'][^\"']*/" + name + "(\\.vue)?[\"']\\s*\\)" + "|<" + kebab + "[\\s>/]|<" + name + "[\\s>/]");
const flags = new Map(); // caller file -> Set of line numbers
for (const p of files) {
const t = await read(p);
if (!t || rel(p).endsWith(`/${name}.vue`)) continue;
- t.split("\n").forEach((l, i) => { if (re.test(l)) (flags.get(p) ?? flags.set(p, new Set()).get(p)).add(i); });
+ t.split("\n").forEach((l, i) => {
+ if (re.test(l)) (flags.get(p) ?? flags.set(p, new Set()).get(p)).add(i);
+ });
}
for (const [p, lines] of flags) {
// a dynamic import renames the component (const X = defineAsyncComponent):
@@ -194,14 +220,19 @@ async function codeContext(diff, src) {
}
if (aliases.size > 1) {
const tagRe = new RegExp("<(?:" + [...aliases].join("|") + "|" + [...aliases].map((a) => a.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()).join("|") + ")[\\s>/]");
- linesArr.forEach((l, i) => { if (tagRe.test(l)) lines.add(i); });
+ linesArr.forEach((l, i) => {
+ if (tagRe.test(l)) lines.add(i);
+ });
}
// the usage with its v-if/v-show/:open condition, and identifiers in it
const span = [];
for (const i of lines) span.push(linesArr.slice(Math.max(0, i - 6), i + 14).join("\n"));
const cond = span.join("\n");
const chunk = `--- ${rel(p)} renders ${name} (how it is shown):\n${cond.slice(0, 2500)}\n`;
- if (entries.length + chunk.length > 9e3) { entries += "… (cut)\n"; break; }
+ if (entries.length + chunk.length > 9e3) {
+ entries += "… (cut)\n";
+ break;
+ }
entries += chunk;
// who flips those conditions: identifiers in v-if / :open / v-model near the usage
const ids = [...new Set([...cond.matchAll(/(?:v-if|v-show|:open|v-model:open|:model-value)="\s*([A-Za-z_$][\w$.]*)/g)].map((m) => m[1]))].slice(0, 4);
@@ -270,9 +301,10 @@ async function glide(page, x, y) {
const n = Math.max(8, Math.round(ms / 16));
const from = { ...mouse };
for (let i = 1; i <= n; i++) {
- const t = i / n, e = t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
+ const t = i / n,
+ e = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
// a slight arc, not a ruler line
- const bend = Math.sin(Math.PI * t) * Math.min(40, d * .08);
+ const bend = Math.sin(Math.PI * t) * Math.min(40, d * 0.08);
const nx = from.x + (x - from.x) * e - ((y - from.y) / (d || 1)) * bend;
const ny = from.y + (y - from.y) * e + ((x - from.x) / (d || 1)) * bend;
await page.mouse.move(nx, ny);
@@ -319,11 +351,14 @@ async function locate(page, step) {
const el = await hit.evaluateHandle((n, text) => {
for (;;) {
const kids = [...n.children].filter((c) => (c.textContent || "").includes(text) && c.getBoundingClientRect().width > 0);
- if (kids.length === 1) n = kids[0]; else return n;
+ if (kids.length === 1) n = kids[0];
+ else return n;
}
}, text);
return el.asElement() || hit;
- } catch { return hit; }
+ } catch {
+ return hit;
+ }
};
let loc = text ? all.filter({ hasText: text }) : all;
let hit = await visible(loc);
@@ -356,7 +391,6 @@ async function locate(page, step) {
throw new Error(`nothing visible matches ${step.target}${step.text ? ` with "${step.text}"` : ""} (${await loc.count()} in the page)`);
}
-
// Out of sight is scrolled to as a reader would, with the wheel over what
// scrolls. Each view that hides it, the innermost first, is wheeled in small
// steps until the element is in the middle of it, or at its top when it's
@@ -378,19 +412,22 @@ async function bringIntoView(page, loc) {
const root = s === document.scrollingElement;
const b = root ? clip : s.getBoundingClientRect();
// what of it shows: inside the window and every view around it
- let top = Math.max(b.top, 0), bottom = Math.min(b.bottom, innerHeight);
- let left = Math.max(b.left, 0), right = Math.min(b.right, innerWidth);
+ let top = Math.max(b.top, 0),
+ bottom = Math.min(b.bottom, innerHeight);
+ let left = Math.max(b.left, 0),
+ right = Math.min(b.right, innerWidth);
for (let p = s.parentElement; p && !root; p = p.parentElement) {
if (!scrollers.includes(p) || p === document.scrollingElement) continue;
const pb = p.getBoundingClientRect();
- top = Math.max(top, pb.top); bottom = Math.min(bottom, pb.bottom);
- left = Math.max(left, pb.left); right = Math.min(right, pb.right);
+ top = Math.max(top, pb.top);
+ bottom = Math.min(bottom, pb.bottom);
+ left = Math.max(left, pb.left);
+ right = Math.min(right, pb.right);
}
const room = bottom - top - 2 * PAD;
const want = r.height <= room ? (r.top + r.bottom) / 2 - (top + bottom) / 2 : r.top - (top + PAD);
const shown = r.top >= top + Math.min(PAD, Math.max(0, room - r.height) / 2) && (r.height <= room ? r.bottom <= bottom - PAD : r.top <= top + PAD * 2);
- out.push({ shown, dy: Math.max(-s.scrollTop, Math.min(s.scrollHeight - s.clientHeight - s.scrollTop, want)),
- x: (left + right) / 2, y: (top + bottom) / 2, seen: bottom - top > 20 && right - left > 20 });
+ out.push({ shown, dy: Math.max(-s.scrollTop, Math.min(s.scrollHeight - s.clientHeight - s.scrollTop, want)), x: (left + right) / 2, y: (top + bottom) / 2, seen: bottom - top > 20 && right - left > 20 });
}
return out;
}, PAD);
@@ -416,7 +453,8 @@ async function pointAt(page, loc) {
await bringIntoView(page, loc);
const b = await loc.boundingBox();
if (!b) throw new Error("the element has no box");
- const x = b.x + Math.min(b.width / 2, 60), y = b.y + b.height / 2;
+ const x = b.x + Math.min(b.width / 2, 60),
+ y = b.y + b.height / 2;
await glide(page, x, y);
return b;
}
@@ -425,10 +463,16 @@ async function pointAt(page, loc) {
// the seeded connections painted in, is ready
async function settle(page) {
await page.waitForLoadState("load").catch(() => {});
- await page.waitForFunction(() => {
- const txt = document.body.innerText || "";
- return txt.includes("新建连接") || txt.includes("New Connection") || document.querySelectorAll("button").length >= 6;
- }, null, { timeout: 20000 }).catch(() => {});
+ await page
+ .waitForFunction(
+ () => {
+ const txt = document.body.innerText || "";
+ return txt.includes("新建连接") || txt.includes("New Connection") || document.querySelectorAll("button").length >= 6;
+ },
+ null,
+ { timeout: 20000 },
+ )
+ .catch(() => {});
await sleep(1500);
}
@@ -440,13 +484,18 @@ async function runStep(page, step, scene) {
switch (step.do) {
case "click": {
const loc = await locate(page, step);
- const label = (await loc.innerText().catch(() => "")) + " " + (await loc.getAttribute("title").catch(() => "") || "");
+ const label = (await loc.innerText().catch(() => "")) + " " + ((await loc.getAttribute("title").catch(() => "")) || "");
if (FORBIDDEN.test(label)) throw new Error(`won't press "${label.trim().slice(0, 40)}"`);
await pointAt(page, loc);
await sleep(180);
if (step.dblclick) {
- await page.mouse.down(); await sleep(60); await page.mouse.up();
- await sleep(120); await page.mouse.down(); await sleep(60); await page.mouse.up();
+ await page.mouse.down();
+ await sleep(60);
+ await page.mouse.up();
+ await sleep(120);
+ await page.mouse.down();
+ await sleep(60);
+ await page.mouse.up();
} else {
await page.mouse.down();
await sleep(90);
@@ -465,7 +514,9 @@ async function runStep(page, step, scene) {
case "type": {
const loc = await locate(page, step);
await pointAt(page, loc);
- await page.mouse.down(); await sleep(80); await page.mouse.up();
+ await page.mouse.down();
+ await sleep(80);
+ await page.mouse.up();
await loc.fill("");
await page.keyboard.type(String(step.value ?? ""), { delay: 70 });
await sleep(600);
@@ -517,7 +568,8 @@ async function runStep(page, step, scene) {
if (b) {
// the target with room around it, never so small that a button
// is shown without where it is
- const w = Math.min(VIEW.width, Math.max(560, b.width + 48)), h = Math.min(VIEW.height, Math.max(340, b.height + 48));
+ const w = Math.min(VIEW.width, Math.max(560, b.width + 48)),
+ h = Math.min(VIEW.height, Math.max(340, b.height + 48));
const x = Math.min(VIEW.width - w, Math.max(0, b.x + b.width / 2 - w / 2));
const y = Math.min(VIEW.height - h, Math.max(0, b.y + b.height / 2 - h / 2));
clip = { x, y, width: w, height: h };
@@ -553,7 +605,9 @@ async function main() {
await lp.goto(BASE);
await settle(lp);
outlines.app = await lp.evaluate(outline);
- } catch (e) { outlines.app = `(failed to load: ${e.message})`; }
+ } catch (e) {
+ outlines.app = `(failed to load: ${e.message})`;
+ }
await look.close();
const MAXDIFF = 150e3;
@@ -561,13 +615,18 @@ async function main() {
log(`code context: ${code.length} characters`);
const ask = [
{ role: "system", content: GUIDE },
- { role: "user", content: `PR title: ${env("PR_TITLE", "")}\n\nPR description:\n${body.slice(0, 6000)}\n\nDiff${diff.length > MAXDIFF ? " (cut)" : ""}:\n${diff.slice(0, MAXDIFF)}\n\n` +
- `=== outline: app ===\n${outlines.app}` +
- (code ? `\n\n=== the changed frontend files (the PR's version) ===\n${code}` : "") },
+ {
+ role: "user",
+ content: `PR title: ${env("PR_TITLE", "")}\n\nPR description:\n${body.slice(0, 6000)}\n\nDiff${diff.length > MAXDIFF ? " (cut)" : ""}:\n${diff.slice(0, MAXDIFF)}\n\n` + `=== outline: app ===\n${outlines.app}` + (code ? `\n\n=== the changed frontend files (the PR's version) ===\n${code}` : ""),
+ },
];
let plan;
- try { plan = env("PLAN_FILE") ? JSON.parse(await fs.readFile(env("PLAN_FILE"), "utf8")) : await askPlanner(ask); }
- catch (e) { plan = null; manifest.errors.push(`plan: ${e.message}`); }
+ try {
+ plan = env("PLAN_FILE") ? JSON.parse(await fs.readFile(env("PLAN_FILE"), "utf8")) : await askPlanner(ask);
+ } catch (e) {
+ plan = null;
+ manifest.errors.push(`plan: ${e.message}`);
+ }
if (!plan || plan.ui_change === false || !plan.scenes?.length) {
manifest.summary = plan?.summary || "";
manifest.skipped = plan ? "no visible UI change found in the diff" : "no plan";
@@ -582,7 +641,8 @@ async function main() {
if (plan.scenes.length) {
const rec = await browser.newContext({ ...ctxOpts, recordVideo: { dir: path.join(OUT, "raw"), size: VIEW } });
await rec.addInitScript({ path: path.join(here, "cursor.js") });
- let repairs = 0, interactive = false;
+ let repairs = 0,
+ interactive = false;
// one take per scene: every scene starts from the app as it opens, and
// its video keeps only what happens once the app has settled — the
// load never reaches the final cut, so a scene change reads as a cut,
@@ -613,8 +673,9 @@ async function main() {
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (["click", "hover", "type", "press", "scroll", "upload"].includes(step.do)) interactive = true;
- try { await runStep(page, step, scene); }
- catch (e) {
+ try {
+ await runStep(page, step, scene);
+ } catch (e) {
if (manifest.leak) break;
manifest.errors.push(`${scene.title} · ${step.do} ${step.target || step.name || ""}: ${e.message.split("\n")[0]}`);
log("step failed:", e.message);
@@ -640,13 +701,27 @@ async function main() {
repairs++;
// the page as it is now, and what went wrong: the rest of the scene again
try {
- const fix = await askPlanner([...ask, { role: "assistant", content: JSON.stringify(plan) },
- { role: "user", content: `Step ${i + 1} of scene "${scene.title}" failed: ${e.message.split("\n")[0]}\nThe page now:\n${await page.evaluate(outline)}\n\nIf the failed selector came from the diff, remember the changed-file paths above say which dialog, panel or component holds it — a different one may have to be opened (or closed) first, not just a different selector for this page. Reply with JSON {"steps": [...]}: the steps to do instead of that one and the ones after it in this scene.` }]);
- if (Array.isArray(fix.steps)) { steps = [...steps.slice(0, i + 1), ...fix.steps.slice(0, 20)]; log("repaired:", JSON.stringify(fix.steps)); }
- } catch (e2) { manifest.errors.push(`repair: ${e2.message}`); }
+ const fix = await askPlanner([
+ ...ask,
+ { role: "assistant", content: JSON.stringify(plan) },
+ {
+ role: "user",
+ content: `Step ${i + 1} of scene "${scene.title}" failed: ${e.message.split("\n")[0]}\nThe page now:\n${await page.evaluate(outline)}\n\nIf the failed selector came from the diff, remember the changed-file paths above say which dialog, panel or component holds it — a different one may have to be opened (or closed) first, not just a different selector for this page. Reply with JSON {"steps": [...]}: the steps to do instead of that one and the ones after it in this scene.`,
+ },
+ ]);
+ if (Array.isArray(fix.steps)) {
+ steps = [...steps.slice(0, i + 1), ...fix.steps.slice(0, 20)];
+ log("repaired:", JSON.stringify(fix.steps));
+ }
+ } catch (e2) {
+ manifest.errors.push(`repair: ${e2.message}`);
+ }
}
}
- if (manifest.leak) { await page.close().catch(() => {}); break; }
+ if (manifest.leak) {
+ await page.close().catch(() => {});
+ break;
+ }
await caption(page, "");
await sleep(600);
takes.push({ video: await page.video().path(), from: Math.max(0, (ready - began) / 1000 - 0.2) });
@@ -659,9 +734,7 @@ async function main() {
// the takes joined, each already trimmed of its load
const args = ["-y", "-loglevel", "error"];
const pins = takes.map((t, i) => (args.push("-ss", t.from.toFixed(2), "-i", t.video), `[${i}:v]`));
- args.push("-filter_complex", `${pins.join("")}concat=n=${takes.length}:v=1:a=0[v]`, "-map", "[v]",
- "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", "30",
- path.join(OUT, "preview.mp4"));
+ args.push("-filter_complex", `${pins.join("")}concat=n=${takes.length}:v=1:a=0[v]`, "-map", "[v]", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", "30", path.join(OUT, "preview.mp4"));
execFileSync("ffmpeg", args);
manifest.video = "preview.mp4";
manifest.poster = await poster(browser);
@@ -671,7 +744,8 @@ async function main() {
await browser.close();
if (manifest.leak) {
for (const f of await fs.readdir(OUT)) await fs.rm(path.join(OUT, f), { recursive: true, force: true });
- manifest.scenes = []; manifest.video = manifest.poster = null;
+ manifest.scenes = [];
+ manifest.video = manifest.poster = null;
}
await fs.writeFile(path.join(OUT, "manifest.json"), JSON.stringify(manifest, null, 2));
log("done:", JSON.stringify({ shots: manifest.scenes.reduce((n, s) => n + s.shots.length, 0), video: manifest.video, errors: manifest.errors.length, leak: manifest.leak }));
diff --git a/.github/ui-preview/seed.mjs b/.github/ui-preview/seed.mjs
index e212cd5b6c..a750de36c1 100755
--- a/.github/ui-preview/seed.mjs
+++ b/.github/ui-preview/seed.mjs
@@ -31,14 +31,22 @@ const DBS = [
name: "商店 Shop (SQLite)",
note: "订单、商品与客户示例库",
tables: "customers · products · orders · audit_log(含视图 v_daily_revenue、外键与触发器)",
- warm: ["SELECT * FROM orders ORDER BY created_at DESC LIMIT 50", "SELECT c.city, COUNT(*) AS orders, SUM(o.amount) AS revenue\nFROM orders o JOIN customers c ON c.id = o.customer_id\nGROUP BY c.city ORDER BY revenue DESC", "SELECT category, COUNT(*) AS n, ROUND(AVG(price), 2) AS avg_price\nFROM products WHERE listed = 1 GROUP BY category ORDER BY n DESC"],
+ warm: [
+ "SELECT * FROM orders ORDER BY created_at DESC LIMIT 50",
+ "SELECT c.city, COUNT(*) AS orders, SUM(o.amount) AS revenue\nFROM orders o JOIN customers c ON c.id = o.customer_id\nGROUP BY c.city ORDER BY revenue DESC",
+ "SELECT category, COUNT(*) AS n, ROUND(AVG(price), 2) AS avg_price\nFROM products WHERE listed = 1 GROUP BY category ORDER BY n DESC",
+ ],
},
{
file: "dbx-analytics.sqlite",
name: "分析 Analytics (SQLite)",
note: "事件流与每日指标",
tables: "events(5000 行,看分页)· daily_stats(90 天)",
- warm: ["SELECT event_type, COUNT(*) AS n, ROUND(AVG(duration_ms)) AS avg_ms\nFROM events GROUP BY event_type ORDER BY n DESC", "SELECT date(ts) AS day, COUNT(DISTINCT user_id) AS dau\nFROM events GROUP BY day ORDER BY day DESC LIMIT 14", "SELECT * FROM daily_stats ORDER BY stat_date DESC LIMIT 30"],
+ warm: [
+ "SELECT event_type, COUNT(*) AS n, ROUND(AVG(duration_ms)) AS avg_ms\nFROM events GROUP BY event_type ORDER BY n DESC",
+ "SELECT date(ts) AS day, COUNT(DISTINCT user_id) AS dau\nFROM events GROUP BY day ORDER BY day DESC LIMIT 14",
+ "SELECT * FROM daily_stats ORDER BY stat_date DESC LIMIT 30",
+ ],
},
];
@@ -112,7 +120,11 @@ async function api(base, p, init = {}) {
});
const text = await res.text();
if (!res.ok) throw new Error(`${init.method || "GET"} ${p}: ${res.status} ${text.slice(0, 300)}`);
- try { return JSON.parse(text); } catch { return text; }
+ try {
+ return JSON.parse(text);
+ } catch {
+ return text;
+ }
}
async function seedApi() {
@@ -166,8 +178,14 @@ async function seedApi() {
await api(base, "/api/saved-sql", {
method: "POST",
body: JSON.stringify({
- ...s, connectionId: conns[i % conns.length].id, folderId: folder?.id || "seed-folder-daily",
- database: "main", orderIndex: i, openCount: 3 + i, createdAt: iso(36e5 * (70 - i)), updatedAt: iso(36e5 * (24 - i * 6)),
+ ...s,
+ connectionId: conns[i % conns.length].id,
+ folderId: folder?.id || "seed-folder-daily",
+ database: "main",
+ orderIndex: i,
+ openCount: 3 + i,
+ createdAt: iso(36e5 * (70 - i)),
+ updatedAt: iso(36e5 * (24 - i * 6)),
}),
}).catch((e) => console.log("[ui-preview] saved-sql:", e.message));
}
@@ -188,9 +206,17 @@ async function seedApi() {
method: "POST",
body: JSON.stringify({
entry: {
- id: `seed-h-${Math.random().toString(36).slice(2, 10)}`, connection_id: c.id, connection_name: c.name,
- database: "main", sql, executed_at: iso(36e5 * (2 + Math.random() * 60)), execution_time_ms: ms, success: ok,
- error: ok ? null : "demo entry: would have modified rows (not really run)", activity_kind: "query", source: "editor",
+ id: `seed-h-${Math.random().toString(36).slice(2, 10)}`,
+ connection_id: c.id,
+ connection_name: c.name,
+ database: "main",
+ sql,
+ executed_at: iso(36e5 * (2 + Math.random() * 60)),
+ execution_time_ms: ms,
+ success: ok,
+ error: ok ? null : "demo entry: would have modified rows (not really run)",
+ activity_kind: "query",
+ source: "editor",
},
}),
}).catch((e) => console.log("[ui-preview] history:", e.message));
@@ -205,12 +231,13 @@ if (cmd === "dbs") {
python(dir);
// a small importable file, for import/mapping dialogs (record.mjs's
// "upload" step resolves names inside this directory)
- fs.writeFileSync(path.join(dir, "sample.csv"),
- "name,category,price,stock\n" +
- Array.from({ length: 8 }, (_, i) => `键盘 K${i + 100},键盘,${(199 + i * 23).toFixed(2)},${40 - i * 3}`).join("\n") + "\n");
+ fs.writeFileSync(path.join(dir, "sample.csv"), "name,category,price,stock\n" + Array.from({ length: 8 }, (_, i) => `键盘 K${i + 100},键盘,${(199 + i * 23).toFixed(2)},${40 - i * 3}`).join("\n") + "\n");
console.log("[ui-preview] databases written to", dir);
} else if (cmd === "api") {
- seedApi().catch((e) => { console.error(e); process.exit(1); });
+ seedApi().catch((e) => {
+ console.error(e);
+ process.exit(1);
+ });
} else {
console.error("usage: seed.mjs dbs | seed.mjs api [--as ]");
process.exit(1);
diff --git a/agents/scripts/build_offline_jdbc_payload.mjs b/agents/scripts/build_offline_jdbc_payload.mjs
index e728af6828..d6281ce54c 100644
--- a/agents/scripts/build_offline_jdbc_payload.mjs
+++ b/agents/scripts/build_offline_jdbc_payload.mjs
@@ -40,14 +40,7 @@ function loadAssetManifest(path) {
}
function resolveBundle(resolver, coordinate, localRepository, repositories) {
- const javaToolOptions = [
- process.env.JAVA_TOOL_OPTIONS,
- "-Djava.net.preferIPv4Stack=true",
- "-Daether.connector.connectTimeout=30000",
- "-Daether.connector.requestTimeout=300000",
- ]
- .filter(Boolean)
- .join(" ");
+ const javaToolOptions = [process.env.JAVA_TOOL_OPTIONS, "-Djava.net.preferIPv4Stack=true", "-Daether.connector.connectTimeout=30000", "-Daether.connector.requestTimeout=300000"].filter(Boolean).join(" ");
const args = ["resolve", "--coordinate", coordinate, "--local-repo", localRepository];
for (const repository of repositories) args.push("--repo", repository);
const result = spawnSync(resolver, args, {
@@ -178,14 +171,8 @@ async function main() {
if (workDir) rmSync(workDir, { recursive: true, force: true });
}
- writeFileSync(
- join(payloadDir, "third-party-notices.json"),
- `${JSON.stringify({ format_version: 1, bundles: notices }, null, 2)}\n`,
- );
- writeFileSync(
- join(payloadDir, "offline-manifest.json"),
- `${JSON.stringify({ format_version: 1, plugin_entry: "jdbc/plugin.zip", third_party_entry: "jdbc/third-party-notices.json", bundles: manifestBundles }, null, 2)}\n`,
- );
+ writeFileSync(join(payloadDir, "third-party-notices.json"), `${JSON.stringify({ format_version: 1, bundles: notices }, null, 2)}\n`);
+ writeFileSync(join(payloadDir, "offline-manifest.json"), `${JSON.stringify({ format_version: 1, plugin_entry: "jdbc/plugin.zip", third_party_entry: "jdbc/third-party-notices.json", bundles: manifestBundles }, null, 2)}\n`);
console.log(`Prepared offline JDBC payload in ${payloadDir}`);
}
diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue
index 1120e73c88..9936298267 100644
--- a/apps/desktop/src/components/editor/AiAssistant.vue
+++ b/apps/desktop/src/components/editor/AiAssistant.vue
@@ -3195,7 +3195,10 @@ function imageAttachmentDetail(attachment: AiImageAttachment): string {
}
function activeImageAttachmentSupportError(attachments: readonly AiImageAttachment[]) {
- return imageAttachmentSupportError(activeFullConfig.value?.provider, attachments.map((attachment) => attachment.mediaType));
+ return imageAttachmentSupportError(
+ activeFullConfig.value?.provider,
+ attachments.map((attachment) => attachment.mediaType),
+ );
}
function imageAttachmentSupportErrorMessage(error: "provider" | "format"): string {
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index ac7aa79be4..802ef44ca1 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -475,13 +475,12 @@ function onLocaleOpenChange(open: boolean) {
if (!open) restoreLocaleOptionPreview();
}
-const appThemePaletteOptions = computed(
- (): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
- APP_THEME_PALETTES.map((palette) => ({
- value: palette.value,
- label: t(palette.labelKey),
- previewColor: palette.previewColor,
- })),
+const appThemePaletteOptions = computed((): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
+ APP_THEME_PALETTES.map((palette) => ({
+ value: palette.value,
+ label: t(palette.labelKey),
+ previewColor: palette.previewColor,
+ })),
);
const selectedThemePaletteOption = computed(() => appThemePaletteOptions.value.find((option) => option.value === themePalette.value) ?? appThemePaletteOptions.value[0]);
const selectedLocaleOption = computed(() => LOCALE_OPTIONS.find((locale) => locale.value === currentLocale()) ?? LOCALE_OPTIONS[0]);
@@ -10029,8 +10028,7 @@ onUnmounted(() => {
SELECT t.*
FROM ${1:table} t
WHERE t.del_flag = 0
-LIMIT 100;
+LIMIT 100;
diff --git a/apps/desktop/src/components/editor/queryEditorCodeMirrorRuntime.ts b/apps/desktop/src/components/editor/queryEditorCodeMirrorRuntime.ts
index 52374d2a98..dfe164e28a 100644
--- a/apps/desktop/src/components/editor/queryEditorCodeMirrorRuntime.ts
+++ b/apps/desktop/src/components/editor/queryEditorCodeMirrorRuntime.ts
@@ -92,67 +92,69 @@ interface CodeMirrorBindings {
}
type ReadyCodeMirrorBindings = CodeMirrorBindings & {
- [Key in
- | "editorViewModule"
- | "hoverCloseEffect"
- | "codeMirrorLineNumbers"
- | "codeMirrorPrec"
- | "codeMirrorEditorSelection"
- | "codeMirrorSnippetCompletion"
- | "fontThemeComp"
- | "codeMirrorTheme"
- | "wordWrapComp"
- | "showWhitespaceComp"
- | "lineNumbersComp"
- | "vimModeComp"
- | "closeBracketsComp"
- | "sqlLanguageComp"
- | "sqlSemanticHighlightComp"
- | "sqlSignatureComp"
- | "codeMirrorCloseBrackets"
- | "codeMirrorCloseBracketsKeymap"
- | "readOnlyComp"
- | "runGutterComp"
- | "runKeymapComp"
- | "historyResetComp"
- | "defaultKeymapComp"
- | "completionComp"
- | "diagnosticComp"
- | "previewRangeComp"
- | "indentComp"
- | "setSqlDiagnosticsEffect"
- | "statementBoundariesRefreshEffect"
- | "codeMirrorCompletionStatus"
- | "codeMirrorAcceptCompletion"
- | "codeMirrorCurrentCompletions"
- | "codeMirrorSelectedCompletion"
- | "codeMirrorSelectedCompletionIndex"
- | "codeMirrorSetSelectedCompletion"
- | "codeMirrorMoveCompletionSelection"
- | "codeMirrorSelectFirstCompletion"
- | "codeMirrorCloseCompletion"
- | "codeMirrorStartCompletion"
- | "codeMirrorInsertCompletionText"
- | "codeMirrorNextSnippetField"
- | "codeMirrorIndentMore"
- | "codeMirrorIndentLess"
- | "codeMirrorCopyLineDown"
- | "codeMirrorCopyLineUp"
- | "codeMirrorDeleteLine"
- | "codeMirrorMoveLineUp"
- | "codeMirrorMoveLineDown"
- | "codeMirrorUndo"
- | "codeMirrorRedo"
- | "codeMirrorHistory"
- | "codeMirrorSelectAll"
- | "codeMirrorInsertNewlineKeepIndent"
- | "codeMirrorToggleLineComment"
- | "codeMirrorToggleBlockComment"
- | "codeMirrorDefaultKeymap"
- | "codeMirrorToggleFold"
- | "codeMirrorFoldAll"
- | "codeMirrorUnfoldAll"
- | "codeMirrorIndentUnit"]: NonNullable
;
+ [
+ Key in
+ | "editorViewModule"
+ | "hoverCloseEffect"
+ | "codeMirrorLineNumbers"
+ | "codeMirrorPrec"
+ | "codeMirrorEditorSelection"
+ | "codeMirrorSnippetCompletion"
+ | "fontThemeComp"
+ | "codeMirrorTheme"
+ | "wordWrapComp"
+ | "showWhitespaceComp"
+ | "lineNumbersComp"
+ | "vimModeComp"
+ | "closeBracketsComp"
+ | "sqlLanguageComp"
+ | "sqlSemanticHighlightComp"
+ | "sqlSignatureComp"
+ | "codeMirrorCloseBrackets"
+ | "codeMirrorCloseBracketsKeymap"
+ | "readOnlyComp"
+ | "runGutterComp"
+ | "runKeymapComp"
+ | "historyResetComp"
+ | "defaultKeymapComp"
+ | "completionComp"
+ | "diagnosticComp"
+ | "previewRangeComp"
+ | "indentComp"
+ | "setSqlDiagnosticsEffect"
+ | "statementBoundariesRefreshEffect"
+ | "codeMirrorCompletionStatus"
+ | "codeMirrorAcceptCompletion"
+ | "codeMirrorCurrentCompletions"
+ | "codeMirrorSelectedCompletion"
+ | "codeMirrorSelectedCompletionIndex"
+ | "codeMirrorSetSelectedCompletion"
+ | "codeMirrorMoveCompletionSelection"
+ | "codeMirrorSelectFirstCompletion"
+ | "codeMirrorCloseCompletion"
+ | "codeMirrorStartCompletion"
+ | "codeMirrorInsertCompletionText"
+ | "codeMirrorNextSnippetField"
+ | "codeMirrorIndentMore"
+ | "codeMirrorIndentLess"
+ | "codeMirrorCopyLineDown"
+ | "codeMirrorCopyLineUp"
+ | "codeMirrorDeleteLine"
+ | "codeMirrorMoveLineUp"
+ | "codeMirrorMoveLineDown"
+ | "codeMirrorUndo"
+ | "codeMirrorRedo"
+ | "codeMirrorHistory"
+ | "codeMirrorSelectAll"
+ | "codeMirrorInsertNewlineKeepIndent"
+ | "codeMirrorToggleLineComment"
+ | "codeMirrorToggleBlockComment"
+ | "codeMirrorDefaultKeymap"
+ | "codeMirrorToggleFold"
+ | "codeMirrorFoldAll"
+ | "codeMirrorUnfoldAll"
+ | "codeMirrorIndentUnit"
+ ]: NonNullable;
};
export function createQueryEditorCodeMirrorRuntime() {
diff --git a/apps/desktop/src/components/grid/DataGridCellDetailPanel.vue b/apps/desktop/src/components/grid/DataGridCellDetailPanel.vue
index be6679e6d9..c6be4209f5 100644
--- a/apps/desktop/src/components/grid/DataGridCellDetailPanel.vue
+++ b/apps/desktop/src/components/grid/DataGridCellDetailPanel.vue
@@ -216,8 +216,7 @@ defineExpose({ openSearch });
class="dbx-data-grid-value-font overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words cursor-pointer hover:border-primary/50"
:class="[{ 'cursor-text': detail.isEditable }, panelIsBottom && detail.imagePreviewUrl ? 'min-h-24 max-h-32 shrink-0' : '', valueFillsHeight && !detail.imagePreviewUrl ? 'min-h-0 flex-1' : '']"
@dblclick="emit('startEdit')"
- >{{ presentedValuePreview }}
+ >{{ presentedValuePreview }}
{{ t("grid.largeValuePreviewHint", { count: detail.rawValuePreview.length }) }}
diff --git a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsoleDirectionFilter.spec.ts b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsoleDirectionFilter.spec.ts
index 2e70a8e739..1fbaa7d110 100644
--- a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsoleDirectionFilter.spec.ts
+++ b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsoleDirectionFilter.spec.ts
@@ -11,15 +11,13 @@ const { mqttGetMessagesMock } = vi.hoisted(() => ({
}));
vi.mock("@/lib/backend/api", () => ({
- mqttGetBrokerInfo: vi.fn(
- async (): Promise => ({
- brokerUrl: "mqtt://localhost:1883",
- clientId: "dbx-test",
- connected: true,
- protocolVersion: "5.0",
- subscriptionCount: 1,
- }),
- ),
+ mqttGetBrokerInfo: vi.fn(async (): Promise => ({
+ brokerUrl: "mqtt://localhost:1883",
+ clientId: "dbx-test",
+ connected: true,
+ protocolVersion: "5.0",
+ subscriptionCount: 1,
+ })),
mqttListTopics: vi.fn(async () => [["device/status", "atmostonce"]]),
mqttListSavedTopicConfigs: vi.fn(async () => [{ topic: "device/status", qos: "atmostonce", enabled: true, noLocal: false }]),
mqttGetMessages: mqttGetMessagesMock,
diff --git a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePause.spec.ts b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePause.spec.ts
index b356bfcc61..8431836969 100644
--- a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePause.spec.ts
+++ b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePause.spec.ts
@@ -11,15 +11,13 @@ const { mqttGetMessagesMock } = vi.hoisted(() => ({
}));
vi.mock("@/lib/backend/api", () => ({
- mqttGetBrokerInfo: vi.fn(
- async (): Promise => ({
- brokerUrl: "mqtt://localhost:1883",
- clientId: "dbx-test",
- connected: true,
- protocolVersion: "5.0",
- subscriptionCount: 1,
- }),
- ),
+ mqttGetBrokerInfo: vi.fn(async (): Promise => ({
+ brokerUrl: "mqtt://localhost:1883",
+ clientId: "dbx-test",
+ connected: true,
+ protocolVersion: "5.0",
+ subscriptionCount: 1,
+ })),
mqttListTopics: vi.fn(async () => [["device/status", "atmostonce"]]),
mqttListSavedTopicConfigs: vi.fn(async () => [{ topic: "device/status", qos: "atmostonce", enabled: true, noLocal: false }]),
mqttGetMessages: mqttGetMessagesMock,
diff --git a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadCollapse.spec.ts b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadCollapse.spec.ts
index e7eb425672..9d7c86726e 100644
--- a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadCollapse.spec.ts
+++ b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadCollapse.spec.ts
@@ -12,15 +12,13 @@ const { mqttClearMessagesMock, mqttGetMessagesMock } = vi.hoisted(() => ({
}));
vi.mock("@/lib/backend/api", () => ({
- mqttGetBrokerInfo: vi.fn(
- async (): Promise => ({
- brokerUrl: "mqtt://localhost:1883",
- clientId: "dbx-test",
- connected: true,
- protocolVersion: "5.0",
- subscriptionCount: 1,
- }),
- ),
+ mqttGetBrokerInfo: vi.fn(async (): Promise => ({
+ brokerUrl: "mqtt://localhost:1883",
+ clientId: "dbx-test",
+ connected: true,
+ protocolVersion: "5.0",
+ subscriptionCount: 1,
+ })),
mqttListTopics: vi.fn(async () => [["device/status", "atmostonce"]]),
mqttListSavedTopicConfigs: vi.fn(async () => [{ topic: "device/status", qos: "atmostonce", enabled: true, noLocal: false }]),
mqttGetMessages: mqttGetMessagesMock,
diff --git a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadSearch.spec.ts b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadSearch.spec.ts
index 17b613d2e2..015bc8182c 100644
--- a/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadSearch.spec.ts
+++ b/apps/desktop/src/components/mqtt/__tests__/MqttAdminConsolePayloadSearch.spec.ts
@@ -11,15 +11,13 @@ const { mqttGetMessagesMock } = vi.hoisted(() => ({
}));
vi.mock("@/lib/backend/api", () => ({
- mqttGetBrokerInfo: vi.fn(
- async (): Promise => ({
- brokerUrl: "mqtt://localhost:1883",
- clientId: "dbx-test",
- connected: true,
- protocolVersion: "5.0",
- subscriptionCount: 1,
- }),
- ),
+ mqttGetBrokerInfo: vi.fn(async (): Promise => ({
+ brokerUrl: "mqtt://localhost:1883",
+ clientId: "dbx-test",
+ connected: true,
+ protocolVersion: "5.0",
+ subscriptionCount: 1,
+ })),
mqttListTopics: vi.fn(async () => [["device/status", "atmostonce"]]),
mqttListSavedTopicConfigs: vi.fn(async () => [{ topic: "device/status", qos: "atmostonce", enabled: true, noLocal: false }]),
mqttGetMessages: mqttGetMessagesMock,
diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
index 3d4a436d72..b53646f806 100644
--- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue
+++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
@@ -3291,7 +3291,7 @@ async function prepareLegacyGroupingView() {
let anchor = groupedKeyListRef.value?.getViewportAnchor();
let rowIndex = Math.min(anchor?.rowIndex ?? 0, Math.max(0, snapshot.visibleRows.length - 1));
if (anchor?.keyRaw) {
- for (let offset = 0; offset < snapshot.visibleRows.length; ) {
+ for (let offset = 0; offset < snapshot.visibleRows.length;) {
if (!current()) return;
const end = Math.min(offset + 512, snapshot.visibleRows.length);
let found = false;
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index 3dfddeefa8..39edb687b0 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -4492,38 +4492,34 @@ function columnIndexContextMenuItems(targets: EditableStructureColumn[]): Contex
disabled: () => disabled() || primaryColumns.length > 0 || targets.some(isPrimaryKeyDisabled),
action: () => setColumnsPrimaryKey(targets),
},
- ...([false, true] as const).map(
- (unique): ContextMenuItem => ({
- label: t(unique ? "structureEditor.createUniqueIndex" : "structureEditor.createNormalIndex"),
- icon: KeyRound,
- iconClass: indexIconClass(unique ? "unique" : "index"),
- disabled: () =>
- disabled() ||
- !structureCapabilities.value.createIndex ||
- indexes.value.some(
- (index) =>
- !index.markedForDrop &&
- !index.isPrimary &&
- index.isUnique === unique &&
- !index.filter &&
- (!index.indexType || index.indexType.toUpperCase() === "BTREE") &&
- index.columns.length === targets.length &&
- index.columns.every((name, position) => structureColumnComparisonKey(name) === structureColumnComparisonKey(targets[position].name)),
- ),
- action: () => createIndexFromColumns(targets, unique),
- }),
- ),
+ ...([false, true] as const).map((unique): ContextMenuItem => ({
+ label: t(unique ? "structureEditor.createUniqueIndex" : "structureEditor.createNormalIndex"),
+ icon: KeyRound,
+ iconClass: indexIconClass(unique ? "unique" : "index"),
+ disabled: () =>
+ disabled() ||
+ !structureCapabilities.value.createIndex ||
+ indexes.value.some(
+ (index) =>
+ !index.markedForDrop &&
+ !index.isPrimary &&
+ index.isUnique === unique &&
+ !index.filter &&
+ (!index.indexType || index.indexType.toUpperCase() === "BTREE") &&
+ index.columns.length === targets.length &&
+ index.columns.every((name, position) => structureColumnComparisonKey(name) === structureColumnComparisonKey(targets[position].name)),
+ ),
+ action: () => createIndexFromColumns(targets, unique),
+ })),
...["FULLTEXT", "SPATIAL"]
.filter((type) => indexTypeOptions.value.includes(type))
- .map(
- (type): ContextMenuItem => ({
- label: t(type === "FULLTEXT" ? "structureEditor.createFulltextIndex" : "structureEditor.createSpatialIndex"),
- icon: KeyRound,
- iconClass: indexIconClass(type === "FULLTEXT" ? "fulltext" : "spatial"),
- disabled: () => disabled() || !structureCapabilities.value.createIndex || !!specialIndexColumnIssue(structureDialect.value, type, targets),
- action: () => createIndexFromColumns(targets, false, type),
- }),
- ),
+ .map((type): ContextMenuItem => ({
+ label: t(type === "FULLTEXT" ? "structureEditor.createFulltextIndex" : "structureEditor.createSpatialIndex"),
+ icon: KeyRound,
+ iconClass: indexIconClass(type === "FULLTEXT" ? "fulltext" : "spatial"),
+ disabled: () => disabled() || !structureCapabilities.value.createIndex || !!specialIndexColumnIssue(structureDialect.value, type, targets),
+ action: () => createIndexFromColumns(targets, false, type),
+ })),
];
const addItems: ContextMenuItem[] = indexes.value
.filter((index) => !index.markedForDrop && !index.isPrimary)
diff --git a/apps/desktop/src/components/transfer/queryResultTransferDdl.ts b/apps/desktop/src/components/transfer/queryResultTransferDdl.ts
index 3fb23c061b..907239d696 100644
--- a/apps/desktop/src/components/transfer/queryResultTransferDdl.ts
+++ b/apps/desktop/src/components/transfer/queryResultTransferDdl.ts
@@ -204,7 +204,7 @@ function sqlServerDescriptionParameters(tokens: SqlSemanticToken[]): Map();
- for (let index = 4; index < tokens.length; ) {
+ for (let index = 4; index < tokens.length;) {
const name = tokens[index++];
if (name?.kind !== "word" || !name.normalized.startsWith("@") || tokens[index++]?.text !== "=" || parameters.has(name.normalized)) return undefined;
const valueTokens: SqlSemanticToken[] = [];
diff --git a/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts
index abafec98dc..a451c809c8 100644
--- a/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts
+++ b/apps/desktop/src/lib/__tests__/database/jdbcxBuiltinDriver.spec.ts
@@ -154,29 +154,27 @@ describe("jdbcxBuiltinDriver", () => {
it("rejects ambiguous installed JDBCX runtimes until the connection selects one", async () => {
const config = jdbcxConfig();
config.jdbc_driver_paths = ["/drivers/mysql-connector-j-9.2.0.jar"];
- const bundles = ["0.8.0", "0.9.0"].map(
- (version): JdbcMavenBundleInfo => ({
- id: `jdbcx-${version}`,
- coordinate: `io.github.jdbcx:jdbcx-driver:${version}`,
- scope: "runtime",
- repositories: [],
- installed_at: "2026-07-15T00:00:00Z",
- path: `/drivers/jdbcx-${version}`,
- artifacts: [
- {
- group_id: "io.github.jdbcx",
- artifact_id: "jdbcx-driver",
- version,
- classifier: "",
- extension: "jar",
- file_name: `jdbcx-driver-${version}.jar`,
- path: `/drivers/jdbcx-${version}/jdbcx-driver-${version}.jar`,
- size: 1,
- sha256: version,
- },
- ],
- }),
- );
+ const bundles = ["0.8.0", "0.9.0"].map((version): JdbcMavenBundleInfo => ({
+ id: `jdbcx-${version}`,
+ coordinate: `io.github.jdbcx:jdbcx-driver:${version}`,
+ scope: "runtime",
+ repositories: [],
+ installed_at: "2026-07-15T00:00:00Z",
+ path: `/drivers/jdbcx-${version}`,
+ artifacts: [
+ {
+ group_id: "io.github.jdbcx",
+ artifact_id: "jdbcx-driver",
+ version,
+ classifier: "",
+ extension: "jar",
+ file_name: `jdbcx-driver-${version}.jar`,
+ path: `/drivers/jdbcx-${version}/jdbcx-driver-${version}.jar`,
+ size: 1,
+ sha256: version,
+ },
+ ],
+ }));
await expect(ensureJdbcxRuntimeDrivers(config, runtimeApi([], bundles))).rejects.toThrow("Select exactly one JDBCX bundle/runtime");
});
diff --git a/apps/desktop/src/lib/__tests__/database/managedJdbcDrivers.spec.ts b/apps/desktop/src/lib/__tests__/database/managedJdbcDrivers.spec.ts
index 49ccc415a1..a4581c5509 100644
--- a/apps/desktop/src/lib/__tests__/database/managedJdbcDrivers.spec.ts
+++ b/apps/desktop/src/lib/__tests__/database/managedJdbcDrivers.spec.ts
@@ -26,17 +26,15 @@ describe("managedJdbcDrivers", () => {
it("marks both managed drivers installed when the offline payload contains every bundle", () => {
const coordinates = [PRESTOSQL_JDBC_DRIVER_COORDINATE, ...PHOENIX_MANAGED_MAVEN_COORDINATES];
- const bundles = coordinates.map(
- (coordinate): JdbcMavenBundleInfo => ({
- id: coordinate.replace(/[^A-Za-z0-9.-]/g, "_"),
- coordinate,
- scope: "runtime",
- repositories: ["https://repo.maven.apache.org/maven2/"],
- installed_at: "2026-08-14T00:00:00Z",
- path: `/plugins/jdbc/drivers/maven/${coordinate}`,
- artifacts: [],
- }),
- );
+ const bundles = coordinates.map((coordinate): JdbcMavenBundleInfo => ({
+ id: coordinate.replace(/[^A-Za-z0-9.-]/g, "_"),
+ coordinate,
+ scope: "runtime",
+ repositories: ["https://repo.maven.apache.org/maven2/"],
+ installed_at: "2026-08-14T00:00:00Z",
+ path: `/plugins/jdbc/drivers/maven/${coordinate}`,
+ artifacts: [],
+ }));
expect(managedJdbcDriverRows(bundles, pluginStatus()).map(({ db_type, installed }) => ({ db_type, installed }))).toEqual([
{ db_type: "prestosql", installed: true },
diff --git a/apps/desktop/src/lib/dataGrid/__tests__/gridNewRowPlacement.spec.ts b/apps/desktop/src/lib/dataGrid/__tests__/gridNewRowPlacement.spec.ts
index 6ffc6bbe83..bd7f1e0aeb 100644
--- a/apps/desktop/src/lib/dataGrid/__tests__/gridNewRowPlacement.spec.ts
+++ b/apps/desktop/src/lib/dataGrid/__tests__/gridNewRowPlacement.spec.ts
@@ -159,13 +159,10 @@ describe("buildOrderedGridRows", () => {
it("merges the maximum result page without quadratic placement scans", () => {
const sourceIndices = Array.from({ length: 100_000 }, (_, index) => index);
- const meta = Array.from(
- { length: 1_000 },
- (_, index): GridNewRowMeta => ({
- token: index + 1,
- placement: { anchorId: index * 100, position: index % 2 === 0 ? "above" : "below" },
- }),
- );
+ const meta = Array.from({ length: 1_000 }, (_, index): GridNewRowMeta => ({
+ token: index + 1,
+ placement: { anchorId: index * 100, position: index % 2 === 0 ? "above" : "below" },
+ }));
const startedAt = performance.now();
const result = entries(sourceIndices, meta);
diff --git a/apps/desktop/src/lib/editor/codemirrorShowWhitespace.ts b/apps/desktop/src/lib/editor/codemirrorShowWhitespace.ts
index 6e71b2f535..1b72496033 100644
--- a/apps/desktop/src/lib/editor/codemirrorShowWhitespace.ts
+++ b/apps/desktop/src/lib/editor/codemirrorShowWhitespace.ts
@@ -38,7 +38,7 @@ export function createShowWhitespaceExtension(view: Pick {
- for (let offset = this.consumed; offset < keys.length; ) {
+ for (let offset = this.consumed; offset < keys.length;) {
if (!work.current()) return false;
const end = Math.min(offset + CHUNK, keys.length);
for (; offset < end; offset++) {
diff --git a/apps/desktop/src/lib/sql/insertValueHints.ts b/apps/desktop/src/lib/sql/insertValueHints.ts
index 6ac3f41fb0..fe331d3205 100644
--- a/apps/desktop/src/lib/sql/insertValueHints.ts
+++ b/apps/desktop/src/lib/sql/insertValueHints.ts
@@ -623,7 +623,7 @@ export function findEnclosingDollarQuoteStartInText(doc: SqlTextSlice, position:
chunkEnd = Math.min(chunkEnd + DOLLAR_QUOTE_CHUNK_TAIL_EXTENSION, end);
}
const chunk = doc.sliceString(scanned, chunkEnd);
- for (let local = 0; local < chunk.length; ) {
+ for (let local = 0; local < chunk.length;) {
const index = scanned + local;
if (index >= end) break;
if (chunk[local] === "$") {
diff --git a/apps/desktop/src/lib/sql/oracleSyntaxDiagnostics.ts b/apps/desktop/src/lib/sql/oracleSyntaxDiagnostics.ts
index f22375fef4..7b93a28643 100644
--- a/apps/desktop/src/lib/sql/oracleSyntaxDiagnostics.ts
+++ b/apps/desktop/src/lib/sql/oracleSyntaxDiagnostics.ts
@@ -178,7 +178,7 @@ function offsetToPosition(source: string, offset: number): { line: number; colum
function tokenizeOracleSyntax(source: string, baseOffset = 0): OracleSyntaxToken[] {
const tokens: OracleSyntaxToken[] = [];
- for (let index = 0; index < source.length; ) {
+ for (let index = 0; index < source.length;) {
const ignoredEnd = skipIgnorableOracleSyntax(source, index);
if (ignoredEnd !== null) {
index = ignoredEnd;
diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts
index 56c8bdcbb0..c9a3de3bcf 100644
--- a/apps/desktop/src/lib/sql/sqlFormatter.ts
+++ b/apps/desktop/src/lib/sql/sqlFormatter.ts
@@ -245,7 +245,7 @@ function restoreProtectedEmptyLines(sql: string, markers: readonly string[]): st
const markerSet = new Set(markers);
const lines = sql.split(/\r\n|\r|\n/);
- for (let index = 0; index < lines.length; ) {
+ for (let index = 0; index < lines.length;) {
if (!markerSet.has(lines[index].trim())) {
index += 1;
continue;
diff --git a/apps/desktop/src/lib/sql/sqlSyntaxTreeWindow.ts b/apps/desktop/src/lib/sql/sqlSyntaxTreeWindow.ts
index d9f684a189..56a260cc20 100644
--- a/apps/desktop/src/lib/sql/sqlSyntaxTreeWindow.ts
+++ b/apps/desktop/src/lib/sql/sqlSyntaxTreeWindow.ts
@@ -49,7 +49,7 @@ function hasUnresolvedDollarQuoteBefore(doc: Text, from: number, lookback: numbe
const scanStart = Math.max(0, from - lookback);
const slice = doc.sliceString(scanStart, from);
let openTag: string | null = null;
- for (let index = 0; index < slice.length; ) {
+ for (let index = 0; index < slice.length;) {
if (slice[index] === "$") {
const marker = matchDollarQuoteTag(slice, index);
if (marker) {
diff --git a/apps/desktop/src/stores/__tests__/queryStore.updateDrafts.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.updateDrafts.spec.ts
index be4c33af24..31a56076df 100644
--- a/apps/desktop/src/stores/__tests__/queryStore.updateDrafts.spec.ts
+++ b/apps/desktop/src/stores/__tests__/queryStore.updateDrafts.spec.ts
@@ -25,7 +25,10 @@ describe("update draft recovery", () => {
const { UPDATE_RESTORE_KEY } = await import("@/lib/app/updatePreparation");
const store = useQueryStore();
const id = store.createTab("conn", "db");
- Object.assign(store.tabs.find((tab) => tab.id === id)!, { savedSqlId: "saved", sql, originalSql: "SELECT 1;" });
+ Object.assign(
+ store.tabs.find((tab) => tab.id === id)!,
+ { savedSqlId: "saved", sql, originalSql: "SELECT 1;" },
+ );
await store.flushPendingPersist();
localStorage.setItem(UPDATE_RESTORE_KEY, "1");
setActivePinia(createPinia());
diff --git a/crates/dbx-ai-provider/assets/pi-mcp-bridge.mjs b/crates/dbx-ai-provider/assets/pi-mcp-bridge.mjs
index 49c87ddeec..0fb81e6955 100644
--- a/crates/dbx-ai-provider/assets/pi-mcp-bridge.mjs
+++ b/crates/dbx-ai-provider/assets/pi-mcp-bridge.mjs
@@ -30,11 +30,7 @@ function piContent(content) {
for (const item of content ?? []) {
if (item?.type === "text" && typeof item.text === "string") {
result.push({ type: "text", text: item.text });
- } else if (
- item?.type === "image" &&
- typeof item.data === "string" &&
- typeof item.mimeType === "string"
- ) {
+ } else if (item?.type === "image" && typeof item.data === "string" && typeof item.mimeType === "string") {
result.push({ type: "image", data: item.data, mimeType: item.mimeType });
}
}
@@ -81,9 +77,7 @@ export default async function registerDbxMcpBridge(pi) {
child.on("exit", (code, signal) => {
closed = true;
const detail = stderr.trim();
- rejectPending(
- `DBX MCP process exited (${signal ?? code ?? "unknown"})${detail ? `: ${detail}` : ""}`,
- );
+ rejectPending(`DBX MCP process exited (${signal ?? code ?? "unknown"})${detail ? `: ${detail}` : ""}`);
});
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
diff --git a/docs/scripts/preview-server.mjs b/docs/scripts/preview-server.mjs
index 009d056598..f1012bcf41 100644
--- a/docs/scripts/preview-server.mjs
+++ b/docs/scripts/preview-server.mjs
@@ -73,9 +73,7 @@ createServer(async (req, res) => {
res.writeHead(400);
return res.end();
}
- const hit =
- (await tryFile(path)) ??
- (path.endsWith("/") ? await tryFile(join(path, "index.html")) : await tryFile(`${path}.html`));
+ const hit = (await tryFile(path)) ?? (path.endsWith("/") ? await tryFile(join(path, "index.html")) : await tryFile(`${path}.html`));
if (hit) {
res.writeHead(200, { "content-type": hit.type, "cache-control": "no-store" });
return res.end(await readFile(hit.file));
diff --git a/docs/scripts/update-contributors.mjs b/docs/scripts/update-contributors.mjs
index c6de67610e..a2fa7b47d3 100644
--- a/docs/scripts/update-contributors.mjs
+++ b/docs/scripts/update-contributors.mjs
@@ -74,11 +74,7 @@ function ensureContributor(contributors, user) {
return contributor;
}
-const [repositoryInfo, commitContributors, pullRequests] = await Promise.all([
- request(`${apiBase}/repos/${repository}`).then((response) => response.json()),
- fetchAll(`/repos/${repository}/contributors?anon=0`),
- fetchAll(`/repos/${repository}/pulls?state=closed&sort=created&direction=desc`),
-]);
+const [repositoryInfo, commitContributors, pullRequests] = await Promise.all([request(`${apiBase}/repos/${repository}`).then((response) => response.json()), fetchAll(`/repos/${repository}/contributors?anon=0`), fetchAll(`/repos/${repository}/pulls?state=closed&sort=created&direction=desc`)]);
const contributors = new Map();
@@ -97,9 +93,11 @@ for (const entry of commitContributors) {
if (contributor) contributor.commits = entry.contributions || 0;
}
-const rankedContributors = [...contributors.values()].filter((contributor) => contributor.mergedPullRequests > 0).sort((left, right) => {
- return right.commits - left.commits || right.mergedPullRequests - left.mergedPullRequests || left.login.localeCompare(right.login);
-});
+const rankedContributors = [...contributors.values()]
+ .filter((contributor) => contributor.mergedPullRequests > 0)
+ .sort((left, right) => {
+ return right.commits - left.commits || right.mergedPullRequests - left.mergedPullRequests || left.login.localeCompare(right.login);
+ });
const output = {
repository,
diff --git a/docs/scripts/verify-doc-content.mjs b/docs/scripts/verify-doc-content.mjs
index e604ac94b4..77b10d4e62 100644
--- a/docs/scripts/verify-doc-content.mjs
+++ b/docs/scripts/verify-doc-content.mjs
@@ -78,10 +78,7 @@ for (const file of files) {
}
const languagePattern = LANGUAGES.join("|");
- const links = [
- ...content.matchAll(new RegExp(`\\]\\(/(${languagePattern})/docs/([^)#?]+)(?:#[^)]*)?\\)`, "g")),
- ...content.matchAll(new RegExp(`href="/(${languagePattern})/docs/([^"#?]+)(?:#[^"]*)?"`, "g")),
- ];
+ const links = [...content.matchAll(new RegExp(`\\]\\(/(${languagePattern})/docs/([^)#?]+)(?:#[^)]*)?\\)`, "g")), ...content.matchAll(new RegExp(`href="/(${languagePattern})/docs/([^"#?]+)(?:#[^"]*)?"`, "g"))];
for (const link of links) {
const [, language, slug] = link;
@@ -93,18 +90,14 @@ for (const file of files) {
const databaseSupportSource = fs.readFileSync(path.join(repositoryDir, "docs/data/databaseSupport.ts"), "utf8");
const connectionProfilesSource = fs.readFileSync(path.join(repositoryDir, "apps/desktop/src/types/generated/connectionProfiles.ts"), "utf8");
const jdbcProductProfilesSource = fs.readFileSync(path.join(repositoryDir, "apps/desktop/src/lib/database/jdbcProductProfiles.ts"), "utf8");
-const connectionOptionsMatch = connectionProfilesSource.match(
- /export const CONNECTION_PICKER_OPTIONS = \[([\s\S]*?)\n\] as const satisfies readonly ConnectionPickerOption\[\];/,
-);
+const connectionOptionsMatch = connectionProfilesSource.match(/export const CONNECTION_PICKER_OPTIONS = \[([\s\S]*?)\n\] as const satisfies readonly ConnectionPickerOption\[\];/);
const jdbcProductProfilesMatch = jdbcProductProfilesSource.match(/JDBC_PRODUCT_PROFILES = \[([^\]]*)\]/);
if (!connectionOptionsMatch) {
errors.push("Unable to read database options from connectionProfiles.ts");
} else {
const connectionModeIds = new Set(["custom_mysql", "custom_postgres"]);
- const connectionIds = [...connectionOptionsMatch[1].matchAll(/value: "([^"]+)"/g)]
- .map((match) => match[1])
- .filter((id) => !connectionModeIds.has(id));
+ const connectionIds = [...connectionOptionsMatch[1].matchAll(/value: "([^"]+)"/g)].map((match) => match[1]).filter((id) => !connectionModeIds.has(id));
if (!jdbcProductProfilesMatch) {
errors.push("Unable to read registered JDBC product profiles");
@@ -129,9 +122,7 @@ if (!connectionOptionsMatch) {
}
}
- const websiteIds = [...databaseSupportSource.matchAll(/\{ id: "([^"]+)"/g)]
- .map((match) => match[1])
- .filter((id) => id !== "request");
+ const websiteIds = [...databaseSupportSource.matchAll(/\{ id: "([^"]+)"/g)].map((match) => match[1]).filter((id) => id !== "request");
for (const [label, ids] of [
["connectionProfiles.ts", connectionIds],
diff --git a/docs/scripts/verify-static-export.mjs b/docs/scripts/verify-static-export.mjs
index 63f46a16e7..6f5d8d28cb 100644
--- a/docs/scripts/verify-static-export.mjs
+++ b/docs/scripts/verify-static-export.mjs
@@ -64,13 +64,7 @@ for (const requirement of requiredContent) {
console.log("Static export metadata verified for language, software schema, and llms.txt accuracy.");
-const localizedHtmlFiles = files.filter(
- (file) =>
- extname(file) === ".html" &&
- LANGUAGES.some(
- (lang) => file === join(outputDirectory, `${lang}.html`) || file.startsWith(`${join(outputDirectory, lang)}${sep}`),
- ),
-);
+const localizedHtmlFiles = files.filter((file) => extname(file) === ".html" && LANGUAGES.some((lang) => file === join(outputDirectory, `${lang}.html`) || file.startsWith(`${join(outputDirectory, lang)}${sep}`)));
for (const file of localizedHtmlFiles) {
const content = await readFile(file, "utf8");
diff --git a/packages/plugin-cli/scripts/stage-dev.mjs b/packages/plugin-cli/scripts/stage-dev.mjs
index 120936d84e..d891f12b9b 100644
--- a/packages/plugin-cli/scripts/stage-dev.mjs
+++ b/packages/plugin-cli/scripts/stage-dev.mjs
@@ -1,17 +1,17 @@
-import { cpSync, existsSync, rmSync } from 'node:fs';
-import { dirname, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { spawnSync } from 'node:child_process';
-const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
-const root = resolve(packageRoot, '../../plugins/sdk/dev-host');
-const destination = resolve(packageRoot, 'dev-runtime');
-if (process.argv.includes('--clean')) {
+import { cpSync, existsSync, rmSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { spawnSync } from "node:child_process";
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const root = resolve(packageRoot, "../../plugins/sdk/dev-host");
+const destination = resolve(packageRoot, "dev-runtime");
+if (process.argv.includes("--clean")) {
rmSync(destination, { recursive: true, force: true });
} else {
- if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('Building dev runtime requires Node.js 22+');
- if (!existsSync(resolve(root, 'node_modules/vite'))) throw new Error('Run npm ci --prefix plugins/sdk/dev-host before packing the CLI');
- const result = spawnSync(process.execPath, [resolve(root, 'build.mjs')], { stdio: 'inherit' });
- if (result.status !== 0) throw new Error('Development runtime build failed');
+ if (Number(process.versions.node.split(".")[0]) < 22) throw new Error("Building dev runtime requires Node.js 22+");
+ if (!existsSync(resolve(root, "node_modules/vite"))) throw new Error("Run npm ci --prefix plugins/sdk/dev-host before packing the CLI");
+ const result = spawnSync(process.execPath, [resolve(root, "build.mjs")], { stdio: "inherit" });
+ if (result.status !== 0) throw new Error("Development runtime build failed");
rmSync(destination, { recursive: true, force: true });
- cpSync(resolve(root, 'dist'), destination, { recursive: true });
+ cpSync(resolve(root, "dist"), destination, { recursive: true });
}
diff --git a/packages/plugin-cli/scripts/stage-sdk.mjs b/packages/plugin-cli/scripts/stage-sdk.mjs
index a0c7c2dfe4..c8b3916a97 100644
--- a/packages/plugin-cli/scripts/stage-sdk.mjs
+++ b/packages/plugin-cli/scripts/stage-sdk.mjs
@@ -15,9 +15,7 @@ const packageManifest = JSON.parse(readFileSync(join(packageRoot, "package.json"
const cliManifest = readFileSync(join(repositoryRoot, "plugins/sdk/cli/Cargo.toml"), "utf8");
const cliVersion = cliManifest.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
if (cliVersion !== packageManifest.version) {
- throw new Error(
- `@dbx-app/plugin-cli version ${packageManifest.version} does not match dbx-plugin-cli ${cliVersion ?? "unknown"}.`,
- );
+ throw new Error(`@dbx-app/plugin-cli version ${packageManifest.version} does not match dbx-plugin-cli ${cliVersion ?? "unknown"}.`);
}
const sources = [
@@ -43,7 +41,4 @@ for (const [sourceRelative, destinationRelative] of sources) {
});
}
-writeFileSync(
- join(destinationRoot, "README.md"),
- "# Bundled DBX Plugin SDK\n\nThis directory is bundled for `dbx-plugin package` and is managed by `@dbx-app/plugin-cli`.\n",
-);
+writeFileSync(join(destinationRoot, "README.md"), "# Bundled DBX Plugin SDK\n\nThis directory is bundled for `dbx-plugin package` and is managed by `@dbx-app/plugin-cli`.\n");
diff --git a/packages/plugin-cli/tests/dev-launcher.test.mjs b/packages/plugin-cli/tests/dev-launcher.test.mjs
index 0b082bc2a4..44da5d1a49 100644
--- a/packages/plugin-cli/tests/dev-launcher.test.mjs
+++ b/packages/plugin-cli/tests/dev-launcher.test.mjs
@@ -1,21 +1,30 @@
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import { spawnSync } from 'node:child_process';
-import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
-import { tmpdir } from 'node:os';
-import { dirname, resolve, join } from 'node:path';
-import { fileURLToPath } from 'node:url';
-const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
-test('dev selects the bundled runtime and current Node without changing ordinary launcher behavior', () => {
- const cwd = mkdtempSync(join(tmpdir(), 'dbx-dev-launcher-'));
+import test from "node:test";
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, resolve, join } from "node:path";
+import { fileURLToPath } from "node:url";
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+test("dev selects the bundled runtime and current Node without changing ordinary launcher behavior", () => {
+ const cwd = mkdtempSync(join(tmpdir(), "dbx-dev-launcher-"));
try {
- writeFileSync(join(cwd, 'dev'), 'console.log(JSON.stringify({runtime:process.env.DBX_PLUGIN_DEV_RUNTIME,node:process.env.DBX_PLUGIN_NODE,args:process.argv.slice(2)}))');
- const invoke = extra => spawnSync(process.execPath, [join(root, 'bin/dbx-plugin.js'), 'dev', '--port', '5190'], {
- cwd, encoding: 'utf8', env: { ...process.env, DBX_PLUGIN_CLI_BINARY: process.execPath, DBX_PLUGIN_NODE: '', DBX_PLUGIN_DEV_RUNTIME: '', ...extra },
- });
- const result = invoke({}); assert.equal(result.status, 0, result.stderr);
- const data = JSON.parse(result.stdout); assert.equal(data.runtime, join(root, 'dev-runtime/runtime.mjs')); assert.equal(data.node, process.execPath); assert.deepEqual(data.args, ['--port','5190']);
- const explicit = invoke({ DBX_PLUGIN_DEV_RUNTIME: '/custom/runtime.mjs', DBX_PLUGIN_NODE: '/custom/node' });
- assert.equal(JSON.parse(explicit.stdout).runtime, '/custom/runtime.mjs');
- } finally { rmSync(cwd, { recursive: true, force: true }); }
+ writeFileSync(join(cwd, "dev"), "console.log(JSON.stringify({runtime:process.env.DBX_PLUGIN_DEV_RUNTIME,node:process.env.DBX_PLUGIN_NODE,args:process.argv.slice(2)}))");
+ const invoke = (extra) =>
+ spawnSync(process.execPath, [join(root, "bin/dbx-plugin.js"), "dev", "--port", "5190"], {
+ cwd,
+ encoding: "utf8",
+ env: { ...process.env, DBX_PLUGIN_CLI_BINARY: process.execPath, DBX_PLUGIN_NODE: "", DBX_PLUGIN_DEV_RUNTIME: "", ...extra },
+ });
+ const result = invoke({});
+ assert.equal(result.status, 0, result.stderr);
+ const data = JSON.parse(result.stdout);
+ assert.equal(data.runtime, join(root, "dev-runtime/runtime.mjs"));
+ assert.equal(data.node, process.execPath);
+ assert.deepEqual(data.args, ["--port", "5190"]);
+ const explicit = invoke({ DBX_PLUGIN_DEV_RUNTIME: "/custom/runtime.mjs", DBX_PLUGIN_NODE: "/custom/node" });
+ assert.equal(JSON.parse(explicit.stdout).runtime, "/custom/runtime.mjs");
+ } finally {
+ rmSync(cwd, { recursive: true, force: true });
+ }
});
diff --git a/packages/plugin-cli/tests/launcher.test.mjs b/packages/plugin-cli/tests/launcher.test.mjs
index 3edf722547..de7ff6535a 100644
--- a/packages/plugin-cli/tests/launcher.test.mjs
+++ b/packages/plugin-cli/tests/launcher.test.mjs
@@ -14,18 +14,14 @@ test("forwards arguments and provides the bundled SDK root", () => {
const stage = spawnSync(process.execPath, [stageSdk], { encoding: "utf8" });
assert.equal(stage.status, 0, stage.stderr);
try {
- const result = spawnSync(
- process.execPath,
- [launcher, "-e", "process.stdout.write(process.env.DBX_PLUGIN_SDK_ROOT || '')"],
- {
- encoding: "utf8",
- env: {
- ...process.env,
- DBX_PLUGIN_CLI_BINARY: process.execPath,
- DBX_PLUGIN_SDK_ROOT: "",
- },
+ const result = spawnSync(process.execPath, [launcher, "-e", "process.stdout.write(process.env.DBX_PLUGIN_SDK_ROOT || '')"], {
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ DBX_PLUGIN_CLI_BINARY: process.execPath,
+ DBX_PLUGIN_SDK_ROOT: "",
},
- );
+ });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, sdkRoot);
assert.ok(existsSync(join(sdkRoot, "plugins", "sdk", "rust", "dbx-plugin-sdk", "Cargo.toml")));
@@ -36,18 +32,14 @@ test("forwards arguments and provides the bundled SDK root", () => {
});
test("preserves an explicit SDK root", () => {
- const result = spawnSync(
- process.execPath,
- [launcher, "-e", "process.stdout.write(process.env.DBX_PLUGIN_SDK_ROOT || '')"],
- {
- encoding: "utf8",
- env: {
- ...process.env,
- DBX_PLUGIN_CLI_BINARY: process.execPath,
- DBX_PLUGIN_SDK_ROOT: "/custom/dbx-plugin-sdk",
- },
+ const result = spawnSync(process.execPath, [launcher, "-e", "process.stdout.write(process.env.DBX_PLUGIN_SDK_ROOT || '')"], {
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ DBX_PLUGIN_CLI_BINARY: process.execPath,
+ DBX_PLUGIN_SDK_ROOT: "/custom/dbx-plugin-sdk",
},
- );
+ });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "/custom/dbx-plugin-sdk");
});
diff --git a/plugins/examples/hello-workbench/package.mjs b/plugins/examples/hello-workbench/package.mjs
index 89a7a3a496..27fb859a6d 100644
--- a/plugins/examples/hello-workbench/package.mjs
+++ b/plugins/examples/hello-workbench/package.mjs
@@ -33,22 +33,7 @@ try {
await cp(path.join(root, "assets"), path.join(stage, "assets"), { recursive: true });
await cp(path.join(root, "ui"), path.join(stage, "ui"), { recursive: true });
- const packagerArgs = [
- "run",
- "--locked",
- "--release",
- "--manifest-path",
- path.join(root, "..", "..", "sdk", "packager", "Cargo.toml"),
- "--",
- stage,
- output,
- "--artifact-metadata",
- artifactMetadata,
- "--target",
- target,
- "--artifact-url",
- path.basename(output),
- ];
+ const packagerArgs = ["run", "--locked", "--release", "--manifest-path", path.join(root, "..", "..", "sdk", "packager", "Cargo.toml"), "--", stage, output, "--artifact-metadata", artifactMetadata, "--target", target, "--artifact-url", path.basename(output)];
run("cargo", packagerArgs, { CARGO_TARGET_DIR: packagerTarget });
console.log(`Built unsigned candidate ${output}`);
} finally {
diff --git a/plugins/examples/hello-workbench/repository-sign.mjs b/plugins/examples/hello-workbench/repository-sign.mjs
index a822cf54c5..e3f9a81b5a 100644
--- a/plugins/examples/hello-workbench/repository-sign.mjs
+++ b/plugins/examples/hello-workbench/repository-sign.mjs
@@ -8,9 +8,7 @@ import { fileURLToPath } from "node:url";
const root = path.dirname(fileURLToPath(import.meta.url));
const target = process.env.DBX_PLUGIN_TARGET || platformTarget();
const candidate = path.resolve(process.argv[2] || path.join(root, "dist", `dbx.example.hello-1.0.0-${target}.dbxp`));
-const signed = path.resolve(
- process.argv[3] || path.join(root, "dist", `dbx.example.hello-1.0.0-${target}.signed.dbxp`),
-);
+const signed = path.resolve(process.argv[3] || path.join(root, "dist", `dbx.example.hello-1.0.0-${target}.signed.dbxp`));
const metadata = signed.replace(/\.dbxp$/, ".artifact.json");
const keyId = process.env.DBX_PLUGIN_SIGNING_KEY_ID;
const buildRoot = await mkdtemp(path.join(os.tmpdir(), "dbx-repository-sign-"));
@@ -22,25 +20,7 @@ if (!process.env.DBX_PLUGIN_SIGNING_KEY || !keyId) {
try {
run(
"cargo",
- [
- "run",
- "--locked",
- "--release",
- "--manifest-path",
- path.join(root, "..", "..", "sdk", "packager", "Cargo.toml"),
- "--",
- "sign",
- candidate,
- signed,
- "--key-id",
- keyId,
- "--artifact-metadata",
- metadata,
- "--target",
- target,
- "--artifact-url",
- `../hello-workbench/dist/${path.basename(signed)}`,
- ],
+ ["run", "--locked", "--release", "--manifest-path", path.join(root, "..", "..", "sdk", "packager", "Cargo.toml"), "--", "sign", candidate, signed, "--key-id", keyId, "--artifact-metadata", metadata, "--target", target, "--artifact-url", `../hello-workbench/dist/${path.basename(signed)}`],
{ CARGO_TARGET_DIR: path.join(buildRoot, "cargo-target") },
);
diff --git a/plugins/examples/hello-workbench/repository-smoke.mjs b/plugins/examples/hello-workbench/repository-smoke.mjs
index 357b63d28b..7bf00d0aed 100644
--- a/plugins/examples/hello-workbench/repository-smoke.mjs
+++ b/plugins/examples/hello-workbench/repository-smoke.mjs
@@ -16,22 +16,7 @@ const signed = path.join(temporary, `dbx.example.hello-1.0.0-${target}.signed.db
try {
run(process.execPath, [path.join(root, "package.mjs")], { DBX_PLUGIN_OUTPUT_DIR: candidateDirectory });
- run(
- "cargo",
- [
- "run",
- "--quiet",
- "--locked",
- "--manifest-path",
- path.join(repositoryRoot, "plugins", "sdk", "cli", "Cargo.toml"),
- "--",
- "keygen",
- "example-repository-smoke",
- "--output",
- keyFile,
- ],
- { CARGO_TARGET_DIR: path.join(temporary, "cli-target") },
- );
+ run("cargo", ["run", "--quiet", "--locked", "--manifest-path", path.join(repositoryRoot, "plugins", "sdk", "cli", "Cargo.toml"), "--", "keygen", "example-repository-smoke", "--output", keyFile], { CARGO_TARGET_DIR: path.join(temporary, "cli-target") });
const signingEnvironment = await readEnvironmentFile(keyFile);
run(process.execPath, [path.join(root, "repository-sign.mjs"), candidate, signed], {
...signingEnvironment,
diff --git a/scripts/ai-html-runtime-canary.mjs b/scripts/ai-html-runtime-canary.mjs
index dd5ed3d672..107cdbebf8 100644
--- a/scripts/ai-html-runtime-canary.mjs
+++ b/scripts/ai-html-runtime-canary.mjs
@@ -35,26 +35,14 @@ import { fileURLToPath, pathToFileURL } from "node:url";
const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "..", "..");
const CANARY_SPEC = "apps/desktop/src/lib/ai/richContent/__tests__/aiHtmlPreviewCanary.spec.ts";
-const HEADLESS_ARGS = [
- "--headless=new",
- "--disable-gpu",
- "--no-first-run",
- "--no-default-browser-check",
-];
+const HEADLESS_ARGS = ["--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check"];
-const BROWSER_CANDIDATES = process.platform === "win32"
- ? [
- "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
- "C:/Program Files/Microsoft/Edge/Application/msedge.exe",
- "C:/Program Files/Google/Chrome/Application/chrome.exe",
- "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
- ]
- : process.platform === "darwin"
- ? [
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
- ]
- : ["msedge", "google-chrome", "chromium", "chromium-browser"];
+const BROWSER_CANDIDATES =
+ process.platform === "win32"
+ ? ["C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "C:/Program Files/Microsoft/Edge/Application/msedge.exe", "C:/Program Files/Google/Chrome/Application/chrome.exe", "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
+ : process.platform === "darwin"
+ ? ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]
+ : ["msedge", "google-chrome", "chromium", "chromium-browser"];
function browserCandidates() {
const override = process.env.AI_HTML_CANARY_BROWSER;
@@ -116,14 +104,7 @@ function extractWrappedPayload(harness) {
function runHeadless(browser, filePath, userDataDir, netlogPath) {
const fileUrl = pathToFileURL(filePath).href;
const dumpPath = `${netlogPath}.dom.html`;
- const args = [
- ...HEADLESS_ARGS,
- `--user-data-dir=${userDataDir}`,
- `--log-net-log=${netlogPath}`,
- "--virtual-time-budget=8000",
- "--dump-dom",
- fileUrl,
- ];
+ const args = [...HEADLESS_ARGS, `--user-data-dir=${userDataDir}`, `--log-net-log=${netlogPath}`, "--virtual-time-budget=8000", "--dump-dom", fileUrl];
let dom = "";
try {
dom = execFileSync(browser, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
diff --git a/scripts/bench/mongodb-count.mjs b/scripts/bench/mongodb-count.mjs
index 3b4c3a42c9..4d779ab979 100644
--- a/scripts/bench/mongodb-count.mjs
+++ b/scripts/bench/mongodb-count.mjs
@@ -156,20 +156,7 @@ async function ensureMongoContainer(options) {
await run("docker", ["inspect", options.container]);
await run("docker", ["start", options.container]);
} catch {
- await run("docker", [
- "run",
- "-d",
- "--name",
- options.container,
- "--platform",
- "linux/amd64",
- "-p",
- `${options.host}:${options.port}:27017`,
- options.image,
- "--nojournal",
- "--wiredTigerCacheSizeGB",
- "0.5",
- ]);
+ await run("docker", ["run", "-d", "--name", options.container, "--platform", "linux/amd64", "-p", `${options.host}:${options.port}:27017`, options.image, "--nojournal", "--wiredTigerCacheSizeGB", "0.5"]);
}
const deadline = Date.now() + 30_000;
@@ -200,9 +187,7 @@ async function seedMongo(options) {
return { skipped: true, count: current, elapsedMs: 0 };
}
if (!options.seed && !options.forceSeed) {
- throw new Error(
- `Collection count is ${current}, expected ${options.expectedCount}. Re-run with --seed to create the benchmark dataset.`,
- );
+ throw new Error(`Collection count is ${current}, expected ${options.expectedCount}. Re-run with --seed to create the benchmark dataset.`);
}
const startedAt = performance.now();
@@ -408,9 +393,7 @@ function printReport(result) {
console.log("| Case | Value | Payload | Min | P50 | P90 | Max | Avg |");
console.log("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
for (const row of result.measurements) {
- console.log(
- `| ${row.label} | ${row.value} | ${formatBytes(row.payloadBytes)} | ${formatMs(row.minMs)} | ${formatMs(row.p50Ms)} | ${formatMs(row.p90Ms)} | ${formatMs(row.maxMs)} | ${formatMs(row.avgMs)} |`,
- );
+ console.log(`| ${row.label} | ${row.value} | ${formatBytes(row.payloadBytes)} | ${formatMs(row.minMs)} | ${formatMs(row.p50Ms)} | ${formatMs(row.p90Ms)} | ${formatMs(row.maxMs)} | ${formatMs(row.avgMs)} |`);
}
}
diff --git a/scripts/bench/redis-key-search.mjs b/scripts/bench/redis-key-search.mjs
index 796df9ed3a..1ea81a3a38 100644
--- a/scripts/bench/redis-key-search.mjs
+++ b/scripts/bench/redis-key-search.mjs
@@ -132,20 +132,7 @@ async function ensureRedisContainer(options) {
await run("docker", ["inspect", options.container]);
await run("docker", ["start", options.container]);
} catch {
- await run("docker", [
- "run",
- "-d",
- "--name",
- options.container,
- "-p",
- `${options.host}:${options.port}:6379`,
- options.image,
- "redis-server",
- "--save",
- "",
- "--appendonly",
- "no",
- ]);
+ await run("docker", ["run", "-d", "--name", options.container, "-p", `${options.host}:${options.port}:6379`, options.image, "redis-server", "--save", "", "--appendonly", "no"]);
}
const deadline = Date.now() + 15_000;
diff --git a/scripts/benchmark-cli.mjs b/scripts/benchmark-cli.mjs
index 4f4d11d669..86b5853eb1 100644
--- a/scripts/benchmark-cli.mjs
+++ b/scripts/benchmark-cli.mjs
@@ -66,14 +66,24 @@ for (const benchmark of cases) {
run(legacyBinary, benchmark.args);
const samples = { rust: [], legacy: [] };
for (let index = 0; index < benchmark.iterations; index += 1) {
- const order = index % 2 === 0 ? [["rust", rustBinary], ["legacy", legacyBinary]] : [["legacy", legacyBinary], ["rust", rustBinary]];
+ const order =
+ index % 2 === 0
+ ? [
+ ["rust", rustBinary],
+ ["legacy", legacyBinary],
+ ]
+ : [
+ ["legacy", legacyBinary],
+ ["rust", rustBinary],
+ ];
for (const [runtime, binary] of order) samples[runtime].push(run(binary, benchmark.args));
}
- for (const [runtime, binary] of [["rust", rustBinary], ["legacy", legacyBinary]]) {
+ for (const [runtime, binary] of [
+ ["rust", rustBinary],
+ ["legacy", legacyBinary],
+ ]) {
const stats = summarize(samples[runtime]);
const rss = peakRss(binary, benchmark.args);
- console.log(
- `${benchmark.name}\t${runtime}\t${stats.median.toFixed(1)}\t${stats.p90.toFixed(1)}\t${stats.p95.toFixed(1)}\t${stats.mean.toFixed(1)}\t${rss.toFixed(1)}`,
- );
+ console.log(`${benchmark.name}\t${runtime}\t${stats.median.toFixed(1)}\t${stats.p90.toFixed(1)}\t${stats.p95.toFixed(1)}\t${stats.mean.toFixed(1)}\t${rss.toFixed(1)}`);
}
}
diff --git a/scripts/build-knowledge-base.mjs b/scripts/build-knowledge-base.mjs
index 10aaa97b82..ffc4f9234b 100644
--- a/scripts/build-knowledge-base.mjs
+++ b/scripts/build-knowledge-base.mjs
@@ -14,9 +14,7 @@ async function main() {
const lang = args.lang ?? DEFAULT_LANG;
const fileSuffix = lang ? `.${lang}.mdx` : ".mdx";
- const files = (await fs.readdir(inputDir))
- .filter((name) => name.endsWith(fileSuffix))
- .sort((a, b) => a.localeCompare(b));
+ const files = (await fs.readdir(inputDir)).filter((name) => name.endsWith(fileSuffix)).sort((a, b) => a.localeCompare(b));
if (files.length === 0) {
throw new Error(`No ${fileSuffix} files found in ${inputDir}`);
@@ -104,7 +102,10 @@ function parseFrontmatter(source) {
const separator = line.indexOf(":");
if (separator === -1) continue;
const key = line.slice(0, separator).trim();
- const value = line.slice(separator + 1).trim().replace(/^["']|["']$/g, "");
+ const value = line
+ .slice(separator + 1)
+ .trim()
+ .replace(/^["']|["']$/g, "");
fields[key] = value;
}
@@ -234,21 +235,10 @@ function keywordsFor(category, heading, description, text) {
}
function renderMarkdown(documents) {
- const lines = [
- "# DBX 中文知识库",
- "",
- "> 从 `docs/content/docs/*.cn.mdx` 生成的纯 Markdown 知识库内容。",
- "",
- "## 来源索引",
- "",
- "| 来源文件 | 标题 | 描述 | 知识条数 |",
- "| --- | --- | --- | ---: |",
- ];
+ const lines = ["# DBX 中文知识库", "", "> 从 `docs/content/docs/*.cn.mdx` 生成的纯 Markdown 知识库内容。", "", "## 来源索引", "", "| 来源文件 | 标题 | 描述 | 知识条数 |", "| --- | --- | --- | ---: |"];
for (const document of documents) {
- lines.push(
- `| ${escapeTableCell(document.source)} | ${escapeTableCell(document.title)} | ${escapeTableCell(document.description)} | ${document.sections.length} |`,
- );
+ lines.push(`| ${escapeTableCell(document.source)} | ${escapeTableCell(document.title)} | ${escapeTableCell(document.description)} | ${document.sections.length} |`);
}
for (const document of documents) {
@@ -266,7 +256,10 @@ function renderMarkdown(documents) {
}
}
- return `${lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trim()}\n`;
+ return `${lines
+ .join("\n")
+ .replace(/\n{4,}/g, "\n\n\n")
+ .trim()}\n`;
}
function escapeTableCell(value) {
diff --git a/scripts/check-mq-integration.mjs b/scripts/check-mq-integration.mjs
index 56ec9464de..eab42d5104 100644
--- a/scripts/check-mq-integration.mjs
+++ b/scripts/check-mq-integration.mjs
@@ -25,9 +25,15 @@ assertIncludes("apps/desktop/src/stores/queryStore.ts", "openMqAdmin", "Query st
const manifest = JSON.parse(read("crates/dbx-core/assets/database-drivers.manifest.json"));
const drivers = Array.isArray(manifest) ? manifest : manifest.drivers;
-assert.ok(drivers?.some((driver) => driver.dbType === "mq"), "Driver manifest must include dbType=mq.");
+assert.ok(
+ drivers?.some((driver) => driver.dbType === "mq"),
+ "Driver manifest must include dbType=mq.",
+);
const mqDriver = drivers.find((driver) => driver.dbType === "mq");
-assert.ok(mqDriver.driverProfiles?.some((profile) => profile.profile === "rabbitmq"), "MQ driver manifest entry must include a rabbitmq driver profile.");
+assert.ok(
+ mqDriver.driverProfiles?.some((profile) => profile.profile === "rabbitmq"),
+ "MQ driver manifest entry must include a rabbitmq driver profile.",
+);
const mqHttp = read("apps/desktop/src/lib/backend/mq-http.ts");
assert.ok(!mqHttp.includes('post("/mq/'), "MQ HTTP client must not call unprefixed /mq paths.");
@@ -80,16 +86,8 @@ for (const panel of ["PoliciesPanel.vue", "PermissionsPanel.vue", "RawApiPanel.v
for (const panel of ["TenantsPanel.vue", "NamespacesPanel.vue", "TopicsPanel.vue", "SubscriptionsPanel.vue"]) {
assertIncludes(`apps/desktop/src/components/mq/${panel}`, "readOnly", `${panel} must disable mutating actions in read-only mode.`);
}
-assertIncludes(
- "apps/desktop/src/components/mq/TopicsPanel.vue",
- "topics-table-hscroll",
- "TopicsPanel must keep a shared horizontal scroller so wide RocketMQ action columns stay reachable.",
-);
-assertIncludes(
- "apps/desktop/src/components/mq/TopicsPanel.vue",
- "overflow-x: auto",
- "TopicsPanel horizontal scroller must allow overflow-x.",
-);
+assertIncludes("apps/desktop/src/components/mq/TopicsPanel.vue", "topics-table-hscroll", "TopicsPanel must keep a shared horizontal scroller so wide RocketMQ action columns stay reachable.");
+assertIncludes("apps/desktop/src/components/mq/TopicsPanel.vue", "overflow-x: auto", "TopicsPanel horizontal scroller must allow overflow-x.");
for (const panel of ["ExchangesPanel.vue", "SendMessagePanel.vue", "rabbitmq/RabbitMqClientsPanel.vue", "ProducerConsumerPanel.vue"]) {
assertIncludes(`apps/desktop/src/components/mq/${panel}`, "readOnly", `${panel} must disable mutating actions in read-only mode.`);
}
diff --git a/scripts/core-architecture.test.mjs b/scripts/core-architecture.test.mjs
index c170bcefae..e158205586 100644
--- a/scripts/core-architecture.test.mjs
+++ b/scripts/core-architecture.test.mjs
@@ -7,11 +7,13 @@ import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../", import.meta.url));
const read = (name) => readFileSync(path.join(root, name), "utf8");
-const metadata = JSON.parse(execFileSync("cargo", ["metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"], {
- cwd: root,
- encoding: "utf8",
- maxBuffer: 8 * 1024 * 1024,
-}));
+const metadata = JSON.parse(
+ execFileSync("cargo", ["metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"], {
+ cwd: root,
+ encoding: "utf8",
+ maxBuffer: 8 * 1024 * 1024,
+ }),
+);
const members = new Map(metadata.packages.filter((pkg) => metadata.workspace_members.includes(pkg.id)).map((pkg) => [pkg.name, pkg]));
const allowedDependencies = {
"dbx-types": [],
@@ -32,11 +34,7 @@ const allowedDependencies = {
"dbx-driver-postgres": ["dbx-types", "dbx-sql-core", "dbx-driver-support"],
"dbx-driver-redis": ["dbx-types", "dbx-driver-support"],
"dbx-driver-sqlserver": ["dbx-types", "dbx-sql-core", "dbx-sql-data", "dbx-driver-support"],
- "dbx-drivers": [
- "dbx-types", "dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-platform", "dbx-sqlite-worker",
- "dbx-driver-support", "dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb",
- "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver",
- ],
+ "dbx-drivers": ["dbx-types", "dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-platform", "dbx-sqlite-worker", "dbx-driver-support", "dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb", "dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver"],
};
const core = members.get("dbx-core");
@@ -58,12 +56,27 @@ for (const [name, allowed] of Object.entries(allowedDependencies)) {
test("core owns application orchestration in real business directories", () => {
const ownedFiles = [
- "connection/mod.rs", "connection/connection_secrets.rs", "connection/driver_runtime.rs",
- "query/mod.rs", "query/query_cancel.rs", "query/document_ops.rs",
- "schema/mod.rs", "schema/table_structure_sql/sqlite_rebuild.rs",
- "data/transfer.rs", "data/table_import.rs", "data/docs/export.rs", "data/cloudflare_d1/import.rs",
- "ai/agent_loop.rs", "ai/agent_tools.rs", "admin/consul/mod.rs", "admin/mq/mod.rs", "admin/mqtt/mod.rs",
- "persistence/storage.rs", "persistence/config/mod.rs", "safety/production_safety.rs", "host/update.rs",
+ "connection/mod.rs",
+ "connection/connection_secrets.rs",
+ "connection/driver_runtime.rs",
+ "query/mod.rs",
+ "query/query_cancel.rs",
+ "query/document_ops.rs",
+ "schema/mod.rs",
+ "schema/table_structure_sql/sqlite_rebuild.rs",
+ "data/transfer.rs",
+ "data/table_import.rs",
+ "data/docs/export.rs",
+ "data/cloudflare_d1/import.rs",
+ "ai/agent_loop.rs",
+ "ai/agent_tools.rs",
+ "admin/consul/mod.rs",
+ "admin/mq/mod.rs",
+ "admin/mqtt/mod.rs",
+ "persistence/storage.rs",
+ "persistence/config/mod.rs",
+ "safety/production_safety.rs",
+ "host/update.rs",
];
for (const name of ownedFiles) {
assert.ok(existsSync(path.join(root, "crates/dbx-core/src", name)), `missing owned business source: ${name}`);
@@ -97,10 +110,7 @@ test("code generators and runtime assets belong to their implementation crates",
assert.match(read("crates/dbx-sql-dialect/build.rs"), /plugins\/dialects/);
assert.match(read("crates/dbx-ai-provider/src/ai_pi_agent_cli.rs"), /assets\/pi-mcp-bridge\.mjs/);
assert.ok(existsSync(path.join(root, "crates/dbx-ai-provider/assets/pi-mcp-bridge.mjs")));
- assert.deepEqual(
- JSON.parse(read("crates/dbx-driver-agent/assets/agent-protocol-v2.json")),
- JSON.parse(read("agents/common/src/main/resources/agent-protocol-v2.json")),
- );
+ assert.deepEqual(JSON.parse(read("crates/dbx-driver-agent/assets/agent-protocol-v2.json")), JSON.parse(read("agents/common/src/main/resources/agent-protocol-v2.json")));
assert.match(read("crates/dbx-platform/src/lib.rs"), /#\[cfg\(all\(target_os = "windows", target_env = "gnu"\)\)\]\s*mod nanosleep_stub/);
});
@@ -131,9 +141,12 @@ test("CI and release tracking follow the new source owners", () => {
});
test("the standalone DuckDB lockfile includes core's internal dependency closure", () => {
- const lockedPackages = new Map(read("agents/drivers/duckdb/Cargo.lock")
- .split("\n[[package]]\n").slice(1)
- .map((block) => [block.match(/^name = "([^"]+)"$/m)[1], block]));
+ const lockedPackages = new Map(
+ read("agents/drivers/duckdb/Cargo.lock")
+ .split("\n[[package]]\n")
+ .slice(1)
+ .map((block) => [block.match(/^name = "([^"]+)"$/m)[1], block]),
+ );
const visited = new Set();
function inspect(name) {
if (visited.has(name)) return;
diff --git a/scripts/database-env.mjs b/scripts/database-env.mjs
index 46637ec1f5..840c60327c 100644
--- a/scripts/database-env.mjs
+++ b/scripts/database-env.mjs
@@ -1,15 +1,15 @@
#!/usr/bin/env node
-import { existsSync, readFileSync, readdirSync } from 'node:fs';
-import { basename, dirname, join, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { spawnSync } from 'node:child_process';
+import { existsSync, readFileSync, readdirSync } from "node:fs";
+import { basename, dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { spawnSync } from "node:child_process";
-const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
-export const DEFAULT_RECIPES_ROOT = join(REPO_ROOT, 'deploy', 'database');
-const DEFAULT_MAKEFILE_PATH = join(REPO_ROOT, 'Makefile');
-const DEFAULT_PASSWORD = '123456';
-const DEFAULT_DATABASE = 'dbx';
+const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+export const DEFAULT_RECIPES_ROOT = join(REPO_ROOT, "deploy", "database");
+const DEFAULT_MAKEFILE_PATH = join(REPO_ROOT, "Makefile");
+const DEFAULT_PASSWORD = "123456";
+const DEFAULT_DATABASE = "dbx";
const DEFAULT_HOST_PORT_RANGES = {
mysql: [10100, 10199],
mariadb: [10200, 10299],
@@ -28,18 +28,18 @@ const DEFAULT_HOST_PORT_RANGES = {
elasticsearch: [11500, 11599],
};
const DBX_DEEP_LINK_TYPES = {
- clickhouse: 'clickhouse',
- consul: 'consul',
- elasticsearch: 'elasticsearch',
- etcd: 'etcd',
- mariadb: 'mariadb',
- mongodb: 'mongodb',
- mysql: 'mysql',
- postgresql: 'postgres',
- qdrant: 'qdrant',
- redis: 'redis',
- rnacos: 'r-nacos',
- zookeeper: 'zookeeper',
+ clickhouse: "clickhouse",
+ consul: "consul",
+ elasticsearch: "elasticsearch",
+ etcd: "etcd",
+ mariadb: "mariadb",
+ mongodb: "mongodb",
+ mysql: "mysql",
+ postgresql: "postgres",
+ qdrant: "qdrant",
+ redis: "redis",
+ rnacos: "r-nacos",
+ zookeeper: "zookeeper",
};
export function discoverRecipes(root = DEFAULT_RECIPES_ROOT) {
@@ -50,13 +50,13 @@ export function discoverRecipes(root = DEFAULT_RECIPES_ROOT) {
const databaseDir = join(root, database.name);
for (const version of readdirSync(databaseDir, { withFileTypes: true })) {
if (!version.isDirectory()) continue;
- const recipePath = join(databaseDir, version.name, 'recipe.json');
+ const recipePath = join(databaseDir, version.name, "recipe.json");
if (!existsSync(recipePath)) continue;
- const recipe = JSON.parse(readFileSync(recipePath, 'utf8'));
+ const recipe = JSON.parse(readFileSync(recipePath, "utf8"));
recipes.push({ ...recipe, directory: dirname(recipePath), recipePath });
}
}
- return recipes.sort((a, b) => `${a.database}/${a.displayVersion}`.localeCompare(`${b.database}/${b.displayVersion}`, 'en'));
+ return recipes.sort((a, b) => `${a.database}/${a.displayVersion}`.localeCompare(`${b.database}/${b.displayVersion}`, "en"));
}
export function recipeSelector(recipe) {
@@ -64,37 +64,34 @@ export function recipeSelector(recipe) {
}
export function formatTable(headers, rows) {
- const widths = headers.map((header, index) => Math.max(
- header.length,
- ...rows.map((row) => String(row[index] ?? '').length),
- ));
- const renderRow = (row) => row.map((value, index) => {
- const text = String(value ?? '');
- return index === row.length - 1 ? text : text.padEnd(widths[index]);
- }).join(' ');
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => String(row[index] ?? "").length)));
+ const renderRow = (row) =>
+ row
+ .map((value, index) => {
+ const text = String(value ?? "");
+ return index === row.length - 1 ? text : text.padEnd(widths[index]);
+ })
+ .join(" ");
- return [headers, ...rows].map(renderRow).join('\n');
+ return [headers, ...rows].map(renderRow).join("\n");
}
export function formatBorderedTable(headers, rows, groupByFirstColumn = false) {
- const widths = headers.map((header, index) => Math.max(
- header.length,
- ...rows.map((row) => String(row[index] ?? '').length),
- ));
- const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
- const renderRow = (row) => `| ${row.map((value, index) => String(value ?? '').padEnd(widths[index])).join(' | ')} |`;
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => String(row[index] ?? "").length)));
+ const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
+ const renderRow = (row) => `| ${row.map((value, index) => String(value ?? "").padEnd(widths[index])).join(" | ")} |`;
const body = [];
for (const [index, row] of rows.entries()) {
body.push(renderRow(row));
- if (groupByFirstColumn && index < rows.length - 1 && rows[index + 1][0] !== '') body.push(border);
+ if (groupByFirstColumn && index < rows.length - 1 && rows[index + 1][0] !== "") body.push(border);
}
- return [border, renderRow(headers), border, ...body, border].join('\n');
+ return [border, renderRow(headers), border, ...body, border].join("\n");
}
export function discoverMakeTargets(makefilePath = DEFAULT_MAKEFILE_PATH) {
if (!existsSync(makefilePath)) return [];
- const source = readFileSync(makefilePath, 'utf8').replace(/\\\r?\n/g, ' ');
+ const source = readFileSync(makefilePath, "utf8").replace(/\\\r?\n/g, " ");
const targets = [];
for (const line of source.split(/\r?\n/)) {
const match = line.match(/^\.PHONY:\s*(.+)$/);
@@ -104,54 +101,54 @@ export function discoverMakeTargets(makefilePath = DEFAULT_MAKEFILE_PATH) {
}
export function platformForArchitecture(architecture = process.arch) {
- if (architecture === 'x64') return 'linux/amd64';
- if (architecture === 'arm64') return 'linux/arm64';
+ if (architecture === "x64") return "linux/amd64";
+ if (architecture === "arm64") return "linux/arm64";
return null;
}
export function architectureWarning(recipe, architecture = process.arch) {
const platform = platformForArchitecture(architecture);
if (!platform || recipe.platforms?.includes(platform)) return null;
- return `${recipe.image} does not support ${platform}. Docker must use ${recipe.platforms.join(', ')} emulation and startup may be slower.`;
+ return `${recipe.image} does not support ${platform}. Docker must use ${recipe.platforms.join(", ")} emulation and startup may be slower.`;
}
function printQuickStart(recipes) {
- console.log('Database test environments');
- console.log('Copy one command to start an environment:');
- console.log('');
+ console.log("Database test environments");
+ console.log("Copy one command to start an environment:");
+ console.log("");
for (const recipe of recipes) console.log(` make db DB=${recipeSelector(recipe)}`);
- console.log('');
- console.log('Optional parameters:');
- console.log(' DB_BIND_ADDRESS= Override the default bind address (127.0.0.1).');
- console.log(' DB_PORT= Override the default host port.');
- console.log(' DB_PASSWORD= Override the default password (123456).');
- console.log('');
- console.log('Next steps: make db-verify DB=@, make db-down DB=@');
- console.log('Tab completion: make db-completion');
+ console.log("");
+ console.log("Optional parameters:");
+ console.log(" DB_BIND_ADDRESS= Override the default bind address (127.0.0.1).");
+ console.log(" DB_PORT= Override the default host port.");
+ console.log(" DB_PASSWORD= Override the default password (123456).");
+ console.log("");
+ console.log("Next steps: make db-verify DB=@, make db-down DB=@");
+ console.log("Tab completion: make db-completion");
}
function printCompletionSetup() {
- console.log('Bash: source deploy/database/completion/dbx-make.bash');
- console.log('Zsh: autoload -Uz compinit && compinit && source deploy/database/completion/_dbx-make.zsh');
- console.log('PowerShell: . .\\deploy\\database\\completion\\Dbx.Make.ps1');
+ console.log("Bash: source deploy/database/completion/dbx-make.bash");
+ console.log("Zsh: autoload -Uz compinit && compinit && source deploy/database/completion/_dbx-make.zsh");
+ console.log("PowerShell: . .\\deploy\\database\\completion\\Dbx.Make.ps1");
}
export function resolveRecipe(recipes, database, version) {
- if (!database) throw new Error('DB is required. Run `make db-list` to see available environments.');
+ if (!database) throw new Error("DB is required. Run `make db-list` to see available environments.");
const matches = recipes.filter((item) => item.database === database && (!version || item.displayVersion === version || item.version === version));
- if (matches.length === 0) throw new Error(`No database environment found for ${database}${version ? ` ${version}` : ''}.`);
- if (matches.length > 1) throw new Error(`${database} has multiple versions (${matches.map((item) => item.displayVersion).join(', ')}). Use DB=${database}@ (or DB_VERSION).`);
+ if (matches.length === 0) throw new Error(`No database environment found for ${database}${version ? ` ${version}` : ""}.`);
+ if (matches.length > 1) throw new Error(`${database} has multiple versions (${matches.map((item) => item.displayVersion).join(", ")}). Use DB=${database}@ (or DB_VERSION).`);
return matches[0];
}
export function parseDatabaseSelection(selection, explicitVersion) {
if (!selection) return { database: selection, version: explicitVersion };
- const selectorIndex = selection.lastIndexOf('@');
+ const selectorIndex = selection.lastIndexOf("@");
if (selectorIndex === -1) return { database: selection, version: explicitVersion };
const database = selection.slice(0, selectorIndex);
const selectorVersion = selection.slice(selectorIndex + 1);
- if (!database || !selectorVersion) throw new Error('DB must use the format @, for example DB=mysql@8.4.');
+ if (!database || !selectorVersion) throw new Error("DB must use the format @, for example DB=mysql@8.4.");
if (explicitVersion && explicitVersion !== selectorVersion) {
throw new Error(`DB version ${selectorVersion} conflicts with DB_VERSION=${explicitVersion}.`);
}
@@ -159,11 +156,11 @@ export function parseDatabaseSelection(selection, explicitVersion) {
}
export function assertResetConfirmed(value) {
- if (value !== '1') throw new Error('Reset deletes the environment volume. Re-run with CONFIRM=1.');
+ if (value !== "1") throw new Error("Reset deletes the environment volume. Re-run with CONFIRM=1.");
}
export function expectedContainerName(recipe) {
- return `dbx-${recipe.database}-${recipe.displayVersion}`.toLowerCase().replace(/[^a-z0-9_.-]+/g, '-');
+ return `dbx-${recipe.database}-${recipe.displayVersion}`.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
}
export function defaultHostPortMappings(compose) {
@@ -172,48 +169,39 @@ export function defaultHostPortMappings(compose) {
for (const line of compose.split(/\r?\n/)) {
const match = line.match(pattern);
if (!match) continue;
- mappings.push({ environment: match[1], hostPort: Number(match[2]), containerPort: Number(match[3]), protocol: match[4] || 'tcp' });
+ mappings.push({ environment: match[1], hostPort: Number(match[2]), containerPort: Number(match[3]), protocol: match[4] || "tcp" });
}
return mappings;
}
export function recipePortMappings(recipe) {
- const composePath = join(recipe.directory, 'compose.yaml');
- if (!existsSync(composePath)) return '';
- const mappings = defaultHostPortMappings(readFileSync(composePath, 'utf8')).sort((left, right) => (
- left.hostPort - right.hostPort
- || left.containerPort - right.containerPort
- || left.protocol.localeCompare(right.protocol)
- ));
- return mappings.map((mapping) => {
- const sharesPortWithAnotherProtocol = mappings.some((candidate) => (
- candidate !== mapping
- && candidate.containerPort === mapping.containerPort
- && candidate.hostPort === mapping.hostPort
- && candidate.protocol !== mapping.protocol
- ));
- const sourcePort = sharesPortWithAnotherProtocol
- ? `${mapping.containerPort}/${mapping.protocol}`
- : mapping.containerPort;
- return `${sourcePort} -> ${mapping.hostPort}`;
- }).join(', ');
+ const composePath = join(recipe.directory, "compose.yaml");
+ if (!existsSync(composePath)) return "";
+ const mappings = defaultHostPortMappings(readFileSync(composePath, "utf8")).sort((left, right) => left.hostPort - right.hostPort || left.containerPort - right.containerPort || left.protocol.localeCompare(right.protocol));
+ return mappings
+ .map((mapping) => {
+ const sharesPortWithAnotherProtocol = mappings.some((candidate) => candidate !== mapping && candidate.containerPort === mapping.containerPort && candidate.hostPort === mapping.hostPort && candidate.protocol !== mapping.protocol);
+ const sourcePort = sharesPortWithAnotherProtocol ? `${mapping.containerPort}/${mapping.protocol}` : mapping.containerPort;
+ return `${sourcePort} -> ${mapping.hostPort}`;
+ })
+ .join(", ");
}
export function databaseListRows(recipes) {
let previousDatabase = null;
return recipes.map((recipe) => {
- const database = recipe.database === previousDatabase ? '' : recipe.name;
+ const database = recipe.database === previousDatabase ? "" : recipe.name;
previousDatabase = recipe.database;
- return [database, recipe.displayVersion, recipePortMappings(recipe), recipe.image, recipe.platforms.join(',')];
+ return [database, recipe.displayVersion, recipePortMappings(recipe), recipe.image, recipe.platforms.join(",")];
});
}
function validateHostPorts(recipe) {
const errors = [];
const hostPorts = recipe.hostPorts;
- if (!hostPorts || typeof hostPorts !== 'object' || Array.isArray(hostPorts)) return ['hostPorts must be a non-empty object'];
+ if (!hostPorts || typeof hostPorts !== "object" || Array.isArray(hostPorts)) return ["hostPorts must be a non-empty object"];
const entries = Object.entries(hostPorts);
- if (entries.length === 0) return ['hostPorts must be a non-empty object'];
+ if (entries.length === 0) return ["hostPorts must be a non-empty object"];
const [start, end] = DEFAULT_HOST_PORT_RANGES[recipe.database] ?? [];
if (!start) {
errors.push(`database ${recipe.database} has no allocated default host port range`);
@@ -224,8 +212,8 @@ function validateHostPorts(recipe) {
errors.push(`hostPorts.${environment} must be an integer in the ${start}-${end} range`);
}
}
- if (new Set(entries.map(([, port]) => port)).size !== entries.length) errors.push('hostPorts must not reuse a port within a recipe');
- if (recipe.connection?.port !== hostPorts.DB_PORT) errors.push('connection.port must match hostPorts.DB_PORT');
+ if (new Set(entries.map(([, port]) => port)).size !== entries.length) errors.push("hostPorts must not reuse a port within a recipe");
+ if (recipe.connection?.port !== hostPorts.DB_PORT) errors.push("connection.port must match hostPorts.DB_PORT");
return errors;
}
@@ -246,46 +234,46 @@ export function validateHostPortAllocation(recipes) {
export function validateRecipe(recipe) {
const errors = [];
- for (const field of ['database', 'version', 'displayVersion', 'service', 'connection', 'hostPorts', 'smoke', 'shell', 'defaultPort']) {
+ for (const field of ["database", "version", "displayVersion", "service", "connection", "hostPorts", "smoke", "shell", "defaultPort"]) {
if (recipe[field] === undefined) errors.push(`missing ${field}`);
}
- if (!recipe.image || !/^[^\s]+:[^\s]+$/.test(recipe.image)) errors.push('image must be a pinned image reference');
- if (recipe.connection?.host !== '127.0.0.1') errors.push('connection.host must be 127.0.0.1');
+ if (!recipe.image || !/^[^\s]+:[^\s]+$/.test(recipe.image)) errors.push("image must be a pinned image reference");
+ if (recipe.connection?.host !== "127.0.0.1") errors.push("connection.host must be 127.0.0.1");
if (!Number.isInteger(recipe.defaultPort) || recipe.defaultPort < 1 || recipe.defaultPort > 65534) {
- errors.push('defaultPort must be an integer between 1 and 65534');
+ errors.push("defaultPort must be an integer between 1 and 65534");
}
errors.push(...validateHostPorts(recipe));
- if (recipe.connection?.authentication === 'none') {
- if (recipe.connection.password !== undefined) errors.push('unauthenticated recipes must not declare connection.password');
+ if (recipe.connection?.authentication === "none") {
+ if (recipe.connection.password !== undefined) errors.push("unauthenticated recipes must not declare connection.password");
} else if (recipe.connection?.password !== DEFAULT_PASSWORD) errors.push(`connection.password must be ${DEFAULT_PASSWORD}`);
- if (recipe.database === 'redis' ? recipe.connection?.database !== 0 : recipe.connection?.database !== DEFAULT_DATABASE) {
- errors.push(`connection.database must be ${recipe.database === 'redis' ? '0 for Redis' : DEFAULT_DATABASE}`);
+ if (recipe.database === "redis" ? recipe.connection?.database !== 0 : recipe.connection?.database !== DEFAULT_DATABASE) {
+ errors.push(`connection.database must be ${recipe.database === "redis" ? "0 for Redis" : DEFAULT_DATABASE}`);
}
- if (!Array.isArray(recipe.platforms) || recipe.platforms.length === 0) errors.push('platforms must not be empty');
- else if (recipe.platforms.some((platform) => !['linux/amd64', 'linux/arm64'].includes(platform))) {
- errors.push('platforms contains an unsupported platform');
+ if (!Array.isArray(recipe.platforms) || recipe.platforms.length === 0) errors.push("platforms must not be empty");
+ else if (recipe.platforms.some((platform) => !["linux/amd64", "linux/arm64"].includes(platform))) {
+ errors.push("platforms contains an unsupported platform");
}
if (basename(recipe.directory) !== recipe.displayVersion || basename(dirname(recipe.directory)) !== recipe.database) {
- errors.push('recipe directory must match database/displayVersion');
+ errors.push("recipe directory must match database/displayVersion");
}
- const composePath = join(recipe.directory, 'compose.yaml');
- if (!existsSync(composePath)) errors.push('missing compose.yaml');
+ const composePath = join(recipe.directory, "compose.yaml");
+ if (!existsSync(composePath)) errors.push("missing compose.yaml");
else {
- const compose = readFileSync(composePath, 'utf8');
+ const compose = readFileSync(composePath, "utf8");
if (!compose.includes(`container_name: ${expectedContainerName(recipe)}`)) {
errors.push(`compose.yaml container_name must be ${expectedContainerName(recipe)}`);
}
- if (/image:\s*[^\n]*:latest(?:\s|$)/.test(compose)) errors.push('compose.yaml must not use latest');
- if (!/healthcheck\s*:/.test(compose)) errors.push('compose.yaml must define a healthcheck');
- if (!/^volumes\s*:/m.test(compose)) errors.push('compose.yaml must define a named volume');
- if (!compose.includes(`image: ${recipe.image}`)) errors.push('compose.yaml image must match recipe.image');
+ if (/image:\s*[^\n]*:latest(?:\s|$)/.test(compose)) errors.push("compose.yaml must not use latest");
+ if (!/healthcheck\s*:/.test(compose)) errors.push("compose.yaml must define a healthcheck");
+ if (!/^volumes\s*:/m.test(compose)) errors.push("compose.yaml must define a named volume");
+ if (!compose.includes(`image: ${recipe.image}`)) errors.push("compose.yaml image must match recipe.image");
if (recipe.platforms?.length === 1 && !compose.includes(`platform: ${recipe.platforms[0]}`)) {
errors.push(`single-platform image must set compose platform to ${recipe.platforms[0]}`);
}
if (!allPortMappingsDefaultToLoopback(compose)) {
- errors.push('compose.yaml ports must default every mapping to 127.0.0.1 via DB_BIND_ADDRESS');
+ errors.push("compose.yaml ports must default every mapping to 127.0.0.1 via DB_BIND_ADDRESS");
}
- if (!serviceHasNamedVolume(compose, recipe.service)) errors.push('target service must mount a named volume');
+ if (!serviceHasNamedVolume(compose, recipe.service)) errors.push("target service must mount a named volume");
const portMappings = defaultHostPortMappings(compose);
const mappedPorts = new Map();
for (const mapping of portMappings) {
@@ -297,26 +285,26 @@ export function validateRecipe(recipe) {
}
const configuredPorts = Object.entries(recipe.hostPorts ?? {});
if (mappedPorts.size !== configuredPorts.length || configuredPorts.some(([environment, port]) => mappedPorts.get(environment) !== port)) {
- errors.push('compose.yaml port defaults must match recipe hostPorts');
+ errors.push("compose.yaml port defaults must match recipe hostPorts");
}
}
- const initDirectory = join(recipe.directory, 'init');
- if (!existsSync(initDirectory) || readdirSync(initDirectory).length === 0) errors.push('init directory must contain a file');
- if (!Array.isArray(recipe.smoke?.steps) || recipe.smoke.steps.length === 0) errors.push('smoke.steps must not be empty');
+ const initDirectory = join(recipe.directory, "init");
+ if (!existsSync(initDirectory) || readdirSync(initDirectory).length === 0) errors.push("init directory must contain a file");
+ if (!Array.isArray(recipe.smoke?.steps) || recipe.smoke.steps.length === 0) errors.push("smoke.steps must not be empty");
for (const step of recipe.smoke?.steps ?? []) {
- if (!step.name || !Array.isArray(step.command) || step.command.length === 0) errors.push('every smoke step needs a name and command');
- if (typeof step.expect !== 'string' || step.expect.length === 0) errors.push('every smoke step needs an expected output');
+ if (!step.name || !Array.isArray(step.command) || step.command.length === 0) errors.push("every smoke step needs a name and command");
+ if (typeof step.expect !== "string" || step.expect.length === 0) errors.push("every smoke step needs an expected output");
}
- if (!Array.isArray(recipe.shell) || recipe.shell.length === 0) errors.push('shell must be a non-empty command array');
+ if (!Array.isArray(recipe.shell) || recipe.shell.length === 0) errors.push("shell must be a non-empty command array");
if (recipe.bootstrap !== undefined) {
- if (!recipe.bootstrap || typeof recipe.bootstrap !== 'object') errors.push('bootstrap must be an object');
- if (!recipe.bootstrap?.check || !Array.isArray(recipe.bootstrap.check.command) || recipe.bootstrap.check.command.length === 0 || typeof recipe.bootstrap.check.expect !== 'string' || recipe.bootstrap.check.expect.length === 0) {
- errors.push('bootstrap.check needs a command and expected output');
+ if (!recipe.bootstrap || typeof recipe.bootstrap !== "object") errors.push("bootstrap must be an object");
+ if (!recipe.bootstrap?.check || !Array.isArray(recipe.bootstrap.check.command) || recipe.bootstrap.check.command.length === 0 || typeof recipe.bootstrap.check.expect !== "string" || recipe.bootstrap.check.expect.length === 0) {
+ errors.push("bootstrap.check needs a command and expected output");
}
- if (!Array.isArray(recipe.bootstrap?.steps) || recipe.bootstrap.steps.length === 0) errors.push('bootstrap.steps must not be empty');
+ if (!Array.isArray(recipe.bootstrap?.steps) || recipe.bootstrap.steps.length === 0) errors.push("bootstrap.steps must not be empty");
for (const step of recipe.bootstrap?.steps ?? []) {
- if (!step.name || !Array.isArray(step.command) || step.command.length === 0) errors.push('every bootstrap step needs a name and command');
- if (typeof step.expect !== 'string' || step.expect.length === 0) errors.push('every bootstrap step needs an expected output');
+ if (!step.name || !Array.isArray(step.command) || step.command.length === 0) errors.push("every bootstrap step needs a name and command");
+ if (typeof step.expect !== "string" || step.expect.length === 0) errors.push("every bootstrap step needs an expected output");
}
}
return errors;
@@ -328,7 +316,7 @@ function indentation(line) {
function serviceBlock(compose, service) {
const lines = compose.split(/\r?\n/);
- const start = lines.findIndex((line) => new RegExp(`^\\s{2}${service.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*$`).test(line));
+ const start = lines.findIndex((line) => new RegExp(`^\\s{2}${service.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*$`).test(line));
if (start === -1) return [];
const block = [];
for (let index = start + 1; index < lines.length; index += 1) {
@@ -351,7 +339,7 @@ export function allPortMappingsDefaultToLoopback(compose) {
const inlineMappings = trimmed.match(/['\"]([^'\"]+)['\"]/g) ?? [];
if (inlineMappings.length > 0) {
foundMapping = true;
- if (inlineMappings.some((mapping) => !mapping.slice(1, -1).startsWith('${DB_BIND_ADDRESS:-127.0.0.1}:'))) return false;
+ if (inlineMappings.some((mapping) => !mapping.slice(1, -1).startsWith("${DB_BIND_ADDRESS:-127.0.0.1}:"))) return false;
}
continue;
}
@@ -360,10 +348,13 @@ export function allPortMappingsDefaultToLoopback(compose) {
portsIndent = null;
continue;
}
- if (!trimmed.startsWith('-')) continue;
+ if (!trimmed.startsWith("-")) continue;
foundMapping = true;
- const mapping = trimmed.slice(1).trim().replace(/^['\"]|['\"]$/g, '');
- if (!mapping.startsWith('${DB_BIND_ADDRESS:-127.0.0.1}:')) return false;
+ const mapping = trimmed
+ .slice(1)
+ .trim()
+ .replace(/^['\"]|['\"]$/g, "");
+ if (!mapping.startsWith("${DB_BIND_ADDRESS:-127.0.0.1}:")) return false;
}
return foundMapping;
}
@@ -383,36 +374,39 @@ export function serviceHasNamedVolume(compose, service) {
volumesIndent = null;
continue;
}
- if (!trimmed.startsWith('-')) continue;
- const mount = trimmed.slice(1).trim().replace(/^['\"]|['\"]$/g, '');
- if (/^[A-Za-z0-9][A-Za-z0-9_.-]*:/.test(mount) && !mount.startsWith('./') && !mount.startsWith('../')) return true;
+ if (!trimmed.startsWith("-")) continue;
+ const mount = trimmed
+ .slice(1)
+ .trim()
+ .replace(/^['\"]|['\"]$/g, "");
+ if (/^[A-Za-z0-9][A-Za-z0-9_.-]*:/.test(mount) && !mount.startsWith("./") && !mount.startsWith("../")) return true;
}
return false;
}
function composeArgs(recipe, ...args) {
- const project = (process.env.DB_PROJECT || `dbx-${basename(REPO_ROOT)}-${recipe.database}-${recipe.displayVersion}`).toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
- return ['compose', '--project-name', project, '--file', join(recipe.directory, 'compose.yaml'), ...args];
+ const project = (process.env.DB_PROJECT || `dbx-${basename(REPO_ROOT)}-${recipe.database}-${recipe.displayVersion}`).toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
+ return ["compose", "--project-name", project, "--file", join(recipe.directory, "compose.yaml"), ...args];
}
function run(command, args, options = {}) {
- const result = spawnSync(command, args, { cwd: options.cwd ?? REPO_ROOT, env: process.env, encoding: 'utf8', stdio: options.capture ? 'pipe' : 'inherit' });
+ const result = spawnSync(command, args, { cwd: options.cwd ?? REPO_ROOT, env: process.env, encoding: "utf8", stdio: options.capture ? "pipe" : "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) {
- const detail = options.capture ? `\n${result.stderr || result.stdout}` : '';
- throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.${detail}`);
+ const detail = options.capture ? `\n${result.stderr || result.stdout}` : "";
+ throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}.${detail}`);
}
- return options.capture ? `${result.stdout ?? ''}${result.stderr ?? ''}` : '';
+ return options.capture ? `${result.stdout ?? ""}${result.stderr ?? ""}` : "";
}
function runCompose(recipe, args, options) {
- return run('docker', composeArgs(recipe, ...args), options);
+ return run("docker", composeArgs(recipe, ...args), options);
}
function tryRunCompose(recipe, args) {
- const result = spawnSync('docker', composeArgs(recipe, ...args), { cwd: REPO_ROOT, env: process.env, encoding: 'utf8', stdio: 'pipe' });
+ const result = spawnSync("docker", composeArgs(recipe, ...args), { cwd: REPO_ROOT, env: process.env, encoding: "utf8", stdio: "pipe" });
if (result.error) throw result.error;
- return { ok: result.status === 0, output: `${result.stdout ?? ''}${result.stderr ?? ''}` };
+ return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
}
export function expandSmokeCommand(command, recipe, environment = process.env) {
@@ -435,9 +429,9 @@ export function dbxConnectionDeepLink(recipe, environment = process.env) {
port: environment.DB_PORT || String(connection.port),
database: String(connection.database),
});
- if (connection.username) params.set('user', connection.username);
- if (connection.password) params.set('password', environment.DB_PASSWORD || connection.password);
- if (connection.urlParams) params.set('url_params', connection.urlParams);
+ if (connection.username) params.set("user", connection.username);
+ if (connection.password) params.set("password", environment.DB_PASSWORD || connection.password);
+ if (connection.urlParams) params.set("url_params", connection.urlParams);
return `dbx://connection/new?${params}`;
}
@@ -445,12 +439,12 @@ function ensureBootstrap(recipe) {
if (!recipe.bootstrap) return;
// Keep one-shot setup synchronous after Compose health checks so --wait cannot return before credentials exist.
const checkCommand = expandSmokeCommand(recipe.bootstrap.check.command, recipe);
- const check = tryRunCompose(recipe, ['exec', '-T', recipe.service, ...checkCommand]);
+ const check = tryRunCompose(recipe, ["exec", "-T", recipe.service, ...checkCommand]);
if (check.ok && check.output.includes(recipe.bootstrap.check.expect)) return;
for (const step of recipe.bootstrap.steps) {
const command = expandSmokeCommand(step.command, recipe);
- const output = runCompose(recipe, ['exec', '-T', recipe.service, ...command], { capture: true });
+ const output = runCompose(recipe, ["exec", "-T", recipe.service, ...command], { capture: true });
if (!output.includes(step.expect)) throw new Error(`Bootstrap check did not contain expected text: ${step.expect}\n${output}`);
console.log(`OK ${step.name}`);
}
@@ -481,14 +475,14 @@ function checkRecipes(recipes) {
const errors = validateRecipe(recipe);
if (errors.length) {
failed = true;
- console.error(`FAIL ${recipe.database}/${recipe.displayVersion}: ${errors.join('; ')}`);
+ console.error(`FAIL ${recipe.database}/${recipe.displayVersion}: ${errors.join("; ")}`);
continue;
}
- const result = spawnSync('docker', composeArgs(recipe, 'config', '--format', 'json'), {
- encoding: 'utf8',
- env: { ...process.env, DB_BIND_ADDRESS: '' },
+ const result = spawnSync("docker", composeArgs(recipe, "config", "--format", "json"), {
+ encoding: "utf8",
+ env: { ...process.env, DB_BIND_ADDRESS: "" },
});
- if (result.error?.code === 'ENOENT') {
+ if (result.error?.code === "ENOENT") {
console.log(`OK ${recipe.database}/${recipe.displayVersion} (static checks; Docker unavailable)`);
} else if (result.status !== 0) {
failed = true;
@@ -503,90 +497,86 @@ function checkRecipes(recipes) {
}
}
}
- if (failed) throw new Error('Database environment checks failed.');
+ if (failed) throw new Error("Database environment checks failed.");
}
export function validateRenderedCompose(recipe, rendered) {
const service = rendered.services?.[recipe.service];
- if (!service) throw new Error('rendered Compose config is missing the target service');
+ if (!service) throw new Error("rendered Compose config is missing the target service");
if (service.container_name !== expectedContainerName(recipe)) {
throw new Error(`rendered target service container_name must be ${expectedContainerName(recipe)}`);
}
- if (!service.healthcheck?.test) throw new Error('rendered target service is missing a healthcheck');
+ if (!service.healthcheck?.test) throw new Error("rendered target service is missing a healthcheck");
const ports = service.ports ?? [];
- if (ports.length === 0 || ports.some((port) => typeof port !== 'object' || port.host_ip !== '127.0.0.1')) {
- throw new Error('rendered target service has a port mapping that does not default to 127.0.0.1');
+ if (ports.length === 0 || ports.some((port) => typeof port !== "object" || port.host_ip !== "127.0.0.1")) {
+ throw new Error("rendered target service has a port mapping that does not default to 127.0.0.1");
}
const volumes = service.volumes ?? [];
- if (!volumes.some((volume) => typeof volume === 'object' && volume.type === 'volume')) {
- throw new Error('rendered target service does not mount a named volume');
+ if (!volumes.some((volume) => typeof volume === "object" && volume.type === "volume")) {
+ throw new Error("rendered target service does not mount a named volume");
}
}
export function main(argv = process.argv.slice(2)) {
- const [command = 'list', positionalDatabase, positionalVersion] = argv.filter((argument) => argument && argument !== '--');
+ const [command = "list", positionalDatabase, positionalVersion] = argv.filter((argument) => argument && argument !== "--");
const recipes = discoverRecipes();
- if (command === 'list') {
- console.log(formatBorderedTable(
- ['DATABASE', 'VERSION', 'PORTS (CONTAINER -> HOST)', 'IMAGE', 'PLATFORMS'],
- databaseListRows(recipes),
- true,
- ));
+ if (command === "list") {
+ console.log(formatBorderedTable(["DATABASE", "VERSION", "PORTS (CONTAINER -> HOST)", "IMAGE", "PLATFORMS"], databaseListRows(recipes), true));
return;
}
- if (command === 'quick-start') return printQuickStart(recipes);
- if (command === 'completion') return printCompletionSetup();
- if (command === 'selectors') {
+ if (command === "quick-start") return printQuickStart(recipes);
+ if (command === "completion") return printCompletionSetup();
+ if (command === "selectors") {
for (const recipe of recipes) console.log(recipeSelector(recipe));
return;
}
- if (command === 'make-targets') {
+ if (command === "make-targets") {
for (const target of discoverMakeTargets()) console.log(target);
return;
}
- if (command === 'check') return checkRecipes(recipes);
+ if (command === "check") return checkRecipes(recipes);
const { database, version } = parseDatabaseSelection(process.env.DB || positionalDatabase, process.env.DB_VERSION || positionalVersion);
- if (command === 'start' && !database) return printQuickStart(recipes);
+ if (command === "start" && !database) return printQuickStart(recipes);
const recipe = resolveRecipe(recipes, database, version);
switch (command) {
- case 'info':
+ case "info":
printConnection(recipe);
break;
- case 'start':
- case 'up':
- runCompose(recipe, ['up', '-d', '--wait']);
+ case "start":
+ case "up":
+ runCompose(recipe, ["up", "-d", "--wait"]);
ensureBootstrap(recipe);
printConnection(recipe);
break;
- case 'status':
- runCompose(recipe, ['ps']);
+ case "status":
+ runCompose(recipe, ["ps"]);
break;
- case 'logs':
- runCompose(recipe, process.env.FOLLOW === '1' ? ['logs', '--follow'] : ['logs']);
+ case "logs":
+ runCompose(recipe, process.env.FOLLOW === "1" ? ["logs", "--follow"] : ["logs"]);
break;
- case 'shell':
- runCompose(recipe, ['exec', recipe.service, ...expandSmokeCommand(recipe.shell, recipe)]);
+ case "shell":
+ runCompose(recipe, ["exec", recipe.service, ...expandSmokeCommand(recipe.shell, recipe)]);
break;
- case 'verify':
- runCompose(recipe, ['up', '-d', '--wait']);
+ case "verify":
+ runCompose(recipe, ["up", "-d", "--wait"]);
ensureBootstrap(recipe);
for (const step of recipe.smoke.steps) {
const command = expandSmokeCommand(step.command, recipe);
- const output = runCompose(recipe, ['exec', '-T', recipe.service, ...command], { capture: true });
+ const output = runCompose(recipe, ["exec", "-T", recipe.service, ...command], { capture: true });
if (step.expect && !output.includes(step.expect)) throw new Error(`Smoke check did not contain expected text: ${step.expect}\n${output}`);
console.log(`OK ${step.name}`);
}
break;
- case 'down':
- runCompose(recipe, ['down', '--remove-orphans']);
+ case "down":
+ runCompose(recipe, ["down", "--remove-orphans"]);
break;
- case 'reset':
+ case "reset":
assertResetConfirmed(process.env.CONFIRM);
- runCompose(recipe, ['down', '--volumes', '--remove-orphans']);
+ runCompose(recipe, ["down", "--volumes", "--remove-orphans"]);
break;
default:
throw new Error(`Unknown command: ${command}`);
diff --git a/scripts/database-env.test.mjs b/scripts/database-env.test.mjs
index 540518d838..85d989b9ab 100644
--- a/scripts/database-env.test.mjs
+++ b/scripts/database-env.test.mjs
@@ -1,115 +1,149 @@
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
-import { join } from 'node:path';
-import { tmpdir } from 'node:os';
-import { spawnSync } from 'node:child_process';
-import { DEFAULT_RECIPES_ROOT, allPortMappingsDefaultToLoopback, architectureWarning, assertResetConfirmed, databaseListRows, dbxConnectionDeepLink, defaultHostPortMappings, discoverMakeTargets, discoverRecipes, expandSmokeCommand, expectedContainerName, formatBorderedTable, formatTable, parseDatabaseSelection, platformForArchitecture, recipeSelector, resolveRecipe, serviceHasNamedVolume, validateHostPortAllocation, validateRecipe, validateRenderedCompose } from './database-env.mjs';
+import test from "node:test";
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { spawnSync } from "node:child_process";
+import {
+ DEFAULT_RECIPES_ROOT,
+ allPortMappingsDefaultToLoopback,
+ architectureWarning,
+ assertResetConfirmed,
+ databaseListRows,
+ dbxConnectionDeepLink,
+ defaultHostPortMappings,
+ discoverMakeTargets,
+ discoverRecipes,
+ expandSmokeCommand,
+ expectedContainerName,
+ formatBorderedTable,
+ formatTable,
+ parseDatabaseSelection,
+ platformForArchitecture,
+ recipeSelector,
+ resolveRecipe,
+ serviceHasNamedVolume,
+ validateHostPortAllocation,
+ validateRecipe,
+ validateRenderedCompose,
+} from "./database-env.mjs";
-const bashAvailable = spawnSync('bash', ['--version'], { stdio: 'ignore' }).status === 0;
-const zshAvailable = spawnSync('zsh', ['--version'], { stdio: 'ignore' }).status === 0;
-const dockerComposeAvailable = spawnSync('docker', ['compose', 'version'], { stdio: 'ignore' }).status === 0;
+const bashAvailable = spawnSync("bash", ["--version"], { stdio: "ignore" }).status === 0;
+const zshAvailable = spawnSync("zsh", ["--version"], { stdio: "ignore" }).status === 0;
+const dockerComposeAvailable = spawnSync("docker", ["compose", "version"], { stdio: "ignore" }).status === 0;
function fixture(database, version) {
const hostPort = { mysql: 10100, postgresql: 10300, redis: 10500 }[database] ?? 10100;
- const root = mkdtempSync(join(tmpdir(), 'dbx-db-env-'));
+ const root = mkdtempSync(join(tmpdir(), "dbx-db-env-"));
const directory = join(root, database, version);
- mkdirSync(join(directory, 'init'), { recursive: true });
- writeFileSync(join(directory, 'init', '001-smoke.txt'), 'fixture initialization');
- writeFileSync(join(directory, 'recipe.json'), JSON.stringify({ database, version, displayVersion: version, name: database, image: 'test:1', platforms: ['linux/amd64'], service: 'database', defaultPort: 1234, connection: { host: '127.0.0.1', port: hostPort, password: '123456', database: database === 'redis' ? 0 : 'dbx' }, hostPorts: { DB_PORT: hostPort }, smoke: { steps: [{ name: 'smoke', command: ['true'], expect: 'true' }] }, shell: ['true'] }));
- writeFileSync(join(directory, 'compose.yaml'), `services:\n database:\n image: test:1\n platform: linux/amd64\n container_name: dbx-${database}-${version}\n ports:\n - "\${DB_BIND_ADDRESS:-127.0.0.1}:\${DB_PORT:-${hostPort}}:1234"\n volumes:\n - data:/var/lib/database\n healthcheck:\n test: ["CMD", "true"]\nvolumes:\n data:\n`);
+ mkdirSync(join(directory, "init"), { recursive: true });
+ writeFileSync(join(directory, "init", "001-smoke.txt"), "fixture initialization");
+ writeFileSync(
+ join(directory, "recipe.json"),
+ JSON.stringify({
+ database,
+ version,
+ displayVersion: version,
+ name: database,
+ image: "test:1",
+ platforms: ["linux/amd64"],
+ service: "database",
+ defaultPort: 1234,
+ connection: { host: "127.0.0.1", port: hostPort, password: "123456", database: database === "redis" ? 0 : "dbx" },
+ hostPorts: { DB_PORT: hostPort },
+ smoke: { steps: [{ name: "smoke", command: ["true"], expect: "true" }] },
+ shell: ["true"],
+ }),
+ );
+ writeFileSync(
+ join(directory, "compose.yaml"),
+ `services:\n database:\n image: test:1\n platform: linux/amd64\n container_name: dbx-${database}-${version}\n ports:\n - "\${DB_BIND_ADDRESS:-127.0.0.1}:\${DB_PORT:-${hostPort}}:1234"\n volumes:\n - data:/var/lib/database\n healthcheck:\n test: ["CMD", "true"]\nvolumes:\n data:\n`,
+ );
return root;
}
-test('discovers versioned recipes', () => {
- const recipes = discoverRecipes(fixture('mysql', '5.7'));
+test("discovers versioned recipes", () => {
+ const recipes = discoverRecipes(fixture("mysql", "5.7"));
assert.equal(recipes.length, 1);
- assert.equal(recipes[0].database, 'mysql');
+ assert.equal(recipes[0].database, "mysql");
});
-test('requires DB and rejects ambiguous versions', () => {
- assert.throws(() => resolveRecipe([], ''), /DB is required/);
- const recipes = [{ database: 'mysql', displayVersion: '5.7' }, { database: 'mysql', displayVersion: '8.4' }];
- assert.throws(() => resolveRecipe(recipes, 'mysql'), /multiple versions/);
- assert.equal(resolveRecipe(recipes, 'mysql', '8.4').displayVersion, '8.4');
+test("requires DB and rejects ambiguous versions", () => {
+ assert.throws(() => resolveRecipe([], ""), /DB is required/);
+ const recipes = [
+ { database: "mysql", displayVersion: "5.7" },
+ { database: "mysql", displayVersion: "8.4" },
+ ];
+ assert.throws(() => resolveRecipe(recipes, "mysql"), /multiple versions/);
+ assert.equal(resolveRecipe(recipes, "mysql", "8.4").displayVersion, "8.4");
});
-test('accepts the compact product@version database selector', () => {
- assert.deepEqual(parseDatabaseSelection('mysql@8.4'), { database: 'mysql', version: '8.4' });
- assert.deepEqual(parseDatabaseSelection('mysql@8.4', '8.4'), { database: 'mysql', version: '8.4' });
- assert.throws(() => parseDatabaseSelection('mysql@8.4', '5.7'), /conflicts/);
- assert.throws(() => parseDatabaseSelection('mysql@'), /format/);
+test("accepts the compact product@version database selector", () => {
+ assert.deepEqual(parseDatabaseSelection("mysql@8.4"), { database: "mysql", version: "8.4" });
+ assert.deepEqual(parseDatabaseSelection("mysql@8.4", "8.4"), { database: "mysql", version: "8.4" });
+ assert.throws(() => parseDatabaseSelection("mysql@8.4", "5.7"), /conflicts/);
+ assert.throws(() => parseDatabaseSelection("mysql@"), /format/);
});
-test('formats a recipe as a copyable compact selector', () => {
- assert.equal(recipeSelector({ database: 'mysql', displayVersion: '8.4' }), 'mysql@8.4');
+test("formats a recipe as a copyable compact selector", () => {
+ assert.equal(recipeSelector({ database: "mysql", displayVersion: "8.4" }), "mysql@8.4");
});
-test('formats lists as left-aligned tables with dynamic column widths', () => {
+test("formats lists as left-aligned tables with dynamic column widths", () => {
assert.equal(
formatTable(
- ['DATABASE', 'VERSION', 'IMAGE'],
+ ["DATABASE", "VERSION", "IMAGE"],
[
- ['etcd', '3.7', 'etcd:v3.7.0'],
- ['clickhouse', '24.8', 'clickhouse-server:24.8.14.39'],
+ ["etcd", "3.7", "etcd:v3.7.0"],
+ ["clickhouse", "24.8", "clickhouse-server:24.8.14.39"],
],
),
- 'DATABASE VERSION IMAGE\n'
- + 'etcd 3.7 etcd:v3.7.0\n'
- + 'clickhouse 24.8 clickhouse-server:24.8.14.39',
+ "DATABASE VERSION IMAGE\n" + "etcd 3.7 etcd:v3.7.0\n" + "clickhouse 24.8 clickhouse-server:24.8.14.39",
);
});
-test('formats bordered tables with header and group separators', () => {
+test("formats bordered tables with header and group separators", () => {
assert.equal(
formatBorderedTable(
- ['DATABASE', 'VERSION'],
- [['MySQL', '5.7'], ['', '8.4'], ['PostgreSQL', '17.4']],
+ ["DATABASE", "VERSION"],
+ [
+ ["MySQL", "5.7"],
+ ["", "8.4"],
+ ["PostgreSQL", "17.4"],
+ ],
true,
),
- '+------------+---------+\n'
- + '| DATABASE | VERSION |\n'
- + '+------------+---------+\n'
- + '| MySQL | 5.7 |\n'
- + '| | 8.4 |\n'
- + '+------------+---------+\n'
- + '| PostgreSQL | 17.4 |\n'
- + '+------------+---------+',
+ "+------------+---------+\n" + "| DATABASE | VERSION |\n" + "+------------+---------+\n" + "| MySQL | 5.7 |\n" + "| | 8.4 |\n" + "+------------+---------+\n" + "| PostgreSQL | 17.4 |\n" + "+------------+---------+",
);
});
-test('groups database versions and shows every container-to-host port mapping', () => {
+test("groups database versions and shows every container-to-host port mapping", () => {
const rows = databaseListRows(discoverRecipes());
- const mysql57 = rows.find((row) => row[1] === '5.7');
- const mysql84 = rows.find((row) => row[1] === '8.4');
- const consul = rows.find((row) => row[0] === 'Consul');
+ const mysql57 = rows.find((row) => row[1] === "5.7");
+ const mysql84 = rows.find((row) => row[1] === "8.4");
+ const consul = rows.find((row) => row[0] === "Consul");
- assert.deepEqual(mysql57.slice(0, 3), ['MySQL', '5.7', '3306 -> 10100']);
- assert.deepEqual(mysql84.slice(0, 3), ['', '8.4', '3306 -> 10101']);
- assert.equal(consul[2], '8500 -> 10900, 8502 -> 10901, 8600/tcp -> 10902, 8600/udp -> 10902');
+ assert.deepEqual(mysql57.slice(0, 3), ["MySQL", "5.7", "3306 -> 10100"]);
+ assert.deepEqual(mysql84.slice(0, 3), ["", "8.4", "3306 -> 10101"]);
+ assert.equal(consul[2], "8500 -> 10900, 8502 -> 10901, 8600/tcp -> 10902, 8600/udp -> 10902");
});
-test('discovers all public Make targets for shell completion', () => {
+test("discovers all public Make targets for shell completion", () => {
const targets = discoverMakeTargets();
- for (const target of ['help', 'build', 'test', 'docs', 'db', 'db-verify', 'db-completion']) {
+ for (const target of ["help", "build", "test", "docs", "db", "db-verify", "db-completion"]) {
assert.ok(targets.includes(target), `missing Make target: ${target}`);
}
assert.equal(new Set(targets).size, targets.length);
});
-test('Bash completion preserves all Make targets and completes database selectors', { skip: !bashAvailable }, () => {
- const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..');
- const completionScript = join(DEFAULT_RECIPES_ROOT, 'completion', 'dbx-make.bash');
- const result = spawnSync(
- 'bash',
- [
- '--noprofile',
- '--norc',
- '-c',
- 'source "$COMPLETION_SCRIPT"; COMP_WORDS=(make ""); COMP_CWORD=1; _dbx_make; printf "TARGET:%s\\n" "${COMPREPLY[@]}"; COMP_WORDS=(make db DB=); COMP_CWORD=2; _dbx_make; printf "SELECTOR:%s\\n" "${COMPREPLY[@]}"',
- ],
- { cwd: repoRoot, env: { ...process.env, COMPLETION_SCRIPT: completionScript }, encoding: 'utf8' },
- );
+test("Bash completion preserves all Make targets and completes database selectors", { skip: !bashAvailable }, () => {
+ const repoRoot = join(DEFAULT_RECIPES_ROOT, "..", "..");
+ const completionScript = join(DEFAULT_RECIPES_ROOT, "completion", "dbx-make.bash");
+ const result = spawnSync("bash", ["--noprofile", "--norc", "-c", 'source "$COMPLETION_SCRIPT"; COMP_WORDS=(make ""); COMP_CWORD=1; _dbx_make; printf "TARGET:%s\\n" "${COMPREPLY[@]}"; COMP_WORDS=(make db DB=); COMP_CWORD=2; _dbx_make; printf "SELECTOR:%s\\n" "${COMPREPLY[@]}"'], {
+ cwd: repoRoot,
+ env: { ...process.env, COMPLETION_SCRIPT: completionScript },
+ encoding: "utf8",
+ });
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^TARGET:build$/m);
assert.match(result.stdout, /^TARGET:test$/m);
@@ -117,164 +151,156 @@ test('Bash completion preserves all Make targets and completes database selector
assert.match(result.stdout, /^SELECTOR:DB=mysql@8\.4$/m);
});
-test('Bash completion delegates to the previous completer outside the repository', { skip: !bashAvailable }, () => {
- const completionScript = join(DEFAULT_RECIPES_ROOT, 'completion', 'dbx-make.bash');
- const result = spawnSync(
- 'bash',
- [
- '--noprofile',
- '--norc',
- '-c',
- '_original_make_completion() { COMPREPLY=(ORIGINAL); }; complete -F _original_make_completion make; source "$COMPLETION_SCRIPT"; cd "$TMPDIR"; COMP_WORDS=(make db DB=); COMP_CWORD=2; _dbx_make; printf "%s\\n" "${COMPREPLY[@]}"',
- ],
- { env: { ...process.env, COMPLETION_SCRIPT: completionScript, TMPDIR: tmpdir() }, encoding: 'utf8' },
- );
+test("Bash completion delegates to the previous completer outside the repository", { skip: !bashAvailable }, () => {
+ const completionScript = join(DEFAULT_RECIPES_ROOT, "completion", "dbx-make.bash");
+ const result = spawnSync("bash", ["--noprofile", "--norc", "-c", '_original_make_completion() { COMPREPLY=(ORIGINAL); }; complete -F _original_make_completion make; source "$COMPLETION_SCRIPT"; cd "$TMPDIR"; COMP_WORDS=(make db DB=); COMP_CWORD=2; _dbx_make; printf "%s\\n" "${COMPREPLY[@]}"'], {
+ env: { ...process.env, COMPLETION_SCRIPT: completionScript, TMPDIR: tmpdir() },
+ encoding: "utf8",
+ });
assert.equal(result.status, 0, result.stderr);
- assert.equal(result.stdout.trim(), 'ORIGINAL');
+ assert.equal(result.stdout.trim(), "ORIGINAL");
});
-test('Zsh completion delegates ordinary Make completion and handles DB selectors', { skip: !zshAvailable }, () => {
- const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..');
- const completionScript = join(DEFAULT_RECIPES_ROOT, 'completion', '_dbx-make.zsh');
+test("Zsh completion delegates ordinary Make completion and handles DB selectors", { skip: !zshAvailable }, () => {
+ const repoRoot = join(DEFAULT_RECIPES_ROOT, "..", "..");
+ const completionScript = join(DEFAULT_RECIPES_ROOT, "completion", "_dbx-make.zsh");
const result = spawnSync(
- 'zsh',
+ "zsh",
[
- '-f',
- '-c',
+ "-f",
+ "-c",
'autoload -Uz compinit && compinit -D; _original_make_completion() { print ORIGINAL; }; compdef _original_make_completion make; source "$COMPLETION_SCRIPT"; words=(make ""); CURRENT=2; _dbx_make; _describe() { local values_name="$2"; print -l -- "${(@P)values_name}"; }; compset() { return 0; }; words=(make db DB=); CURRENT=3; _dbx_make',
],
- { cwd: repoRoot, env: { ...process.env, COMPLETION_SCRIPT: completionScript }, encoding: 'utf8' },
+ { cwd: repoRoot, env: { ...process.env, COMPLETION_SCRIPT: completionScript }, encoding: "utf8" },
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^ORIGINAL$/m);
assert.match(result.stdout, /^mysql@8\.4$/m);
});
-test('records the MySQL 5.7 CNB mirror as a multi-architecture image', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'mysql@5.7');
+test("records the MySQL 5.7 CNB mirror as a multi-architecture image", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "mysql@5.7");
assert.ok(recipe);
- assert.deepEqual(recipe.platforms, ['linux/amd64', 'linux/arm64']);
+ assert.deepEqual(recipe.platforms, ["linux/amd64", "linux/arm64"]);
assert.equal(recipe.notes, undefined);
});
-test('warns only when the current architecture is unsupported', () => {
- const recipe = { image: 'mirror/redis:3', platforms: ['linux/amd64'] };
- assert.equal(platformForArchitecture('arm64'), 'linux/arm64');
- assert.equal(architectureWarning(recipe, 'x64'), null);
- assert.match(architectureWarning(recipe, 'arm64'), /does not support linux\/arm64/);
+test("warns only when the current architecture is unsupported", () => {
+ const recipe = { image: "mirror/redis:3", platforms: ["linux/amd64"] };
+ assert.equal(platformForArchitecture("arm64"), "linux/arm64");
+ assert.equal(architectureWarning(recipe, "x64"), null);
+ assert.match(architectureWarning(recipe, "arm64"), /does not support linux\/arm64/);
});
-test('uses the approved CNB mirror images and matching recipe versions', () => {
- const recipes = Object.fromEntries(
- discoverRecipes().map((recipe) => [recipeSelector(recipe), { version: recipe.version, image: recipe.image, platforms: recipe.platforms }]),
- );
+test("uses the approved CNB mirror images and matching recipe versions", () => {
+ const recipes = Object.fromEntries(discoverRecipes().map((recipe) => [recipeSelector(recipe), { version: recipe.version, image: recipe.image, platforms: recipe.platforms }]));
assert.deepEqual(recipes, {
- 'clickhouse@24.8': { version: '24.8.14.39', image: 'docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39', platforms: ['linux/amd64', 'linux/arm64'] },
- 'consul@2.0.2': { version: '2.0.2', image: 'docker.cnb.cool/znb/images/consul:2.0.2', platforms: ['linux/amd64', 'linux/arm64'] },
- 'elasticsearch@6.8': { version: '6.8', image: 'docker.cnb.cool/znb/images/elasticsearch:6.8', platforms: ['linux/amd64', 'linux/arm64'] },
- 'etcd@3.5': { version: '3.5.21', image: 'quay.io/coreos/etcd:v3.5.21', platforms: ['linux/amd64', 'linux/arm64'] },
- 'etcd@3.7': { version: '3.7.0', image: 'docker.cnb.cool/znb/images/etcd:v3.7.0', platforms: ['linux/amd64', 'linux/arm64'] },
- 'kafka@4.3': { version: '4.3.1', image: 'docker.cnb.cool/znb/images/kafka:4.3.1', platforms: ['linux/amd64', 'linux/arm64'] },
- 'mariadb@10.11': { version: '10.11.11', image: 'docker.cnb.cool/znb/images/mariadb:10.11.11', platforms: ['linux/amd64', 'linux/arm64'] },
- 'mongodb@5.0': { version: '5.0.5', image: 'docker.cnb.cool/znb/images/mongo:5.0.5', platforms: ['linux/amd64', 'linux/arm64'] },
- 'mongodb@8.2': { version: '8.2.3', image: 'docker.cnb.cool/znb/images/mongo:8.2.3-noble', platforms: ['linux/amd64', 'linux/arm64'] },
- 'mysql@5.7': { version: '5.7.44', image: 'docker.cnb.cool/znb/images/mysql:5.7.44', platforms: ['linux/amd64', 'linux/arm64'] },
- 'mysql@8.4': { version: '8.4.6', image: 'docker.cnb.cool/znb/images/mysql:8.4.6', platforms: ['linux/amd64', 'linux/arm64'] },
- 'nacos@2.5': { version: '2.5.4', image: 'docker.cnb.cool/znb/images/nacos-server:v2.5.4', platforms: ['linux/amd64', 'linux/arm64'] },
- 'nacos@3.2': { version: '3.2.3', image: 'docker.cnb.cool/znb/images/nacos-server:v3.2.3', platforms: ['linux/amd64', 'linux/arm64'] },
- 'postgresql@14.23': { version: '14.23', image: 'docker.cnb.cool/znb/images/postgres:14.23', platforms: ['linux/amd64', 'linux/arm64'] },
- 'postgresql@17.4': { version: '17.4', image: 'docker.cnb.cool/znb/images/postgres:17.4', platforms: ['linux/amd64', 'linux/arm64'] },
- 'pulsar@4.2': { version: '4.2.3', image: 'docker.cnb.cool/znb/images/pulsar:4.2.3', platforms: ['linux/amd64', 'linux/arm64'] },
- 'qdrant@1.8': { version: '1.8.3', image: 'docker.cnb.cool/znb/images/qdrant:v1.8.3', platforms: ['linux/amd64', 'linux/arm64'] },
- 'redis@3.0.7': { version: '3.0.7', image: 'docker.cnb.cool/znb/images/redis:3.0.7-alpine', platforms: ['linux/amd64'] },
- 'redis@7.4': { version: '7.4.9', image: 'docker.cnb.cool/znb/images/redis:7.4.9-alpine', platforms: ['linux/amd64', 'linux/arm64'] },
- 'rnacos@0.8': { version: '0.8.7', image: 'docker.cnb.cool/znb/images/rnacos:v0.8.7', platforms: ['linux/amd64', 'linux/arm64'] },
- 'zookeeper@3.9': { version: '3.9.5', image: 'docker.cnb.cool/znb/images/zookeeper:3.9.5', platforms: ['linux/amd64', 'linux/arm64'] },
+ "clickhouse@24.8": { version: "24.8.14.39", image: "docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39", platforms: ["linux/amd64", "linux/arm64"] },
+ "consul@2.0.2": { version: "2.0.2", image: "docker.cnb.cool/znb/images/consul:2.0.2", platforms: ["linux/amd64", "linux/arm64"] },
+ "elasticsearch@6.8": { version: "6.8", image: "docker.cnb.cool/znb/images/elasticsearch:6.8", platforms: ["linux/amd64", "linux/arm64"] },
+ "etcd@3.5": { version: "3.5.21", image: "quay.io/coreos/etcd:v3.5.21", platforms: ["linux/amd64", "linux/arm64"] },
+ "etcd@3.7": { version: "3.7.0", image: "docker.cnb.cool/znb/images/etcd:v3.7.0", platforms: ["linux/amd64", "linux/arm64"] },
+ "kafka@4.3": { version: "4.3.1", image: "docker.cnb.cool/znb/images/kafka:4.3.1", platforms: ["linux/amd64", "linux/arm64"] },
+ "mariadb@10.11": { version: "10.11.11", image: "docker.cnb.cool/znb/images/mariadb:10.11.11", platforms: ["linux/amd64", "linux/arm64"] },
+ "mongodb@5.0": { version: "5.0.5", image: "docker.cnb.cool/znb/images/mongo:5.0.5", platforms: ["linux/amd64", "linux/arm64"] },
+ "mongodb@8.2": { version: "8.2.3", image: "docker.cnb.cool/znb/images/mongo:8.2.3-noble", platforms: ["linux/amd64", "linux/arm64"] },
+ "mysql@5.7": { version: "5.7.44", image: "docker.cnb.cool/znb/images/mysql:5.7.44", platforms: ["linux/amd64", "linux/arm64"] },
+ "mysql@8.4": { version: "8.4.6", image: "docker.cnb.cool/znb/images/mysql:8.4.6", platforms: ["linux/amd64", "linux/arm64"] },
+ "nacos@2.5": { version: "2.5.4", image: "docker.cnb.cool/znb/images/nacos-server:v2.5.4", platforms: ["linux/amd64", "linux/arm64"] },
+ "nacos@3.2": { version: "3.2.3", image: "docker.cnb.cool/znb/images/nacos-server:v3.2.3", platforms: ["linux/amd64", "linux/arm64"] },
+ "postgresql@14.23": { version: "14.23", image: "docker.cnb.cool/znb/images/postgres:14.23", platforms: ["linux/amd64", "linux/arm64"] },
+ "postgresql@17.4": { version: "17.4", image: "docker.cnb.cool/znb/images/postgres:17.4", platforms: ["linux/amd64", "linux/arm64"] },
+ "pulsar@4.2": { version: "4.2.3", image: "docker.cnb.cool/znb/images/pulsar:4.2.3", platforms: ["linux/amd64", "linux/arm64"] },
+ "qdrant@1.8": { version: "1.8.3", image: "docker.cnb.cool/znb/images/qdrant:v1.8.3", platforms: ["linux/amd64", "linux/arm64"] },
+ "redis@3.0.7": { version: "3.0.7", image: "docker.cnb.cool/znb/images/redis:3.0.7-alpine", platforms: ["linux/amd64"] },
+ "redis@7.4": { version: "7.4.9", image: "docker.cnb.cool/znb/images/redis:7.4.9-alpine", platforms: ["linux/amd64", "linux/arm64"] },
+ "rnacos@0.8": { version: "0.8.7", image: "docker.cnb.cool/znb/images/rnacos:v0.8.7", platforms: ["linux/amd64", "linux/arm64"] },
+ "zookeeper@3.9": { version: "3.9.5", image: "docker.cnb.cool/znb/images/zookeeper:3.9.5", platforms: ["linux/amd64", "linux/arm64"] },
});
- assert.equal(discoverRecipes().find((recipe) => recipeSelector(recipe) === 'redis@3.0.7').connection.username, undefined);
+ assert.equal(discoverRecipes().find((recipe) => recipeSelector(recipe) === "redis@3.0.7").connection.username, undefined);
});
-test('initializes both Nacos versions with the shared administrator credentials', () => {
- for (const recipe of discoverRecipes().filter((item) => item.database === 'nacos')) {
- assert.equal(recipe.connection.username, 'nacos');
- assert.equal(recipe.connection.password, '123456');
- assert.equal(recipe.connection.namespace, 'public');
- assert.equal(recipe.bootstrap.check.expect, 'accessToken');
+test("initializes both Nacos versions with the shared administrator credentials", () => {
+ for (const recipe of discoverRecipes().filter((item) => item.database === "nacos")) {
+ assert.equal(recipe.connection.username, "nacos");
+ assert.equal(recipe.connection.password, "123456");
+ assert.equal(recipe.connection.namespace, "public");
+ assert.equal(recipe.bootstrap.check.expect, "accessToken");
assert.equal(recipe.bootstrap.steps.length, 1);
- assert.match(recipe.bootstrap.steps[0].command.join(' '), /administrator initialized/);
+ assert.match(recipe.bootstrap.steps[0].command.join(" "), /administrator initialized/);
- const compose = readFileSync(join(recipe.directory, 'compose.yaml'), 'utf8');
+ const compose = readFileSync(join(recipe.directory, "compose.yaml"), "utf8");
assert.doesNotMatch(compose, /^ initialize:/m);
assert.doesNotMatch(compose, /tail -f \/dev\/null/);
assert.deepEqual(validateRecipe(recipe), []);
}
});
-test('verifies Nacos 2.5 rejects unauthenticated configuration reads', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'nacos@2.5');
- const authenticationStep = recipe.smoke.steps.find((step) => step.name === 'reject unauthenticated configuration reads');
+test("verifies Nacos 2.5 rejects unauthenticated configuration reads", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "nacos@2.5");
+ const authenticationStep = recipe.smoke.steps.find((step) => step.name === "reject unauthenticated configuration reads");
assert.ok(authenticationStep);
- assert.match(authenticationStep.command.join(' '), /nacos\/v1\/cs\/configs/);
- assert.match(authenticationStep.command.join(' '), /403/);
- assert.equal(authenticationStep.expect, 'unauthenticated access rejected');
+ assert.match(authenticationStep.command.join(" "), /nacos\/v1\/cs\/configs/);
+ assert.match(authenticationStep.command.join(" "), /403/);
+ assert.equal(authenticationStep.expect, "unauthenticated access rejected");
});
-test('configures r-nacos with its admin account and console port', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'rnacos@0.8');
- assert.equal(recipe.connection.username, 'admin');
- assert.equal(recipe.connection.password, '123456');
+test("configures r-nacos with its admin account and console port", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "rnacos@0.8");
+ assert.equal(recipe.connection.username, "admin");
+ assert.equal(recipe.connection.password, "123456");
assert.equal(recipe.connection.grpcPort, 11101);
assert.equal(recipe.connection.consolePort, 11102);
- const compose = readFileSync(join(recipe.directory, 'compose.yaml'), 'utf8');
+ const compose = readFileSync(join(recipe.directory, "compose.yaml"), "utf8");
assert.match(compose, /RNACOS_CONSOLE_PORT/);
assert.doesNotMatch(compose, /curl/);
assert.deepEqual(validateRecipe(recipe), []);
});
-test('configures Qdrant with the shared API key', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'qdrant@1.8');
- assert.equal(recipe.connection.username, '');
- assert.equal(recipe.connection.password, '123456');
+test("configures Qdrant with the shared API key", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "qdrant@1.8");
+ assert.equal(recipe.connection.username, "");
+ assert.equal(recipe.connection.password, "123456");
assert.equal(recipe.connection.apiKey, undefined);
});
-test('bootstraps etcd with the shared root credentials', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'etcd@3.7');
- assert.equal(recipe.connection.username, 'root');
- assert.equal(recipe.connection.password, '123456');
+test("bootstraps etcd with the shared root credentials", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "etcd@3.7");
+ assert.equal(recipe.connection.username, "root");
+ assert.equal(recipe.connection.password, "123456");
assert.ok(recipe.bootstrap);
});
-test('bootstraps ZooKeeper with the shared Digest credentials', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'zookeeper@3.9');
- assert.equal(recipe.connection.username, 'root');
- assert.equal(recipe.connection.password, '123456');
- assert.equal(recipe.connection.authScheme, 'digest');
+test("bootstraps ZooKeeper with the shared Digest credentials", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "zookeeper@3.9");
+ assert.equal(recipe.connection.username, "root");
+ assert.equal(recipe.connection.password, "123456");
+ assert.equal(recipe.connection.authScheme, "digest");
});
-test('keeps existing authenticated bootstrap recipes valid', () => {
- for (const selector of ['etcd@3.7', 'zookeeper@3.9']) {
+test("keeps existing authenticated bootstrap recipes valid", () => {
+ for (const selector of ["etcd@3.7", "zookeeper@3.9"]) {
const recipe = discoverRecipes().find((item) => recipeSelector(item) === selector);
assert.deepEqual(validateRecipe(recipe), []);
}
});
-test('marks the unauthenticated Kafka and Pulsar development recipes explicitly', () => {
- for (const selector of ['kafka@4.3', 'pulsar@4.2']) {
+test("marks the unauthenticated Kafka and Pulsar development recipes explicitly", () => {
+ for (const selector of ["kafka@4.3", "pulsar@4.2"]) {
const recipe = discoverRecipes().find((item) => recipeSelector(item) === selector);
- assert.equal(recipe.connection.authentication, 'none');
+ assert.equal(recipe.connection.authentication, "none");
assert.equal(recipe.connection.password, undefined);
}
});
-test('start without DB prints every copyable database command', () => {
- const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..');
- const result = spawnSync(process.execPath, ['scripts/database-env.mjs', 'start'], {
+test("start without DB prints every copyable database command", () => {
+ const repoRoot = join(DEFAULT_RECIPES_ROOT, "..", "..");
+ const result = spawnSync(process.execPath, ["scripts/database-env.mjs", "start"], {
cwd: repoRoot,
- env: { ...process.env, DB: '', DB_VERSION: '' },
- encoding: 'utf8',
+ env: { ...process.env, DB: "", DB_VERSION: "" },
+ encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
for (const recipe of discoverRecipes()) {
@@ -282,136 +308,139 @@ test('start without DB prints every copyable database command', () => {
}
});
-test('reset requires explicit confirmation', () => {
- assert.throws(() => assertResetConfirmed(''), /CONFIRM=1/);
- assert.doesNotThrow(() => assertResetConfirmed('1'));
+test("reset requires explicit confirmation", () => {
+ assert.throws(() => assertResetConfirmed(""), /CONFIRM=1/);
+ assert.doesNotThrow(() => assertResetConfirmed("1"));
});
-test('static validation accepts a minimal safe recipe', () => {
- const [recipe] = discoverRecipes(fixture('redis', '7.4'));
+test("static validation accepts a minimal safe recipe", () => {
+ const [recipe] = discoverRecipes(fixture("redis", "7.4"));
assert.deepEqual(validateRecipe(recipe), []);
});
-test('smoke commands use password and port overrides without invoking a shell', () => {
- const recipe = { connection: { password: 'default-password', port: 3306 } };
- assert.deepEqual(
- expandSmokeCommand(['client', '--password=${DB_PASSWORD}', '--port=${DB_PORT}'], recipe, { DB_PASSWORD: 'override', DB_PORT: '13306' }),
- ['client', '--password=override', '--port=13306'],
- );
+test("smoke commands use password and port overrides without invoking a shell", () => {
+ const recipe = { connection: { password: "default-password", port: 3306 } };
+ assert.deepEqual(expandSmokeCommand(["client", "--password=${DB_PASSWORD}", "--port=${DB_PORT}"], recipe, { DB_PASSWORD: "override", DB_PORT: "13306" }), ["client", "--password=override", "--port=13306"]);
});
-test('generates DBX deep links from effective recipe connection values', () => {
- const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'postgresql@17.4');
+test("generates DBX deep links from effective recipe connection values", () => {
+ const recipe = discoverRecipes().find((item) => recipeSelector(item) === "postgresql@17.4");
assert.ok(recipe);
- const link = dbxConnectionDeepLink(recipe, { DB_PORT: '15433', DB_PASSWORD: 'p@ss & word' });
+ const link = dbxConnectionDeepLink(recipe, { DB_PORT: "15433", DB_PASSWORD: "p@ss & word" });
assert.ok(link);
const params = new URL(link).searchParams;
- assert.equal(params.get('type'), 'postgres');
- assert.equal(params.get('name'), 'PostgreSQL 17.4 (local)');
- assert.equal(params.get('host'), '127.0.0.1');
- assert.equal(params.get('port'), '15433');
- assert.equal(params.get('user'), 'postgres');
- assert.equal(params.get('password'), 'p@ss & word');
- assert.equal(params.get('database'), 'dbx');
+ assert.equal(params.get("type"), "postgres");
+ assert.equal(params.get("name"), "PostgreSQL 17.4 (local)");
+ assert.equal(params.get("host"), "127.0.0.1");
+ assert.equal(params.get("port"), "15433");
+ assert.equal(params.get("user"), "postgres");
+ assert.equal(params.get("password"), "p@ss & word");
+ assert.equal(params.get("database"), "dbx");
});
-test('prints DBX deep links for compatible connection types', () => {
- const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..');
- const postgres = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'postgresql', '17.4'], {
+test("prints DBX deep links for compatible connection types", () => {
+ const repoRoot = join(DEFAULT_RECIPES_ROOT, "..", "..");
+ const postgres = spawnSync(process.execPath, ["scripts/database-env.mjs", "info", "postgresql", "17.4"], {
cwd: repoRoot,
- encoding: 'utf8',
+ encoding: "utf8",
});
assert.equal(postgres.status, 0, postgres.stderr);
assert.match(postgres.stdout, /DBX connection link: dbx:\/\/connection\/new\?type=postgres&/);
- const elasticsearch = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'elasticsearch', '6.8'], {
+ const elasticsearch = spawnSync(process.execPath, ["scripts/database-env.mjs", "info", "elasticsearch", "6.8"], {
cwd: repoRoot,
- encoding: 'utf8',
+ encoding: "utf8",
});
assert.equal(elasticsearch.status, 0, elasticsearch.stderr);
assert.match(elasticsearch.stdout, /DBX connection link: dbx:\/\/connection\/new\?type=elasticsearch&/);
- const consul = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'consul', '2.0.2'], {
+ const consul = spawnSync(process.execPath, ["scripts/database-env.mjs", "info", "consul", "2.0.2"], {
cwd: repoRoot,
- encoding: 'utf8',
+ encoding: "utf8",
});
assert.equal(consul.status, 0, consul.stderr);
assert.match(consul.stdout, /DBX connection link: dbx:\/\/connection\/new\?type=consul&/);
});
-test('generates canonical service deep-link types from recipes', () => {
+test("generates canonical service deep-link types from recipes", () => {
const expected = {
- 'etcd@3.7': 'etcd',
- 'consul@2.0.2': 'consul',
- 'nacos@2.5': 'nacos-v2',
- 'nacos@3.2': 'nacos-v3',
- 'rnacos@0.8': 'r-nacos',
+ "etcd@3.7": "etcd",
+ "consul@2.0.2": "consul",
+ "nacos@2.5": "nacos-v2",
+ "nacos@3.2": "nacos-v3",
+ "rnacos@0.8": "r-nacos",
};
for (const [selector, type] of Object.entries(expected)) {
const recipe = discoverRecipes().find((item) => recipeSelector(item) === selector);
assert.ok(recipe, `missing ${selector}`);
const params = new URL(dbxConnectionDeepLink(recipe)).searchParams;
- assert.equal(params.get('type'), type);
- assert.equal(params.get('port'), String(recipe.connection.port));
+ assert.equal(params.get("type"), type);
+ assert.equal(params.get("port"), String(recipe.connection.port));
}
});
-test('Nacos recipes declare their version-specific deep-link type', () => {
- const recipes = discoverRecipes().filter((item) => item.database === 'nacos');
- assert.deepEqual(recipes.map((recipe) => [recipe.displayVersion, recipe.deepLinkType]), [
- ['2.5', 'nacos-v2'],
- ['3.2', 'nacos-v3'],
- ]);
- assert.notEqual(recipes.find((recipe) => recipe.displayVersion === '3.2').connection.port, recipes.find((recipe) => recipe.displayVersion === '3.2').connection.consolePort);
+test("Nacos recipes declare their version-specific deep-link type", () => {
+ const recipes = discoverRecipes().filter((item) => item.database === "nacos");
+ assert.deepEqual(
+ recipes.map((recipe) => [recipe.displayVersion, recipe.deepLinkType]),
+ [
+ ["2.5", "nacos-v2"],
+ ["3.2", "nacos-v3"],
+ ],
+ );
+ assert.notEqual(recipes.find((recipe) => recipe.displayVersion === "3.2").connection.port, recipes.find((recipe) => recipe.displayVersion === "3.2").connection.consolePort);
});
-test('MongoDB commands pass reserved-character passwords as a distinct argument', () => {
- const password = 'p@ss/word#with?reserved&characters';
- for (const recipe of discoverRecipes().filter((item) => item.database === 'mongodb')) {
+test("MongoDB commands pass reserved-character passwords as a distinct argument", () => {
+ const password = "p@ss/word#with?reserved&characters";
+ for (const recipe of discoverRecipes().filter((item) => item.database === "mongodb")) {
for (const command of [recipe.shell, ...recipe.smoke.steps.map((step) => step.command)]) {
const expanded = expandSmokeCommand(command, recipe, { DB_PASSWORD: password });
- const passwordIndex = expanded.indexOf('--password');
+ const passwordIndex = expanded.indexOf("--password");
assert.notEqual(passwordIndex, -1);
assert.equal(expanded[passwordIndex + 1], password);
- assert.equal(expanded.some((argument) => argument.includes('mongodb://')), false);
+ assert.equal(
+ expanded.some((argument) => argument.includes("mongodb://")),
+ false,
+ );
}
}
});
-test('requires every port mapping to default to loopback with an explicit override', () => {
+test("requires every port mapping to default to loopback with an explicit override", () => {
assert.equal(allPortMappingsDefaultToLoopback('services:\n database:\n ports:\n - "127.0.0.1:3306:3306"'), false);
assert.equal(allPortMappingsDefaultToLoopback('services:\n database:\n ports:\n - "${DB_BIND_ADDRESS:-127.0.0.1}:3306:3306"'), true);
assert.equal(allPortMappingsDefaultToLoopback('services:\n database:\n ports:\n - "${DB_BIND_ADDRESS:-0.0.0.0}:3306:3306"'), false);
});
-test('rendered Compose recipes bind to loopback by default', { skip: !dockerComposeAvailable }, () => {
+test("rendered Compose recipes bind to loopback by default", { skip: !dockerComposeAvailable }, () => {
const recipes = discoverRecipes();
for (const recipe of recipes) {
- const result = spawnSync('docker', ['compose', '--file', join(recipe.directory, 'compose.yaml'), 'config', '--format', 'json'], {
- encoding: 'utf8',
- env: { ...process.env, DB_BIND_ADDRESS: '' },
+ const result = spawnSync("docker", ["compose", "--file", join(recipe.directory, "compose.yaml"), "config", "--format", "json"], {
+ encoding: "utf8",
+ env: { ...process.env, DB_BIND_ADDRESS: "" },
});
assert.equal(result.status, 0, result.stderr);
assert.doesNotThrow(() => validateRenderedCompose(recipe, JSON.parse(result.stdout)));
}
- const remoteResult = spawnSync('docker', ['compose', '--file', join(recipes[0].directory, 'compose.yaml'), 'config', '--format', 'json'], {
- encoding: 'utf8',
- env: { ...process.env, DB_BIND_ADDRESS: '0.0.0.0' },
+ const remoteResult = spawnSync("docker", ["compose", "--file", join(recipes[0].directory, "compose.yaml"), "config", "--format", "json"], {
+ encoding: "utf8",
+ env: { ...process.env, DB_BIND_ADDRESS: "0.0.0.0" },
});
assert.equal(remoteResult.status, 0, remoteResult.stderr);
const remoteService = JSON.parse(remoteResult.stdout).services[recipes[0].service];
- assert.ok(remoteService.ports.every((port) => port.host_ip === '0.0.0.0'));
+ assert.ok(remoteService.ports.every((port) => port.host_ip === "0.0.0.0"));
});
-test('healthchecks safely reference configurable passwords', { skip: !dockerComposeAvailable }, () => {
+test("healthchecks safely reference configurable passwords", { skip: !dockerComposeAvailable }, () => {
const password = "space ' password";
- const recipes = discoverRecipes().filter((recipe) => ['mariadb', 'mongodb', 'mysql', 'redis'].includes(recipe.database));
+ const recipes = discoverRecipes().filter((recipe) => ["mariadb", "mongodb", "mysql", "redis"].includes(recipe.database));
for (const recipe of recipes) {
- const result = spawnSync('docker', ['compose', '--file', join(recipe.directory, 'compose.yaml'), 'config', '--format', 'json'], {
- encoding: 'utf8',
+ const result = spawnSync("docker", ["compose", "--file", join(recipe.directory, "compose.yaml"), "config", "--format", "json"], {
+ encoding: "utf8",
env: { ...process.env, DB_PASSWORD: password },
});
assert.equal(result.status, 0, result.stderr);
@@ -420,101 +449,101 @@ test('healthchecks safely reference configurable passwords', { skip: !dockerComp
const command = service.healthcheck.test[1];
assert.equal(command.includes(password), false, `${recipeSelector(recipe)} interpolates the password into CMD-SHELL`);
- if (recipe.database === 'mongodb') assert.match(command, /--password "\$\$MONGO_INITDB_ROOT_PASSWORD"/);
- if (recipe.database === 'mysql') assert.match(command, /-p"\$\$MYSQL_ROOT_PASSWORD"/);
- if (recipe.database === 'mariadb') assert.match(command, /-p"\$\$MARIADB_ROOT_PASSWORD"/);
- if (recipe.database === 'redis') {
+ if (recipe.database === "mongodb") assert.match(command, /--password "\$\$MONGO_INITDB_ROOT_PASSWORD"/);
+ if (recipe.database === "mysql") assert.match(command, /-p"\$\$MYSQL_ROOT_PASSWORD"/);
+ if (recipe.database === "mariadb") assert.match(command, /-p"\$\$MARIADB_ROOT_PASSWORD"/);
+ if (recipe.database === "redis") {
assert.equal(service.environment.REDIS_PASSWORD, password);
assert.match(command, /-a "\$\$REDIS_PASSWORD"/);
}
}
});
-test('requires the target service to mount a named volume', () => {
- const bindOnly = 'services:\n database:\n volumes:\n - ./init:/docker-entrypoint-initdb.d:ro\nvolumes:\n data:\n';
- const namedVolume = 'services:\n database:\n volumes:\n - data:/var/lib/database\nvolumes:\n data:\n';
- assert.equal(serviceHasNamedVolume(bindOnly, 'database'), false);
- assert.equal(serviceHasNamedVolume(namedVolume, 'database'), true);
+test("requires the target service to mount a named volume", () => {
+ const bindOnly = "services:\n database:\n volumes:\n - ./init:/docker-entrypoint-initdb.d:ro\nvolumes:\n data:\n";
+ const namedVolume = "services:\n database:\n volumes:\n - data:/var/lib/database\nvolumes:\n data:\n";
+ assert.equal(serviceHasNamedVolume(bindOnly, "database"), false);
+ assert.equal(serviceHasNamedVolume(namedVolume, "database"), true);
});
-test('derives the required dbx-prefixed container name', () => {
- assert.equal(expectedContainerName({ database: 'mysql', displayVersion: '5.7' }), 'dbx-mysql-5.7');
- assert.equal(expectedContainerName({ database: 'PostgreSQL', displayVersion: '17.4' }), 'dbx-postgresql-17.4');
+test("derives the required dbx-prefixed container name", () => {
+ assert.equal(expectedContainerName({ database: "mysql", displayVersion: "5.7" }), "dbx-mysql-5.7");
+ assert.equal(expectedContainerName({ database: "PostgreSQL", displayVersion: "17.4" }), "dbx-postgresql-17.4");
});
-test('rejects a recipe whose Compose container name is not standardized', () => {
- const [recipe] = discoverRecipes(fixture('mysql', '8.4'));
- const composePath = join(recipe.directory, 'compose.yaml');
- writeFileSync(composePath, readFileSync(composePath, 'utf8').replace('container_name: dbx-mysql-8.4', 'container_name: custom-mysql'));
- assert.match(validateRecipe(recipe).join('; '), /container_name must be dbx-mysql-8.4/);
+test("rejects a recipe whose Compose container name is not standardized", () => {
+ const [recipe] = discoverRecipes(fixture("mysql", "8.4"));
+ const composePath = join(recipe.directory, "compose.yaml");
+ writeFileSync(composePath, readFileSync(composePath, "utf8").replace("container_name: dbx-mysql-8.4", "container_name: custom-mysql"));
+ assert.match(validateRecipe(recipe).join("; "), /container_name must be dbx-mysql-8.4/);
});
-test('requires the shared default password and database name', () => {
- const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
- recipe.connection.password = 'different';
- recipe.connection.database = 'other';
- assert.match(validateRecipe(recipe).join('; '), /connection.password must be 123456/);
- assert.match(validateRecipe(recipe).join('; '), /connection.database must be dbx/);
+test("requires the shared default password and database name", () => {
+ const [recipe] = discoverRecipes(fixture("postgresql", "17.4"));
+ recipe.connection.password = "different";
+ recipe.connection.database = "other";
+ assert.match(validateRecipe(recipe).join("; "), /connection.password must be 123456/);
+ assert.match(validateRecipe(recipe).join("; "), /connection.database must be dbx/);
});
-test('allows recipes that explicitly declare no authentication', () => {
- const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
- recipe.connection.authentication = 'none';
+test("allows recipes that explicitly declare no authentication", () => {
+ const [recipe] = discoverRecipes(fixture("postgresql", "17.4"));
+ recipe.connection.authentication = "none";
delete recipe.connection.password;
assert.deepEqual(validateRecipe(recipe), []);
});
-test('requires the primary connection port to match the allocated DB_PORT', () => {
- const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
+test("requires the primary connection port to match the allocated DB_PORT", () => {
+ const [recipe] = discoverRecipes(fixture("postgresql", "17.4"));
recipe.connection.port = 10301;
- assert.match(validateRecipe(recipe).join('; '), /connection.port must match hostPorts.DB_PORT/);
+ assert.match(validateRecipe(recipe).join("; "), /connection.port must match hostPorts.DB_PORT/);
});
-test('requires Compose port defaults to match the declared host-port allocation', () => {
- const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
- const composePath = join(recipe.directory, 'compose.yaml');
- writeFileSync(composePath, readFileSync(composePath, 'utf8').replace('DB_PORT:-10300', 'DB_PORT:-10301'));
- assert.match(validateRecipe(recipe).join('; '), /compose.yaml port defaults must match recipe hostPorts/);
+test("requires Compose port defaults to match the declared host-port allocation", () => {
+ const [recipe] = discoverRecipes(fixture("postgresql", "17.4"));
+ const composePath = join(recipe.directory, "compose.yaml");
+ writeFileSync(composePath, readFileSync(composePath, "utf8").replace("DB_PORT:-10300", "DB_PORT:-10301"));
+ assert.match(validateRecipe(recipe).join("; "), /compose.yaml port defaults must match recipe hostPorts/);
});
-test('allocates unique default host ports across checked-in recipes', () => {
+test("allocates unique default host ports across checked-in recipes", () => {
const recipes = discoverRecipes();
assert.deepEqual(validateHostPortAllocation(recipes), []);
const duplicate = structuredClone(recipes[0]);
duplicate.hostPorts = { DB_PORT: recipes[1].hostPorts.DB_PORT };
- assert.match(validateHostPortAllocation([recipes[1], duplicate]).join('; '), /host port .* is allocated to both/);
+ assert.match(validateHostPortAllocation([recipes[1], duplicate]).join("; "), /host port .* is allocated to both/);
});
-test('parses every declared default host-port mapping, including shared UDP and TCP DNS', () => {
- const consul = discoverRecipes().find((recipe) => recipeSelector(recipe) === 'consul@2.0.2');
+test("parses every declared default host-port mapping, including shared UDP and TCP DNS", () => {
+ const consul = discoverRecipes().find((recipe) => recipeSelector(recipe) === "consul@2.0.2");
assert.ok(consul);
- const mappings = defaultHostPortMappings(readFileSync(join(consul.directory, 'compose.yaml'), 'utf8'));
- assert.deepEqual(mappings.filter((mapping) => mapping.environment === 'CONSUL_DNS_PORT'), [
- { environment: 'CONSUL_DNS_PORT', hostPort: 10902, containerPort: 8600, protocol: 'tcp' },
- { environment: 'CONSUL_DNS_PORT', hostPort: 10902, containerPort: 8600, protocol: 'udp' },
- ]);
+ const mappings = defaultHostPortMappings(readFileSync(join(consul.directory, "compose.yaml"), "utf8"));
+ assert.deepEqual(
+ mappings.filter((mapping) => mapping.environment === "CONSUL_DNS_PORT"),
+ [
+ { environment: "CONSUL_DNS_PORT", hostPort: 10902, containerPort: 8600, protocol: "tcp" },
+ { environment: "CONSUL_DNS_PORT", hostPort: 10902, containerPort: 8600, protocol: "udp" },
+ ],
+ );
});
-test('reports missing smoke structures without throwing a TypeError', () => {
- const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
+test("reports missing smoke structures without throwing a TypeError", () => {
+ const [recipe] = discoverRecipes(fixture("postgresql", "17.4"));
delete recipe.smoke;
let errors;
assert.doesNotThrow(() => {
errors = validateRecipe(recipe);
});
- assert.match(errors.join('; '), /missing smoke/);
- assert.match(errors.join('; '), /smoke\.steps must not be empty/);
+ assert.match(errors.join("; "), /missing smoke/);
+ assert.match(errors.join("; "), /smoke\.steps must not be empty/);
recipe.smoke = {};
assert.doesNotThrow(() => validateRecipe(recipe));
- assert.match(validateRecipe(recipe).join('; '), /smoke\.steps must not be empty/);
+ assert.match(validateRecipe(recipe).join("; "), /smoke\.steps must not be empty/);
});
-test('documents every checked-in recipe on both website pages', () => {
- const pages = [
- join(DEFAULT_RECIPES_ROOT, '..', '..', 'docs', 'content', 'docs', 'database-lab.mdx'),
- join(DEFAULT_RECIPES_ROOT, '..', '..', 'docs', 'content', 'docs', 'database-lab.cn.mdx'),
- ].map((path) => readFileSync(path, 'utf8'));
+test("documents every checked-in recipe on both website pages", () => {
+ const pages = [join(DEFAULT_RECIPES_ROOT, "..", "..", "docs", "content", "docs", "database-lab.mdx"), join(DEFAULT_RECIPES_ROOT, "..", "..", "docs", "content", "docs", "database-lab.cn.mdx")].map((path) => readFileSync(path, "utf8"));
for (const recipe of discoverRecipes()) {
const entry = `\`${recipe.database}/${recipe.displayVersion}\``;
diff --git a/scripts/dev-backend.mjs b/scripts/dev-backend.mjs
index 3c0d6250d2..5945da9b7e 100644
--- a/scripts/dev-backend.mjs
+++ b/scripts/dev-backend.mjs
@@ -8,9 +8,7 @@ const env = {
const hasCargoWatch = await commandSucceeds("cargo", ["watch", "--version"]);
if (!hasCargoWatch) {
- console.warn(
- "cargo-watch is not installed; running dbx-web without hot reload. Install with: cargo install cargo-watch",
- );
+ console.warn("cargo-watch is not installed; running dbx-web without hot reload. Install with: cargo install cargo-watch");
}
const args = hasCargoWatch ? ["watch", "-x", "run -p dbx-web"] : ["run", "-p", "dbx-web"];
diff --git a/scripts/release.mjs b/scripts/release.mjs
index 3f558ee344..e26d8a9d0e 100644
--- a/scripts/release.mjs
+++ b/scripts/release.mjs
@@ -3,13 +3,7 @@ import { readFileSync } from "node:fs";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { isDesktopVersionOnlyCargoLockChange } from "./release-lock.mjs";
-import {
- evaluateAgentVersionBump,
- getAgentVersionChanges,
- isAgentPublishRelevantFile,
- parseLegacyStandaloneProjects,
- resolveAgentReleaseBaseline,
-} from "../.github/scripts/bump-agent-versions.mjs";
+import { evaluateAgentVersionBump, getAgentVersionChanges, isAgentPublishRelevantFile, parseLegacyStandaloneProjects, resolveAgentReleaseBaseline } from "../.github/scripts/bump-agent-versions.mjs";
const REPO = "t8y2/dbx";
const PACKAGES_WORKFLOW = "mcp-release.yml";
@@ -54,15 +48,7 @@ const PACKAGE_RELEASE_PATHS = [
"Cargo.lock",
".github/workflows/mcp-release.yml",
];
-const AGENT_RELEASE_PATHS = [
- "agents/build.gradle",
- "agents/settings.gradle",
- "agents/versions.json",
- "agents/common/build.gradle",
- "agents/common/src/main/",
- "agents/drivers/",
- ".github/workflows/agents-release.yml",
-];
+const AGENT_RELEASE_PATHS = ["agents/build.gradle", "agents/settings.gradle", "agents/versions.json", "agents/common/build.gradle", "agents/common/src/main/", "agents/drivers/", ".github/workflows/agents-release.yml"];
const args = process.argv.slice(2);
let target = null;
@@ -371,17 +357,9 @@ function validateRollbackRelease(rollbackRelease, latestRelease) {
const version = formatVersion(rollbackVersion);
// Rollback targets may predate the aarch64 -> arm64 dmg rename, so either
// asset name satisfies the Apple Silicon requirement.
- const requiredAssets = [
- ["latest.json"],
- [`DBX_${version}_arm64.dmg`, `DBX_${version}_aarch64.dmg`],
- [`DBX_${version}_x64.dmg`],
- [`DBX_${version}_x64-setup.exe`],
- [`DBX_${version}_arm64-setup.exe`],
- ];
+ const requiredAssets = [["latest.json"], [`DBX_${version}_arm64.dmg`, `DBX_${version}_aarch64.dmg`], [`DBX_${version}_x64.dmg`], [`DBX_${version}_x64-setup.exe`], [`DBX_${version}_arm64-setup.exe`]];
const assetNames = new Set(rollbackRelease.assets.map((asset) => asset.name));
- const missingAssets = requiredAssets
- .filter((candidates) => !candidates.some((asset) => assetNames.has(asset)))
- .map((candidates) => candidates.join(" or "));
+ const missingAssets = requiredAssets.filter((candidates) => !candidates.some((asset) => assetNames.has(asset))).map((candidates) => candidates.join(" or "));
if (missingAssets.length > 0) {
fail(`Rollback target ${rollbackRelease.tagName} is missing required distribution assets: ${missingAssets.join(", ")}`);
}
@@ -458,8 +436,8 @@ function getLatestAgentTag() {
});
if (legacyRepoCheck.status === 0) {
- const legacyTags = run("git", ["-C", legacyRepo, "tag", "--list", "v*"]).stdout
- .split(/\r?\n/)
+ const legacyTags = run("git", ["-C", legacyRepo, "tag", "--list", "v*"])
+ .stdout.split(/\r?\n/)
.map((legacyTag) => legacyTag.trim())
.filter(Boolean)
.map((legacyTag) => ({ legacyTag, version: parseVersion(legacyTag.replace(/^v/, "")) }))
@@ -488,17 +466,13 @@ function getAgentReleaseStatus(latest = getLatestAgentTag()) {
}
const effectiveBaseline = resolveAgentReleaseBaseline({ prevTag: latest.tag });
- const changedFiles = effectiveBaseline.changedFiles
- .filter((file) => AGENT_RELEASE_PATHS.some((path) => file === path || file.startsWith(path)))
- .filter(isAgentPublishRelevantFile);
+ const changedFiles = effectiveBaseline.changedFiles.filter((file) => AGENT_RELEASE_PATHS.some((path) => file === path || file.startsWith(path))).filter(isAgentPublishRelevantFile);
return {
needed: changedFiles.length > 0,
baseline: latest.tag,
changedFiles,
previousVersions: effectiveBaseline.versions,
- versionBaseline: effectiveBaseline.syncCommit
- ? `${effectiveBaseline.syncCommit.slice(0, 10)} (post-release version sync)`
- : latest.tag,
+ versionBaseline: effectiveBaseline.syncCommit ? `${effectiveBaseline.syncCommit.slice(0, 10)} (post-release version sync)` : latest.tag,
};
}
@@ -548,8 +522,7 @@ function printAgentVersionChanges(status) {
legacyStandaloneModules,
manualVersionsChanged: status.changedFiles.includes("agents/versions.json"),
});
- const changes = getAgentVersionChanges(status.previousVersions, result.versions)
- .map(({ moduleName, previousVersion, nextVersion }) => `${moduleName}: ${previousVersion ?? "new"} -> ${nextVersion}`);
+ const changes = getAgentVersionChanges(status.previousVersions, result.versions).map(({ moduleName, previousVersion, nextVersion }) => `${moduleName}: ${previousVersion ?? "new"} -> ${nextVersion}`);
if (changes.length === 0) {
console.log(dim("No automatic driver version changes detected."));
@@ -571,8 +544,8 @@ function formatStatusSummary(status) {
}
function getLatestSemverTag(prefix) {
- const tags = run("git", ["tag", "--list", `${prefix}*`]).stdout
- .split(/\r?\n/)
+ const tags = run("git", ["tag", "--list", `${prefix}*`])
+ .stdout.split(/\r?\n/)
.map((tag) => tag.trim())
.filter(Boolean)
.map((tag) => ({ tag, version: parseVersion(tag.replace(prefix, "")) }))
@@ -632,7 +605,10 @@ function resolveReleaseVersion(bump, latestVersion, explicitTagPrefix) {
}
function normalizeExplicitVersion(value, explicitTagPrefix) {
- const trimmed = value.trim().replace(new RegExp(`^${escapeRegExp(explicitTagPrefix)}`), "").replace(/^v/, "");
+ const trimmed = value
+ .trim()
+ .replace(new RegExp(`^${escapeRegExp(explicitTagPrefix)}`), "")
+ .replace(/^v/, "");
const version = parseVersion(trimmed);
return version ? formatVersion(version) : null;
}
@@ -684,14 +660,7 @@ function ensureGhWorkflowsReady(workflows) {
}
function fetchReleaseTags() {
- run("git", [
- "fetch",
- "--quiet",
- "origin",
- "+refs/tags/v*:refs/tags/v*",
- "+refs/tags/packages-v*:refs/tags/packages-v*",
- "+refs/tags/agents-v*:refs/tags/agents-v*",
- ]);
+ run("git", ["fetch", "--quiet", "origin", "+refs/tags/v*:refs/tags/v*", "+refs/tags/packages-v*:refs/tags/packages-v*", "+refs/tags/agents-v*:refs/tags/agents-v*"]);
}
function run(command, commandArgs, options = {}) {
diff --git a/scripts/release.test.mjs b/scripts/release.test.mjs
index 827f37249f..17cf4594bf 100644
--- a/scripts/release.test.mjs
+++ b/scripts/release.test.mjs
@@ -83,11 +83,7 @@ test("rollback dispatches each distribution workflow after validation", () => {
assert.equal(result.status, 0, result.stderr);
const commands = readFileSync(logPath, "utf8").trim().split("\n");
- assert.deepEqual(commands, [
- "workflow run publish-packages.yml --repo t8y2/dbx -f tag=v0.5.63 -f notify=false",
- "workflow run sync-cnb-release-assets.yml --repo t8y2/dbx -f tag=v0.5.63",
- "workflow run rollback-docker-latest.yml --repo t8y2/dbx -f tag=v0.5.63",
- ]);
+ assert.deepEqual(commands, ["workflow run publish-packages.yml --repo t8y2/dbx -f tag=v0.5.63 -f notify=false", "workflow run sync-cnb-release-assets.yml --repo t8y2/dbx -f tag=v0.5.63", "workflow run rollback-docker-latest.yml --repo t8y2/dbx -f tag=v0.5.63"]);
});
test("rollback rejects a target that is not older than latest", () => {
@@ -185,10 +181,7 @@ test("Linux releases pin the Wayland-capable AppImage bundler before upload", ()
assert.ok(prepareStep < buildStep);
assert.ok(buildStep < verifyStep);
assert.match(workflow, /XDG_CACHE_HOME: \$\{\{ runner\.temp \}\}\/dbx-tauri-cache/);
- assert.match(
- workflow,
- /LINUXDEPLOY_EXCLUDED_LIBRARIES: "libwayland-\*\.so\*;libxkbcommon\.so\*;libxcb-randr\.so\*;libxcb-render\.so\*;libxcb-shm\.so\*;libXau\.so\*;libXdmcp\.so\*"/,
- );
+ assert.match(workflow, /LINUXDEPLOY_EXCLUDED_LIBRARIES: "libwayland-\*\.so\*;libxkbcommon\.so\*;libxcb-randr\.so\*;libxcb-render\.so\*;libxcb-shm\.so\*;libXau\.so\*;libXdmcp\.so\*"/);
assert.match(setup, /tauri_fix_commit=8e7028331ad37ac2db74d4ec20e66be5cacf2c40/);
assert.match(setup, /linuxdeploy_revision=07333c6/);
assert.match(setup, /36a2d7e274d12e1050d0e9ecfe11d339ed54720b2bec464c286d53f8b07f5c62/);
@@ -209,19 +202,11 @@ test("AppImage GTK wrapper removes bundled display-stack libraries", () => {
const nestedLibDir = join(libDir, "nested");
copyFileSync(appImageGtkWrapper, wrapper);
- writeFileSync(
- upstream,
- '#!/usr/bin/env bash\nif [[ "$1" == "--plugin-api-version" ]]; then printf "0\\n"; fi\n',
- );
+ writeFileSync(upstream, '#!/usr/bin/env bash\nif [[ "$1" == "--plugin-api-version" ]]; then printf "0\\n"; fi\n');
chmodSync(wrapper, 0o755);
chmodSync(upstream, 0o755);
mkdirSync(nestedLibDir, { recursive: true });
- for (const path of [
- join(libDir, "libwayland-client.so.0"),
- join(libDir, "libxkbcommon.so.0"),
- join(nestedLibDir, "libXau.so.6"),
- join(libDir, "libsafe.so.1"),
- ]) {
+ for (const path of [join(libDir, "libwayland-client.so.0"), join(libDir, "libxkbcommon.so.0"), join(nestedLibDir, "libXau.so.6"), join(libDir, "libsafe.so.1")]) {
writeFileSync(path, "");
}
diff --git a/scripts/sync-connection-types.mjs b/scripts/sync-connection-types.mjs
index 290b493cf9..cdb4be00ec 100644
--- a/scripts/sync-connection-types.mjs
+++ b/scripts/sync-connection-types.mjs
@@ -46,24 +46,7 @@ const profileCatalogKeys = new Set(["schemaVersion", "profiles"]);
const connectionProfileKeys = new Set(["id", "dbType", "label", "pickerLabel", "icon", "pickerIcon", "port", "user", "host", "urlParams", "category"]);
const driverProfileKeys = new Set(["profile", "agentKey", "packageKey", "label", "storeVisible", "storeOrder"]);
const managedDriverKeys = new Set(["key", "label", "storeVisible", "storeOrder"]);
-const capabilityKeys = new Set([
- "queryExecution",
- "metadataBrowse",
- "objectBrowser",
- "objectSource",
- "schemaSearch",
- "diagram",
- "tableDataEdit",
- "tableStructureEdit",
- "tableImport",
- "dataTransfer",
- "sqlFileExecution",
- "databaseCreate",
- "fieldLineage",
- "sqlExplain",
- "userAdmin",
- "driverManagement",
-]);
+const capabilityKeys = new Set(["queryExecution", "metadataBrowse", "objectBrowser", "objectSource", "schemaSearch", "diagram", "tableDataEdit", "tableStructureEdit", "tableImport", "dataTransfer", "sqlFileExecution", "databaseCreate", "fieldLineage", "sqlExplain", "userAdmin", "driverManagement"]);
const traitKeys = new Set(["schemaAware", "databaseSchemaQualified", "singleDatabase", "clearableQuerySchema", "fetchFirst", "treeSchema", "databaseObjectTree", "pgVacuum", "pgLikeStructure", "diagramSql"]);
function connectionTypeFiles() {
diff --git a/scripts/verify-cli-migration.mjs b/scripts/verify-cli-migration.mjs
index 3226c5788d..05246b510c 100644
--- a/scripts/verify-cli-migration.mjs
+++ b/scripts/verify-cli-migration.mjs
@@ -130,9 +130,7 @@ assert.equal(csv.status, 0, csv.stderr);
assert.match(csv.stdout, /^csv_value\n"a,b"$/);
console.log("PASS csv escaping");
-const limited = jsonOutput(
- run(rustBinary, ["query", connection, "select 1 as n union all select 2 union all select 3", "--limit", "2", "--json"]),
-);
+const limited = jsonOutput(run(rustBinary, ["query", connection, "select 1 as n union all select 2 union all select 3", "--limit", "2", "--json"]));
assert.equal(limited.rows.length, 2);
assert.equal(limited.row_count, 2);
console.log("PASS query limit");
diff --git a/scripts/verify-nacos-replace-history.mjs b/scripts/verify-nacos-replace-history.mjs
index a0da37b8ad..1fa5c2e685 100644
--- a/scripts/verify-nacos-replace-history.mjs
+++ b/scripts/verify-nacos-replace-history.mjs
@@ -18,12 +18,12 @@ const runId = randomUUID();
const group = `DBX_VERIFY_ONLY_${runId}`;
const oldValue = `mysql-old-${runId}.invalid:3306`;
const newValue = `mysql-new-${runId}.invalid:3306`;
-const keys = ["one.yaml", "two.properties"].map(name => ({ namespace: "", group, dataId: `${runId}-${name}` }));
+const keys = ["one.yaml", "two.properties"].map((name) => ({ namespace: "", group, dataId: `${runId}-${name}` }));
const created = [];
const outputDir = process.env.DBX_TEST_OUTPUT || path.resolve("outputs/nacos-history-verification");
await mkdir(outputDir, { recursive: true });
const metadata = JSON.parse(await readFile("node_modules/.vite/deps/_metadata.json", "utf8"));
-const dependencyUrl = name => `/@fs/${path.resolve("node_modules/.vite/deps", metadata.optimized[name].file).replaceAll("\\", "/")}?v=${metadata.browserHash}`;
+const dependencyUrl = (name) => `/@fs/${path.resolve("node_modules/.vite/deps", metadata.optimized[name].file).replaceAll("\\", "/")}?v=${metadata.browserHash}`;
async function request(endpoint, body) {
const response = await fetch(`${backend}${endpoint}`, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : undefined);
@@ -31,12 +31,14 @@ async function request(endpoint, body) {
return response.json();
}
function checksum() {
- return execFileSync("docker", ["exec", "dbx-nacos-mysql", "sh", "-c",
- 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --batch --skip-column-names -e "SET SESSION group_concat_max_len=16777216; SELECT COUNT(*), MD5(GROUP_CONCAT(CONCAT(id, CHAR(58), MD5(content)) ORDER BY id)) FROM nacos.config_info;"',
- ], { encoding: "utf8" }).trim();
+ return execFileSync(
+ "docker",
+ ["exec", "dbx-nacos-mysql", "sh", "-c", 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --batch --skip-column-names -e "SET SESSION group_concat_max_len=16777216; SELECT COUNT(*), MD5(GROUP_CONCAT(CONCAT(id, CHAR(58), MD5(content)) ORDER BY id)) FROM nacos.config_info;"'],
+ { encoding: "utf8" },
+ ).trim();
}
const connections = await request("/api/connection/list");
-const connection = connections.find(item => item.id === connectionId);
+const connection = connections.find((item) => item.id === connectionId);
assert(connection?.db_type === "nacos");
const addr = new URL(connection.external_config.serverAddr);
assert(["host.docker.internal", "127.0.0.1", "localhost", "database"].includes(addr.hostname), "Only local Docker Nacos is allowed");
@@ -51,11 +53,17 @@ try {
const context = await browser.newContext({ locale: "zh-CN", viewport: { width: 1366, height: 900 } });
const page = await context.newPage();
const pageErrors = [];
- page.on("pageerror", error => { pageErrors.push(error.message); console.error("Browser module error:", error.message); });
- page.on("requestfailed", req => console.error("Browser module request failed:", new URL(req.url()).pathname));
+ page.on("pageerror", (error) => {
+ pageErrors.push(error.message);
+ console.error("Browser module error:", error.message);
+ });
+ page.on("requestfailed", (req) => console.error("Browser module request failed:", new URL(req.url()).pathname));
// Mount the real component in an isolated harness without touching user tabs
// or the normal app's saved connections, layout and editor state.
- await page.route(`${frontend}/`, route => route.fulfill({ contentType: "text/html", body: `
` }));
- await page.route("**/api/nacos/configs/publish", async route => {
+