CodeNomad/scripts/test-opencode-proxy-native.mjs
Pascal André 9ebc6534d6
feat(composer): unify native device attachments and project references (#729)
Use one Attach file action for native device selection and retain project-scoped @ references in the active session worktree. Remove the duplicate upload entry and composer server browser, document the outside-project scope change, and translate attachment origins and feedback across all locales.

Share serialized, bounded byte ingestion across picker, drop and clipboard. Fence open pickers and late reads across session/worktree/lifecycle changes, preserve edits and newer selections without stealing focus, release rejected-read budgets, and restore failed custom-command drafts by attachment identity. Validate project directories through existing workspace ownership before filesystem listing/search.

Cover the behavior with 20 attachment and 11 session-aside browser scenarios, attachment/submission/persistence units, server budget/proxy/ownership regressions, and isolated native Windows picker fixtures. Three independent review passes corrected five P2 findings and ended at zero; the final merged-head review confirmed that result. UI checks/build, Tauri release build and native picker smoke passed. Remote CI was still in progress at the explicitly requested admin merge.
2026-09-24 23:12:44 +02:00

107 lines
6.3 KiB
JavaScript

import assert from "node:assert/strict"
import path from "node:path"
import Fastify from "fastify"
import replyFrom from "@fastify/reply-from"
import pino from "pino"
import { OpenCode } from "@opencode/client"
import { tsImport } from "tsx/esm/api"
// Called only by the isolated native fixture; no discovery or user storage.
export async function testNativeProxy({ client, baseUrl, root, authorization, runtimeFetch, connection, exercise }) {
const { registerInstanceProxyRoutes } = await tsImport("../packages/server/src/server/http-server.ts", import.meta.url)
const { PROMPT_INLINE_FILE_LIMITS } = await tsImport("../packages/server/src/api-types.ts", import.meta.url)
const { sessionEnvironment } = await tsImport("../packages/server/src/workspaces/session-environment.ts", import.meta.url)
const { createInstanceFetch } = await tsImport("../packages/ui/src/lib/sdk-manager.ts", import.meta.url)
const app = Fastify()
await app.register(replyFrom)
const owns = candidate => path.resolve(candidate) === path.resolve(root)
registerInstanceProxyRoutes(app, {
workspaceManager: {
get: () => ({ id: "native", path: root }),
getSharedServiceEndpoint: async () => ({ url: baseUrl }),
...(connection ? { getSharedServiceConnection: async () => connection } : {}),
getInstanceAuthorizationHeader: () => authorization,
getServiceDirectory: () => root,
getSharedServiceClient: async () => client,
getSessionEnvironment: () => sessionEnvironment({}),
...(runtimeFetch ? { getSharedServiceFetch: async () => runtimeFetch } : {}),
getWorktreeIdentityForPath: async (_id, directory) => owns(directory) ? root : undefined,
ownsDirectory: async (_id, directory) => owns(directory),
ownsLocation: async (_id, location) => owns(location.directory) && location.workspaceID === undefined,
ownsPath: async (_id, candidate) => owns(candidate),
},
worktreeDeletionFence: { enter: () => () => {} },
logger: pino({ level: "silent" }),
})
await app.listen({ host: "127.0.0.1", port: 0 })
const proxyBase = `http://127.0.0.1:${app.server.address().port}/workspaces/native/instance/`
const forward = createInstanceFetch(proxyBase)
const proxy = OpenCode.make({ baseUrl: proxyBase, fetch: async (input, init) => {
const response = await forward(input, init)
if (!response.ok) assert.fail(`${init?.method} ${input}: ${response.status} ${await response.text()}`)
return response
} })
const created = []
try {
const location = { directory: root }
const info = await proxy.location.get({ location })
assert.ok(info.project.id)
assert.ok(Array.isArray(await proxy.project.list()))
for (const resource of [proxy.agent, proxy.provider, proxy.model, proxy.command, proxy.plugin, proxy.mcp]) {
assert.ok(Array.isArray((await resource.list({ location })).data))
}
assert.ok(Array.isArray((await proxy.permission.request.list({ location })).data))
for (let index = 0; index < 3; index++) created.push(await proxy.session.create({ location }))
const sessionID = created[0].id
await proxy.session.update({ sessionID, title: "Stable proxy fixture" })
assert.equal((await proxy.session.get({ sessionID })).title, "Stable proxy fixture")
const page = await proxy.session.list({ directory: root, limit: 1 })
assert.equal(page.data.length, 1)
assert.ok(page.cursor.next)
const continuation = await proxy.session.list({ cursor: page.cursor.next, limit: 1 })
assert.equal(continuation.data.length, 1)
assert.notEqual(continuation.data[0].id, page.data[0].id)
assert.equal(typeof await proxy.session.active(), "object")
await proxy.session.instructions.entry.put({ sessionID, key: "fixture", value: "Fixture instruction" })
await proxy.session.instructions.entry.remove({ sessionID, key: "fixture" })
await proxy.session.wait({ sessionID })
const header = { headers: { "x-opencode-directory": encodeURIComponent(root) } }
for (const owner of [sessionID, "global"]) {
for (const action of ["reply", "cancel"]) {
const form = await client.session.form.create({
sessionID: owner, title: "Fixture form", fields: [{ key: "answer", type: "string" }],
}, owner === "global" ? header : undefined)
const listed = await proxy.form.list({ location })
assert.ok(listed.data.some(item => item.id === form.id))
const input = { sessionID: owner, formID: form.id }
if (action === "reply") await proxy.session.form.reply({ ...input, answer: { answer: "ok" } }, header)
else await proxy.session.form.cancel(input, header)
assert.ok(!(await proxy.form.list({ location })).data.some(item => item.id === form.id))
}
}
await exercise?.(proxy, sessionID)
const inlineSessionID = created[1].id
const inlineFile = Buffer.alloc(PROMPT_INLINE_FILE_LIMITS.maxFileBytes, 0x61).toString("base64")
await proxy.session.prompt({
sessionID: inlineSessionID,
text: "Acknowledge the attached transport fixture.",
files: [{ name: "inline-limit.txt", uri: `data:text/plain;base64,${inlineFile}` }],
})
await proxy.session.wait({ sessionID: inlineSessionID }, { signal: AbortSignal.timeout(60_000) })
assert((await proxy.message.list({ sessionID: inlineSessionID, limit: 20 })).data.some(message => message.type === "user"))
console.log(`PASS: ${PROMPT_INLINE_FILE_LIMITS.maxFileBytes}-byte inline file through real proxy and OpenCode endpoint`)
// Export remains a direct native read; it is deliberately not added to the
// guarded UI allowlist merely for this fixture.
const exported = await client.session.export({ sessionID })
await proxy.session.remove({ sessionID })
const restored = await proxy.session.import({ ...exported, location })
assert.equal(restored.id, sessionID)
assert.equal((await proxy.message.list({ sessionID, limit: 100 })).data.length, exported.messages.length)
console.log("PASS: generated stable client through real proxy: catalogs, sessions, native cursors, active envelope, instructions, wait and session/global Forms")
} finally {
// Import can fail after its source has already been removed. Cleanup must
// neither replace that failure with a 404 nor leave the HTTP listener open.
try { await Promise.allSettled(created.map(session => client.session.remove({ sessionID: session.id }))) }
finally { await app.close() }
}
}