Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Exceptions for the OSV scan of the files in this directory. OSV-Scanner reads
# this file for the lockfiles beside it and for no other directory. One advisory
# per entry, and every entry has a date it stops working on.
#
# The rule is to raise what can be raised - pull request #181 fixed thirty
# findings rather than listing them here. An entry belongs here only when no
# version anybody can install has the fix.

# mcp 1.29.0, in requirements-semgrep.txt for the scanner and nothing else.
#
# Every semgrep release, 1.181.0 of 2026-10-14 included, asks for mcp==1.29.0
# exactly, so no set pip can install holds the fixed 1.30.0. Dependabot's own
# security update said the same on 2026-10-15: latest resolvable 1.29.0, lowest
# fixed 1.30.0.
#
# The advisory is about an MCP server over Streamable HTTP keeping its sessions
# without bound. The job runs `semgrep scan` and nothing else, which starts no
# server and listens nowhere, on a runner that lives for one scan.
#
# When semgrep moves to a fixed mcp, Dependabot brings that release as a pull
# request against requirements-semgrep.in, and this entry has nothing left to
# hide - remove it then. If that has not happened by the date below, the weekly
# scan goes red again, which is the reminder.
[[IgnoredVulns]]
id = "GHSA-84m7-p3x7-pcfv"
ignoreUntil = 2026-11-15
reason = "semgrep pins mcp==1.29.0 in every release, and the advisory is about an MCP HTTP server, which semgrep scan never starts"
35 changes: 30 additions & 5 deletions internal/guard/boxwidth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ func TestABoxIsWideEnoughForItsOwnPlaceholder(t *testing.T) {
continue
}
hint := placeholderShownFor(p)
words := fyne.MeasureText(hint, theme.TextSize(), fyne.TextStyle{})
if got := typedInWidth(control); got < words.Width {
t.Errorf("%s.%s shows %q, which needs %.0f px, in a box %.0f px wide - so it is cut off",
d.ID, p.Name, hint, words.Width, got)
if needs, got := boxNeededFor(hint), typedInWidth(control); got < needs {
t.Errorf("%s.%s shows %q, which needs a box of %.0f px, in a box %.0f px wide - so it is cut off",
d.ID, p.Name, hint, needs, got)
}
checked++
}
Expand All @@ -106,6 +105,20 @@ func placeholderShownFor(p format.Property) string {
return text.PlaceholderLeftEmpty(p.Default)
}

// boxNeededFor is how wide a box has to be to show words without cutting
// them: the words, and the two paddings on each side the toolkit lays an
// entry's text out inside.
//
// Written out here rather than asked of parts.roomFor, which says the same
// thing, because a guard that called it would agree with it whatever it said.
// One copy for both screens since a review of #195: the single batch screen
// compared the words with the whole box, the batch screen with the room inside
// it, and "worked out from the size" - 165 px of words - fits the first in a
// 179 px box and not the second.
func boxNeededFor(words string) float32 {
return fyne.MeasureText(words, theme.TextSize(), fyne.TextStyle{}).Width + theme.InnerPadding()*2 + theme.Padding()*2
}

// formatsLaidOut opens the window, puts it on a canvas and hands back a way to
// change format that leaves the screen laid out afterwards.
//
Expand Down Expand Up @@ -259,10 +272,22 @@ func TestABoxForANumberIsNotAsWideAsTheFormOnTheBatchScreen(t *testing.T) {
t.Errorf("%s declares %q and the batch screen draws no field for it", d.ID, p.Name)
continue
}
if width := typedInWidth(control); width > float32(parts.ColumnWidth)/2 {
width := typedInWidth(control)
if width > float32(parts.ColumnWidth)/2 {
t.Errorf("on the batch screen %s.%s holds a %s and its box is %.0f px of a %.0f px column",
d.ID, p.Name, p.Kind, width, float32(parts.ColumnWidth))
}
// And no narrower than what it shows when left alone, which is the
// half the grid made the one that matters: since 2026-09-23 a box
// left unshaped takes one column, 179 px here, and "worked out from
// the size" needs 189 px of box (boxNeededFor). Measured on
// 2026-10-15, shaped 374 px and unshaped 179, and until then this
// guard asked only for the ceiling, which both pass.
hint := placeholderShownFor(p)
if needs := boxNeededFor(hint); width < needs {
t.Errorf("on the batch screen %s.%s shows %q, which needs %.0f px, in a box %.0f px wide - so it is cut off",
d.ID, p.Name, hint, needs, width)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
checked++
}
}
Expand Down
53 changes: 53 additions & 0 deletions internal/guard/environment_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package guard

import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
Expand Down Expand Up @@ -47,6 +49,14 @@ func TestADirectoryWeCannotWriteInIsAPermissionProblem(t *testing.T) {
if strings.Contains(errOut, "already exists") {
t.Errorf("a permission problem is reported as a name that is taken:\n%s", errOut)
}
// And as what it is. The first thing a run creates in the directory is the
// lock that keeps a second run out, so this is where the refusal comes from,
// and a failure there that is not "something is already there" is reported
// as one. Without this the guard passed on a refusal calling the directory
// busy with another run - which says nothing about a taken name either.
if !strings.Contains(errOut, "cannot start a run in") {
t.Errorf("a directory nobody can write in is not reported as a run that could not start there:\n%s", errOut)
}
if n := strings.Count(errOut, "tfg: "); n != 1 {
t.Errorf("one fault produced %d messages:\n%s", n, errOut)
}
Expand All @@ -61,7 +71,50 @@ func TestADirectoryWeCannotWriteInIsAPermissionProblem(t *testing.T) {
// California describe the same instant the same way. A local time would make
// two manifests of the same run look like different runs, and a reader
// comparing them has no way to tell which.
//
// Asked in a zone that is not UTC, because in UTC the two are the same text.
// The runners keep their clocks in UTC, so a manifest that recorded local time
// passed here on every one of them - the mutation run of 2026-10-15 found it,
// and only the owner's machine, east of UTC, could tell. The zone is set in a
// child process of its own rather than here, because time.Local is a variable
// every goroutine of this binary reads and the race detector runs these guards.
// Not through TZ, which Go on Windows does not read: measured on 2026-10-16,
// TZ=Asia/Kathmandu and TZ=UTC both left a program on this machine in CEST.
func TestTheRecordedTimeDoesNotDependOnTheLocalZone(t *testing.T) {
if os.Getenv(awayFromUTCChild) == "1" {
recordedTimeAwayFromUTC(t)
return
}
// A ceiling on the child, so one that hangs ends this guard rather than
// the whole run. And the child's own PASS line rather than the word: a
// pattern that matched nothing ends with "no tests to run" and PASS too
// (a review of #195).
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
run := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestTheRecordedTimeDoesNotDependOnTheLocalZone$", "-test.v")
run.Env = append(os.Environ(), awayFromUTCChild+"=1")
out, err := run.CombinedOutput()
if err != nil {
t.Fatalf("the manifest's time moved with the zone:\n%s", out)
}
if !strings.Contains(string(out), "--- PASS: TestTheRecordedTimeDoesNotDependOnTheLocalZone") {
t.Fatalf("the child said nothing about passing, which means it never ran:\n%s", out)
}
}

const awayFromUTCChild = "TFG_AWAY_FROM_UTC_CHILD"

// recordedTimeAwayFromUTC is the guard above, in a process whose local zone is
// five hours and forty five minutes east of UTC - a zone no runner and no
// developer's machine is likely to be in already.
func recordedTimeAwayFromUTC(t *testing.T) {
t.Helper()
time.Local = time.FixedZone("UTC+05:45", 5*60*60+45*60)
// In the state this is about, or it proves nothing.
if now := time.Now().Format(time.RFC3339); strings.HasSuffix(now, "Z") {
t.Fatalf("the local time reads %s, which is UTC, so this cannot tell a local time from a universal one", now)
}

dir := t.TempDir()
code, stdout, errOut := run(t,
"generate", "--format", "txt", "--size", "1kb", "--json", "--out", dir)
Expand Down
23 changes: 23 additions & 0 deletions internal/guard/formwidth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ func TestABoxForANumberIsTheWidthOfANumber(t *testing.T) {
screenName, label, got, parts.NumericWidth*2)
}
}

// And the other bound, in the narrowest window the form allows. A
// number's box asks for NumericWidth, and that is what makes the grid
// lay a narrow window out in fewer, wider columns. Measured on
// 2026-10-15 in a 580 px window: 262 px boxes, and 123 px - narrower than a
// number - with parts.Numeric handing the control over unsized. At the
// width above the two look the same, which is how the mutation run
// scored that change a HOLE.
w.Resize(fyne.NewSize(content.MinSize().Width, 760))
content.Refresh()
// In the state this is about, or it proves nothing (a review of #195):
// a resize the window ignored would measure the wide layout again.
if got, least := content.Size().Width, content.MinSize().Width; got > least+1 {
t.Fatalf("%s: the form is %.0f px wide after a resize to its least width of %.0f px, so this did not measure the narrowest layout",
screenName, got, least)
}
for _, label := range numeric {
box := entryUnder(t, content, label)
if got := box.Size().Width; got < parts.NumericWidth-1 {
t.Errorf("%s: in the narrowest window the box for %q is %.0f px wide, under the %d px of a number",
screenName, label, got, parts.NumericWidth)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
w.Close()
}
}
Expand Down
25 changes: 20 additions & 5 deletions internal/guard/gogl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package guard

import (
"encoding/json"
"go/build"
"io/fs"
"os"
"os/exec"
Expand Down Expand Up @@ -312,18 +313,32 @@ func firstDifferingLine(a, b string) int {
// generated file with C in its comments, and no guard reading the shape of
// this module's code sees it. A walk that stopped skipping would hand that
// file to fourteen guards at once, and they would all be right to refuse it.
//
// Whether the walk lists a package of that module is asked only where the
// module holds a package this build sees. Its files use cgo, so with cgo off -
// the mutation runners build that way - packages() would find nothing in it
// even after walking in: the run of 2026-10-15 scored the walk that stopped
// skipping as a HOLE on those runners and caught it where cgo is on. That one
// question skips there, as a subtest, and the list of skipped modules is
// asked everywhere, because it does not depend on cgo (a review of #195).
func TestANestedModuleIsNotAPackageOfThisModule(t *testing.T) {
root := repoRoot(t)
generated := filepath.Join(root, filepath.FromSlash(bindingCopy), "v2.1", "gl", "package.go")
if _, err := os.Stat(generated); err != nil {
t.Fatalf("the nested module this guard expects to be skipped is not there: %v", err)
}
for _, p := range packages(t) {
if strings.HasPrefix(p.rel, "third_party/") {
t.Errorf("packages() lists %s, which is inside another module.\n"+
"A directory with its own go.mod is outside \"./...\" and has to be outside this walk.", p.rel)
listed := packages(t)
t.Run("no package of it is listed", func(t *testing.T) {
if _, err := build.ImportDir(filepath.Dir(generated), 0); err != nil {
t.Skipf("this build sees no package in %s (%v), so a walk into it would list nothing and this cannot tell", bindingCopy, err)
}
}
for _, p := range listed {
if strings.HasPrefix(p.rel, "third_party/") {
t.Errorf("packages() lists %s, which is inside another module.\n"+
"A directory with its own go.mod is outside \"./...\" and has to be outside this walk.", p.rel)
}
}
})
// And the walk skipped exactly that module. A go.mod dropped into a
// first party directory would take it out of every guard that reads
// packages(), and nothing but this line would say so.
Expand Down
42 changes: 30 additions & 12 deletions internal/guard/manifestpaths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,39 @@ func TestVerifyNeverReachesOutsideTheDirectory(t *testing.T) {
// a name no disk has, the file is reported unreachable, cleanup leaves it and
// ends with 5 anyway. The code was right by accident while the manifest was
// being resolved exactly as before.
//
// Two spellings, because each system reaches a different check. A path from
// this system's own temporary directory starts with a separator on Linux and
// macOS, so there the check for a leading separator refused it and the check
// for a volume letter was never reached - the mutation run of 2026-10-15
// scored that check a HOLE on the runners and caught it on Windows. A volume
// written out reaches it everywhere, which is the point of reading a manifest
// with the same rules on every system.
func TestAManifestPathIsNeverAbsolute(t *testing.T) {
out, victim := outsideVictim(t)
mf := escapingManifest(t, out, filepath.ToSlash(victim), 21)
for _, c := range []struct {
what string
path func(victim string) string
}{
{"this system's absolute path", func(victim string) string { return filepath.ToSlash(victim) }},
{"a path naming a disk volume", func(string) string { return "C:/somewhere/else/victim.txt" }},
} {
t.Run(c.what, func(t *testing.T) {
out, victim := outsideVictim(t)
mf := escapingManifest(t, out, c.path(victim), 21)

code, _, errOut := run(t, "cleanup", mf, "--yes", "--force")
victimSurvives(t, victim)
code, _, errOut := run(t, "cleanup", mf, "--yes", "--force")
victimSurvives(t, victim)

if code != cli.ExitIO {
t.Errorf("exit %d, expected %d - an absolute path in a manifest is refused, not resolved:\n%s", code, cli.ExitIO, errOut)
}
if !strings.Contains(errOut, "cannot be read as a manifest") {
t.Errorf("the manifest was acted on rather than turned away - an absolute path has to be refused at the door, not discovered to be unreachable later:\n%s", errOut)
}
if !strings.Contains(errOut, "outside") {
t.Errorf("the refusal does not say what is wrong with the path:\n%s", errOut)
if code != cli.ExitIO {
t.Errorf("exit %d, expected %d - an absolute path in a manifest is refused, not resolved:\n%s", code, cli.ExitIO, errOut)
}
if !strings.Contains(errOut, "cannot be read as a manifest") {
t.Errorf("the manifest was acted on rather than turned away - an absolute path has to be refused at the door, not discovered to be unreachable later:\n%s", errOut)
}
if !strings.Contains(errOut, "outside") {
t.Errorf("the refusal does not say what is wrong with the path:\n%s", errOut)
}
})
}
}

Expand Down
Loading
Loading