diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index 5c044ec60f..5190e58384 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -241,32 +241,16 @@ export default { `) yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) - yield* tx.run( - `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, - ) - yield* tx.run( - `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, - ) + yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`) + yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) - yield* tx.run( - `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, - ) - yield* tx.run( - `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, - ) - yield* tx.run( - `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, - ) - yield* tx.run( - `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, - ) - yield* tx.run( - `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, - ) - yield* tx.run( - `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, - ) + yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index c4bdd806c9..22c54ceda1 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -1,35 +1,10 @@ import { describe, expect } from "bun:test" -import { Effect, Layer } from "effect" -import { Credential } from "@opencode-ai/core/credential" -import { Integration } from "@opencode-ai/core/integration" -import { Database } from "@opencode-ai/core/database/database" +import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { EventV2 } from "@opencode-ai/core/event" -import { Location } from "@opencode-ai/core/location" import { PluginV2 } from "@opencode-ai/core/plugin" import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure" import { ProviderV2 } from "@opencode-ai/core/provider" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { location } from "../fixture/location" -import { testEffect } from "../lib/effect" -import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper" - -const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) -const preferences = Credential.layer.pipe(Layer.provide(database)) -const accounts = Layer.merge( - Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), - preferences, -) -const itWithAccount = testEffect( - Catalog.locationLayer.pipe( - Layer.provideMerge(accounts), - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge( - Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), - ), - Layer.provideMerge(npmLayer), - ), -) +import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" describe("AzurePlugin", () => { it.effect("resolves resourceName from env", () => @@ -73,36 +48,6 @@ describe("AzurePlugin", () => { ), ) - itWithAccount.effect("prefers account resourceName over env", () => - withEnv( - { - AZURE_RESOURCE_NAME: "from-env", - }, - () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const credentials = yield* Credential.Service - const catalog = yield* Catalog.Service - yield* credentials.create({ - integrationID: Integration.ID.make("azure"), - value: new Credential.Key({ - type: "key", - key: "key", - metadata: { resourceName: "from-account" }, - }), - }) - yield* plugin.add(AzurePlugin) - const transform = yield* catalog.transform() - yield* transform((catalog) => { - catalog.provider.update(ProviderV2.ID.azure, (item) => { - item.api = { type: "aisdk", package: "@ai-sdk/azure" } - }) - }) - expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account") - }), - ), - ) - it.effect("falls back to env when configured resourceName is blank", () => withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => Effect.gen(function* () { diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 208ab8710d..d4e26aafd1 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,36 +1,11 @@ import { describe, expect } from "bun:test" -import { Effect, Layer } from "effect" -import { Credential } from "@opencode-ai/core/credential" -import { Integration } from "@opencode-ai/core/integration" -import { Database } from "@opencode-ai/core/database/database" +import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Location } from "@opencode-ai/core/location" -import { EventV2 } from "@opencode-ai/core/event" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai" import { ProviderV2 } from "@opencode-ai/core/provider" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { location } from "../fixture/location" -import { testEffect } from "../lib/effect" -import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper" - -const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) -const preferences = Credential.layer.pipe(Layer.provide(database)) -const accounts = Layer.merge( - Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), - preferences, -) -const itWithAccount = testEffect( - Catalog.locationLayer.pipe( - Layer.provideMerge(accounts), - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge( - Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), - ), - Layer.provideMerge(npmLayer), - ), -) +import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper" function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") { return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel( @@ -126,42 +101,6 @@ describe("CloudflareWorkersAIPlugin", () => { ), ) - itWithAccount.effect("falls back to account metadata when account env is absent", () => - withEnv( - { - CLOUDFLARE_ACCOUNT_ID: undefined, - CLOUDFLARE_API_KEY: undefined, - }, - () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const credentials = yield* Credential.Service - const catalog = yield* Catalog.Service - yield* credentials.create({ - integrationID: Integration.ID.make("cloudflare-workers-ai"), - value: new Credential.Key({ - type: "key", - key: "account-key", - metadata: { accountId: "account-acct" }, - }), - }) - yield* plugin.add(CloudflareWorkersAIPlugin) - const transform = yield* catalog.transform() - yield* transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.api = { type: "aisdk", package: "test-provider" } - }), - ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).request.body).toMatchObject( - { - apiKey: "account-key", - accountId: "account-acct", - }, - ) - }), - ), - ) - it.effect("uses env account ID over configured account ID", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () => Effect.gen(function* () { diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index dab52a1f7f..62b965ab27 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -1,26 +1,10 @@ import { describe, expect, mock } from "bun:test" -import { Effect, Layer } from "effect" -import { Credential } from "@opencode-ai/core/credential" -import { Integration } from "@opencode-ai/core/integration" -import { Database } from "@opencode-ai/core/database/database" -import { Catalog } from "@opencode-ai/core/catalog" -import { EventV2 } from "@opencode-ai/core/event" -import { Location } from "@opencode-ai/core/location" +import { Effect } from "effect" import { PluginV2 } from "@opencode-ai/core/plugin" import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { location } from "../fixture/location" -import { testEffect } from "../lib/effect" -import { it, model, npmLayer, withEnv } from "./provider-helper" +import { it, model, withEnv } from "./provider-helper" const gitlabSDKOptions: Record[] = [] -const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) -const preferences = Credential.layer.pipe(Layer.provide(database)) -const accounts = Layer.merge( - Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), - preferences, -) void mock.module("gitlab-ai-provider", () => ({ VERSION: "test-version", @@ -35,17 +19,6 @@ void mock.module("gitlab-ai-provider", () => ({ isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact", })) -const itWithAccount = testEffect( - Catalog.locationLayer.pipe( - Layer.provideMerge(accounts), - Layer.provideMerge(EventV2.defaultLayer), - Layer.provideMerge( - Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))), - ), - Layer.provideMerge(npmLayer), - ), -) - describe("GitLabPlugin", () => { it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () => withEnv( @@ -163,78 +136,6 @@ describe("GitLabPlugin", () => { }), ) - itWithAccount.effect("uses active account API token over GITLAB_TOKEN", () => - withEnv( - { - GITLAB_TOKEN: "env-token", - }, - () => - Effect.gen(function* () { - gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service - const credentials = yield* Credential.Service - const catalog = yield* Catalog.Service - yield* credentials.create({ - integrationID: Integration.ID.make("gitlab"), - value: new Credential.Key({ type: "key", key: "account-token" }), - }) - yield* plugin.add(GitLabPlugin) - const transform = yield* catalog.transform() - yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {})) - const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab")) - yield* plugin.trigger( - "aisdk.sdk", - { - model: model("gitlab", "claude"), - package: "gitlab-ai-provider", - options: provider.request.body, - }, - {}, - ) - expect(gitlabSDKOptions[0].apiKey).toBe("account-token") - }), - ), - ) - - itWithAccount.effect("uses active account OAuth access token when no API token exists", () => - withEnv( - { - GITLAB_TOKEN: undefined, - }, - () => - Effect.gen(function* () { - gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service - const credentials = yield* Credential.Service - const catalog = yield* Catalog.Service - yield* credentials.create({ - integrationID: Integration.ID.make("gitlab"), - value: new Credential.OAuth({ - type: "oauth", - methodID: Integration.MethodID.make("oauth"), - refresh: "refresh-token", - access: "account-oauth-token", - expires: 9999999999999, - }), - }) - yield* plugin.add(GitLabPlugin) - const transform = yield* catalog.transform() - yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {})) - const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab")) - yield* plugin.trigger( - "aisdk.sdk", - { - model: model("gitlab", "claude"), - package: "gitlab-ai-provider", - options: provider.request.body, - }, - {}, - ) - expect(gitlabSDKOptions[0].apiKey).toBe("account-oauth-token") - }), - ), - ) - it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/core/test/public-opencode.test.ts b/packages/core/test/public-opencode.test.ts index c5f90e92c4..a62b03f22a 100644 --- a/packages/core/test/public-opencode.test.ts +++ b/packages/core/test/public-opencode.test.ts @@ -50,7 +50,7 @@ describe("public native OpenCode API", () => { Effect.gen(function* () { yield* writeProvider(tmp.path) const opencode = yield* OpenCode.Service - const sessionID = Session.ID.make("ses_public_switch_available") + const sessionID = Session.ID.create() const model = ref({ variant: "fast" }) yield* opencode.sessions.create({ id: sessionID, @@ -75,8 +75,8 @@ describe("public native OpenCode API", () => { yield* writeProvider(available.path) yield* writeProvider(disabled.path, true) const opencode = yield* OpenCode.Service - const availableID = Session.ID.make("ses_public_switch_exact_available") - const disabledID = Session.ID.make("ses_public_switch_exact_disabled") + const availableID = Session.ID.create() + const disabledID = Session.ID.create() yield* opencode.sessions.create({ id: availableID, location: Location.Ref.make({ directory: AbsolutePath.make(available.path) }), @@ -112,7 +112,7 @@ describe("public native OpenCode API", () => { Effect.gen(function* () { yield* writeProvider(tmp.path) const opencode = yield* OpenCode.Service - const sessionID = Session.ID.make("ses_public_switch_variant") + const sessionID = Session.ID.create() const selected = ref({ variant: "fast" }) yield* opencode.sessions.create({ id: sessionID, @@ -134,7 +134,7 @@ describe("public native OpenCode API", () => { it.effect("preserves the typed not-found error for a missing Session", () => Effect.gen(function* () { const opencode = yield* OpenCode.Service - const sessionID = Session.ID.make("ses_public_switch_missing") + const sessionID = Session.ID.create() const error = yield* opencode.sessions .switchModel({ sessionID, diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index af17de1759..cfec091861 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -704,7 +704,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Moved, { sessionID, timestamp: DateTime.makeUnsafe(1), - location: { directory: AbsolutePath.make("/moved") }, + location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }), }) expect( yield* db @@ -762,7 +762,7 @@ describe("SessionRunnerLLM", () => { .publish(SessionEvent.Moved, { sessionID, timestamp: DateTime.makeUnsafe(1), - location: { directory: AbsolutePath.make("/moved") }, + location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }), }) .pipe(Effect.asVoid) }) diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 57a354fc7c..d8f96a5803 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -404,7 +404,7 @@ test("keeps the locked edit schema, semantics docstring, and deferred TODOs visi expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"]) expect(source).toContain( - "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.", + "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.", ) for (const todo of [ "Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.", diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index de5c7c264a..a81b32d37d 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -279,7 +279,7 @@ test("keeps the locked write schema, semantics docstring, and deferred UX TODOs expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["content", "path"]) expect(source).toContain( - "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.", + "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.", ) for (const todo of [ "Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.", diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index fb36c75cc0..0f71b39a9d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -138,11 +138,12 @@ export const layer = Layer.effect( const { Server } = yield* Effect.promise(() => import("../server/server")) + const serverUrl = Server.url const client = createOpencodeClient({ - baseUrl: "http://localhost:4096", + baseUrl: serverUrl?.toString() ?? "http://localhost:4096", directory: ctx.directory, headers: ServerAuth.headers(), - fetch: async (...args) => Server.Default().app.fetch(...args), + ...(serverUrl ? {} : { fetch: async (...args) => Server.Default().app.fetch(...args) }), }) const cfg = yield* config.get() const input: PluginInput = { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index e8958c7ba7..5145e26d6c 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -67,7 +67,7 @@ export async function openapi() { return OpenApi.fromApi(PublicApi) } -export let url: URL +export let url: URL | undefined export async function listen(opts: ListenOptions): Promise { const listener = await Effect.runPromise(listenEffect(opts)) @@ -84,15 +84,14 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect) { +function makeStop(state: ListenerState, unpublishMdns: Effect.Effect, listenerUrl: URL) { return Effect.gen(function* () { const forceCloseOnce = yield* Effect.cached(forceClose(state).pipe(Effect.ignore)) - const closeScopeOnce = yield* Effect.cached(Scope.close(state.scope, Exit.void).pipe(Effect.ignore)) + const closeScopeOnce = yield* Effect.cached( + Scope.close(state.scope, Exit.void).pipe( + Effect.ignore, + Effect.ensuring( + Effect.sync(() => { + if (url === listenerUrl) url = undefined + }), + ), + ), + ) return (close?: boolean) => Effect.gen(function* () { diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index 805c11d62a..585c59cb43 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" import net from "node:net" +import path from "node:path" +import { pathToFileURL } from "node:url" import { Flag } from "@opencode-ai/core/flag/flag" import { Server } from "../../src/server/server" import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" @@ -300,6 +302,57 @@ describe("HttpApi Server.listen", () => { expect(output).not.toContain("Sent HTTP response") }) + test("plugin client requests reuse the listening server instance", async () => { + await using tmp = await tmpdir({ + init: async (directory) => { + const plugin = path.join(directory, "plugin.ts") + const initialized = path.join(directory, "initialized.txt") + const completed = path.join(directory, "completed.txt") + await Bun.write( + plugin, + [ + "export default async function plugin(input) {", + ` await Bun.write(${JSON.stringify(initialized)}, (await Bun.file(${JSON.stringify(initialized)}).text().catch(() => "")) + "initialized\\n")`, + " setTimeout(async () => {", + " await input.client.config.get()", + ` await Bun.write(${JSON.stringify(completed)}, "completed")`, + " }, 50)", + " return {}", + "}", + "", + ].join("\n"), + ) + await Bun.write( + path.join(directory, "opencode.json"), + JSON.stringify({ formatter: false, lsp: false, plugin: [pathToFileURL(plugin).href] }), + ) + return { initialized, completed } + }, + }) + const previous = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS + process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" + let listener: Awaited> | undefined + try { + listener = await startListener() + const response = await fetch(new URL("/config", listener.url), { + headers: { authorization: authorization(), "x-opencode-directory": tmp.path }, + }) + expect(response.status).toBe(200) + await withTimeout( + (async () => { + while (!(await Bun.file(tmp.extra.completed).exists())) await Bun.sleep(10) + })(), + 5_000, + "timed out waiting for plugin client request", + ) + expect(await Bun.file(tmp.extra.initialized).text()).toBe("initialized\n") + } finally { + if (listener) await stop(listener, "timed out cleaning up plugin client listener").catch(() => undefined) + if (previous === undefined) delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS + else process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = previous + } + }) + test("port 0 prefers 4096 when free", async () => { if (!(await isPortFree(4096))) return const listener = await startListener() diff --git a/turbo.json b/turbo.json index 220cd96e58..f4ea931b9b 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,11 @@ "outputs": [], "passThroughEnv": ["*"] }, + "@opencode-ai/core#test": { + "dependsOn": ["^build"], + "outputs": [], + "passThroughEnv": ["*"] + }, "@opencode-ai/app#test": { "dependsOn": ["^build"], "outputs": []