WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type stubTenantConfigLoader struct {
|
||||
result ResolvedTenantSandboxConfig
|
||||
hits int
|
||||
}
|
||||
|
||||
func (s *stubTenantConfigLoader) Load(
|
||||
context.Context, uint64, string,
|
||||
) (ResolvedTenantSandboxConfig, error) {
|
||||
s.hits++
|
||||
return s.result, nil
|
||||
}
|
||||
|
||||
func newTestResolver(t *testing.T, loader TenantSandboxConfigLoader) (TenantSandboxResolver, Manager) {
|
||||
t.Helper()
|
||||
fallback := NewDisabledManager()
|
||||
resolver, err := NewTenantSandboxResolver(TenantSandboxResolverDeps{
|
||||
GlobalConfig: &Config{
|
||||
Type: SandboxTypeE2B,
|
||||
DefaultTimeout: 60 * time.Second,
|
||||
E2BAPIKey: "global-key",
|
||||
E2BAPIURL: "https://203.0.113.30",
|
||||
E2BTemplate: "global-template",
|
||||
E2BSandboxTTL: time.Minute,
|
||||
E2BHTTPTimeout: 30 * time.Second,
|
||||
},
|
||||
DefaultManager: fallback,
|
||||
Loader: loader,
|
||||
Store: NewMemorySessionSandboxBindingStore(),
|
||||
Checker: PermissiveSessionExistenceChecker{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return resolver, fallback
|
||||
}
|
||||
|
||||
func TestResolveEmptyConfigIDIsDisabled(t *testing.T) {
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, SandboxTypeDisabled, mgr.GetType())
|
||||
}
|
||||
|
||||
// A cordoned config must not produce a manager: the whole point of the cordon
|
||||
// is that no new sandbox may be created on credentials about to be replaced.
|
||||
func TestResolveRefusesCordonedConfig(t *testing.T) {
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{Found: true, Cordoned: true},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 7, "cfg-a")
|
||||
|
||||
require.Nil(t, mgr)
|
||||
require.ErrorIs(t, err, ErrSandboxConfigCordoned)
|
||||
}
|
||||
|
||||
// A missing config must fail loudly. Falling back to the global default would
|
||||
// silently run the agent on a DIFFERENT backend than the one it was pointed at.
|
||||
func TestResolveRefusesMissingConfig(t *testing.T) {
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{Found: false},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 7, "cfg-gone")
|
||||
|
||||
require.Nil(t, mgr)
|
||||
require.ErrorIs(t, err, ErrSandboxConfigNotFound)
|
||||
}
|
||||
|
||||
// Construction must not perform a Health round-trip (it happens per request).
|
||||
// A tenant that chose E2B must fail at first use if that backend is unreachable,
|
||||
// rather than substituting a different execution environment.
|
||||
func TestResolveBuildsRemoteManagerWithoutHealthProbe(t *testing.T) {
|
||||
resolver, fallback := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{
|
||||
SandboxType: "e2b",
|
||||
E2B: &types.E2BSandboxConfig{
|
||||
APIKey: "tenant-key", TemplateID: "tenant-template",
|
||||
},
|
||||
},
|
||||
Found: true,
|
||||
},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "cfg-a")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotSame(t, fallback, mgr)
|
||||
require.Equal(t, SandboxTypeE2B, mgr.GetType())
|
||||
}
|
||||
|
||||
// Docker is a session-scoped backend like the MicroVM ones: it must resolve to
|
||||
// a SessionBoundManager that advertises shell_exec and the session file store,
|
||||
// not to the stateless DefaultManager it used to get.
|
||||
func TestResolveBuildsSessionBoundManagerForDocker(t *testing.T) {
|
||||
// Pinned so the resolved daemon endpoint does not depend on whatever the
|
||||
// machine running the tests points its docker CLI at: a developer on a
|
||||
// tcp:// daemon would otherwise trip the TLS requirement.
|
||||
t.Setenv("DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
isolateDockerEnabled(t)
|
||||
t.Setenv(DockerBackendEnabledEnv, "true")
|
||||
|
||||
resolver, fallback := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{
|
||||
SandboxType: "docker",
|
||||
Docker: &types.DockerSandboxConfig{
|
||||
Image: "wechatopenai/weknora-sandbox:test",
|
||||
},
|
||||
},
|
||||
Found: true,
|
||||
},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "cfg-docker")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotSame(t, fallback, mgr)
|
||||
require.Equal(t, SandboxTypeDocker, mgr.GetType())
|
||||
|
||||
capabilities, ok := mgr.(SessionCapabilityProvider)
|
||||
require.True(t, ok, "docker must expose session-scoped capabilities")
|
||||
require.NotNil(t, capabilities.SessionShellExecutor())
|
||||
require.NotNil(t, capabilities.SessionFileStore())
|
||||
}
|
||||
|
||||
func TestResolveRefusesDockerWhenDisabled(t *testing.T) {
|
||||
isolateDockerEnabled(t)
|
||||
t.Setenv(DockerBackendEnabledEnv, "false")
|
||||
t.Setenv("DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{
|
||||
SandboxType: "docker",
|
||||
Docker: &types.DockerSandboxConfig{
|
||||
Image: "wechatopenai/weknora-sandbox:test",
|
||||
},
|
||||
},
|
||||
Found: true,
|
||||
},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "cfg-docker")
|
||||
|
||||
require.Nil(t, mgr)
|
||||
require.ErrorIs(t, err, ErrDockerBackendDisabled)
|
||||
}
|
||||
|
||||
// The image is this backend's template. Without it there is nothing to create a
|
||||
// sandbox from, so the config must be refused up front rather than at the first
|
||||
// execution.
|
||||
func TestResolveRefusesDockerConfigWithoutImage(t *testing.T) {
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{SandboxType: "docker"},
|
||||
Found: true,
|
||||
},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "cfg-docker")
|
||||
|
||||
require.Nil(t, mgr)
|
||||
require.ErrorIs(t, err, ErrSandboxConfigIncomplete)
|
||||
}
|
||||
|
||||
func TestResolveRefusesStoredLocalConfig(t *testing.T) {
|
||||
resolver, _ := newTestResolver(t, &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{
|
||||
SandboxType: "local",
|
||||
DefaultTimeoutSec: 17,
|
||||
},
|
||||
Found: true,
|
||||
},
|
||||
})
|
||||
|
||||
mgr, err := resolver.Resolve(context.Background(), 42, "cfg-local")
|
||||
|
||||
require.Nil(t, mgr)
|
||||
require.ErrorIs(t, err, ErrUnsupportedSandboxType)
|
||||
}
|
||||
|
||||
// No caching: the loader is consulted on every Resolve, which is what makes a
|
||||
// config change take effect on the next request with no invalidation step.
|
||||
func TestResolveBuildsFreshManagerEveryCall(t *testing.T) {
|
||||
loader := &stubTenantConfigLoader{
|
||||
result: ResolvedTenantSandboxConfig{
|
||||
Config: &types.TenantSandboxConfig{
|
||||
SandboxType: "e2b",
|
||||
E2B: &types.E2BSandboxConfig{
|
||||
APIKey: "tenant-key", TemplateID: "tenant-template",
|
||||
},
|
||||
},
|
||||
Found: true,
|
||||
},
|
||||
}
|
||||
resolver, _ := newTestResolver(t, loader)
|
||||
|
||||
first, err := resolver.Resolve(context.Background(), 42, "cfg-a")
|
||||
require.NoError(t, err)
|
||||
second, err := resolver.Resolve(context.Background(), 42, "cfg-a")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotSame(t, first, second)
|
||||
require.Equal(t, 2, loader.hits)
|
||||
}
|
||||
Reference in New Issue
Block a user