Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
90 lines
6.0 KiB
TypeScript
90 lines
6.0 KiB
TypeScript
// Native inbox acceptance and delivery changes against a local deterministic model.
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
|
|
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
|
import { ResponseInputTracker } from "../../src/chat/response-inputs.js";
|
|
import { streamPromptResponse } from "../../src/routes/chatStream.js";
|
|
import { config } from "../../src/config.js";
|
|
|
|
const root = await mkdtemp(join(tmpdir(), "tjwater-response-input-"));
|
|
config.LLM_REQUEST_AUDIT_LOG_PATH = join(root, "fixture-audit.log");
|
|
let release = () => {};
|
|
const gate = new Promise<void>((resolve) => { release = resolve; });
|
|
let releaseSecond = () => {};
|
|
const secondGate = new Promise<void>((resolve) => { releaseSecond = resolve; });
|
|
const prompts: unknown[] = [];
|
|
const fixture = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) {
|
|
if (new URL(request.url).pathname !== "/v1/chat/completions") return new Response(null, { status: 404 });
|
|
prompts.push(await request.json());
|
|
if (prompts.length === 1) await gate;
|
|
if (prompts.length === 2) await secondGate;
|
|
const chunk = (delta: object, finish_reason: string | null) => JSON.stringify({ id: "fixture", object: "chat.completion.chunk", created: 1,
|
|
model: "fake", choices: [{ index: 0, delta, finish_reason }] });
|
|
return new Response(`data: ${chunk({ role: "assistant", content: "Local completed." }, null)}\n\ndata: ${chunk({}, "stop")}\n\ndata: [DONE]\n\n`,
|
|
{ headers: { "Content-Type": "text/event-stream" } });
|
|
} });
|
|
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
|
|
const wait = async (label: string, predicate: () => boolean | Promise<boolean>) => {
|
|
const deadline = Date.now() + 15_000;
|
|
while (!await predicate()) { if (Date.now() > deadline) throw new Error(`native input smoke timed out: ${label}, fixture requests=${prompts.length}`); await Bun.sleep(20); }
|
|
};
|
|
try {
|
|
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
|
|
config: { model: "fixture/fake", agents: { instruction: { mode: "primary", model: "fixture/fake", system: "Reply with a short local fixture answer." } },
|
|
providers: { fixture: { name: "Fixture", package: "@opencode/ai/providers/openai-compatible",
|
|
settings: { baseURL: `http://127.0.0.1:${fixture.port}/v1`, apiKey: "LOCAL_FIXTURE" },
|
|
models: { fake: { capabilities: { tools: true }, limit: { context: 32768, output: 1024 } } } } } },
|
|
env: { OPENCODE_DB: join(root, "native.db"), XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"),
|
|
XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"), TJWATER_AGENT_MCP_CONNECTION_PATH: join(root, "no-connection") },
|
|
});
|
|
const client = native.client;
|
|
const session = await client.session.create({ title: "Inbox smoke fixture", location: { directory: root } });
|
|
await client.session.switchModel({ sessionID: session.id, model: { providerID: "fixture", id: "fake" } });
|
|
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
|
|
const consumed: string[] = [];
|
|
const tracker = new ResponseInputTracker(runtime, session.id, (input) => { if (input.status === "delivered") consumed.push(input.id); });
|
|
const tokens: string[] = [];
|
|
let finished = false;
|
|
const running = streamPromptResponse({ runtime, sessionId: session.id, clientSessionId: session.id, message: "Original local task", model: "fixture/fake",
|
|
beforePrompt: () => tracker.restore(), onPromptDispatched: () => tracker.markReady(),
|
|
onInboxEvent: (type, data) => tracker.observe(type, data), waitForUserInput: async () => !await tracker.canClose(),
|
|
write: (event, data) => { if (event === "token") tokens.push(String(data.content)); } });
|
|
void running.catch(() => {});
|
|
void running.then(() => { finished = true; }, () => { finished = true; });
|
|
await wait("first local generation", () => prompts.length >= 1);
|
|
const item = await tracker.submit({ id: `msg_${"1".repeat(32)}`, text: "Additional local user input", delivery: "queue", status: "pending" });
|
|
assert.equal(item.delivery, "queue");
|
|
assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "queue");
|
|
await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "steer" });
|
|
assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "steer");
|
|
await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "queue" });
|
|
release();
|
|
await wait("queued continuation", () => prompts.length >= 2);
|
|
await Bun.sleep(100);
|
|
assert.equal(finished, false, "managed stream closed while queued generation was running");
|
|
releaseSecond();
|
|
let finishTimer: ReturnType<typeof setTimeout> | undefined;
|
|
const result = await Promise.race([running, new Promise<never>((_resolve, reject) => {
|
|
finishTimer = setTimeout(() => reject(new Error("managed stream did not finish")), 10_000);
|
|
finishTimer.unref();
|
|
})]).finally(() => clearTimeout(finishTimer));
|
|
assert.equal(result.failed, false);
|
|
assert.equal(result.aborted, false);
|
|
assert.ok(consumed.includes(item.id));
|
|
assert.ok(tokens.join("").split("Local completed.").length >= 3, "managed stream closed before queued answer");
|
|
assert.ok(JSON.stringify(prompts[1]).includes("Additional local user input"));
|
|
assert.ok(!(await client.session.inbox.list({ sessionID: session.id })).some((entry) => entry.id === item.id));
|
|
await client.session.remove({ sessionID: session.id });
|
|
console.log("Native queue delivery, pending steer/queue changes and automatic continuation passed; local fixture only.");
|
|
} finally {
|
|
release();
|
|
releaseSecond();
|
|
native?.close();
|
|
if (native) await native.exited;
|
|
fixture.stop(true);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|