fix(skills): bootstrap generated lbs wrapper

This commit is contained in:
huanghuoguoguo
2026-06-20 17:19:35 +08:00
parent e9dd584792
commit 869567c975
6 changed files with 128 additions and 43 deletions
+1
View File
@@ -39,6 +39,7 @@ bin/lbs index
```
Use `bin/lbs env show` to inspect defaults and `bin/lbs env doctor` when diagnosing local environment readiness. Env output is redacted by default; do not work around that by printing raw secrets.
`bin/lbs` is a generated local wrapper. If it is missing on a fresh checkout, run `npm run bootstrap` from this directory first; `npm install` also regenerates it via `prepare`.
Use `bin/lbs fixture check` before fixture-heavy cases such as MCP, RAG, multimodal, or plugin smoke tests.
Use `bin/lbs case list --ready` for cases that have no missing machine inputs and no manual preconditions. Use `bin/lbs case list --machine-ready` when you want to keep `manual-check` candidates and confirm their preconditions yourself.
+5 -1
View File
@@ -35,9 +35,13 @@ and LangBot's own Local Agent) working with the LangBot ecosystem.
## The `lbs` CLI
The testing assets ship with a small CLI (`bin/lbs`, Node 22.6):
The testing assets ship with a small CLI (`bin/lbs`, Node >= 22.6). The
`bin/lbs` wrapper is a generated local entrypoint; on a fresh checkout, run
`npm run bootstrap` once if it is missing. `npm install` also regenerates it via
the `prepare` script.
```bash
npm run bootstrap # create bin/lbs if missing
bin/lbs validate # validate skills/cases/troubleshooting structure
bin/lbs index # regenerate skills.index.json
bin/lbs env show # inspect resolved env defaults (redacted)
+7
View File
@@ -5,6 +5,13 @@
"lbs": "./bin/lbs"
},
"scripts": {
"bootstrap": "node scripts/bootstrap-lbs.mjs",
"prepare": "node scripts/bootstrap-lbs.mjs",
"prevalidate": "node scripts/bootstrap-lbs.mjs",
"preindex": "node scripts/bootstrap-lbs.mjs",
"preindex:check": "node scripts/bootstrap-lbs.mjs",
"pretest": "node scripts/bootstrap-lbs.mjs",
"precheck": "node scripts/bootstrap-lbs.mjs",
"lbs": "node src/lbs.ts",
"test": "node test/lbs-cli.test.ts",
"validate": "node src/lbs.ts validate",
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env node
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const binDir = resolve(root, "bin");
const lbsPath = resolve(binDir, "lbs");
const wrapper = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
'exec node "$SCRIPT_DIR/../src/lbs.ts" "$@"',
"",
].join("\n");
await mkdir(binDir, { recursive: true });
let current = "";
try {
current = await readFile(lbsPath, "utf8");
} catch {
// Missing wrapper is the normal first-run path.
}
if (current !== wrapper) {
await writeFile(lbsPath, wrapper, "utf8");
await chmod(lbsPath, 0o755);
}
+1 -1
View File
@@ -61,7 +61,7 @@ export function repoRoot(start: string): string {
// root of this assets tree). Check it first so that when the tree lives
// inside a larger repo (e.g. LangBot/skills/), we stop at the assets root
// and not at the outer repo's .git/README.md.
if (existsSync(`${current}/skills.index.json`) && existsSync(`${current}/bin/lbs`)) {
if (existsSync(`${current}/skills.index.json`) && existsSync(`${current}/schemas/case.schema.json`)) {
return current;
}
if (existsSync(`${current}/.git`) && existsSync(`${current}/README.md`)) {
+83 -41
View File
@@ -15,6 +15,7 @@ import { commandTroubleSearch } from "../src/commands/trouble.ts";
import { commandValidate } from "../src/commands/validate.ts";
import { commandIndex } from "../src/commands/skill.ts";
import { loadEnv } from "../src/fs.ts";
import { repoRoot } from "../src/cli.ts";
import {
classifyDebugChatResult,
findNewFailureSignal,
@@ -23,6 +24,19 @@ import {
const root = process.cwd();
test("repo root detects the skills tree before generated bin exists", () => {
const tmp = mkdtempSync(join(tmpdir(), "lbs-root-no-bin-"));
try {
mkdirSync(join(tmp, "schemas"), { recursive: true });
mkdirSync(join(tmp, "skills", "langbot-testing"), { recursive: true });
writeFileSync(join(tmp, "skills.index.json"), "{}");
writeFileSync(join(tmp, "schemas", "case.schema.json"), "{}");
assert.equal(repoRoot(join(tmp, "skills", "langbot-testing")), tmp);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
function ctx(args: string[]): CommandContext {
return { root, args };
}
@@ -75,6 +89,19 @@ function suiteResult(caseId: string, runId: string, status = "pass", evidence =
});
}
function withEnv<T>(values: Record<string, string>, fn: () => T): T {
const previous = new Map(Object.keys(values).map((key) => [key, process.env[key]]));
try {
for (const [key, value] of Object.entries(values)) process.env[key] = value;
return fn();
} finally {
for (const [key, value] of previous) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}
async function captureAsync(fn: () => Promise<number>): Promise<{ code: number; output: string }> {
const originalLog = console.log;
const lines: string[] = [];
@@ -1417,15 +1444,20 @@ test("generic pipeline readiness accepts either URL or name target", () => {
const originalUrl = process.env.LANGBOT_PIPELINE_URL;
const originalName = process.env.LANGBOT_PIPELINE_NAME;
try {
process.env.LANGBOT_PIPELINE_URL = "http://127.0.0.1:3000/home/pipelines?id=only-url";
process.env.LANGBOT_PIPELINE_NAME = "";
withEnv({
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
}, () => {
process.env.LANGBOT_PIPELINE_URL = "http://127.0.0.1:3000/home/pipelines?id=only-url";
process.env.LANGBOT_PIPELINE_NAME = "";
const ready = capture(() => commandTestPlan(ctx(["test", "plan", "pipeline-debug-chat", "--json"])));
assert.equal(ready.code, 0);
const plan = JSON.parse(ready.output);
assert.equal(plan.env_readiness.status, "ready");
assert.equal(plan.automation_readiness.status, "ready");
assert.ok(plan.automation_readiness.required.includes("LANGBOT_PIPELINE_URL|LANGBOT_PIPELINE_NAME"));
const ready = capture(() => commandTestPlan(ctx(["test", "plan", "pipeline-debug-chat", "--json"])));
assert.equal(ready.code, 0);
const plan = JSON.parse(ready.output);
assert.equal(plan.env_readiness.status, "ready");
assert.equal(plan.automation_readiness.status, "ready");
assert.ok(plan.automation_readiness.required.includes("LANGBOT_PIPELINE_URL|LANGBOT_PIPELINE_NAME"));
});
process.env.LANGBOT_PIPELINE_URL = "";
process.env.LANGBOT_PIPELINE_NAME = "";
@@ -2174,27 +2206,32 @@ test("local-agent effective prompt case has runnable automation defaults", () =>
});
test("local-agent basic case can setup the local-agent pipeline env", () => {
const result = capture(() => commandTestRun(ctx([
"test",
"run",
"local-agent-basic-debug-chat",
"--dry-run",
"--json",
])));
assert.equal(result.code, 0);
const run = JSON.parse(result.output);
assert.deepEqual(run.setup_automation.map((item: { entry: string }) => item.entry), [
"node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env",
]);
withEnv({
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
}, () => {
const result = capture(() => commandTestRun(ctx([
"test",
"run",
"local-agent-basic-debug-chat",
"--dry-run",
"--json",
])));
assert.equal(result.code, 0);
const run = JSON.parse(result.output);
assert.deepEqual(run.setup_automation.map((item: { entry: string }) => item.entry), [
"node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env",
]);
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "local-agent-basic-debug-chat", "--json"])));
assert.equal(planResult.code, 0);
const plan = JSON.parse(planResult.output);
assert.deepEqual(plan.setup_provides_env, [
"LANGBOT_LOCAL_AGENT_PIPELINE_URL",
"LANGBOT_LOCAL_AGENT_PIPELINE_NAME",
]);
assert.equal(plan.automation_readiness.status, "ready");
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "local-agent-basic-debug-chat", "--json"])));
assert.equal(planResult.code, 0);
const plan = JSON.parse(planResult.output);
assert.deepEqual(plan.setup_provides_env, [
"LANGBOT_LOCAL_AGENT_PIPELINE_URL",
"LANGBOT_LOCAL_AGENT_PIPELINE_NAME",
]);
assert.equal(plan.automation_readiness.status, "ready");
});
});
test("local-agent nonstreaming case disables stream output through automation defaults", () => {
@@ -2355,20 +2392,25 @@ test("AgentRunner QA Debug Chat case uses dedicated pipeline env", () => {
});
test("AgentRunner QA Debug Chat setup automation removes manual readiness", () => {
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "agent-runner-qa-debug-chat", "--json"])));
assert.equal(planResult.code, 0);
const plan = JSON.parse(planResult.output);
assert.equal(plan.manual_readiness.status, "not_required");
assert.deepEqual(plan.setup_provides_env, [
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_URL",
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_NAME",
]);
assert.equal(plan.automation_readiness.status, "ready");
withEnv({
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
}, () => {
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "agent-runner-qa-debug-chat", "--json"])));
assert.equal(planResult.code, 0);
const plan = JSON.parse(planResult.output);
assert.equal(plan.manual_readiness.status, "not_required");
assert.deepEqual(plan.setup_provides_env, [
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_URL",
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_NAME",
]);
assert.equal(plan.automation_readiness.status, "ready");
const suiteResult = capture(() => commandSuitePlan(ctx(["suite", "plan", "agent-runner-release-gate", "--json"])));
assert.equal(suiteResult.code, 0);
const suite = JSON.parse(suiteResult.output);
assert.ok(!suite.readiness.manual_check_cases.includes("agent-runner-qa-debug-chat"));
const suiteResult = capture(() => commandSuitePlan(ctx(["suite", "plan", "agent-runner-release-gate", "--json"])));
assert.equal(suiteResult.code, 0);
const suite = JSON.parse(suiteResult.output);
assert.ok(!suite.readiness.manual_check_cases.includes("agent-runner-qa-debug-chat"));
});
});
test("ACP AgentRunner Debug Chat case setups the ACP pipeline env", () => {