Skip to content

Commit 01391f8

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents c0e2de1 + 15884f8 commit 01391f8

5 files changed

Lines changed: 332 additions & 37 deletions

File tree

internal/dun/agent.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ func resolveInputs(root string, inputs []string) ([]PromptInput, error) {
232232
for _, path := range files {
233233
content, err := os.ReadFile(path)
234234
if err != nil {
235+
if os.IsNotExist(err) {
236+
continue
237+
}
235238
return nil, err
236239
}
237240
rel, err := relPath(root, path)

internal/dun/agent_extra_test.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,12 @@ func TestResolveInputsGlobAndMissing(t *testing.T) {
100100
t.Fatalf("expected 2 inputs, got %d", len(inputs))
101101
}
102102

103-
_, err = resolveInputs(dir, []string{"missing.txt"})
104-
if err == nil {
105-
t.Fatalf("expected error for missing input")
103+
inputs, err = resolveInputs(dir, []string{"missing.txt"})
104+
if err != nil {
105+
t.Fatalf("resolve missing input: %v", err)
106+
}
107+
if len(inputs) != 0 {
108+
t.Fatalf("expected no inputs for missing file, got %d", len(inputs))
106109
}
107110
}
108111

@@ -298,14 +301,24 @@ func TestRunAgentCheckAutoSuccessAndInvalidResponse(t *testing.T) {
298301
}
299302
}
300303

301-
func TestRunAgentCheckBuildPromptEnvelopeError(t *testing.T) {
304+
func TestRunAgentCheckBuildPromptEnvelopeSkipsMissingInputs(t *testing.T) {
302305
dir := t.TempDir()
303306
writeFile(t, filepath.Join(dir, "prompt.md"), "hi")
304307
plugin := Plugin{FS: os.DirFS(dir), Base: "."}
305308
check := Check{ID: "test", Prompt: "prompt.md", Inputs: []string{"missing.txt"}}
306309
opts := Options{AgentMode: "prompt", AutomationMode: "auto"}
307-
if _, err := runAgentCheck(dir, plugin, check, opts); err == nil {
308-
t.Fatalf("expected build prompt error")
310+
res, err := runAgentCheck(dir, plugin, check, opts)
311+
if err != nil {
312+
t.Fatalf("run agent check: %v", err)
313+
}
314+
if res.Status != "prompt" {
315+
t.Fatalf("expected prompt, got %s", res.Status)
316+
}
317+
if res.Prompt == nil {
318+
t.Fatalf("expected prompt envelope")
319+
}
320+
if len(res.Prompt.Inputs) != 0 {
321+
t.Fatalf("expected no inputs, got %v", res.Prompt.Inputs)
309322
}
310323
}
311324

@@ -359,8 +372,12 @@ func TestBuildPromptEnvelopeMissingInput(t *testing.T) {
359372
writeFile(t, filepath.Join(dir, "prompt.md"), "hello")
360373
plugin := Plugin{FS: os.DirFS(dir), Base: "."}
361374
check := Check{ID: "id", Prompt: "prompt.md", Inputs: []string{"missing.txt"}}
362-
if _, err := buildPromptEnvelope(dir, plugin, check, "auto"); err == nil {
363-
t.Fatalf("expected missing input error")
375+
envelope, err := buildPromptEnvelope(dir, plugin, check, "auto")
376+
if err != nil {
377+
t.Fatalf("build prompt envelope: %v", err)
378+
}
379+
if len(envelope.Inputs) != 0 {
380+
t.Fatalf("expected no inputs, got %v", envelope.Inputs)
364381
}
365382
}
366383

internal/dun/engine_extra_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ func TestCheckRepoReturnsError(t *testing.T) {
102102
}
103103
}
104104

105+
func TestCheckRepoEmptyFolder(t *testing.T) {
106+
root := t.TempDir()
107+
if _, err := CheckRepo(root, Options{}); err != nil {
108+
t.Fatalf("check repo empty: %v", err)
109+
}
110+
}
111+
112+
func TestCheckRepoDocsHelixEmpty(t *testing.T) {
113+
root := t.TempDir()
114+
if err := os.MkdirAll(filepath.Join(root, "docs", "helix"), 0755); err != nil {
115+
t.Fatalf("mkdir docs/helix: %v", err)
116+
}
117+
if _, err := CheckRepo(root, Options{}); err != nil {
118+
t.Fatalf("check repo helix empty: %v", err)
119+
}
120+
}
121+
105122
func TestCheckRepoRunCheckError(t *testing.T) {
106123
orig := loadBuiltins
107124
loadBuiltins = func() ([]Plugin, error) {

internal/update/update.go

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
package update
33

44
import (
5+
"archive/tar"
6+
"compress/gzip"
57
"crypto/sha256"
68
"encoding/hex"
79
"encoding/json"
@@ -101,6 +103,15 @@ func (u *Updater) DownloadRelease(release *Release) (string, error) {
101103
return "", fmt.Errorf("no asset found for %s/%s", runtime.GOOS, runtime.GOARCH)
102104
}
103105

106+
checksums, err := u.downloadChecksums(release)
107+
if err != nil {
108+
return "", err
109+
}
110+
expectedChecksum := checksums[asset.Name]
111+
if expectedChecksum == "" {
112+
return "", fmt.Errorf("checksum not found for %s", asset.Name)
113+
}
114+
104115
req, err := http.NewRequest(http.MethodGet, asset.DownloadURL, nil)
105116
if err != nil {
106117
return "", fmt.Errorf("create request: %w", err)
@@ -138,6 +149,22 @@ func (u *Updater) DownloadRelease(release *Release) (string, error) {
138149
return "", fmt.Errorf("size mismatch: expected %d, got %d", asset.Size, written)
139150
}
140151

152+
actualChecksum := hex.EncodeToString(hasher.Sum(nil))
153+
if !strings.EqualFold(actualChecksum, expectedChecksum) {
154+
os.Remove(tmpFile.Name())
155+
return "", fmt.Errorf("checksum mismatch for %s", asset.Name)
156+
}
157+
158+
if strings.HasSuffix(strings.ToLower(asset.Name), ".tar.gz") {
159+
path, err := extractArchiveBinary(tmpFile.Name(), u.BinaryName)
160+
if err != nil {
161+
os.Remove(tmpFile.Name())
162+
return "", err
163+
}
164+
os.Remove(tmpFile.Name())
165+
return path, nil
166+
}
167+
141168
return tmpFile.Name(), nil
142169
}
143170

@@ -263,12 +290,14 @@ func (u *Updater) findAsset(release *Release) *Asset {
263290
aliases = []string{archName}
264291
}
265292

293+
var fallback *Asset
266294
for i := range release.Assets {
267295
asset := &release.Assets[i]
268296
name := strings.ToLower(asset.Name)
269297

270298
// Skip checksums and signatures
271-
if strings.HasSuffix(name, ".sha256") ||
299+
if name == "checksums.txt" ||
300+
strings.HasSuffix(name, ".sha256") ||
272301
strings.HasSuffix(name, ".sig") ||
273302
strings.HasSuffix(name, ".asc") {
274303
continue
@@ -280,12 +309,18 @@ func (u *Updater) findAsset(release *Release) *Asset {
280309

281310
for _, arch := range aliases {
282311
if strings.Contains(name, arch) {
283-
return asset
312+
if strings.HasSuffix(name, ".tar.gz") {
313+
return asset
314+
}
315+
if fallback == nil {
316+
fallback = asset
317+
}
318+
break
284319
}
285320
}
286321
}
287322

288-
return nil
323+
return fallback
289324
}
290325

291326
func (u *Updater) httpClient() HTTPClient {
@@ -387,3 +422,106 @@ func ComputeChecksum(path string) (string, error) {
387422

388423
return hex.EncodeToString(h.Sum(nil)), nil
389424
}
425+
426+
func (u *Updater) downloadChecksums(release *Release) (map[string]string, error) {
427+
asset := findChecksumsAsset(release)
428+
if asset == nil {
429+
return nil, errors.New("checksums.txt not found in release assets")
430+
}
431+
432+
req, err := http.NewRequest(http.MethodGet, asset.DownloadURL, nil)
433+
if err != nil {
434+
return nil, fmt.Errorf("create checksums request: %w", err)
435+
}
436+
req.Header.Set("User-Agent", fmt.Sprintf("%s-updater", u.BinaryName))
437+
438+
client := u.httpClient()
439+
resp, err := client.Do(req)
440+
if err != nil {
441+
return nil, fmt.Errorf("download checksums: %w", err)
442+
}
443+
defer resp.Body.Close()
444+
445+
if resp.StatusCode != http.StatusOK {
446+
return nil, fmt.Errorf("checksums status: %d", resp.StatusCode)
447+
}
448+
449+
data, err := io.ReadAll(resp.Body)
450+
if err != nil {
451+
return nil, fmt.Errorf("read checksums: %w", err)
452+
}
453+
454+
return parseChecksums(data), nil
455+
}
456+
457+
func findChecksumsAsset(release *Release) *Asset {
458+
for i := range release.Assets {
459+
asset := &release.Assets[i]
460+
if strings.EqualFold(asset.Name, "checksums.txt") {
461+
return asset
462+
}
463+
}
464+
return nil
465+
}
466+
467+
func parseChecksums(data []byte) map[string]string {
468+
out := make(map[string]string)
469+
lines := strings.Split(string(data), "\n")
470+
for _, line := range lines {
471+
fields := strings.Fields(line)
472+
if len(fields) < 2 {
473+
continue
474+
}
475+
out[fields[1]] = fields[0]
476+
}
477+
return out
478+
}
479+
480+
func extractArchiveBinary(archivePath, binaryName string) (string, error) {
481+
f, err := os.Open(archivePath)
482+
if err != nil {
483+
return "", fmt.Errorf("open archive: %w", err)
484+
}
485+
defer f.Close()
486+
487+
gz, err := gzip.NewReader(f)
488+
if err != nil {
489+
return "", fmt.Errorf("read gzip: %w", err)
490+
}
491+
defer gz.Close()
492+
493+
tr := tar.NewReader(gz)
494+
for {
495+
hdr, err := tr.Next()
496+
if err == io.EOF {
497+
break
498+
}
499+
if err != nil {
500+
return "", fmt.Errorf("read tar: %w", err)
501+
}
502+
if hdr.Typeflag != tar.TypeReg {
503+
continue
504+
}
505+
if filepath.Base(hdr.Name) != binaryName {
506+
continue
507+
}
508+
509+
tmpFile, err := os.CreateTemp("", fmt.Sprintf("%s-extract-*", binaryName))
510+
if err != nil {
511+
return "", fmt.Errorf("create temp file: %w", err)
512+
}
513+
defer tmpFile.Close()
514+
515+
if _, err := io.Copy(tmpFile, tr); err != nil {
516+
os.Remove(tmpFile.Name())
517+
return "", fmt.Errorf("extract binary: %w", err)
518+
}
519+
if err := tmpFile.Chmod(0755); err != nil {
520+
os.Remove(tmpFile.Name())
521+
return "", fmt.Errorf("chmod extracted binary: %w", err)
522+
}
523+
return tmpFile.Name(), nil
524+
}
525+
526+
return "", fmt.Errorf("binary %s not found in archive", binaryName)
527+
}

0 commit comments

Comments
 (0)