Skip to content

Commit 5d38dbe

Browse files
committed
fix(e2e): echo unique launch replies in mock inference
1 parent 2677154 commit 5d38dbe

4 files changed

Lines changed: 46 additions & 2 deletions

File tree

test/e2e/fixtures/fake-openai-compatible.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface FakeOpenAiCompatibleServerOptions {
4040
readonly apiKey?: string;
4141
readonly chatContent?: string;
4242
readonly forbiddenMarkers?: readonly string[];
43+
readonly launchReplyFromPrompt?: boolean;
4344
/** Non-secret marker expected in a request under test. */
4445
readonly requestCanaryMarker?: string;
4546
readonly host?: string;
@@ -184,6 +185,7 @@ export async function startFakeOpenAiCompatibleServer(
184185
NEMOCLAW_FAKE_OPENAI_FORBIDDEN_MARKERS: JSON.stringify(options.forbiddenMarkers ?? []),
185186
NEMOCLAW_FAKE_OPENAI_HOST: host,
186187
NEMOCLAW_FAKE_OPENAI_LOG_FILE: logFile,
188+
NEMOCLAW_FAKE_OPENAI_LAUNCH_REPLY_FROM_PROMPT: options.launchReplyFromPrompt ? "1" : "0",
187189
NEMOCLAW_FAKE_OPENAI_MAX_MODEL_LEN:
188190
options.maxModelLen !== undefined ? String(options.maxModelLen) : "",
189191
NEMOCLAW_FAKE_OPENAI_MODEL: options.model ?? "test-model",

test/e2e/fixtures/inference-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export async function createE2EInferenceAdapter(
390390
// alias, so listen on the bridge-facing interfaces. The workflow uses an
391391
// ephemeral ubuntu-latest VM, an OS-assigned port, and a per-run credential.
392392
host: "0.0.0.0",
393+
launchReplyFromPrompt: true,
393394
model,
394395
publicHost: SANDBOX_HOST_ALIAS,
395396
progress: options.progress,

test/e2e/lib/fake-openai-compatible-api.mts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const requireAuth = process.env.NEMOCLAW_FAKE_OPENAI_REQUIRE_AUTH === "1";
2828
const requireAuthModels = process.env.NEMOCLAW_FAKE_OPENAI_REQUIRE_AUTH_MODELS === "1";
2929
const chatContent = process.env.NEMOCLAW_FAKE_OPENAI_CHAT_CONTENT || "ok";
3030
const responseText = process.env.NEMOCLAW_FAKE_OPENAI_RESPONSE_TEXT || chatContent;
31+
const launchReplyFromPrompt =
32+
process.env.NEMOCLAW_FAKE_OPENAI_LAUNCH_REPLY_FROM_PROMPT === "1";
3133
const requestCanaryMarker = process.env.NEMOCLAW_FAKE_OPENAI_REQUEST_CANARY_MARKER || "";
3234
const forbiddenMarkers = (() => {
3335
try {
@@ -145,6 +147,27 @@ function requestCanaryPresent(req: IncomingMessage, raw: Buffer): boolean | unde
145147
return requestMaterial.includes(requestCanaryMarker);
146148
}
147149

150+
function latestUserPrompt(payload: JsonObject): string | null {
151+
const entries = Array.isArray(payload.messages) ? payload.messages : [];
152+
for (let index = entries.length - 1; index >= 0; index -= 1) {
153+
const entry = entries[index];
154+
if (!entry || typeof entry !== "object") continue;
155+
const message = entry as JsonObject;
156+
if (message.role !== "user") continue;
157+
if (typeof message.content === "string") return message.content;
158+
}
159+
return null;
160+
}
161+
162+
function requestedLaunchReply(payload: JsonObject): string | null {
163+
if (!launchReplyFromPrompt) return null;
164+
const prompt = latestUserPrompt(payload);
165+
const match = prompt?.match(
166+
/^Join these four fragments with underscores and put only the result on its own line: NEMOCLAW, ([0-9A-F]{12}), (FIRST|SECOND), OK\. Do not use tools\.$/u,
167+
);
168+
return match ? `NEMOCLAW_${match[1]}_${match[2]}_OK` : null;
169+
}
170+
148171
const server = createServer(async (req, res) => {
149172
const path = requestPath(req);
150173

@@ -198,8 +221,9 @@ const server = createServer(async (req, res) => {
198221
sendJson(res, 401, { error: { message: "missing bearer credential" } });
199222
return;
200223
}
224+
const content = requestedLaunchReply(payload) ?? chatContent;
201225
if (payload.stream) {
202-
sendChatSse(res, chatContent);
226+
sendChatSse(res, content);
203227
return;
204228
}
205229
sendJson(res, 200, {
@@ -210,7 +234,7 @@ const server = createServer(async (req, res) => {
210234
choices: [
211235
{
212236
index: 0,
213-
message: { role: "assistant", content: chatContent },
237+
message: { role: "assistant", content },
214238
finish_reason: "stop",
215239
},
216240
],

test/e2e/support/inference-adapter.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,23 @@ describe("E2E inference adapter", () => {
180180
);
181181
});
182182

183+
it("returns the unique launch reply requested by the mock prompt (#9046)", async () => {
184+
const adapter = await createAdapter({ env: {} });
185+
const expectedReply = "NEMOCLAW_0123456789AB_FIRST_OK";
186+
const prompt =
187+
"Join these four fragments with underscores and put only the result on its own line: " +
188+
"NEMOCLAW, 0123456789AB, FIRST, OK. Do not use tools.";
189+
190+
expect(await adapter.directChat(prompt)).toMatchObject({
191+
choices: [{ message: { content: expectedReply } }],
192+
});
193+
expect(
194+
await adapter.directChat(
195+
"Join these four fragments with underscores: NEMOCLAW, 0123456789AB, FIRST, OK.",
196+
),
197+
).toMatchObject({ choices: [{ message: { content: "PONG" } }] });
198+
});
199+
183200
it("keeps unrelated ambient secrets out of adapter and fake-server child environments", async () => {
184201
const secretName = "UNRELATED_E2E_SENTINEL_SECRET";
185202
const secretValue = "sentinel-value-that-must-not-propagate";

0 commit comments

Comments
 (0)