mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(memory): preserve QMD results in noisy command output (#115023)
This commit is contained in:
committed by
GitHub
parent
521d05f290
commit
81628fb323
@@ -25,6 +25,35 @@ complete`,
|
||||
expect(results).toEqual([{ docid: "abc", score: 0.5 }]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "bracketed log prefix",
|
||||
stdout: '[qmd] initializing search\n[{"docid":"abc","score":0.5}]',
|
||||
},
|
||||
{
|
||||
name: "bracketed log with an empty array",
|
||||
stdout: '[qmd] filters=[]\n[{"docid":"abc","score":0.5}]',
|
||||
},
|
||||
{
|
||||
name: "bracketed log with result-shaped data",
|
||||
stdout: '[qmd] previous=[{"docid":"stale","score":1}]\n[{"docid":"abc","score":0.5}]',
|
||||
},
|
||||
{
|
||||
name: "leading no-results marker",
|
||||
stdout: 'warning: no results found\n[{"docid":"abc","score":0.5}]',
|
||||
},
|
||||
{
|
||||
name: "trailing no-results marker",
|
||||
stdout: '[{"docid":"abc","score":0.5}]\nwarning: no results found',
|
||||
},
|
||||
{
|
||||
name: "bracketed no-results marker",
|
||||
stdout: '[qmd] warning: no results found\n[{"docid":"abc","score":0.5}]',
|
||||
},
|
||||
])("preserves query results after a $name", ({ stdout }) => {
|
||||
expect(parseQmdQueryJson(stdout, "")).toEqual([{ docid: "abc", score: 0.5 }]);
|
||||
});
|
||||
|
||||
it("preserves explicit qmd line metadata when present", () => {
|
||||
const results = parseQmdQueryJson(
|
||||
'[{"docid":"abc","score":0.5,"start_line":4,"end_line":6,"snippet":"@@ -10,1\\nignored"}]',
|
||||
@@ -58,6 +87,9 @@ complete`,
|
||||
it("treats prefixed no-results marker output as an empty result set", () => {
|
||||
expect(parseQmdQueryJson("warning: no results found", "")).toStrictEqual([]);
|
||||
expect(parseQmdQueryJson("", "[qmd] warning: no results found\n")).toStrictEqual([]);
|
||||
expect(
|
||||
parseQmdQueryJson("[qmd] initializing search\nwarning: no results found", ""),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bounded stderr context UTF-16 safe", () => {
|
||||
@@ -81,6 +113,12 @@ complete`,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed bracket-heavy stdout without repeated rescanning", () => {
|
||||
expect(() => parseQmdQueryJson("[".repeat(4_096), "")).toThrow(
|
||||
/qmd query returned invalid JSON/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("routes invalid-output diagnostics through console capture", () => {
|
||||
vi.stubEnv("VITEST", "");
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
|
||||
@@ -21,9 +21,8 @@ export type QmdQueryResult = {
|
||||
export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] {
|
||||
const trimmedStdout = stdout.trim();
|
||||
const trimmedStderr = stderr.trim();
|
||||
const stdoutIsMarker = trimmedStdout.length > 0 && isQmdNoResultsOutput(trimmedStdout);
|
||||
const stderrIsMarker = trimmedStderr.length > 0 && isQmdNoResultsOutput(trimmedStderr);
|
||||
if (stdoutIsMarker || (!trimmedStdout && stderrIsMarker)) {
|
||||
if (!trimmedStdout && stderrIsMarker) {
|
||||
return [];
|
||||
}
|
||||
if (!trimmedStdout) {
|
||||
@@ -39,6 +38,9 @@ export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResul
|
||||
}
|
||||
const noisyPayload = extractFirstJsonArray(trimmedStdout);
|
||||
if (!noisyPayload) {
|
||||
if (isQmdNoResultsOutput(trimmedStdout)) {
|
||||
return [];
|
||||
}
|
||||
throw new Error("qmd query JSON response was not an array");
|
||||
}
|
||||
const fallback = parseQmdQueryResultArray(noisyPayload);
|
||||
@@ -127,26 +129,43 @@ function parseQmdLineNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/** Extract the first complete JSON array from noisy stdout. */
|
||||
/** Extract the first complete, standalone JSON result array from noisy stdout. */
|
||||
function extractFirstJsonArray(raw: string): string | null {
|
||||
const start = raw.indexOf("[");
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
let start = -1;
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let i = start; i < raw.length; i += 1) {
|
||||
let atLineStart = true;
|
||||
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
const char = raw[i];
|
||||
if (char === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (start < 0) {
|
||||
if (char === "\n") {
|
||||
atLineStart = true;
|
||||
continue;
|
||||
}
|
||||
if (atLineStart && (char === " " || char === "\t" || char === "\r")) {
|
||||
continue;
|
||||
}
|
||||
// QMD emits result arrays on their own line; log fields can contain arrays too.
|
||||
if (!atLineStart || char !== "[") {
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
start = i;
|
||||
depth = 1;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
} else if (char === "\\") {
|
||||
escaped = true;
|
||||
} else if (char === '"') {
|
||||
inString = false;
|
||||
@@ -159,12 +178,17 @@ function extractFirstJsonArray(raw: string): string | null {
|
||||
}
|
||||
if (char === "[") {
|
||||
depth += 1;
|
||||
} else if (char === "]") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return raw.slice(start, i + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char !== "]" || --depth !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = raw.slice(start, i + 1);
|
||||
if (parseQmdQueryResultArray(candidate) !== null) {
|
||||
return candidate;
|
||||
}
|
||||
start = -1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user