feat(pi): update to the latest stable release by default
This commit is contained in:
@@ -54,8 +54,10 @@ Commands:
|
||||
Select closed backend defaults interactively on a TTY; all flags are required otherwise.
|
||||
pi restart --yes [--drain]
|
||||
Recreate only core with the currently selected Pi image and verify readiness.
|
||||
pi update [--version V]
|
||||
Rebuild the latest stable Pi release, or an explicit version, and recreate only core.
|
||||
pi update --version V --source build --yes [--drain]
|
||||
Rebuild a pinned Pi version and recreate only core.
|
||||
Advanced explicit build form retained for compatibility.
|
||||
pi update --version V --source pull --image IMAGE@sha256:DIGEST --yes [--drain]
|
||||
Pull an immutable candidate and recreate only core.
|
||||
pi rollback --yes Restore the image recorded by the latest Pi update.
|
||||
@@ -75,6 +77,11 @@ Commands:
|
||||
workspace preprocess run --workspace ID [--resume RUN] [--json]
|
||||
`
|
||||
|
||||
var (
|
||||
newPiRegistryClient = pi.NewNPMRegistryClient
|
||||
readPiRuntimePackageName = pi.ReadRuntimePackageName
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
@@ -487,12 +494,21 @@ func piCommand(ctx context.Context, installation config.Installation, runner com
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, err.Error())
|
||||
}
|
||||
result, err := pi.Update(ctx, controlled, request)
|
||||
var registry pi.RegistryClient
|
||||
packageName := ""
|
||||
if request.Version == "" {
|
||||
packageName, err = readPiRuntimePackageName(installation.ProjectDirectory)
|
||||
if err != nil {
|
||||
return commandUsageError(stderr, "pi update latest-version lookup is unavailable: "+err.Error())
|
||||
}
|
||||
registry = newPiRegistryClient()
|
||||
}
|
||||
result, err := pi.UpdateWithResolvedVersion(ctx, controlled, request, packageName, registry)
|
||||
if err != nil {
|
||||
return piFailure(stderr, err, secretValues)
|
||||
}
|
||||
if result.Phase == pi.PhaseNoop {
|
||||
fmt.Fprintf(stdout, "Pi already runs requested version %s; no container was recreated.\n", request.Version)
|
||||
fmt.Fprintf(stdout, "Pi already runs requested version %s; no container was recreated.\n", result.Version)
|
||||
return 0
|
||||
}
|
||||
fmt.Fprintf(stdout, "Pi update verified. Recovery metadata: %s\n", result.StatePath)
|
||||
@@ -647,6 +663,7 @@ func parsePiConfigureArgs(args []string) (pi.Defaults, error) {
|
||||
|
||||
func parsePiUpdateArgs(args []string, statePath, restartStatePath string) (pi.Request, error) {
|
||||
request := pi.Request{StatePath: statePath, RestartStatePath: restartStatePath}
|
||||
sourceSpecified := false
|
||||
for len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "--version":
|
||||
@@ -659,6 +676,7 @@ func parsePiUpdateArgs(args []string, statePath, restartStatePath string) (pi.Re
|
||||
return pi.Request{}, errors.New("--source requires build or pull")
|
||||
}
|
||||
request.Source, args = pi.Source(args[1]), args[2:]
|
||||
sourceSpecified = true
|
||||
case "--image":
|
||||
if len(args) < 2 || request.Image != "" {
|
||||
return pi.Request{}, errors.New("--image requires one digest-pinned image reference")
|
||||
@@ -678,11 +696,17 @@ func parsePiUpdateArgs(args []string, statePath, restartStatePath string) (pi.Re
|
||||
return pi.Request{}, fmt.Errorf("unknown pi update option %q", args[0])
|
||||
}
|
||||
}
|
||||
if request.Version == "" {
|
||||
return pi.Request{}, errors.New("pi update requires --version <pinned-version>")
|
||||
if !sourceSpecified {
|
||||
if request.Image != "" {
|
||||
return pi.Request{}, errors.New("--image is valid only with --source pull")
|
||||
}
|
||||
request.Source = pi.BuildSource
|
||||
request.Confirm = true
|
||||
request.Drain = true
|
||||
return request, nil
|
||||
}
|
||||
if request.Source == "" {
|
||||
return pi.Request{}, errors.New("pi update requires explicit --source build or pull")
|
||||
if request.Version == "" {
|
||||
return pi.Request{}, errors.New("pi update with --source requires --version <pinned-version>")
|
||||
}
|
||||
if request.Source != pi.BuildSource && request.Source != pi.PullSource {
|
||||
return pi.Request{}, errors.New("--source requires build or pull")
|
||||
|
||||
+138
-40
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/doctor"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/pi"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/setup"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
|
||||
@@ -60,7 +62,7 @@ func TestRootCommandIdentity(t *testing.T) {
|
||||
name: "retired command is not an alias",
|
||||
args: []string{"--installation", fixture.installationPath, retiredCommand},
|
||||
wantCode: 2,
|
||||
wantText: `tht: unknown command "` + retiredCommand + `"`,
|
||||
wantText: "tht: unknown command \"" + retiredCommand + "\"",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -313,6 +315,35 @@ func TestParsePiRestartArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePiUpdateArgsDefaultsToLatestStableBuildForTheShortCommand(t *testing.T) {
|
||||
statePath := "/var/lib/tht/update-state.json"
|
||||
restartPath := "/var/lib/tht/restart-state.json"
|
||||
got, err := parsePiUpdateArgs(nil, statePath, restartPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := pi.Request{
|
||||
StatePath: statePath,
|
||||
RestartStatePath: restartPath,
|
||||
Source: pi.BuildSource,
|
||||
Confirm: true,
|
||||
Drain: true,
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("parsePiUpdateArgs(nil) = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePiUpdateArgsAllowsExplicitVersionWithoutAdvancedFlags(t *testing.T) {
|
||||
got, err := parsePiUpdateArgs([]string{"--version", "0.81.0"}, "update.json", "restart.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Version != "0.81.0" || got.Source != pi.BuildSource || !got.Confirm || !got.Drain {
|
||||
t.Fatalf("parsePiUpdateArgs() = %#v, want explicit version with safe build defaults", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSessionsMigrateRequiresExplicitConfirmationBeforeDocker(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "")
|
||||
fixture.setProfile(t, "server")
|
||||
@@ -569,8 +600,8 @@ func TestRunRedactsSecretWhenDoctorFails(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 41 {
|
||||
t.Errorf("run() exit code = %d, want 41", exitCode)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("run() exit code = %d, want normalized doctor failure exit 1", exitCode)
|
||||
}
|
||||
if strings.Contains(stdout.String()+stderr.String(), "doctor-secret") {
|
||||
t.Errorf("doctor failure exposed secret: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
||||
@@ -800,15 +831,9 @@ func TestRunDoctorValidatesTheRenderedInstallation(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.String() != "Doctor checks passed.\n" {
|
||||
t.Errorf("stdout = %q, want doctor success", stdout.String())
|
||||
}
|
||||
report := runDoctorJSON(t, fixture)
|
||||
assertDoctorCheck(t, report, "configuration", doctor.StatusPassed)
|
||||
assertDoctorCheck(t, report, "services", doctor.StatusPassed)
|
||||
}
|
||||
|
||||
func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T) {
|
||||
@@ -820,15 +845,9 @@ func TestRunDoctorDoesNotDereferenceSymlinksDuringLineEndingCheck(t *testing.T)
|
||||
filepath.Join(fixture.projectDirectory, "legacy-workspace.yaml"),
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.String() != "Doctor checks passed.\n" {
|
||||
t.Errorf("stdout = %q, want doctor success", stdout.String())
|
||||
}
|
||||
report := runDoctorJSON(t, fixture)
|
||||
assertDoctorCheck(t, report, "files", doctor.StatusPassed)
|
||||
assertDoctorCheck(t, report, "configuration", doctor.StatusPassed)
|
||||
}
|
||||
|
||||
func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) {
|
||||
@@ -837,18 +856,14 @@ func TestRunDoctorAcceptsComposeJSONLinesServiceStatus(t *testing.T) {
|
||||
t.Setenv(
|
||||
"THT_FAKE_PS",
|
||||
"{\"Service\":\"core\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
|
||||
"{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}",
|
||||
"{\"Service\":\"frontend\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
|
||||
"{\"Service\":\"qdrant\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
|
||||
"{\"Service\":\"embedding\",\"State\":\"running\",\"Health\":\"healthy\"}\n"+
|
||||
"{\"Service\":\"embedding-model-init\",\"State\":\"exited\",\"ExitCode\":0}",
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit code = %d, stderr = %s", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.String() != "Doctor checks passed.\n" {
|
||||
t.Errorf("stdout = %q, want doctor success", stdout.String())
|
||||
}
|
||||
report := runDoctorJSON(t, fixture)
|
||||
assertDoctorCheck(t, report, "services", doctor.StatusPassed)
|
||||
}
|
||||
|
||||
func TestRunPreservesChildExitCodes(t *testing.T) {
|
||||
@@ -883,6 +898,28 @@ func TestRunPiStatusUsesImageBundledPi(t *testing.T) {
|
||||
assertInvocationContains(t, fixture.invocations(t), "exec", "-T", "core", "pi", "--version")
|
||||
}
|
||||
|
||||
func TestRunPiUpdateResolvesLatestStableVersionWhenVersionIsOmitted(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
||||
fixture.setEnvironment(t)
|
||||
if err := os.WriteFile(filepath.Join(fixture.projectDirectory, "docker", "core.Dockerfile"), []byte("ARG PI_VERSION=0.79.0\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousRegistry := newPiRegistryClient
|
||||
newPiRegistryClient = func() pi.RegistryClient {
|
||||
return testRegistryClient{version: "0.80.3"}
|
||||
}
|
||||
t.Cleanup(func() { newPiRegistryClient = previousRegistry })
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(context.Background(), []string{"--installation", fixture.installationPath, "pi", "update"}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("run() exit code = %d, stderr = %s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Pi already runs requested version 0.80.3") {
|
||||
t.Fatalf("stdout = %q, want registry-selected version", stdout.String())
|
||||
}
|
||||
assertInvocationNotContains(t, fixture.invocations(t), "build --pull")
|
||||
}
|
||||
|
||||
func TestRunPiUpdateRequiresExplicitConfirmationWithoutInvokingDocker(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
||||
fixture.setEnvironment(t)
|
||||
@@ -942,22 +979,21 @@ func TestRunPiRestartSanitizesSuccessOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPiUpdateRequiresExplicitSourceWithoutInvokingDocker(t *testing.T) {
|
||||
func TestRunPiUpdateAcceptsExplicitVersionWithoutAdvancedFlags(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "THT_LLM_URL=https://llm.example.invalid\n")
|
||||
fixture.setEnvironment(t)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{
|
||||
"--installation", fixture.installationPath, "pi", "update", "--version", "0.81.0", "--yes",
|
||||
"--installation", fixture.installationPath, "pi", "update", "--version", "0.80.3",
|
||||
}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 2 {
|
||||
t.Errorf("run() exit code = %d, want 2", exitCode)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit code = %d, stderr = %q", exitCode, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "requires explicit --source build or pull") {
|
||||
t.Errorf("stderr = %q, want source guidance", stderr.String())
|
||||
if !strings.Contains(stdout.String(), "Pi already runs requested version 0.80.3") {
|
||||
t.Fatalf("stdout = %q, want explicit version result", stdout.String())
|
||||
}
|
||||
assertDockerNotInvoked(t, fixture)
|
||||
}
|
||||
|
||||
func TestRunPiConfigureReportsTheActualHostAuthFile(t *testing.T) {
|
||||
@@ -1203,6 +1239,15 @@ type cliFixture struct {
|
||||
envTemplate string
|
||||
}
|
||||
|
||||
type testRegistryClient struct {
|
||||
version string
|
||||
err error
|
||||
}
|
||||
|
||||
func (client testRegistryClient) LatestStable(context.Context, string) (string, error) {
|
||||
return client.version, client.err
|
||||
}
|
||||
|
||||
func newProjectWithoutInstallation(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
@@ -1232,6 +1277,18 @@ func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
|
||||
if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(projectDirectory, "docker"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(projectDirectory, "docker", "core.Dockerfile"), []byte("ARG PI_VERSION=0.80.3\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(projectDirectory, "docker", "pi-runtime"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(projectDirectory, "docker", "pi-runtime", "package.json"), []byte(`{"dependencies":{"@earendil-works/pi-coding-agent":"0.80.3"}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{filepath.Join(projectDirectory, "compose.yaml"), filepath.Join(projectDirectory, "deploy", "compose.local.yaml")} {
|
||||
if err := os.WriteFile(path, []byte("services: {}\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1257,7 +1314,7 @@ case " $* " in
|
||||
if [ -n "${THT_FAKE_CONFIG:-}" ]; then
|
||||
printf '%s\n' "$THT_FAKE_CONFIG"
|
||||
else
|
||||
printf '%s\n' '{"volumes":{"settings":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}'
|
||||
printf '%s\n' '{"volumes":{"settings":{},"pi-state":{},"workspace-registry":{},"workspace-secrets":{},"sessions":{},"qdrant-data":{},"embedding-models":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}'
|
||||
fi ;;
|
||||
*" run --rm --no-deps --no-TTY session-migrate "*)
|
||||
if [ "${THT_FAKE_MIGRATION_EXIT:-0}" -ne 0 ]; then
|
||||
@@ -1268,6 +1325,8 @@ case " $* " in
|
||||
*" ps --all --format json "*) printf '%s\n' "$THT_FAKE_PS" ;;
|
||||
*" ps --format json "*) printf '%s\n' "$THT_FAKE_PS" ;;
|
||||
*" ps -q core "*) printf '%s\n' 'core-id' ;;
|
||||
*" version --format {{.Client.Version}}"*) printf '%s\n' '27.0.0' ;;
|
||||
*"compose version --short"*) printf '%s\n' 'v2.30.0' ;;
|
||||
*" image inspect --format {{.Id}} "*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;;
|
||||
*"inspect --format {{.Image}} core-id"*) printf '%s\n' "${THT_FAKE_IMAGE_ID:-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}" ;;
|
||||
*"inspect --format {{json .Mounts}} core-id"*) printf '%s\n' '[{"Type":"volume","Name":"settings","Source":"settings","Destination":"/home/thoth/.pi","RW":true}]' ;;
|
||||
@@ -1278,6 +1337,7 @@ case " $* " in
|
||||
*"settings-cli.js --snapshot"*) printf '%s\n' '{"exists":false,"rawBase64":""}' ;;
|
||||
*"/settings "*) printf '%s\n' '{"provider":"provider","model":"model","thinking":"medium"}' ;;
|
||||
*"/pi-management/test "*) printf '%s\n' '{"ready":true}' ;;
|
||||
*" tht doctor --json"*) printf '%s\n' '{"ok":true}' ;;
|
||||
*"/sessions?scope="*) printf '%s\n' '[]' ;;
|
||||
*"/internal/maintenance/activate "*) printf '%s\n' '{"active":true,"admissions":0}' ;;
|
||||
*"/internal/maintenance/deactivate "*) printf '%s\n' '{"active":false,"admissions":0}' ;;
|
||||
@@ -1321,7 +1381,7 @@ func (f cliFixture) setEnvContents(t *testing.T, env string) {
|
||||
t.Setenv("THT_FAKE_FAILURE", "")
|
||||
t.Setenv("THT_FAKE_FAIL_ON", "")
|
||||
t.Setenv("THT_FAKE_STOPPED_PS", "[]")
|
||||
t.Setenv("THT_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"}]`)
|
||||
t.Setenv("THT_FAKE_PS", `[{"Service":"core","State":"running","Health":"healthy"},{"Service":"frontend","State":"running","Health":"healthy"},{"Service":"qdrant","State":"running","Health":"healthy"},{"Service":"embedding","State":"running","Health":"healthy"},{"Service":"embedding-model-init","State":"exited","ExitCode":0}]`)
|
||||
t.Setenv("THT_FAKE_CONFIG", "")
|
||||
t.Setenv("THT_FAKE_MIGRATION_FAILURE", "")
|
||||
t.Setenv("THT_FAKE_MIGRATION_EXIT", "0")
|
||||
@@ -1368,6 +1428,44 @@ func assertDockerNotInvoked(t *testing.T, fixture cliFixture) {
|
||||
}
|
||||
}
|
||||
|
||||
func runDoctorJSON(t *testing.T, fixture cliFixture) doctor.Report {
|
||||
t.Helper()
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := run(context.Background(), []string{"--installation", fixture.installationPath, "doctor", "--json"}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("doctor exit = %d, stderr = %s, stdout = %s", code, stderr.String(), stdout.String())
|
||||
}
|
||||
var report doctor.Report
|
||||
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil {
|
||||
t.Fatalf("doctor JSON = %q: %v", stdout.String(), err)
|
||||
}
|
||||
if !report.OK {
|
||||
t.Fatalf("doctor report = %#v, want all checks passed", report)
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func assertDoctorCheck(t *testing.T, report doctor.Report, name, wantStatus string) {
|
||||
t.Helper()
|
||||
for _, check := range report.Checks {
|
||||
if check.Name == name {
|
||||
if check.Status != wantStatus {
|
||||
t.Fatalf("doctor check %q = %q, want %q", name, check.Status, wantStatus)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("doctor report does not contain check %q", name)
|
||||
}
|
||||
|
||||
func assertInvocationNotContains(t *testing.T, invocations [][]string, fragment string) {
|
||||
t.Helper()
|
||||
for _, invocation := range invocations {
|
||||
if strings.Contains(strings.Join(invocation, " "), fragment) {
|
||||
t.Errorf("Docker invocation %q unexpectedly contains %q", invocation, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertInvocationContains(t *testing.T, invocations [][]string, want ...string) {
|
||||
t.Helper()
|
||||
for _, invocation := range invocations {
|
||||
|
||||
Reference in New Issue
Block a user