mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02051e9cec | |||
| 2311f202cf |
@@ -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 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 INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
|
||||||
yield* tx.run(
|
yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`)
|
||||||
`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 \`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_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 \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||||
yield* tx.run(
|
yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
|
||||||
`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(
|
yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
||||||
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_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 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_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_project_idx\` ON \`session\` (\`project_id\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||||
|
|||||||
@@ -1,35 +1,10 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect } 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 { 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 { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
describe("AzurePlugin", () => {
|
describe("AzurePlugin", () => {
|
||||||
it.effect("resolves resourceName from env", () =>
|
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", () =>
|
it.effect("falls back to env when configured resourceName is blank", () =>
|
||||||
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect } 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 { 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 { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
|
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
|
||||||
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
|
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", () =>
|
it.effect("uses env account ID over configured account ID", () =>
|
||||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,26 +1,10 @@
|
|||||||
import { describe, expect, mock } from "bun:test"
|
import { describe, expect, mock } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect } 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 { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
|
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { it, model, withEnv } from "./provider-helper"
|
||||||
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"
|
|
||||||
|
|
||||||
const gitlabSDKOptions: Record<string, unknown>[] = []
|
const gitlabSDKOptions: Record<string, unknown>[] = []
|
||||||
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", () => ({
|
void mock.module("gitlab-ai-provider", () => ({
|
||||||
VERSION: "test-version",
|
VERSION: "test-version",
|
||||||
@@ -35,17 +19,6 @@ void mock.module("gitlab-ai-provider", () => ({
|
|||||||
isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact",
|
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", () => {
|
describe("GitLabPlugin", () => {
|
||||||
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
|
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
|
||||||
withEnv(
|
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", () =>
|
it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ describe("public native OpenCode API", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* writeProvider(tmp.path)
|
yield* writeProvider(tmp.path)
|
||||||
const opencode = yield* OpenCode.Service
|
const opencode = yield* OpenCode.Service
|
||||||
const sessionID = Session.ID.make("ses_public_switch_available")
|
const sessionID = Session.ID.create()
|
||||||
const model = ref({ variant: "fast" })
|
const model = ref({ variant: "fast" })
|
||||||
yield* opencode.sessions.create({
|
yield* opencode.sessions.create({
|
||||||
id: sessionID,
|
id: sessionID,
|
||||||
@@ -75,8 +75,8 @@ describe("public native OpenCode API", () => {
|
|||||||
yield* writeProvider(available.path)
|
yield* writeProvider(available.path)
|
||||||
yield* writeProvider(disabled.path, true)
|
yield* writeProvider(disabled.path, true)
|
||||||
const opencode = yield* OpenCode.Service
|
const opencode = yield* OpenCode.Service
|
||||||
const availableID = Session.ID.make("ses_public_switch_exact_available")
|
const availableID = Session.ID.create()
|
||||||
const disabledID = Session.ID.make("ses_public_switch_exact_disabled")
|
const disabledID = Session.ID.create()
|
||||||
yield* opencode.sessions.create({
|
yield* opencode.sessions.create({
|
||||||
id: availableID,
|
id: availableID,
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make(available.path) }),
|
location: Location.Ref.make({ directory: AbsolutePath.make(available.path) }),
|
||||||
@@ -112,7 +112,7 @@ describe("public native OpenCode API", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* writeProvider(tmp.path)
|
yield* writeProvider(tmp.path)
|
||||||
const opencode = yield* OpenCode.Service
|
const opencode = yield* OpenCode.Service
|
||||||
const sessionID = Session.ID.make("ses_public_switch_variant")
|
const sessionID = Session.ID.create()
|
||||||
const selected = ref({ variant: "fast" })
|
const selected = ref({ variant: "fast" })
|
||||||
yield* opencode.sessions.create({
|
yield* opencode.sessions.create({
|
||||||
id: sessionID,
|
id: sessionID,
|
||||||
@@ -134,7 +134,7 @@ describe("public native OpenCode API", () => {
|
|||||||
it.effect("preserves the typed not-found error for a missing Session", () =>
|
it.effect("preserves the typed not-found error for a missing Session", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const opencode = yield* OpenCode.Service
|
const opencode = yield* OpenCode.Service
|
||||||
const sessionID = Session.ID.make("ses_public_switch_missing")
|
const sessionID = Session.ID.create()
|
||||||
const error = yield* opencode.sessions
|
const error = yield* opencode.sessions
|
||||||
.switchModel({
|
.switchModel({
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* events.publish(SessionEvent.Moved, {
|
yield* events.publish(SessionEvent.Moved, {
|
||||||
sessionID,
|
sessionID,
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
location: { directory: AbsolutePath.make("/moved") },
|
location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }),
|
||||||
})
|
})
|
||||||
expect(
|
expect(
|
||||||
yield* db
|
yield* db
|
||||||
@@ -762,7 +762,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
.publish(SessionEvent.Moved, {
|
.publish(SessionEvent.Moved, {
|
||||||
sessionID,
|
sessionID,
|
||||||
timestamp: DateTime.makeUnsafe(1),
|
timestamp: DateTime.makeUnsafe(1),
|
||||||
location: { directory: AbsolutePath.make("/moved") },
|
location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }),
|
||||||
})
|
})
|
||||||
.pipe(Effect.asVoid)
|
.pipe(Effect.asVoid)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"])
|
||||||
expect(source).toContain(
|
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 [
|
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.",
|
"Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.",
|
||||||
|
|||||||
@@ -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(Object.keys(schema.properties ?? {}).sort()).toEqual(["content", "path"])
|
||||||
expect(source).toContain(
|
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 [
|
for (const todo of [
|
||||||
"Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.",
|
"Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.",
|
||||||
|
|||||||
@@ -138,11 +138,12 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
const { Server } = yield* Effect.promise(() => import("../server/server"))
|
const { Server } = yield* Effect.promise(() => import("../server/server"))
|
||||||
|
|
||||||
|
const serverUrl = Server.url
|
||||||
const client = createOpencodeClient({
|
const client = createOpencodeClient({
|
||||||
baseUrl: "http://localhost:4096",
|
baseUrl: serverUrl?.toString() ?? "http://localhost:4096",
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
headers: ServerAuth.headers(),
|
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 cfg = yield* config.get()
|
||||||
const input: PluginInput = {
|
const input: PluginInput = {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export async function openapi() {
|
|||||||
return OpenApi.fromApi(PublicApi)
|
return OpenApi.fromApi(PublicApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
export let url: URL
|
export let url: URL | undefined
|
||||||
|
|
||||||
export async function listen(opts: ListenOptions): Promise<Listener> {
|
export async function listen(opts: ListenOptions): Promise<Listener> {
|
||||||
const listener = await Effect.runPromise(listenEffect(opts))
|
const listener = await Effect.runPromise(listenEffect(opts))
|
||||||
@@ -84,15 +84,14 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unkno
|
|||||||
const state = yield* startWithPortFallback(opts)
|
const state = yield* startWithPortFallback(opts)
|
||||||
const address = yield* tcpAddress(state)
|
const address = yield* tcpAddress(state)
|
||||||
const listenerUrl = makeURL(opts.hostname, address.port)
|
const listenerUrl = makeURL(opts.hostname, address.port)
|
||||||
url = listenerUrl
|
|
||||||
|
|
||||||
const unpublishMdns = yield* setupMdns(opts, address.port, state.scope)
|
const unpublishMdns = yield* setupMdns(opts, address.port, state.scope)
|
||||||
|
url = listenerUrl
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname: opts.hostname,
|
hostname: opts.hostname,
|
||||||
port: address.port,
|
port: address.port,
|
||||||
url: listenerUrl,
|
url: listenerUrl,
|
||||||
stop: yield* makeStop(state, unpublishMdns),
|
stop: yield* makeStop(state, unpublishMdns, listenerUrl),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -169,10 +168,19 @@ function setupMdns(opts: ListenOptions, port: number, scope: Scope.Scope) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStop(state: ListenerState, unpublishMdns: Effect.Effect<void>) {
|
function makeStop(state: ListenerState, unpublishMdns: Effect.Effect<void>, listenerUrl: URL) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const forceCloseOnce = yield* Effect.cached(forceClose(state).pipe(Effect.ignore))
|
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) =>
|
return (close?: boolean) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import net from "node:net"
|
import net from "node:net"
|
||||||
|
import path from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
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")
|
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<ReturnType<typeof startListener>> | 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 () => {
|
test("port 0 prefers 4096 when free", async () => {
|
||||||
if (!(await isPortFree(4096))) return
|
if (!(await isPortFree(4096))) return
|
||||||
const listener = await startListener()
|
const listener = await startListener()
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"passThroughEnv": ["*"]
|
"passThroughEnv": ["*"]
|
||||||
},
|
},
|
||||||
|
"@opencode-ai/core#test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [],
|
||||||
|
"passThroughEnv": ["*"]
|
||||||
|
},
|
||||||
"@opencode-ai/app#test": {
|
"@opencode-ai/app#test": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
|
|||||||
Reference in New Issue
Block a user