Skip to content

Commit 9c2744f

Browse files
committed
test(scripts): require usable memory search in fd repro
1 parent 3aa4604 commit 9c2744f

2 files changed

Lines changed: 220 additions & 5 deletions

File tree

scripts/check-memory-fd-repro.mjs

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import os from "node:os";
77
import path from "node:path";
88
import process from "node:process";
99
import { pathToFileURL } from "node:url";
10+
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
1011
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
1112

1213
const ISSUE_FILE_COUNTS = [
@@ -63,6 +64,28 @@ Options:
6364
}
6465

6566
const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;
67+
const ARGUMENT_FLAGS = new Set([
68+
"--allow-non-darwin",
69+
"--expect-leak",
70+
"--files",
71+
"--full",
72+
"--help",
73+
"--invoke-timeout-ms",
74+
"--keep",
75+
"--max-workspace-reg-fds",
76+
"--min-leaked-fds",
77+
"--mode",
78+
"--output-dir",
79+
"--report-only",
80+
"--sample-delay-ms",
81+
"--settle-delay-ms",
82+
]);
83+
84+
function stripPackageManagerSeparatorForKnownFlags(argv) {
85+
return argv[0] === "--" && ARGUMENT_FLAGS.has(argv[1])
86+
? stripLeadingPackageManagerSeparator(argv)
87+
: argv;
88+
}
6689

6790
export function readNumber(value, label) {
6891
const raw = String(value).trim();
@@ -95,6 +118,7 @@ function readPositiveNumberEnv(name, fallback) {
95118
}
96119

97120
export function parseArgs(argv) {
121+
const args = stripPackageManagerSeparatorForKnownFlags(argv);
98122
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
99123
const options = {
100124
fileCount: undefined,
@@ -109,9 +133,9 @@ export function parseArgs(argv) {
109133
allowNonDarwin: process.env.OPENCLAW_MEMORY_FD_REPRO_ALLOW_NON_DARWIN === "1",
110134
};
111135

112-
parseArgv: for (let i = 0; i < argv.length; i += 1) {
113-
const arg = argv[i];
114-
const next = argv[i + 1];
136+
parseArgv: for (let i = 0; i < args.length; i += 1) {
137+
const arg = args[i];
138+
const next = args[i + 1];
115139
const readValue = () => {
116140
if (!next) {
117141
throw new Error(`Missing value for ${arg}`);
@@ -435,6 +459,122 @@ export async function stopGatewayWithRuntime({
435459

436460
export { readBoundedResponseText };
437461

462+
function parseJsonValue(text) {
463+
try {
464+
return JSON.parse(text);
465+
} catch {
466+
return null;
467+
}
468+
}
469+
470+
function asRecord(value) {
471+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
472+
}
473+
474+
function readStringProperty(record, key) {
475+
const value = record?.[key];
476+
return typeof value === "string" && value.trim() ? value : undefined;
477+
}
478+
479+
function parseToolTextContent(result) {
480+
const content = Array.isArray(result?.content) ? result.content : [];
481+
for (const entry of content) {
482+
const text = entry?.type === "text" && typeof entry.text === "string" ? entry.text : null;
483+
if (!text) {
484+
continue;
485+
}
486+
const parsed = asRecord(parseJsonValue(text));
487+
if (parsed) {
488+
return parsed;
489+
}
490+
}
491+
return null;
492+
}
493+
494+
export function classifyMemorySearchInvokeResponse({ httpOk, status, bodyText }) {
495+
const parsedBody = parseJsonValue(bodyText);
496+
const body = asRecord(parsedBody);
497+
if (!httpOk) {
498+
const errorRecord = asRecord(body?.error);
499+
return {
500+
ok: false,
501+
httpOk,
502+
status,
503+
gatewayOk: body?.ok === true ? true : body?.ok === false ? false : undefined,
504+
error:
505+
readStringProperty(errorRecord, "message") ??
506+
readStringProperty(body, "error") ??
507+
`memory_search HTTP request failed with status ${status}`,
508+
};
509+
}
510+
if (!body) {
511+
return {
512+
ok: false,
513+
httpOk,
514+
status,
515+
error: "memory_search response was not JSON",
516+
};
517+
}
518+
519+
const gatewayOk = body.ok === true ? true : body.ok === false ? false : undefined;
520+
if (gatewayOk === false) {
521+
const errorRecord = asRecord(body.error);
522+
return {
523+
ok: false,
524+
httpOk,
525+
status,
526+
gatewayOk,
527+
error:
528+
readStringProperty(errorRecord, "message") ??
529+
readStringProperty(body, "error") ??
530+
"memory_search gateway invocation failed",
531+
};
532+
}
533+
534+
const result = asRecord(body.result);
535+
const details = asRecord(result?.details);
536+
const directResult = Array.isArray(result?.results) ? result : null;
537+
const directBody =
538+
Array.isArray(body.results) || body.disabled === true || body.unavailable === true
539+
? body
540+
: null;
541+
const payload = details ?? parseToolTextContent(result) ?? directResult ?? directBody;
542+
if (!payload) {
543+
return {
544+
ok: false,
545+
httpOk,
546+
status,
547+
gatewayOk,
548+
error: "memory_search result payload missing or invalid",
549+
};
550+
}
551+
const resultCount = Array.isArray(payload.results) ? payload.results.length : undefined;
552+
const toolDisabled = payload.disabled === true;
553+
const toolUnavailable = payload.unavailable === true;
554+
const toolError = readStringProperty(payload, "error");
555+
const ok = gatewayOk === true && !toolDisabled && !toolUnavailable && !toolError;
556+
557+
return {
558+
ok,
559+
httpOk,
560+
status,
561+
gatewayOk,
562+
resultCount,
563+
toolDisabled,
564+
toolUnavailable,
565+
...(toolError ? { toolError } : {}),
566+
...(ok
567+
? {}
568+
: {
569+
error:
570+
toolError ??
571+
(toolDisabled || toolUnavailable
572+
? "memory_search returned disabled/unavailable"
573+
: "memory_search result payload missing or invalid"),
574+
}),
575+
};
576+
}
577+
438578
async function invokeMemorySearch({ port, token, timeoutMs }) {
439579
const controller = new AbortController();
440580
const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -462,9 +602,13 @@ async function invokeMemorySearch({ port, token, timeoutMs }) {
462602
"memory_search",
463603
MEMORY_SEARCH_RESPONSE_MAX_BYTES,
464604
);
465-
return {
466-
ok: res.ok,
605+
const result = classifyMemorySearchInvokeResponse({
606+
httpOk: res.ok,
467607
status: res.status,
608+
bodyText: text,
609+
});
610+
return {
611+
...result,
468612
durationMs: Date.now() - startedAt,
469613
bodyPreview: text.slice(0, 500),
470614
};

test/scripts/check-memory-fd-repro.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest";
33
import {
44
GATEWAY_READY_OUTPUT_MAX_CHARS,
55
MEMORY_SEARCH_RESPONSE_MAX_BYTES,
6+
classifyMemorySearchInvokeResponse,
67
hasChildExited,
78
parseArgs,
89
readBoundedResponseText,
@@ -117,6 +118,76 @@ describe("check-memory-fd-repro", () => {
117118
});
118119
});
119120

121+
it("accepts the leading package-manager argument separator", () => {
122+
expect(parseArgs(["--", "--files", "20", "--allow-non-darwin"])).toMatchObject({
123+
allowNonDarwin: true,
124+
fileCount: 20,
125+
});
126+
});
127+
128+
it("accepts an available memory_search tool payload", () => {
129+
const result = classifyMemorySearchInvokeResponse({
130+
httpOk: true,
131+
status: 200,
132+
bodyText: JSON.stringify({
133+
ok: true,
134+
result: {
135+
content: [{ type: "text", text: JSON.stringify({ results: [] }) }],
136+
},
137+
}),
138+
});
139+
140+
expect(result).toMatchObject({
141+
ok: true,
142+
gatewayOk: true,
143+
resultCount: 0,
144+
});
145+
});
146+
147+
it("rejects disabled memory_search tool payloads", () => {
148+
const result = classifyMemorySearchInvokeResponse({
149+
httpOk: true,
150+
status: 200,
151+
bodyText: JSON.stringify({
152+
ok: true,
153+
result: {
154+
content: [
155+
{
156+
type: "text",
157+
text: JSON.stringify({
158+
results: [],
159+
disabled: true,
160+
unavailable: true,
161+
error: 'No API key found for provider "openai".',
162+
}),
163+
},
164+
],
165+
},
166+
}),
167+
});
168+
169+
expect(result).toMatchObject({
170+
ok: false,
171+
gatewayOk: true,
172+
toolDisabled: true,
173+
toolUnavailable: true,
174+
toolError: 'No API key found for provider "openai".',
175+
});
176+
});
177+
178+
it("rejects gateway success envelopes without memory_search details", () => {
179+
const result = classifyMemorySearchInvokeResponse({
180+
httpOk: true,
181+
status: 200,
182+
bodyText: JSON.stringify({ ok: true, result: { content: [] } }),
183+
});
184+
185+
expect(result).toMatchObject({
186+
ok: false,
187+
error: "memory_search result payload missing or invalid",
188+
});
189+
});
190+
120191
it("treats signaled gateway children as exited", () => {
121192
expect(hasChildExited({ exitCode: null, signalCode: "SIGTERM" })).toBe(true);
122193
expect(hasChildExited({ exitCode: 0, signalCode: null })).toBe(true);

0 commit comments

Comments
 (0)