Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions cmd/sgai/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3496,6 +3496,7 @@ func TestExecuteAgentProcessFlushesBufferedTextOnInterrupt(t *testing.T) {
ring := newRingWriter()
workflow := newTestWorkflow()
resultCh := make(chan processResult, 1)
var result processResult
go func() {
_, _, errState := executeAgentProcess(ctx, &cfg, []string{"run"}, "", "[test]", ring, &workflow)
resultCh <- processResult{errState: errState}
Expand All @@ -3514,12 +3515,15 @@ func TestExecuteAgentProcessFlushesBufferedTextOnInterrupt(t *testing.T) {

cancel()

select {
case result := <-resultCh:
require.NotNil(t, result.errState)
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for interrupted agent process")
}
require.Eventually(t, func() bool {
select {
case result = <-resultCh:
return true
default:
return false
}
}, gracefulShutdownTimeout+time.Second, 10*time.Millisecond)
require.NotNil(t, result.errState)

assert.Contains(t, logBuf.String(), "interrupted buffered text")
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/sgai/service_adhoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"path/filepath"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -61,6 +62,13 @@ func TestAdhocStartServiceAlreadyRunningReturnsExisting(t *testing.T) {
assert.Contains(t, result.Output, "test output")
}

func TestBuildPromptActionCommandSpecUsesOnlyWorkspaceConfigDirEnv(t *testing.T) {
workspacePath := "/tmp/test-workspace"
spec := buildPromptActionCommandSpec(workspacePath, "summarize", "openai/gpt-5.4")

assert.Equal(t, []string{"OPENCODE_CONFIG_DIR=" + filepath.Join(workspacePath, ".sgai")}, spec.env)
}

func TestGetAdhocStateCreation(t *testing.T) {
srv, rootDir := setupTestServer(t)
wsDir := setupTestWorkspace(t, srv, rootDir, "adhoc-create")
Expand Down
93 changes: 93 additions & 0 deletions cmd/sgai/skel/.sgai/plugin/workbench.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { afterEach, describe, expect, it } from "bun:test";

import { Workbench } from "./workbench";

const sessionEnvKeys = ["SGAI_BIN_PATH", "SGAI_MCP_URL", "SGAI_AGENT_IDENTITY"] as const;

type SessionEnvKey = (typeof sessionEnvKeys)[number];
type SessionEnv = Record<SessionEnvKey, string>;

const originalSessionEnv: Partial<SessionEnv> = Object.fromEntries(
sessionEnvKeys.map((key) => [key, process.env[key]]),
);

const fullSessionEnv: SessionEnv = {
SGAI_BIN_PATH: "/tmp/sgai",
SGAI_MCP_URL: "http://127.0.0.1:9999/mcp",
SGAI_AGENT_IDENTITY: "test-agent",
};

function applySessionEnv(env: Partial<SessionEnv>) {
for (const key of sessionEnvKeys) {
const value = env[key];
if (typeof value === "string") {
process.env[key] = value;
continue;
}
delete process.env[key];
}
}

async function configureWorkbench(env: Partial<SessionEnv>, config: Record<string, unknown>) {
applySessionEnv(env);
const plugin = await Workbench({ directory: "/tmp/test-workspace" } as never);
const nextConfig = structuredClone(config);
await plugin.config?.(nextConfig);
return nextConfig;
}

afterEach(() => {
applySessionEnv(originalSessionEnv);
});

describe("Workbench config", () => {
for (const missingKey of sessionEnvKeys) {
it(`keeps sgai MCP disabled when ${missingKey} is missing`, async () => {
const env: Partial<SessionEnv> = { ...fullSessionEnv };
delete env[missingKey];

const config = await configureWorkbench(env, {
mcp: {
existing: {
type: "local",
command: ["existing"],
},
},
});

expect(config).toMatchObject({
mcp: {
existing: {
type: "local",
command: ["existing"],
},
},
});
expect((config.mcp as Record<string, unknown>).sgai).toBeUndefined();
});
}

it("registers sgai MCP when the session-scoped env is complete", async () => {
const config = await configureWorkbench(fullSessionEnv, {
mcp: {
existing: {
type: "local",
command: ["existing"],
},
},
});

expect(config).toMatchObject({
mcp: {
existing: {
type: "local",
command: ["existing"],
},
sgai: {
type: "local",
command: ["/tmp/sgai", "internal-mcp", "http://127.0.0.1:9999/mcp", "test-agent"],
},
},
});
});
});
34 changes: 21 additions & 13 deletions cmd/sgai/skel/.sgai/plugin/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import type { Plugin } from "@opencode-ai/plugin"
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';

function workbenchSGAIMCPCommand() {
const sgaiBinPath = process.env.SGAI_BIN_PATH?.trim();
const sgaiMCPURL = process.env.SGAI_MCP_URL?.trim();
const sgaiAgentIdentity = process.env.SGAI_AGENT_IDENTITY?.trim();

if (!sgaiBinPath || !sgaiMCPURL || !sgaiAgentIdentity) {
return null;
}

return [sgaiBinPath, "internal-mcp", sgaiMCPURL, sgaiAgentIdentity];
}

export const Workbench: Plugin = async ({ directory }) => {
const stateFilePath = join(directory, ".sgai", "state.json");

Expand All @@ -16,21 +28,17 @@ export const Workbench: Plugin = async ({ directory }) => {
config.instructions?.unshift(directory + "/.sgai/AGENTS.md");
config.model = "opencode/big-pickle";

// Configure MCP server for sgai custom tools via local stdio bridge
if (!config.mcp) {
config.mcp = {};
const sgaiMCPCommand = workbenchSGAIMCPCommand();
if (sgaiMCPCommand) {
if (!config.mcp) {
config.mcp = {};
}
config.mcp.sgai = {
type: "local",
command: sgaiMCPCommand,
};
}
config.mcp.sgai = {
type: "local",
command: [
process.env.SGAI_BIN_PATH || "",
"internal-mcp",
process.env.SGAI_MCP_URL || "",
process.env.SGAI_AGENT_IDENTITY || ""
]
};
},
// Tools are now provided by the MCP server configured above
tool: {},
event: async (input: { event: any; client: any }) => {
if (input.event.type === "todo.updated") {
Expand Down