Compare commits

...
Author SHA1 Message Date
neriousy 97d5a52de5 test(plugin): add issue 44788 reproduction 2026-08-24 22:24:33 +00:00
5 changed files with 173 additions and 0 deletions
@@ -0,0 +1,32 @@
{
"autoupdate": false,
"model": "test/test-model",
"providers": {
"test": {
"name": "Test",
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": {
"apiKey": "test-key",
"baseURL": "http://127.0.0.1:18100/v1"
},
"models": {
"test-model": {
"name": "Test Model",
"capabilities": {
"tools": true,
"input": ["text"],
"output": ["text"]
},
"cost": {
"input": 0,
"output": 0
},
"limit": {
"context": 100000,
"output": 10000
}
}
}
}
}
}
@@ -0,0 +1,33 @@
import { appendFileSync } from "node:fs"
const EVENTS = "/tmp/opencode-44788-events.log"
const HOOKS = "/tmp/opencode-44788-hooks.log"
const log = (path: string, value: string) => appendFileSync(path, `${value}\n`)
export default {
id: "issue-44788-probe",
setup: async (ctx: any) => {
void (async () => {
for await (const event of ctx.event.subscribe()) log(EVENTS, event.type)
})()
await ctx.session.hook("context", async (event: any) => {
const hasSynthetic = JSON.stringify(event.messages).includes("PROBE-TOKEN-C")
log(HOOKS, `session=${event.sessionID} messages=${event.messages.length} synthetic=${hasSynthetic}`)
event.messages.push({
role: "user",
content: [{ type: "text", text: "PROBE-TOKEN-A" }],
})
event.system.push({ type: "text", text: "PROBE-TOKEN-B" })
if (hasSynthetic) return
await ctx.session.synthetic({
sessionID: event.sessionID,
text: "PROBE-TOKEN-C",
resume: false,
})
})
},
}
+52
View File
@@ -0,0 +1,52 @@
# Issue 44788 reproduction
This fixture checks the three plugin paths reported in #44788 against the
actual OpenAI-compatible request body:
- `ctx.event.subscribe()` event delivery
- `ctx.session.hook("context")` message and system mutation
- `ctx.session.synthetic()` delivery on the following dispatch
The local model server writes every request to
`/tmp/opencode-44788-requests.jsonl`. The plugin writes observed event names to
`/tmp/opencode-44788-events.log` and hook invocations to
`/tmp/opencode-44788-hooks.log`.
## Run
Use `opencode2 v0.0.0-beta-18050` to match the report.
```sh
cd reproductions/44788
rm -f /tmp/opencode-44788-{events,hooks,requests}.log \
/tmp/opencode-44788-requests.jsonl
# Terminal 1
bun mock.ts
```
```sh
# Terminal 2, still in reproductions/44788
opencode2 --version
opencode2 run --standalone --format json first | tee /tmp/opencode-44788-first.jsonl
SESSION_ID=$(head -n 1 /tmp/opencode-44788-first.jsonl | \
bun -e 'console.log(JSON.parse(await Bun.stdin.text()).sessionID)')
opencode2 run --standalone --session "$SESSION_ID" --format json second
bun inspect.ts
sort /tmp/opencode-44788-events.log | uniq -c | sort -nr
cat /tmp/opencode-44788-hooks.log
```
Expected results:
- Main model requests contain `PROBE-TOKEN-A` and `PROBE-TOKEN-B`.
- `PROBE-TOKEN-C` is absent from the already-materialized first dispatch and
present after the synthetic input is delivered on a later dispatch.
- The event log contains public events such as `session.inbox.enqueued` and
`session.step.started`.
`ctx.event.subscribe()` returns an `AsyncIterable`; the plugin deliberately
consumes it with `for await`. Passing a callback as an extra argument creates
an iterable but does not consume it.
+19
View File
@@ -0,0 +1,19 @@
const file = Bun.file("/tmp/opencode-44788-requests.jsonl")
if (!(await file.exists())) throw new Error("Run the reproduction before inspecting requests")
const requests = (await file.text())
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line))
for (const [index, request] of requests.entries()) {
const body = JSON.stringify(request.body)
console.log(index + 1, new URL(request.url).pathname, {
messageHook: body.includes("PROBE-TOKEN-A"),
systemHook: body.includes("PROBE-TOKEN-B"),
synthetic: body.includes("PROBE-TOKEN-C"),
first: body.includes("first"),
second: body.includes("second"),
})
}
+37
View File
@@ -0,0 +1,37 @@
import { appendFileSync } from "node:fs"
const OUTPUT = "/tmp/opencode-44788-requests.jsonl"
const server = Bun.serve({
port: 18100,
async fetch(request) {
const text = await request.text()
appendFileSync(
OUTPUT,
`${JSON.stringify({ url: request.url, body: text === "" ? undefined : JSON.parse(text) })}\n`,
)
const id = crypto.randomUUID()
const shared = {
id,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "test-model",
}
const textChunk = {
...shared,
choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }],
}
const stopChunk = {
...shared,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
}
return new Response(
`data: ${JSON.stringify(textChunk)}\n\ndata: ${JSON.stringify(stopChunk)}\n\ndata: [DONE]\n\n`,
{ headers: { "content-type": "text/event-stream" } },
)
},
})
console.log(`Mock model listening on ${server.url}`)