mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 02:00:19 +08:00
Users who install or upgrade lark-cli via make install, go install, or
direct binary download end up with a binary but no AI agent skills,
degrading agent UX. This PR adds a startup-time skills version drift
notice (injected into JSON envelope _notice.skills, mirroring the
existing _notice.update pattern) and unifies lark-cli update's skills
sync across all three branches (npm / manual / already-latest) with
stamp-based dedup, so any explicit update invocation keeps skills in
sync regardless of how the binary was installed.
Changes:
- new internal/skillscheck package: notice (StaleNotice + atomic
pending), stamp (~/.lark-cli/skills.stamp), skip (CI / DEV /
non-release / LARKSUITE_CLI_NO_SKILLS_NOTIFIER opt-out), check
(synchronous Init)
- cmd/root.go: rename setupUpdateNotice -> setupNotices, compose
output.PendingNotice returning {update?, skills?}; capture
build.Version locally before spawning the async update goroutine
- cmd/update/update.go: add runSkillsAndStamp helper with stamp-based
dedup; rewire the three branches through shared applySkillsResult /
emitSkillsTextHints helpers; add skills_status block to --check JSON
output as a pure report (no side effects)
- internal/update: export IsRelease(version) bool / IsCIEnv() bool
for cross-package reuse; refresh UpdateInfo.Message to append
', run: lark-cli update' so both notices recommend the same fix
- AGENTS.md: add Notification Opt-Outs section documenting
LARKSUITE_CLI_NO_UPDATE_NOTIFIER and LARKSUITE_CLI_NO_SKILLS_NOTIFIER
- internal/binding/types.go: bump default exec-provider timeout from
5s to 10s (out-of-scope flake fix for TestResolveExecRef_JSONResponse
under heavy parallel test load)
91 lines
2.3 KiB
Go
91 lines
2.3 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package skillscheck
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func resetPending(t *testing.T) {
|
|
t.Helper()
|
|
SetPending(nil)
|
|
t.Cleanup(func() { SetPending(nil) })
|
|
}
|
|
|
|
func TestInit_InSync_NoNotice(t *testing.T) {
|
|
clearSkillsSkipEnv(t)
|
|
resetPending(t)
|
|
dir := t.TempDir()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
|
if err := WriteStamp("1.0.21"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
Init("1.0.21")
|
|
if got := GetPending(); got != nil {
|
|
t.Errorf("GetPending() = %+v, want nil (in-sync)", got)
|
|
}
|
|
}
|
|
|
|
func TestInit_ColdStart_NoticeWithEmptyCurrent(t *testing.T) {
|
|
clearSkillsSkipEnv(t)
|
|
resetPending(t)
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
Init("1.0.21")
|
|
got := GetPending()
|
|
if got == nil {
|
|
t.Fatal("GetPending() = nil, want non-nil for cold start")
|
|
}
|
|
if got.Current != "" || got.Target != "1.0.21" {
|
|
t.Errorf("notice = %+v, want {Current:\"\", Target:\"1.0.21\"}", got)
|
|
}
|
|
}
|
|
|
|
func TestInit_Drift_NoticeWithStampVersion(t *testing.T) {
|
|
clearSkillsSkipEnv(t)
|
|
resetPending(t)
|
|
dir := t.TempDir()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
|
if err := WriteStamp("1.0.20"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
Init("1.0.21")
|
|
got := GetPending()
|
|
if got == nil {
|
|
t.Fatal("GetPending() = nil, want non-nil for drift")
|
|
}
|
|
if got.Current != "1.0.20" || got.Target != "1.0.21" {
|
|
t.Errorf("notice = %+v, want {Current:\"1.0.20\", Target:\"1.0.21\"}", got)
|
|
}
|
|
}
|
|
|
|
func TestInit_Skipped_NoNotice(t *testing.T) {
|
|
clearSkillsSkipEnv(t)
|
|
resetPending(t)
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
// Even with an empty config dir (no stamp), DEV version should skip
|
|
// the check entirely and never emit a notice.
|
|
Init("DEV")
|
|
if got := GetPending(); got != nil {
|
|
t.Errorf("GetPending() = %+v, want nil (skip rules met)", got)
|
|
}
|
|
}
|
|
|
|
func TestInit_ReadStampError_FailsClosed(t *testing.T) {
|
|
clearSkillsSkipEnv(t)
|
|
resetPending(t)
|
|
dir := t.TempDir()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
|
// Make the stamp path a directory so vfs.ReadFile returns a
|
|
// non-ENOENT I/O error.
|
|
if err := os.MkdirAll(filepath.Join(dir, "skills.stamp"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
Init("1.0.21")
|
|
if got := GetPending(); got != nil {
|
|
t.Errorf("GetPending() = %+v, want nil (fail closed on I/O error)", got)
|
|
}
|
|
}
|