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,51 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/xdg"
|
||||
)
|
||||
|
||||
const ttl = 24 * time.Hour
|
||||
|
||||
func cachePath() (string, error) {
|
||||
return xdg.Path("XDG_CACHE_HOME", ".cache", "server-info.yaml")
|
||||
}
|
||||
|
||||
// LoadCache reads the cached Info. Returns (info, fresh, err).
|
||||
//
|
||||
// info == nil when no cache exists (err == nil)
|
||||
// fresh == false when the cache is missing or TTL-expired
|
||||
func LoadCache() (*Info, bool, error) {
|
||||
p, err := cachePath()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
data, err := os.ReadFile(p)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("read cache: %w", err)
|
||||
}
|
||||
var info Info
|
||||
if err := yaml.Unmarshal(data, &info); err != nil {
|
||||
return nil, false, fmt.Errorf("parse cache: %w", err)
|
||||
}
|
||||
fresh := time.Since(info.ProbedAt) < ttl
|
||||
return &info, fresh, nil
|
||||
}
|
||||
|
||||
// SaveCache atomically writes Info to the cache file (mode 0600).
|
||||
func SaveCache(info *Info) error {
|
||||
p, err := cachePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return xdg.WriteAtomicYAML(p, info)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package compat_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/compat"
|
||||
)
|
||||
|
||||
func TestCache_RoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CACHE_HOME", dir)
|
||||
|
||||
want := &compat.Info{ServerVersion: "1.2.3", ProbedAt: time.Now().UTC().Truncate(time.Second)}
|
||||
if err := compat.SaveCache(want); err != nil {
|
||||
t.Fatalf("SaveCache: %v", err)
|
||||
}
|
||||
|
||||
got, fresh, err := compat.LoadCache()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCache: %v", err)
|
||||
}
|
||||
if !fresh {
|
||||
t.Error("just-saved cache should be fresh")
|
||||
}
|
||||
if got == nil || got.ServerVersion != want.ServerVersion {
|
||||
t.Errorf("ServerVersion = %v, want %q", got, want.ServerVersion)
|
||||
}
|
||||
|
||||
// File should live at $XDG_CACHE_HOME/weknora/server-info.yaml
|
||||
p := filepath.Join(dir, "weknora", "server-info.yaml")
|
||||
if _, err := filepath.Abs(p); err != nil {
|
||||
t.Errorf("path resolution: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Stale(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CACHE_HOME", dir)
|
||||
|
||||
old := &compat.Info{ServerVersion: "1.2.3", ProbedAt: time.Now().Add(-25 * time.Hour)}
|
||||
if err := compat.SaveCache(old); err != nil {
|
||||
t.Fatalf("SaveCache: %v", err)
|
||||
}
|
||||
|
||||
_, fresh, err := compat.LoadCache()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCache: %v", err)
|
||||
}
|
||||
if fresh {
|
||||
t.Error("25h old cache should be stale")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Missing(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||
info, fresh, err := compat.LoadCache()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCache should not error on missing: %v", err)
|
||||
}
|
||||
if fresh {
|
||||
t.Error("missing cache must not be fresh")
|
||||
}
|
||||
if info != nil {
|
||||
t.Errorf("missing cache info should be nil, got %v", info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package compat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Level is the client-server version compat level.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
OK Level = iota // compatible
|
||||
SoftWarn // server outdated; some new features may be unavailable
|
||||
HardError // incompatible; CLI or server must be upgraded
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case OK:
|
||||
return "ok"
|
||||
case SoftWarn:
|
||||
return "soft_warn"
|
||||
case HardError:
|
||||
return "hard_error"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Compat compares client/server versions. Cross-minor drift is allowed:
|
||||
//
|
||||
// same major, client minor <= server minor -> OK
|
||||
// same major, client minor > server minor -> SoftWarn (some new features may be unavailable)
|
||||
// different major -> HardError
|
||||
// unparseable string -> OK (fail-open, do not block)
|
||||
// "(unknown)" / "" -> OK (dev build / server field missing)
|
||||
func Compat(serverVer, cliVer string) (Level, string) {
|
||||
sMaj, sMin, ok := parseSemver(serverVer)
|
||||
if !ok {
|
||||
return OK, ""
|
||||
}
|
||||
cMaj, cMin, ok := parseSemver(cliVer)
|
||||
if !ok {
|
||||
return OK, ""
|
||||
}
|
||||
if sMaj != cMaj {
|
||||
return HardError, fmt.Sprintf("incompatible: client %s vs server %s - upgrade required", cliVer, serverVer)
|
||||
}
|
||||
if cMin > sMin {
|
||||
return SoftWarn, fmt.Sprintf("server is older (server %s, client %s); some new features may be unavailable", serverVer, cliVer)
|
||||
}
|
||||
return OK, ""
|
||||
}
|
||||
|
||||
// parseSemver extracts (major, minor) from "X.Y.Z" or "X.Y.Z-suffix".
|
||||
// Accepts the leading "v" common in `git describe` output and Tencent/WeKnora
|
||||
// tag conventions ("v0.1.0"), since both server `/system/info.version` and
|
||||
// the CLI's own ldflags-injected build.Version may carry it.
|
||||
// Returns ok=false when the string is unrecognizable (empty / "(unknown)" / non-numeric).
|
||||
func parseSemver(s string) (major, minor int, ok bool) {
|
||||
if s == "" || s == "(unknown)" {
|
||||
return 0, 0, false
|
||||
}
|
||||
// Accept the "v" prefix; both `git describe` output and Tencent/WeKnora tags carry it.
|
||||
s = strings.TrimPrefix(s, "v")
|
||||
// Strip prerelease/build metadata.
|
||||
if i := strings.IndexAny(s, "-+"); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
parts := strings.SplitN(s, ".", 3)
|
||||
if len(parts) < 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
maj, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
min, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return maj, min, true
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package compat_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/compat"
|
||||
)
|
||||
|
||||
func TestCompat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serverVer string
|
||||
cliVer string
|
||||
wantLevel compat.Level
|
||||
wantHintHas string
|
||||
}{
|
||||
{"equal", "1.2.3", "1.2.3", compat.OK, ""},
|
||||
{"client minor lower", "1.5.0", "1.2.0", compat.OK, ""},
|
||||
{"client minor higher", "1.2.0", "1.5.0", compat.SoftWarn, "server is older"},
|
||||
{"different major", "1.9.9", "2.0.0", compat.HardError, "incompatible"},
|
||||
{"client unset", "1.2.3", "(unknown)", compat.OK, ""}, // dev build; no warning
|
||||
{"server unset", "", "1.2.3", compat.OK, ""},
|
||||
{"malformed server", "garbage", "1.2.3", compat.OK, ""}, // fail-open
|
||||
|
||||
// "v" prefix tolerance (git describe + Tencent tag convention)
|
||||
{"v-prefix both", "v1.2.0", "v1.5.0", compat.SoftWarn, "server is older"},
|
||||
{"v-prefix server only", "v1.9.9", "2.0.0", compat.HardError, "incompatible"},
|
||||
{"v-prefix cli only", "1.9.9", "v2.0.0", compat.HardError, "incompatible"},
|
||||
|
||||
// Hint must be empty when level == OK (no false positives)
|
||||
{"OK hint empty", "1.2.3", "1.2.3", compat.OK, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gotLevel, gotHint := compat.Compat(tt.serverVer, tt.cliVer)
|
||||
if gotLevel != tt.wantLevel {
|
||||
t.Errorf("%s: Level = %v, want %v", tt.name, gotLevel, tt.wantLevel)
|
||||
}
|
||||
if tt.wantHintHas != "" && !strings.Contains(gotHint, tt.wantHintHas) {
|
||||
t.Errorf("%s: hint = %q, should contain %q", tt.name, gotHint, tt.wantHintHas)
|
||||
}
|
||||
if tt.wantLevel == compat.OK && gotHint != "" {
|
||||
t.Errorf("%s: OK level should have empty hint, got %q", tt.name, gotHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Package compat probes server `/system/info` and decides client-server
|
||||
// version skew level. Used by `weknora doctor`'s server_version check.
|
||||
//
|
||||
// 24h TTL state file cache, three-tier compat semantics
|
||||
// (major-mismatch=hard / minor-mismatch=soft / equal=ok).
|
||||
package compat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sdk "github.com/Tencent/WeKnora/client"
|
||||
)
|
||||
|
||||
// Info is the cached server version snapshot.
|
||||
type Info struct {
|
||||
ServerVersion string `yaml:"server_version"`
|
||||
EngineInfo map[string]string `yaml:"engine_info,omitempty"`
|
||||
ProbedAt time.Time `yaml:"probed_at"`
|
||||
}
|
||||
|
||||
// ProbeService is the narrow SDK surface compat depends on; tests inject a fake.
|
||||
type ProbeService interface {
|
||||
GetSystemInfo(ctx context.Context) (*sdk.SystemInfo, error)
|
||||
}
|
||||
|
||||
// Probe calls server /system/info and returns a fresh Info snapshot.
|
||||
func Probe(ctx context.Context, c ProbeService) (*Info, error) {
|
||||
resp, err := c.GetSystemInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("probe server: %w", err)
|
||||
}
|
||||
return &Info{
|
||||
ServerVersion: resp.Version,
|
||||
ProbedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package compat_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/compat"
|
||||
sdk "github.com/Tencent/WeKnora/client"
|
||||
)
|
||||
|
||||
type fakeProbeClient struct {
|
||||
info *sdk.SystemInfo
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeProbeClient) GetSystemInfo(ctx context.Context) (*sdk.SystemInfo, error) {
|
||||
return f.info, f.err
|
||||
}
|
||||
|
||||
func TestProbe_Success(t *testing.T) {
|
||||
c := &fakeProbeClient{info: &sdk.SystemInfo{Version: "1.2.3"}}
|
||||
got, err := compat.Probe(context.Background(), c)
|
||||
if err != nil {
|
||||
t.Fatalf("Probe: %v", err)
|
||||
}
|
||||
if got.ServerVersion != "1.2.3" {
|
||||
t.Errorf("ServerVersion = %q, want %q", got.ServerVersion, "1.2.3")
|
||||
}
|
||||
if got.ProbedAt.IsZero() {
|
||||
t.Error("ProbedAt should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_Error(t *testing.T) {
|
||||
c := &fakeProbeClient{err: errors.New("HTTP error 500: down")}
|
||||
_, err := compat.Probe(context.Background(), c)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user