CI / test (push) Successful in 8m43s
llmgw's own extension for VS Code, plain JavaScript with no dependencies, packed into a .vsix by the binary at /setup/vscode/llmgw.vsix with the serving gateway's address in it, so it needs no build, no Node and no marketplace. It registers llmgw as a language-model provider: the chat's model picker, agent mode and any extension that asks VS Code for a model get every chat model the key may use, sized and with tool and vision capabilities as the catalog says, streamed, tool calls both ways. VS Code is a tool on the account page's Connect your editor: a key, the install command, and an Open in VS Code link that hands the key to the extension's secret storage. The extension's Sign In opens the page with a state of its own; a link without it is confirmed, the gateway named, since it decides where code is sent. Tested in VS Code 1.138 against the PoC: 27 models listed, an answer from qwen2.5-0.5b-instruct, and qwen3-coder-30b calling a tool and using its result (internal/vscodeext/e2e). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
3.9 KiB
Go
151 lines
3.9 KiB
Go
package vscodeext
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"io"
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestVersion(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"v0.14.9": "0.14.9",
|
|
"v0.14.9-11-g4eb49d0-dev": "0.14.9",
|
|
"1.2.3": "1.2.3",
|
|
"dev": "0.0.0",
|
|
"": "0.0.0",
|
|
} {
|
|
if got := Version(in); got != want {
|
|
t.Errorf("Version(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func unzip(t *testing.T, b []byte) map[string][]byte {
|
|
t.Helper()
|
|
zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := map[string][]byte{}
|
|
for _, f := range zr.File {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(rc)
|
|
rc.Close()
|
|
out[f.Name] = body
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestBuild(t *testing.T) {
|
|
b, err := Build("v0.15.2-3-gabc", "https://llm.example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := unzip(t, b)
|
|
for _, name := range []string{
|
|
"[Content_Types].xml", "extension.vsixmanifest",
|
|
"extension/package.json", "extension/extension.js", "extension/lib.js", "extension/README.md",
|
|
} {
|
|
if _, ok := got[name]; !ok {
|
|
t.Errorf("%s missing from the package", name)
|
|
}
|
|
}
|
|
for name := range got {
|
|
if bytes.HasSuffix([]byte(name), []byte("_test.js")) {
|
|
t.Errorf("%s is a test and should stay out of the package", name)
|
|
}
|
|
}
|
|
|
|
var pkg map[string]any
|
|
if err := json.Unmarshal(got["extension/package.json"], &pkg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pkg["version"] != "0.15.2" {
|
|
t.Errorf("version %v, want 0.15.2", pkg["version"])
|
|
}
|
|
setting, _ := dig(pkg, "contributes", "configuration", "properties", "llmgw.gatewayUrl")
|
|
if setting["default"] != "https://llm.example.com" {
|
|
t.Errorf("gatewayUrl default %v, want the serving gateway", setting["default"])
|
|
}
|
|
|
|
var m struct {
|
|
Metadata struct {
|
|
Identity struct {
|
|
ID string `xml:"Id,attr"`
|
|
Version string `xml:"Version,attr"`
|
|
Publisher string `xml:"Publisher,attr"`
|
|
}
|
|
Properties struct {
|
|
Property []struct {
|
|
ID string `xml:"Id,attr"`
|
|
Value string `xml:"Value,attr"`
|
|
}
|
|
}
|
|
}
|
|
Assets struct {
|
|
Asset []struct {
|
|
Type string `xml:"Type,attr"`
|
|
Path string `xml:"Path,attr"`
|
|
}
|
|
}
|
|
}
|
|
if err := xml.Unmarshal(got["extension.vsixmanifest"], &m); err != nil {
|
|
t.Fatalf("extension.vsixmanifest: %v", err)
|
|
}
|
|
id := m.Metadata.Identity
|
|
if id.Publisher+"."+id.ID != ID || id.Version != "0.15.2" {
|
|
t.Errorf("identity %s.%s@%s, want %s@0.15.2", id.Publisher, id.ID, id.Version, ID)
|
|
}
|
|
props := map[string]string{}
|
|
for _, p := range m.Metadata.Properties.Property {
|
|
props[p.ID] = p.Value
|
|
}
|
|
if props["Microsoft.VisualStudio.Code.Engine"] != "^1.104.0" {
|
|
t.Errorf("engine %q", props["Microsoft.VisualStudio.Code.Engine"])
|
|
}
|
|
if props["Microsoft.VisualStudio.Code.ExtensionKind"] != "ui,workspace" {
|
|
t.Errorf("extension kind %q", props["Microsoft.VisualStudio.Code.ExtensionKind"])
|
|
}
|
|
if !slices.ContainsFunc(m.Assets.Asset, func(a struct {
|
|
Type string `xml:"Type,attr"`
|
|
Path string `xml:"Path,attr"`
|
|
}) bool {
|
|
return a.Type == "Microsoft.VisualStudio.Code.Manifest" && a.Path == "extension/package.json"
|
|
}) {
|
|
t.Error("no manifest asset pointing at extension/package.json")
|
|
}
|
|
}
|
|
|
|
func TestBuildIsReproducible(t *testing.T) {
|
|
a, err := Build("v1.0.0", "https://a")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := Build("v1.0.0", "https://a")
|
|
if !bytes.Equal(a, b) {
|
|
t.Error("the same inputs made different packages")
|
|
}
|
|
}
|
|
|
|
// The address goes into JSON and XML; neither may let it out of its string.
|
|
func TestBuildEscapesGateway(t *testing.T) {
|
|
b, err := Build("v1.0.0", `https://x"},"main":"evil.js`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pkg map[string]any
|
|
if err := json.Unmarshal(unzip(t, b)["extension/package.json"], &pkg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pkg["main"] != "./extension.js" {
|
|
t.Errorf("main = %v: the gateway address escaped its string", pkg["main"])
|
|
}
|
|
}
|