mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-10 01:51:40 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7facc49510 | ||
|
|
e19fc12a59 | ||
|
|
2ff95dbc5e | ||
|
|
f4a646549c | ||
|
|
302b6732a5 |
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/safety/sandbox"
|
||||
)
|
||||
@@ -57,11 +58,11 @@ func TestMemoryBenchAnswerIsolationAtSandboxBoundary(t *testing.T) {
|
||||
if err := validateAnswerIsolation(tasks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
work := t.TempDir()
|
||||
work := testenv.TempDir(t)
|
||||
if err := stageAnswerIsolation(tasks[0].answerRoot, work); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(home, "config.toml"), []byte("[sandbox]\nbash = \"off\"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -105,7 +106,7 @@ func TestAnswerIsolationRefusesUnsafeWorkdirAndExistingConfig(t *testing.T) {
|
||||
if err := stageAnswerIsolation(answerRoot, filepath.Dir(answerRoot)); err == nil {
|
||||
t.Fatal("workdir inside the checkout was accepted")
|
||||
}
|
||||
work := t.TempDir()
|
||||
work := testenv.TempDir(t)
|
||||
path := filepath.Join(work, "reasonix.toml")
|
||||
if err := os.WriteFile(path, []byte("# task fixture\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -120,7 +121,7 @@ func TestAnswerIsolationRefusesUnsafeWorkdirAndExistingConfig(t *testing.T) {
|
||||
|
||||
func TestRunTaskDropsIsolationConfigBeforeGrading(t *testing.T) {
|
||||
requireShellStub(t)
|
||||
suite := t.TempDir()
|
||||
suite := testenv.TempDir(t)
|
||||
taskDir := filepath.Join(suite, "tasks", "demo")
|
||||
if err := os.MkdirAll(filepath.Join(taskDir, "memory", "project"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -129,7 +130,7 @@ func TestRunTaskDropsIsolationConfigBeforeGrading(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(taskDir, "verify.sh"), []byte(verify), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "fake-agent")
|
||||
bin := filepath.Join(testenv.TempDir(t), "fake-agent")
|
||||
script := `#!/usr/bin/env bash
|
||||
set -e
|
||||
test -f reasonix.toml
|
||||
@@ -154,7 +155,7 @@ done
|
||||
|
||||
func TestMemoryBenchRejectsAnswerReadInTrajectory(t *testing.T) {
|
||||
requireShellStub(t)
|
||||
suite := t.TempDir()
|
||||
suite := testenv.TempDir(t)
|
||||
taskDir := filepath.Join(suite, "tasks", "demo")
|
||||
answer := filepath.Join(taskDir, "memory", "project", "fact.md")
|
||||
if err := os.MkdirAll(filepath.Dir(answer), 0o755); err != nil {
|
||||
@@ -166,7 +167,7 @@ func TestMemoryBenchRejectsAnswerReadInTrajectory(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(taskDir, "verify.sh"), []byte("#!/bin/sh\ntest -f answer.txt\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "fake-agent")
|
||||
bin := filepath.Join(testenv.TempDir(t), "fake-agent")
|
||||
script := `#!/usr/bin/env bash
|
||||
set -e
|
||||
printf 'ok\n' > answer.txt
|
||||
@@ -194,7 +195,7 @@ done
|
||||
|
||||
func TestMissingAnswerTrajectoryKeepsOtherTasksRunning(t *testing.T) {
|
||||
requireShellStub(t)
|
||||
root := filepath.Join(t.TempDir(), "tasks")
|
||||
root := filepath.Join(testenv.TempDir(t), "tasks")
|
||||
var tasks []task
|
||||
for _, id := range []string{"missing", "complete"} {
|
||||
dir := filepath.Join(root, id)
|
||||
@@ -206,7 +207,7 @@ func TestMissingAnswerTrajectoryKeepsOtherTasksRunning(t *testing.T) {
|
||||
}
|
||||
tasks = append(tasks, task{ID: id, Prompt: id, TimeoutSec: 30, dir: dir, answerRoot: root})
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "fake-agent")
|
||||
bin := filepath.Join(testenv.TempDir(t), "fake-agent")
|
||||
script := `#!/usr/bin/env bash
|
||||
set -e
|
||||
printf 'ok\n' > answer.txt
|
||||
@@ -251,7 +252,7 @@ func TestAnswerTrajectoryAuditDistinguishesReadsFromListingsAndDenials(t *testin
|
||||
{"glob lists name only", `{"event":{"kind":"tool_result","tool":{"name":"glob","args":"{\"pattern\":\"/repo/tasks/demo/memory/project/*.md\"}","output":"/repo/tasks/demo/memory/project/fact.md"}}}`, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "run.trajectory.jsonl")
|
||||
path := filepath.Join(testenv.TempDir(t), "run.trajectory.jsonl")
|
||||
if err := os.WriteFile(path, []byte(tc.data+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -264,7 +265,7 @@ func TestAnswerTrajectoryAuditDistinguishesReadsFromListingsAndDenials(t *testin
|
||||
}
|
||||
|
||||
func TestAnswerTrajectoryAuditScansEarlierSegments(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "tasks")
|
||||
root := filepath.Join(testenv.TempDir(t), "tasks")
|
||||
answer := filepath.Join(root, "demo", "memory", "project", "fact.md")
|
||||
if err := os.MkdirAll(filepath.Dir(answer), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -272,7 +273,7 @@ func TestAnswerTrajectoryAuditScansEarlierSegments(t *testing.T) {
|
||||
if err := os.WriteFile(answer, []byte("---\nname: fact\n---\nA fact body in the first trajectory leg.\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
for index, data := range []string{
|
||||
`{"event":{"kind":"tool_result","tool":{"name":"bash","args":"{\"command\":\"cat /repo/tasks/demo/memory/project/fact.md\"}","output":"A fact body in the first trajectory leg."}}}`,
|
||||
`{"event":{"kind":"text","text":"done"}}`,
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// fixtureRows is a synthetic batch: three solvable tasks, one of them solved on
|
||||
@@ -64,7 +66,7 @@ func timeAt(s string) time.Time {
|
||||
|
||||
func writeFixtureBatch(t *testing.T) (string, batchManifest) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := writeBatch(fixtureProvenance(dir), fixtureRows()); err != nil {
|
||||
t.Fatalf("write batch: %v", err)
|
||||
}
|
||||
@@ -174,7 +176,7 @@ func TestBatchAuditRoundTrip(t *testing.T) {
|
||||
// The axes carry what changes a figure's meaning, never the operator's machine:
|
||||
// meter/tape/trajectory paths and steer text stay out of the manifest.
|
||||
func TestBatchAxesKeepPathsAndFreeTextOut(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
prov := fixtureProvenance(dir)
|
||||
prov.axes = batchAxes(suiteConfig{
|
||||
profile: "delivery", cacheArm: "warm", effort: "high", anchor: "blind",
|
||||
@@ -260,7 +262,7 @@ func TestBatchAuditNamesTheFigureThatMoved(t *testing.T) {
|
||||
// manifest, no commit to tie the figures to, and rows cut short.
|
||||
func TestBatchAuditRefusesUnverifiableBatches(t *testing.T) {
|
||||
t.Run("missing manifest", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
writeRows(t, dir, fixtureRows())
|
||||
out, err := auditBatch(dir)
|
||||
if err == nil {
|
||||
@@ -387,7 +389,7 @@ const manifestWithoutOptionals = `{
|
||||
`
|
||||
|
||||
func TestBatchAuditReadsAbsentFieldsAsUnknown(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
writeRows(t, dir, fixtureRows())
|
||||
_, canonical, err := readBatch(filepath.Join(dir, batchResultsName))
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
fileencoding "reasonix/internal/base/fileutil/encoding"
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// benchmarksRoot holds every committed task corpus. Validation discovers the
|
||||
@@ -363,7 +364,7 @@ func fullDecisions() map[authoringRule]ruleDecision {
|
||||
func TestCorpusPolicyValidationCatchesGaps(t *testing.T) {
|
||||
universe := func(t *testing.T, corpora ...string) (string, []corpusPolicy) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var policies []corpusPolicy
|
||||
for _, name := range corpora {
|
||||
dir := tempCorpus(t, root, name, "t-"+name, "")
|
||||
@@ -437,7 +438,7 @@ func TestCorpusPolicyValidationCatchesGaps(t *testing.T) {
|
||||
name: "a registry entry that shadows the root's corpus by name",
|
||||
universe: func(t *testing.T) (string, []corpusPolicy) {
|
||||
root, policies := universe(t, "registered")
|
||||
elsewhere := tempCorpus(t, t.TempDir(), "registered", "t-impostor", "")
|
||||
elsewhere := tempCorpus(t, testenv.TempDir(t), "registered", "t-impostor", "")
|
||||
policies[0].Dir = elsewhere
|
||||
return root, policies
|
||||
},
|
||||
@@ -505,7 +506,7 @@ func TestStopPolicyAppliesToEveryRegisteredCorpus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
corpus string
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestWilsonAndNewcombeMatchKnownValues(t *testing.T) {
|
||||
@@ -42,7 +44,7 @@ func writeReport(t *testing.T, dir, name, capability string, pass int, n int) st
|
||||
}
|
||||
|
||||
func TestReproductionSection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
a := writeReport(t, dir, "a.json", "sha256:aaaaaaaaaaaaaaaa", 40, 50)
|
||||
same := writeReport(t, dir, "b.json", "sha256:aaaaaaaaaaaaaaaa", 38, 50)
|
||||
got := reproductionSection(a, same)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func withFakeIO(t *testing.T, w func(string, []byte, os.FileMode) error, r func(string) ([]byte, error)) {
|
||||
@@ -38,7 +40,7 @@ func TestRestoreSourceFailsWhenTheBytesDoNotComeBack(t *testing.T) {
|
||||
|
||||
// The real filesystem is the other witness: writing over a directory fails.
|
||||
func TestRestoreSourceFailsOnARealWriteError(t *testing.T) {
|
||||
if err := restoreSource(t.TempDir(), []byte("original")); err == nil {
|
||||
if err := restoreSource(testenv.TempDir(t), []byte("original")); err == nil {
|
||||
t.Fatal("restoring onto a directory reported success")
|
||||
}
|
||||
}
|
||||
@@ -47,7 +49,7 @@ func TestTreeMatchesSeesADirtyTree(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git unavailable")
|
||||
}
|
||||
repo := t.TempDir()
|
||||
repo := testenv.TempDir(t)
|
||||
git := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
@@ -88,7 +90,7 @@ func TestRunMutationFailsWhenTheRestoreFails(t *testing.T) {
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("go unavailable")
|
||||
}
|
||||
repo := t.TempDir()
|
||||
repo := testenv.TempDir(t)
|
||||
git := func(args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
@@ -150,7 +152,7 @@ func TestDifferentialPerTestFailsWhenRestoreFails(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git unavailable")
|
||||
}
|
||||
repo := t.TempDir()
|
||||
repo := testenv.TempDir(t)
|
||||
git := func(args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
@@ -197,7 +199,7 @@ func TestDifferentialPerTestFailsWhenRestoreFails(t *testing.T) {
|
||||
// tree must not be left on base just because the failure happened early. The
|
||||
// defer that restores is registered before the check for exactly this path.
|
||||
func TestDifferentialPerTestRestoresWhenTheBaseCheckFails(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
repo := testenv.TempDir(t)
|
||||
run := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", append([]string{"-C", repo}, args...)...)
|
||||
|
||||
@@ -29,7 +29,7 @@ printf '%%s' "$metrics" > metrics-path.txt
|
||||
printf '%%s' '%s' > "$metrics%s"
|
||||
exit %d
|
||||
`, body, suffix, code)
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
path := filepath.Join(testenv.TempDir(t), name)
|
||||
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write stub: %v", err)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ case "$metrics" in
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
path := filepath.Join(testenv.TempDir(t), name)
|
||||
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write stub: %v", err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
const tapedStream = "data: {\"choices\":[{\"delta\":{\"content\":\"fixed\"}}]}\n\n" +
|
||||
@@ -44,7 +46,7 @@ func readAll(t *testing.T, resp *http.Response) string {
|
||||
// A run replayed from its tape gets the recorded stream byte for byte, never
|
||||
// reaches the provider, and is metered as the recorded run was.
|
||||
func TestTapeReplaysARecordedRunWithoutTheProvider(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
_, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
live := readAll(t, post(t, base, "/chat/completions", tapedRequest))
|
||||
@@ -71,7 +73,7 @@ func TestTapeReplaysARecordedRunWithoutTheProvider(t *testing.T) {
|
||||
// A kernel change that alters what the model is asked shows as the first
|
||||
// request whose body departs from the tape, named down to the message.
|
||||
func TestTapeReplayNamesTheFirstRequestThatDiffers(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
_, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
readAll(t, post(t, base, "/chat/completions", tapedRequest))
|
||||
@@ -123,7 +125,7 @@ func waitFor(t *testing.T, what string, cond func() bool) {
|
||||
// A recording whose request file cannot be written is reported rather than
|
||||
// treated as a tape: request.json is what marks the exchange complete.
|
||||
func TestTapeSaveFailureIsReportedOnTheMeter(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
orig := tapeWriteFile
|
||||
tapeWriteFile = func(path string, body []byte, mode os.FileMode) error {
|
||||
if strings.HasSuffix(path, "request.json") {
|
||||
@@ -161,7 +163,7 @@ func TestTapeSaveFailureIsReportedOnTheMeter(t *testing.T) {
|
||||
// A tape with no recorded request is incomplete, not a divergence: there is
|
||||
// nothing to compare against, and blaming the harness would be wrong.
|
||||
func TestTapeMissingRequestIsIncompleteNotADivergence(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
_, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
readAll(t, post(t, base, "/chat/completions", tapedRequest))
|
||||
@@ -193,7 +195,7 @@ func TestTapeMissingRequestIsIncompleteNotADivergence(t *testing.T) {
|
||||
// first: stopping is what waits for the handlers whose deferred writes carry the
|
||||
// tape counters. Snapshotting before that misses the last exchange.
|
||||
func TestMeterRecordDrainsHandlersBeforeSnapshot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
m, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
defer stop()
|
||||
@@ -214,7 +216,7 @@ func TestMeterRecordDrainsHandlersBeforeSnapshot(t *testing.T) {
|
||||
// Re-recording over an existing tape must not let the previous run's
|
||||
// completeness marker make a partial new exchange look complete.
|
||||
func TestTapeReRecordClearsStaleCompletenessMarkers(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
_, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
readAll(t, post(t, base, "/chat/completions", tapedRequest))
|
||||
@@ -263,7 +265,7 @@ func TestTapeReRecordClearsStaleCompletenessMarkers(t *testing.T) {
|
||||
// incomplete exchange and prints no line about one. Only the failure paths were
|
||||
// pinned before.
|
||||
func TestTapeCompleteRecordingReportsNothingIncomplete(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
var calls atomic.Int32
|
||||
m, base, stop := tapedMeter(t, tapeRecord, root, &calls)
|
||||
defer stop()
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestUserChoseDirRejectsWhatALauncherInherits(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
base := testenv.TempDir(t)
|
||||
app := filepath.Join(base, "ReasonixStudio")
|
||||
exe := filepath.Join(app, "Reasonix Studio.exe")
|
||||
cases := []struct {
|
||||
@@ -33,7 +34,7 @@ func TestUserChoseDirRejectsWhatALauncherInherits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPickLaunchWorkspaceFallbackOrder(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
base := testenv.TempDir(t)
|
||||
app := filepath.Join(base, "ReasonixStudio")
|
||||
exe := filepath.Join(app, "Reasonix Studio.exe")
|
||||
mk := func(name string) string {
|
||||
@@ -68,7 +69,7 @@ func TestPickLaunchWorkspaceFallbackOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchWorkspaceKeepsLaunchProjectAfterSidebarReorder(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
base := testenv.TempDir(t)
|
||||
home := filepath.Join(base, "state")
|
||||
app := filepath.Join(base, "app")
|
||||
a, b := filepath.Join(base, "a"), filepath.Join(base, "b")
|
||||
@@ -96,7 +97,7 @@ func TestLaunchWorkspaceKeepsLaunchProjectAfterSidebarReorder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchWithNothingChosenOrRememberedOpensTheAutoWorkspaceNotHome(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
base := testenv.TempDir(t)
|
||||
state := filepath.Join(base, "state")
|
||||
home := filepath.Join(base, "home")
|
||||
app := filepath.Join(base, "app")
|
||||
@@ -127,7 +128,7 @@ func TestLaunchWithNothingChosenOrRememberedOpensTheAutoWorkspaceNotHome(t *test
|
||||
}
|
||||
|
||||
func TestPickLaunchWorkspaceCreatesTheAutoWorkspaceLast(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
base := testenv.TempDir(t)
|
||||
app := filepath.Join(base, "app")
|
||||
exe := filepath.Join(app, "Reasonix Studio")
|
||||
auto := filepath.Join(base, "state", "workspaces", "auto")
|
||||
|
||||
@@ -112,7 +112,7 @@ func (r recordingRunner) Run(_ context.Context, input string) error {
|
||||
// hub, its config path and its capabilities are the ones the host builds.
|
||||
func testHub(t *testing.T, cfg config.ServeConfig) (*serve.Hub, chan string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
got := make(chan string, 1)
|
||||
bc := serve.NewBroadcaster()
|
||||
ctrl := control.New(control.Options{
|
||||
@@ -382,7 +382,7 @@ func TestOnlyAPipeIsAParentLease(t *testing.T) {
|
||||
t.Error("the null device was read as a parent holding this host open")
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "not-a-pipe")
|
||||
path := filepath.Join(testenv.TempDir(t), "not-a-pipe")
|
||||
if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -399,7 +399,7 @@ func TestOnlyAPipeIsAParentLease(t *testing.T) {
|
||||
// The whole run, driven the way Electron drives it: a pipe for the lease, one
|
||||
// line of handshake, traffic, then the parent letting go.
|
||||
func TestRunServesUntilTheParentLetsGo(t *testing.T) {
|
||||
page := t.TempDir()
|
||||
page := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(page, "index.html"), []byte("<title>studio</title>"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/platform/packagegrant"
|
||||
)
|
||||
|
||||
func TestStripPackageGrantsOnlyAnswersForAnApplication(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
for name, app := range map[string]string{
|
||||
"no application": "",
|
||||
"a path that is nothing": filepath.Join(dir, "missing.exe"),
|
||||
@@ -27,7 +28,7 @@ func TestStripPackageGrantsOnlyAnswersForAnApplication(t *testing.T) {
|
||||
// The shell reads this line as structure, so both lists are present even when
|
||||
// empty: a null would be a third answer it has to guess about.
|
||||
func TestStripPackageGrantsReportsOneLineOfJSON(t *testing.T) {
|
||||
app := filepath.Join(t.TempDir(), "Reasonix Studio.exe")
|
||||
app := filepath.Join(testenv.TempDir(t), "Reasonix Studio.exe")
|
||||
if err := os.WriteFile(app, []byte("x"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestLinuxReleaseBuildsAndCollectsBothNativeArchitectures(t *testing.T) {
|
||||
@@ -84,7 +86,7 @@ func TestLinuxCIPackageMatrixPreservesUpstreamScope(t *testing.T) {
|
||||
{"pull_request", "desktop/electron/test/unit.mjs", true},
|
||||
} {
|
||||
t.Run(tc.event+"/"+tc.file, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
// A shell function owns command lookup on every host, including Git
|
||||
// Bash on Windows; a PATH shim can resolve the runner's real git.
|
||||
git := "git() { case \"$*\" in 'diff -z --name-only --no-renames fixture HEAD') printf '%s\\0' \"$MATRIX_TEST_FILE\" ;; 'diff --name-only --no-renames fixture HEAD') printf '%s\\n' \"$MATRIX_TEST_FILE\" ;; *) return 1 ;; esac; };\n"
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"aead.dev/minisign"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/platform/update"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,7 @@ func TestSignFiles(t *testing.T) {
|
||||
t.Setenv("MINISIGN_PRIVATE_KEY", string(enc))
|
||||
t.Setenv("MINISIGN_PASSWORD", "pw")
|
||||
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
artifact := filepath.Join(dir, "Reasonix-linux-amd64.tar.gz")
|
||||
payload := []byte("pretend this is a release tarball")
|
||||
if err := os.WriteFile(artifact, payload, 0o644); err != nil {
|
||||
@@ -51,7 +52,7 @@ func TestSignFiles(t *testing.T) {
|
||||
// every platform is listed with a download URL, a parallel .minisig URL, and a
|
||||
// non-empty digest. The .minisig and latest.json files must be ignored.
|
||||
func TestGenManifest(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
names := []string{
|
||||
"Reasonix-darwin-arm64.zip",
|
||||
"Reasonix-darwin-amd64.zip",
|
||||
@@ -158,7 +159,7 @@ func TestGenManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenManifestCanReuseStableNotesForStandaloneRC(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(dir, "Reasonix-linux-amd64.tar.gz"), []byte("rc"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -181,7 +182,7 @@ func TestGenManifestCanReuseStableNotesForStandaloneRC(t *testing.T) {
|
||||
// TestGenManifestIgnoresUnknownNativePackages ensures a .deb without a known
|
||||
// platform key is skipped rather than inventing a native_packages entry.
|
||||
func TestGenManifestIgnoresUnknownNativePackages(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
for _, n := range []string{
|
||||
"Reasonix-linux-amd64.tar.gz",
|
||||
"Reasonix-mystery.deb",
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/platform/update"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
// the shared chunk store named; an unsigned index fails the release instead of
|
||||
// being offered.
|
||||
func TestManifestListsEachSignedDeltaFromTheMirror(t *testing.T) {
|
||||
dir, deltaDir := t.TempDir(), t.TempDir()
|
||||
dir, deltaDir := testenv.TempDir(t), testenv.TempDir(t)
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
if err := os.WriteFile(filepath.Join(dir, "ReasonixStudio-windows-amd64-installer.exe"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/platform/update"
|
||||
)
|
||||
|
||||
func TestManifestRecordsEveryArtifactWithItsSignature(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
body := []byte("studio archive bytes")
|
||||
for _, name := range []string{
|
||||
@@ -71,7 +72,7 @@ func TestManifestRecordsEveryArtifactWithItsSignature(t *testing.T) {
|
||||
// on the Studio side does. Only what installs itself may resolve as a platform
|
||||
// asset, or the version panel offers a move it cannot make.
|
||||
func TestManifestResolvesOnlyWhatCanInstallItself(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
for _, name := range []string{
|
||||
"ReasonixStudio-linux-amd64.tar.gz",
|
||||
@@ -128,7 +129,7 @@ func TestManifestResolvesOnlyWhatCanInstallItself(t *testing.T) {
|
||||
|
||||
func TestEmptyDirIsAFailedRelease(t *testing.T) {
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
if err := run(t.TempDir(), "v0.1.0", "studio-v0.1.0", "", false); err == nil {
|
||||
if err := run(testenv.TempDir(t), "v0.1.0", "studio-v0.1.0", "", false); err == nil {
|
||||
t.Fatal("a release with no artifacts must fail, not publish an empty manifest")
|
||||
}
|
||||
}
|
||||
@@ -136,7 +137,7 @@ func TestEmptyDirIsAFailedRelease(t *testing.T) {
|
||||
// A mirrored release is fetched from the mirror first, with GitHub kept as the
|
||||
// address a client falls back to; every full artifact and its signature alike.
|
||||
func TestMirroredManifestPutsTheMirrorFirst(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
for _, name := range []string{
|
||||
"ReasonixStudio-windows-amd64-installer.exe",
|
||||
@@ -193,7 +194,7 @@ func TestUnmirroredManifestNamesOnlyGitHub(t *testing.T) {
|
||||
// Both Windows architectures ship an installer and a portable archive under
|
||||
// the same naming rule; each installer answers only its own platform key.
|
||||
func TestWindowsArchitecturesResolveToTheirOwnInstallers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
for _, name := range []string{
|
||||
"ReasonixStudio-windows-amd64-installer.exe",
|
||||
@@ -258,7 +259,7 @@ func TestRPMRidesBesideTheDebWithoutMovingItsKey(t *testing.T) {
|
||||
}
|
||||
return m
|
||||
}
|
||||
debOnly, both := t.TempDir(), t.TempDir()
|
||||
debOnly, both := testenv.TempDir(t), testenv.TempDir(t)
|
||||
write(debOnly, "ReasonixStudio-linux-amd64.deb")
|
||||
write(both, "ReasonixStudio-linux-amd64.deb", "ReasonixStudio-linux-amd64.rpm")
|
||||
before, after := generate(debOnly), generate(both)
|
||||
@@ -292,7 +293,7 @@ func TestRPMRidesBesideTheDebWithoutMovingItsKey(t *testing.T) {
|
||||
// the second architecture must not move the first one's entries.
|
||||
func TestLinuxArm64PackagesAreKeyedBesideAmd64(t *testing.T) {
|
||||
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
generate := func() update.Manifest {
|
||||
if err := run(dir, "v2.39.0", "studio-v2.39.0", "", true); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -345,7 +346,7 @@ func TestLinuxArm64PackagesAreKeyedBesideAmd64(t *testing.T) {
|
||||
// replaces: it must not appear as a platform asset or a native package, and
|
||||
// adding it must not move any entry a shipped client reads.
|
||||
func TestAnAppImageIsOfferedButNeverResolvedAsAnUpdate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
for _, name := range []string{"ReasonixStudio-linux-amd64.deb", "ReasonixStudio-linux-amd64.AppImage", "ReasonixStudio-linux-arm64.AppImage"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -324,6 +324,9 @@ the picker and keyboard navigation.
|
||||
1 CSS px in both themes at 100% and 125% device scale.
|
||||
- The compact account button has no row divider; the footer owns the divider
|
||||
above all three controls.
|
||||
- Inside the workbench the file tree is the left column and the open file the right, in the dock as
|
||||
well as full width. Choosing a file leaves the tree open; choosing any other surface takes the
|
||||
whole column and folds it. Below 760px the same rule holds with the tree folded by default.
|
||||
- The docked workbench uses the divider's remembered width. Pointer dragging and
|
||||
keyboard resizing share that width; there is no intermediate width preset.
|
||||
- Full-workbench view preserves the divider width and keeps the conversation and
|
||||
|
||||
@@ -649,8 +649,7 @@
|
||||
.app[data-browser="off"] .pane:has(> .pbody[data-dock]) > .scroll[data-pane="browser"] { visibility: hidden; transition-property: visibility; transition-duration: 0s; transition-delay: var(--t-panel) }
|
||||
.pane:has(> .pbody[data-dock]) .workbench-body:not([data-files]) { grid-template-columns: minmax(0, 1fr) }
|
||||
.pane:has(> .pbody[data-dock]) .workbench-body:not([data-files]) .workbench-explorer { display: none }
|
||||
.pane:has(> .pbody[data-dock]) .workbench-body[data-files] { grid-template-columns: minmax(0, 1fr) }
|
||||
.pane:has(> .pbody[data-dock]) .workbench-body[data-files] .workbench-canvas { display: none }
|
||||
.pane:has(> .pbody[data-dock]) .workbench-body:not([data-files]) .workbench-canvas { grid-column: 1 }
|
||||
.bpanel { flex: 1; display: flex; flex-direction: column; min-height: 0 }
|
||||
.bbar { flex: none; display: flex; flex-direction: column; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--hair) }
|
||||
.bbar .vtabs { overflow-x: auto }
|
||||
|
||||
@@ -1177,12 +1177,13 @@ body { background: var(--page) !important; }
|
||||
.workbench-tab:hover .workbench-tab-close, .workbench-tab[aria-selected="true"] .workbench-tab-close { opacity: 1; }
|
||||
.workbench-tab-close:hover { color: var(--text); }
|
||||
.workbench-tabs svg { width: 13px; height: 13px; flex: none; }
|
||||
.workbench-body { flex: 1; min-height: 0; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) clamp(190px, 25%, 300px); grid-template-rows: minmax(0, 1fr); }
|
||||
.workbench-canvas { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
||||
.workbench-body { flex: 1; min-height: 0; min-width: 0; display: grid; grid-template-columns: clamp(190px, 36%, 260px) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
|
||||
.workbench-canvas { grid-column: 2; grid-row: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
||||
.workbench-content > .studio-browser, .workbench-content > .bpanel { width: 100%; height: 100%; min-width: 0; }
|
||||
.workbench-canvas .bbar > .vtabs { display: none; }
|
||||
.workbench-explorer { min-width: 0; display: flex; flex-direction: column; border-left: 1px solid var(--hair); background: var(--page); }
|
||||
.workbench-explorer { grid-column: 1; grid-row: 1; min-width: 0; display: flex; flex-direction: column; border-right: 1px solid var(--hair); background: var(--page); }
|
||||
.workbench-explorer-head { height: 36px; padding: 0 12px; flex: none; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--hair); color: var(--muted); font: var(--w-med) var(--fs-meta)/1 var(--ui); text-transform: uppercase; }
|
||||
.workbench-explorer-head > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workbench-explorer-head small { color: var(--faint); font-size: var(--fs-micro); }
|
||||
.workbench-explorer-head small { margin-left: auto; }
|
||||
.workbench-open-editor {
|
||||
@@ -1251,9 +1252,9 @@ body { background: var(--page) !important; }
|
||||
.workbench-error.workbench-close-error { flex: none; font-size: var(--fs-meta); white-space: pre-line; overflow-wrap: anywhere; }
|
||||
.workbench-error { padding: 8px 12px; border-bottom: 1px solid var(--err); color: var(--err-ink); font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); }
|
||||
@media (max-width: 760px) {
|
||||
.workbench-body { grid-template-columns: minmax(0, 1fr); }
|
||||
.workbench-body:not([data-files]) { grid-template-columns: minmax(0, 1fr); }
|
||||
.workbench-body:not([data-files]) .workbench-explorer { display: none; }
|
||||
.workbench-body[data-files] .workbench-canvas { display: none; }
|
||||
.workbench-body:not([data-files]) .workbench-canvas { grid-column: 1; }
|
||||
}
|
||||
|
||||
/* Settings is one stable layer in the current window. */
|
||||
|
||||
@@ -405,12 +405,9 @@ describe("WorkbenchPanel", () => {
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
});
|
||||
|
||||
// A file opened from the explorer steps the list aside rather than closing it,
|
||||
// so closing that file comes back to the list. Folding the column instead
|
||||
// leaves someone who opened one file with no workbench at all. With no tabs
|
||||
// the launcher draws a "文件" card as well as the toolbar toggle, so the
|
||||
// The launcher draws a "文件" card as well as the toolbar toggle, so the
|
||||
// toolbar button is named by its action rather than its label.
|
||||
it("returns to the explorer when a file opened from it is closed", async () => {
|
||||
it("keeps the explorer beside a file opened from it, and after that file is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseManual = vi.fn();
|
||||
const { container } = render(<WorkbenchPanel port={new MockPort()} tabs={[]} manual={false} shown scheme="light" changes={[]} onCloseManual={onCloseManual} onSurfaces={vi.fn()} onExternal={vi.fn()} />);
|
||||
@@ -420,18 +417,14 @@ describe("WorkbenchPanel", () => {
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "README.md" }));
|
||||
expect(screen.getByRole("tab", { name: "README.md" })).toBeTruthy();
|
||||
// The list stepped aside for the file.
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(false);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "关闭 README.md" }));
|
||||
expect(onCloseManual).not.toHaveBeenCalled();
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
});
|
||||
|
||||
// The list was stepped aside for that file, not closed on behalf of the one
|
||||
// behind it: closing the file leaves the other tab selected and alone, so the
|
||||
// list must stay out of its way and the column must not fold.
|
||||
it("does not bring the explorer back over a tab that is still open", async () => {
|
||||
it("leaves the explorer as it was when the file picked from it is closed over another tab", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseManual = vi.fn();
|
||||
const page = { id: "b1", target: "t1", url: "https://example.com", title: "My page", active: false };
|
||||
@@ -439,17 +432,14 @@ describe("WorkbenchPanel", () => {
|
||||
|
||||
await user.click(container.querySelector<HTMLElement>('[data-action="workbench.files"]')!);
|
||||
await user.click(await screen.findByRole("button", { name: "README.md" }));
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(false);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "关闭 README.md" }));
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(false);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
expect(onCloseManual).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("tab", { name: "My page" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
// Closing every browser tab at once while a file from the explorer is still
|
||||
// open leaves that file behind, so the column stays and the list stays away.
|
||||
// The batch goes through the same rule as a single close, which is the point.
|
||||
it("keeps the column when every browser tab is closed at once beside an explorer file", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseManual = vi.fn();
|
||||
@@ -470,7 +460,7 @@ describe("WorkbenchPanel", () => {
|
||||
expect(screen.queryByRole("tab", { name: "Agent page" })).toBeNull();
|
||||
expect(screen.getByRole("tab", { name: "README.md" })).toBeTruthy();
|
||||
expect(onCloseManual).not.toHaveBeenCalled();
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(false);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows a page the person opened beside the agent's, while the agent's tab stays active", () => {
|
||||
@@ -518,13 +508,13 @@ describe("WorkbenchPanel", () => {
|
||||
localStorage.removeItem("rx-show-hidden-files");
|
||||
});
|
||||
|
||||
it("shows the file that was picked from the list rather than leaving the list over it", async () => {
|
||||
it("shows the file picked from the list and keeps the list beside it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<WorkbenchPanel port={new MockPort()} tabs={[]} manual={false} shown scheme="light" changes={[]} onCloseManual={vi.fn()} onSurfaces={vi.fn()} onExternal={vi.fn()} />);
|
||||
await user.click(screen.getAllByRole("button", { name: "文件" })[0]);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
await user.click(await screen.findByRole("button", { name: "README.md" }));
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(false);
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
expect(screen.getByRole("tab", { name: "README.md" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
|
||||
@@ -137,9 +137,6 @@ export function WorkbenchPanel({
|
||||
[directories, setDirectories] = useState<string[]>([]),
|
||||
[openFiles, setOpenFiles] = useState<string[]>([]);
|
||||
const [showFiles, setShowFiles] = useState(false);
|
||||
// The file picked out of the explorer, if that is how it was opened: closing
|
||||
// it comes back to the list it was stepped aside from.
|
||||
const fromExplorer = useRef("");
|
||||
const [launcher, setLauncher] = useState(false);
|
||||
const newTab = useRef<HTMLButtonElement>(null);
|
||||
const toggleLauncher = () => { supersede(); setLauncher((open) => !open); };
|
||||
@@ -160,10 +157,10 @@ export function WorkbenchPanel({
|
||||
useOnBump(toggleFiles, toggleExplorer);
|
||||
const [query, setQuery] = useState(""),
|
||||
[selected, setSelected] = useState("");
|
||||
// Docked, the file list hides the canvas a page is drawn in, so choosing any
|
||||
// surface steps the list aside.
|
||||
// The list and the canvas sit side by side, so a file stays beside its tree;
|
||||
// any other surface takes the whole column and steps the list aside.
|
||||
const select = useCallback((key: string) => {
|
||||
setShowFiles(false);
|
||||
if (!key.startsWith("file:")) setShowFiles(false);
|
||||
setLauncher(false);
|
||||
setFailed("");
|
||||
setSelected(key);
|
||||
@@ -333,11 +330,11 @@ export function WorkbenchPanel({
|
||||
// source view is one click away.
|
||||
const filePath = active?.kind === "file" ? active.path : "";
|
||||
useEffect(() => {
|
||||
onActiveFile?.(shown && !launcher && !showFiles && filePath ? filePath : undefined);
|
||||
onActiveFile?.(shown && !launcher && filePath ? filePath : undefined);
|
||||
return () => onActiveFile?.(undefined);
|
||||
}, [filePath, shown, launcher, showFiles, onActiveFile]);
|
||||
}, [filePath, shown, launcher, onActiveFile]);
|
||||
const textMenu = useWorkbenchTextMenu({
|
||||
path: filePath, mode, scope, shown: shown && !launcher && !showFiles,
|
||||
path: filePath, mode, scope, shown: shown && !launcher,
|
||||
onQuote: onQuoteSelection, onError: setFailed,
|
||||
});
|
||||
const grantedFile = parseGrantedFile(filePath);
|
||||
@@ -359,7 +356,7 @@ export function WorkbenchPanel({
|
||||
const unpreviewableText = !hasDocument && errorCode === "workspace.file_unreadable";
|
||||
const revision = file?.revision;
|
||||
useEffect(() => {
|
||||
if (mode !== "preview" || !previewable || !shown || showFiles || launcher || previewTab || (!revision && !fileFailed && !failed)) return;
|
||||
if (mode !== "preview" || !previewable || !shown || launcher || previewTab || (!revision && !fileFailed && !failed)) return;
|
||||
if (missing) {
|
||||
setMode("file");
|
||||
return;
|
||||
@@ -368,7 +365,7 @@ export function WorkbenchPanel({
|
||||
setFailed(reason(e));
|
||||
setMode("file");
|
||||
});
|
||||
}, [mode, previewable, shown, showFiles, launcher, previewTab, revision, fileFailed, missing, failed, filePath, openPreview, tabs]);
|
||||
}, [mode, previewable, shown, launcher, previewTab, revision, fileFailed, missing, failed, filePath, openPreview, tabs]);
|
||||
useEffect(() => {
|
||||
if (mode === "preview" && revision) syncPreview(filePath, revision);
|
||||
}, [mode, filePath, revision, syncPreview]);
|
||||
@@ -391,8 +388,7 @@ export function WorkbenchPanel({
|
||||
useEffect(() => {
|
||||
strip.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
||||
}, [activeKey, surfaces.length]);
|
||||
// Picking a file is asking to read it. Docked, the list and the file share one
|
||||
// column, so the list steps aside; side by side it stays where it is.
|
||||
// Picking a file is asking to read it; the list stays where it is.
|
||||
const [recentFiles, setRecentFiles] = useState(() => readRecentFiles(workspaceRoot ?? ""));
|
||||
const openFile = useCallback((path: string) => {
|
||||
setRecentFiles(rememberFile(workspaceRoot ?? "", path));
|
||||
@@ -402,11 +398,8 @@ export function WorkbenchPanel({
|
||||
n.delete(`file:${path}`);
|
||||
return n;
|
||||
});
|
||||
// A file picked while the explorer is shown leaves that list stepped aside
|
||||
// rather than closed, so closing this file comes back to the list.
|
||||
fromExplorer.current = showFiles ? path : "";
|
||||
pick(`file:${path}`);
|
||||
}, [pick, showFiles, workspaceRoot]);
|
||||
}, [pick, workspaceRoot]);
|
||||
useEffect(() => { if (fileRequest) { openFile(fileRequest.path); fileRequest.consumed?.(); } }, [fileRequest, openFile]);
|
||||
// A rendered document's own links open beside it and its images load from the
|
||||
// tree. Held stable per document, so typing re-renders the text alone.
|
||||
@@ -459,7 +452,7 @@ export function WorkbenchPanel({
|
||||
if (openingBrowser.current === page) openingBrowser.current = null;
|
||||
}
|
||||
};
|
||||
const { close: closeTabs, closing, closeFailure } = useTabClose({ port, surfaces, selected, showFiles: showFiles || launcher, fromExplorer, setBrowsers, setOpenFiles, setAgents, setDismissed, setSelected, setShowFiles, setLauncher, onCloseManual, onCloseSelected: () => setFailed("") });
|
||||
const { close: closeTabs, closing, closeFailure } = useTabClose({ port, surfaces, selected, showFiles: showFiles || launcher, setBrowsers, setOpenFiles, setAgents, setDismissed, setSelected, setLauncher, onCloseManual, onCloseSelected: () => setFailed("") });
|
||||
const close = (targets: Surface[]) => { supersede(); return terminals.guard(targets, closeTabs); };
|
||||
const dismissTabMenu = useCallback(() => setTabMenu(null), []);
|
||||
const [tabMenu, setTabMenu] = useState<{ surface: Surface; x: number; y: number; anchor: HTMLElement } | null>(null);
|
||||
@@ -628,7 +621,7 @@ export function WorkbenchPanel({
|
||||
)}
|
||||
<div className="workbench-content" ref={canvas} hidden={launcher || !active} data-action-contextmenu="workbench.quote-selection" {...textMenu.readerProps}>
|
||||
<ManualBrowsers ids={browsers} active={active?.kind === "manual" ? active.id : ""} onAddress={noteHost} onExternal={onExternal} scheme={scheme} />
|
||||
<WorkbenchTerminals port={port} hook={terminals} active={active?.kind === "terminal" ? active.id : ""} shown={shown && !launcher && !showFiles}
|
||||
<WorkbenchTerminals port={port} hook={terminals} active={active?.kind === "terminal" ? active.id : ""} shown={shown && !launcher}
|
||||
scheme={scheme} onQuote={onQuoteSelection} onClose={(targets) => void close(targets)} />
|
||||
{active?.kind === "browser" && (
|
||||
<AgentBrowserPanel
|
||||
@@ -639,11 +632,11 @@ export function WorkbenchPanel({
|
||||
/>
|
||||
)}
|
||||
{active?.kind === "changes" && (
|
||||
<WorkbenchChangesPage port={port} changes={changes} branch={gitRepos.repos.find((r) => r.dir === "")?.branch} shown={shown && !launcher && !showFiles}
|
||||
<WorkbenchChangesPage port={port} changes={changes} branch={gitRepos.repos.find((r) => r.dir === "")?.branch} shown={shown && !launcher}
|
||||
remote={remote} onOpen={(path) => { openFile(path); setMode("diff"); }} onChanged={onTreeChanged} />
|
||||
)}
|
||||
{active?.kind === "job" && <JobRunView key={active.id} port={port} id={active.id} name={active.name} shown={shown && !launcher && !showFiles} />}
|
||||
{active?.kind === "agent" && <AgentRunView key={active.ref} port={port} run={active} shown={shown && !launcher && !showFiles} onShowCall={onShowCall} />}
|
||||
{active?.kind === "job" && <JobRunView key={active.id} port={port} id={active.id} name={active.name} shown={shown && !launcher} />}
|
||||
{active?.kind === "agent" && <AgentRunView key={active.ref} port={port} run={active} shown={shown && !launcher} onShowCall={onShowCall} />}
|
||||
{active?.kind === "file" && (
|
||||
<>
|
||||
<WorkbenchFileBar
|
||||
@@ -673,7 +666,7 @@ export function WorkbenchPanel({
|
||||
)}
|
||||
{mode === "document" || unpreviewableText ? (
|
||||
<Suspense fallback={<div className="workbench-empty">{t("common.loading")}</div>}>
|
||||
<DocumentPreview key={filePath} port={port} path={filePath} shown={shown && !showFiles && !launcher}
|
||||
<DocumentPreview key={filePath} port={port} path={filePath} shown={shown && !launcher}
|
||||
view={documentState(documentStates, filePath)}
|
||||
refresh={JSON.stringify([changeKey, glance, running, wrote])}
|
||||
onOpenExternal={!remote && port.opensFiles() && /\.(pdf|html?|svg|docx?|xlsx?|pptx?)$/i.test(filePath) ? () => {
|
||||
|
||||
@@ -19,8 +19,10 @@ it("reports actual tab selection and clears it when the canvas is hidden or its
|
||||
fireEvent.click(screen.getAllByRole("button", { name: "one.md" })[0]);
|
||||
await waitFor(() => expect(onActiveFile).toHaveBeenLastCalledWith("docs/one.md"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "文件" }));
|
||||
await waitFor(() => expect(onActiveFile).toHaveBeenLastCalledWith("docs/one.md"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "新建标签" }));
|
||||
await waitFor(() => expect(onActiveFile).toHaveBeenLastCalledWith(undefined));
|
||||
fireEvent.click(screen.getByRole("button", { name: "文件" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "新建标签" }));
|
||||
await waitFor(() => expect(onActiveFile).toHaveBeenLastCalledWith("docs/one.md"));
|
||||
fireEvent.click(screen.getAllByRole("button", { name: "关闭 one.md" })[0]);
|
||||
await waitFor(() => expect(onActiveFile).toHaveBeenLastCalledWith("other/one.md"));
|
||||
|
||||
@@ -68,6 +68,8 @@ it.each(["rail", "sidebar", "palette"])("opens only the focused pane's files fro
|
||||
expect(filesShown(background)).toBe(false);
|
||||
expect(focused().querySelectorAll(".workbench-tab")).toHaveLength(0);
|
||||
await userEvent.click(within(focused()).getByRole("button", { name: "README.md" }));
|
||||
expect(filesShown()).toBe(true);
|
||||
await openFiles();
|
||||
expect(filesShown()).toBe(false);
|
||||
await openFiles();
|
||||
expect(filesShown()).toBe(true);
|
||||
@@ -111,14 +113,15 @@ it("does not replay a Files request into a newly focused session", async () => {
|
||||
await palette("文件");
|
||||
await userEvent.click(within(focused()).getByRole("button", { name: "README.md" }));
|
||||
const previous = focused();
|
||||
const kept = filesShown(previous);
|
||||
fireEvent.click(document.querySelector('.ptab[data-pane="r1"]')!);
|
||||
expect(focused()).not.toBe(previous);
|
||||
expect(filesShown()).toBe(false);
|
||||
await palette("文件");
|
||||
expect(filesShown()).toBe(true);
|
||||
expect(filesShown(previous)).toBe(false);
|
||||
expect(filesShown(previous)).toBe(kept);
|
||||
fireEvent.click(document.querySelector('.ptab[data-pane="r2"]')!);
|
||||
expect(filesShown()).toBe(false);
|
||||
expect(filesShown()).toBe(kept);
|
||||
});
|
||||
|
||||
const railFiles = () => within(document.querySelector<HTMLElement>(".nav")!).getByRole("button", { name: "文件" });
|
||||
|
||||
@@ -101,7 +101,7 @@ it("keeps a superseded successful user tab owned while a separate preview claim
|
||||
});
|
||||
|
||||
describe("opening an HTML file from the explorer", () => {
|
||||
it.each(["Files", "launcher"])("does not start a hidden preview after a late file read and newer %s choice", async (choice) => {
|
||||
it.each(["Files", "launcher"])("starts a preview only while its file is in view after a late read and newer %s choice", async (choice) => {
|
||||
const user = userEvent.setup();
|
||||
const { port, open } = setup();
|
||||
let read!: (file: Awaited<ReturnType<MockPort["workspaceFile"]>>) => void;
|
||||
@@ -113,11 +113,14 @@ describe("opening an HTML file from the explorer", () => {
|
||||
if (choice === "Files") await user.click(container.querySelector('[data-action="workbench.files"]')!);
|
||||
else await user.click(screen.getByRole("button", { name: "新建标签" }));
|
||||
await act(async () => read({ path: "index.html", content: "<h1>Late</h1>", revision: "r1" }));
|
||||
if (choice === "Files") {
|
||||
expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
await waitFor(() => expect(open).toHaveBeenCalledTimes(1));
|
||||
return;
|
||||
}
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
if (choice === "Files") expect(container.querySelector(".workbench-body")?.hasAttribute("data-files")).toBe(true);
|
||||
else expect(screen.getByRole("group", { name: "开始" })).toBeTruthy();
|
||||
if (choice === "Files") await user.click(screen.getByRole("tab", { name: "index.html" }).querySelector(".workbench-tab-pick")!);
|
||||
else await user.keyboard("{Escape}");
|
||||
expect(screen.getByRole("group", { name: "开始" })).toBeTruthy();
|
||||
await user.keyboard("{Escape}");
|
||||
await waitFor(() => expect(open).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ interface Props {
|
||||
port: AgentPort; surfaces: Surface[]; selected: string; showFiles: boolean;
|
||||
setBrowsers: Setter<string[]>; setOpenFiles: Setter<string[]>; setAgents: Setter<RunTab[]>; setDismissed: Setter<Set<string>>;
|
||||
setSelected: Setter<string>; onCloseManual: () => void; onCloseSelected: () => void;
|
||||
fromExplorer?: { current: string }; setShowFiles?: Setter<boolean>; setLauncher?: Setter<boolean>;
|
||||
setLauncher?: Setter<boolean>;
|
||||
}
|
||||
|
||||
export function useTabClose({ port, surfaces, selected, showFiles, setBrowsers, setOpenFiles, setAgents, setDismissed, setSelected, onCloseManual, onCloseSelected, fromExplorer, setShowFiles, setLauncher }: Props) {
|
||||
export function useTabClose({ port, surfaces, selected, showFiles, setBrowsers, setOpenFiles, setAgents, setDismissed, setSelected, onCloseManual, onCloseSelected, setLauncher }: Props) {
|
||||
const current = useRef({ surfaces, selected, showFiles });
|
||||
current.current = { surfaces, selected, showFiles };
|
||||
const pending = useRef(false);
|
||||
@@ -42,19 +42,10 @@ export function useTabClose({ port, surfaces, selected, showFiles, setBrowsers,
|
||||
if (closed.has(current.current.selected)) onCloseSelected();
|
||||
setSelected((v) => closed.has(v) ? "" : v);
|
||||
setCloseFailure(failures.join("\n"));
|
||||
// A file picked out of the explorer leaves that list stepped aside, not
|
||||
// closed. When that close is the one that would fold the column — nothing
|
||||
// left behind, the list not open — the list comes back instead. Any other
|
||||
// close keeps the previous rule, so the list never lands over a tab that
|
||||
// is still open.
|
||||
// Closing the last page with no list open leaves the column with something
|
||||
// to offer rather than folding it away.
|
||||
if (!current.current.showFiles && current.current.surfaces.every((s) => closed.has(keyOf(s)))) {
|
||||
if (fromExplorer?.current && closed.has(`file:${fromExplorer.current}`)) {
|
||||
fromExplorer.current = "";
|
||||
setLauncher?.(false);
|
||||
setShowFiles?.(true);
|
||||
} else if (targets.some((s) => s.kind !== "file")) {
|
||||
// Closing the last page leaves the column with something to offer
|
||||
// rather than folding it away: the same place a new tab starts from.
|
||||
if (targets.some((s) => s.kind !== "file")) {
|
||||
setLauncher?.(true);
|
||||
} else {
|
||||
onCloseManual();
|
||||
|
||||
@@ -49,7 +49,7 @@ it('retires an opening whose tab was observed then closed before the response',
|
||||
expect(unseenPageOpenings(port)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not start a preview after its file read finishes behind a newer Files selection', async () => {
|
||||
it('starts a preview once its file read finishes with the file still in view beside the tree', async () => {
|
||||
vi.spyOn(host(), 'drawsBrowserViews').mockReturnValue(true);
|
||||
const port = new MockPort();
|
||||
vi.spyOn(port, 'workspaceFiles').mockResolvedValue({files:['index.html'],directories:[]});
|
||||
@@ -62,9 +62,7 @@ it('does not start a preview after its file read finishes behind a newer Files s
|
||||
await waitFor(()=>expect(reading).toHaveBeenCalled());
|
||||
fireEvent.click(document.querySelector('[data-action="workbench.files"]')!);
|
||||
await act(async()=>read({path:'index.html',content:'hello',revision:'r1'}));
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
expect(filesShown()).toBe(true);
|
||||
fireEvent.click(document.querySelector('[data-action-click="workbench.tab"][data-target="file:index.html"]')!);
|
||||
await waitFor(()=>expect(open).toHaveBeenCalledOnce());
|
||||
});
|
||||
|
||||
|
||||
@@ -36,12 +36,19 @@ it("offers a terminal in a new tab and opens it as a tab in front", async () =>
|
||||
expect((await screen.findByTestId("screen")).getAttribute("data-shown")).toBe("yes");
|
||||
});
|
||||
|
||||
it("keeps a terminal's screen mounted but not in front when another tab is chosen", async () => {
|
||||
it("keeps a terminal's screen mounted but not in front while the new-tab launcher is up", async () => {
|
||||
const port = new MockPort();
|
||||
await openTerminalFromLauncher(port);
|
||||
fireEvent.click(document.querySelector('[data-action="workbench.new"]')!);
|
||||
expect((await screen.findByTestId("screen")).getAttribute("data-shown")).toBe("no");
|
||||
});
|
||||
|
||||
it("keeps a terminal's screen in front beside the file list", async () => {
|
||||
const port = new MockPort();
|
||||
await openTerminalFromLauncher(port);
|
||||
fireEvent.click(document.querySelector('[data-action="workbench.new"]')!);
|
||||
fireEvent.click(document.querySelector('[data-action="workbench.launch-files"]')!);
|
||||
expect((await screen.findByTestId("screen")).getAttribute("data-shown")).toBe("no");
|
||||
expect((await screen.findByTestId("screen")).getAttribute("data-shown")).toBe("yes");
|
||||
});
|
||||
|
||||
it("numbers terminals once there are two, and names a busy one for the command it runs with a dot", async () => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import "./testkit";
|
||||
import { MockPort } from "../port/mock";
|
||||
import { WorkbenchPanel } from "./WorkbenchPanel";
|
||||
import { boot, STORAGE } from "../i18n";
|
||||
|
||||
beforeEach(() => { localStorage.setItem(STORAGE, "zh"); boot(); });
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); localStorage.clear(); });
|
||||
|
||||
const mount = (port = new MockPort()) =>
|
||||
render(<WorkbenchPanel port={port} tabs={[]} manual shown scheme="light" changes={[]} onCloseManual={vi.fn()} onSurfaces={vi.fn()} onExternal={vi.fn()} />);
|
||||
const filesShown = (c: HTMLElement) => c.querySelector(".workbench-body")!.hasAttribute("data-files");
|
||||
|
||||
async function openTree(c: HTMLElement, user: ReturnType<typeof userEvent.setup>) {
|
||||
await user.click(c.querySelector('[data-action="workbench.files"]')!);
|
||||
await waitFor(() => expect(c.querySelector('[data-action-click="workbench.file"]')).toBeTruthy());
|
||||
}
|
||||
|
||||
it("keeps the file tree in view when a file is picked from it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = mount();
|
||||
await openTree(container, user);
|
||||
const row = container.querySelector<HTMLElement>('[data-action-click="workbench.file"]')!;
|
||||
const path = row.dataset.target!;
|
||||
await user.click(row);
|
||||
expect(screen.getAllByRole("tab").length).toBeGreaterThan(0);
|
||||
expect(filesShown(container)).toBe(true);
|
||||
expect(container.querySelector<HTMLElement>(`[data-action-click="workbench.file"][data-target="${path}"]`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("lets the next file be picked from the same tree without reopening it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const port = new MockPort();
|
||||
vi.spyOn(port, "workspaceFiles").mockResolvedValue({ files: ["a.md", "b.md"], directories: [] });
|
||||
const { container } = mount(port);
|
||||
await openTree(container, user);
|
||||
await user.click(container.querySelector<HTMLElement>('[data-target="a.md"]')!);
|
||||
await user.click(container.querySelector<HTMLElement>('[data-target="b.md"]')!);
|
||||
expect(filesShown(container)).toBe(true);
|
||||
expect(screen.getAllByRole("tab")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("leaves the tree where the user put it when its file is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = mount();
|
||||
await openTree(container, user);
|
||||
await user.click(container.querySelector<HTMLElement>('[data-action-click="workbench.file"]')!);
|
||||
await user.click(screen.getAllByRole("tab")[0].querySelector(".workbench-tab-close")!);
|
||||
expect(filesShown(container)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not bring the tree back after the user folded it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = mount();
|
||||
await openTree(container, user);
|
||||
await user.click(container.querySelector<HTMLElement>('[data-action-click="workbench.file"]')!);
|
||||
await user.click(container.querySelector('[data-action="workbench.files"]')!);
|
||||
expect(filesShown(container)).toBe(false);
|
||||
await user.click(screen.getAllByRole("tab")[0].querySelector(".workbench-tab-close")!);
|
||||
expect(filesShown(container)).toBe(false);
|
||||
});
|
||||
@@ -38,7 +38,7 @@ Boot 效果测试覆盖关闭时缺席、启用时精确的 schema、读取拒
|
||||
|
||||
| Tool | Read-only | Description |
|
||||
| --- | --- | --- |
|
||||
| `bash` | false | Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors. |
|
||||
| `bash` | false | Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors. To search, call tool:grep (contents) or tool:glob (names) through use_capability — not grep, rg or find in the shell; the built-ins honor .gitignore and cap their output. A pipeline over another command's output (git log \| grep) is the exception. |
|
||||
| `bash_output` | true | Read new output from a background job started with bash(run_in_background=true) or task(run_in_background=true). Returns the output produced since the last bash_output call for that job, plus its status (running/done/failed/killed). Does not block. Omit job_id to list the jobs still running, with their kind, label and elapsed time. |
|
||||
| `browser_act` | false | Operate a page with real input. Ordered steps stop at the first failure; target by ref, or screenshot x/y. Returns results, page errors and changes. fill replaces, type inserts; press takes keys like Enter or Control+a; select sets a <select>; scroll takes delta_y or delta_x; drag ends at to_ref or to_x/to_y; upload gives a file input workspace files; dialog answers prompts; eval executes arbitrary page JavaScript from script and awaits promises; resize sets the viewport to width×height; back, forward and reload move this tab. |
|
||||
| `browser_open` | false | Load a page and return its snapshot, one element per line with [eN] refs. url: http(s), about:blank or a workspace file; no url switches to tab; close closes. The user sees tabs: web_fetch reads static text, close tabs when done. |
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/safety/sandbox"
|
||||
)
|
||||
@@ -60,7 +61,7 @@ func TestProjectEnvCannotRedirectAuthorityEndpoints(t *testing.T) {
|
||||
host := shortSocket(t, "HOST-DAEMON")
|
||||
attacker := shortSocket(t, "ATTACKER")
|
||||
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
t.Setenv("HOME", filepath.Join(root, "home"))
|
||||
t.Setenv("USERPROFILE", filepath.Join(root, "home"))
|
||||
t.Setenv("DOCKER_HOST", "unix://"+host)
|
||||
@@ -152,7 +153,7 @@ while time.time() < deadline:
|
||||
t.Fatalf("the descriptor scan cannot detect an inherited socket, so the check below is vacuous: %q", out)
|
||||
}
|
||||
|
||||
spec := sandbox.Spec{Mode: "enforce", WriteRoots: []string{t.TempDir()}, Network: true}
|
||||
spec := sandbox.Spec{Mode: "enforce", WriteRoots: []string{testenv.TempDir(t)}, Network: true}
|
||||
argv, wrapped := sandbox.CommandArgs(spec, []string{"python3", "-c", scan})
|
||||
if !wrapped {
|
||||
t.Fatal("spec did not wrap")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/agentcomposition"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/state/agentpreset"
|
||||
@@ -15,7 +16,7 @@ func TestCompositionRetainsLinkedSkillAcrossRestart(t *testing.T) {
|
||||
for _, layout := range []string{"directory", "file"} {
|
||||
t.Run(layout, func(t *testing.T) {
|
||||
home, root := compositionTestWorkspace(t, "openai")
|
||||
source := t.TempDir()
|
||||
source := testenv.TempDir(t)
|
||||
writeFile(t, source, "SKILL.md", "---\nname: linked\ndescription: linked skill\n---\nORIGINAL SKILL")
|
||||
writeFile(t, source, "references/guide.md", "ORIGINAL REFERENCE")
|
||||
target, link := source, filepath.Join(home, "skills", "linked")
|
||||
@@ -83,7 +84,7 @@ func TestCompositionRetainsLinkedSkillAcrossRestart(t *testing.T) {
|
||||
|
||||
func TestCompositionLinkedSkillRejectsLinkedResources(t *testing.T) {
|
||||
home, root := compositionTestWorkspace(t, "openai")
|
||||
source, outside := t.TempDir(), t.TempDir()
|
||||
source, outside := testenv.TempDir(t), testenv.TempDir(t)
|
||||
writeFile(t, source, "SKILL.md", "---\nname: linked\ndescription: linked skill\n---\nSKILL")
|
||||
writeFile(t, outside, "private.txt", "must not be retained")
|
||||
if err := os.Symlink(filepath.Join(outside, "private.txt"), filepath.Join(source, "reference.md")); err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/agentcomposition"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/ext/plugin"
|
||||
@@ -21,7 +22,7 @@ func TestLiveCompositionDoesNotCaptureUnusedSkillAssets(t *testing.T) {
|
||||
home, root := compositionTestWorkspace(t, "openai")
|
||||
writeFile(t, home, "skills/templates/SKILL.md", "---\nname: templates\ndescription: template library\n---\nRead templates when needed.")
|
||||
// An unused link cannot participate in assembling a capability grant.
|
||||
if err := os.Symlink(t.TempDir(), filepath.Join(home, "skills/templates/unused")); err != nil {
|
||||
if err := os.Symlink(testenv.TempDir(t), filepath.Join(home, "skills/templates/unused")); err != nil {
|
||||
t.Skipf("symlink unavailable: %v", err)
|
||||
}
|
||||
d := agentcomposition.Builtins()[0]
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/secrets"
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/safety/sandbox"
|
||||
)
|
||||
@@ -26,11 +27,11 @@ const probeSecret = "REASONIX-PROBE-PRIVATE-KEY-a7f3c1"
|
||||
// fakeHome plants files under a temporary HOME, returning the write root that
|
||||
// must be bound for them to be visible inside the sandbox, a workspace under
|
||||
// it, and the forbid-read roots. The HOME has to sit under the bound root:
|
||||
// bubblewrap swaps /tmp for a fresh tmpfs, so a t.TempDir() home is absent
|
||||
// bubblewrap swaps /tmp for a fresh tmpfs, so a testenv.TempDir(t) home is absent
|
||||
// inside the sandbox and every deny assertion would pass without denying.
|
||||
func fakeHome(t *testing.T, files map[string]string) (bound, ws string, forbid []string) {
|
||||
t.Helper()
|
||||
bound = t.TempDir()
|
||||
bound = testenv.TempDir(t)
|
||||
home := filepath.Join(bound, "home")
|
||||
ws = filepath.Join(bound, "ws")
|
||||
if err := os.MkdirAll(ws, 0o700); err != nil {
|
||||
@@ -222,7 +223,7 @@ func TestOrdinaryDevWorkIsUnaffected(t *testing.T) {
|
||||
// invocation fails at startup, and npm silently relocates its global prefix —
|
||||
// so they are the broker's problem, not the denylist's.
|
||||
func TestMixedConfigCredentialFilesStayReadable(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
for _, rel := range []string{".config/gh/hosts.yml", ".npmrc", ".docker/config.json"} {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/pricing"
|
||||
"reasonix/internal/contract/provider"
|
||||
@@ -20,7 +21,7 @@ import (
|
||||
func TestEffectAnthropicPartialUsageRecovery(t *testing.T) {
|
||||
for _, mode := range []string{"upstream_error", "premature_eof", "complete_control"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
writeFile(t, root, "fixture.txt", "partial usage fixture")
|
||||
partial := mode != "complete_control"
|
||||
var calls, reads atomic.Int32
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/model/billing"
|
||||
@@ -26,7 +27,7 @@ func buildWithWallet(t *testing.T, balanceFormat string) (*atomic.Int64, billing
|
||||
t.Cleanup(wallet.Close)
|
||||
|
||||
home := statedBootHome(t)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
kind := "boot-wallet-" + strings.ReplaceAll(t.Name(), "/", "-") + "-" + balanceFormat + "-fmt"
|
||||
provider.Register(kind, func(provider.Config) (provider.Provider, error) { return &peerRecorder{}, nil })
|
||||
format := ""
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/session/control"
|
||||
)
|
||||
@@ -29,7 +30,7 @@ func (s *budgetCompletenessSink) RecordRunBudget(sample event.RunBudgetSample) {
|
||||
func TestEffectBudgetCompletenessFromOpenAIStream(t *testing.T) {
|
||||
for _, missing := range []int32{0, 1, 2} {
|
||||
t.Run(fmt.Sprintf("missing_%d", missing), func(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
writeFile(t, root, "fixture.txt", "fixture content")
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/ext/hook"
|
||||
"reasonix/internal/session/control"
|
||||
@@ -40,7 +41,7 @@ func TestEffectCancelledCheckpointAdmissionStopsUserCode(t *testing.T) {
|
||||
scenario = name + "/capability"
|
||||
}
|
||||
t.Run(scenario, func(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
writeFile(t, root, "note.txt", "user bytes")
|
||||
marker := filepath.Join(root, "hook.txt")
|
||||
args := `{"path":"note.txt"}`
|
||||
|
||||
@@ -3,6 +3,7 @@ package boot
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/platform/computer"
|
||||
"strings"
|
||||
@@ -16,7 +17,7 @@ func TestEffectComputerLaunchKeepsItsApprovalSeparateFromCanonicalRead(t *testin
|
||||
}
|
||||
t.Setenv(fakeComputerHelperEnv, "1")
|
||||
t.Setenv("REASONIX_BOOT_CUA_INSTALLED_APP", exe)
|
||||
log := filepath.Join(t.TempDir(), "requests.jsonl")
|
||||
log := filepath.Join(testenv.TempDir(t), "requests.jsonl")
|
||||
t.Setenv("REASONIX_BOOT_CUA_REQUEST_LOG", log)
|
||||
driver := computer.NewDriver(exe, os.Environ())
|
||||
t.Cleanup(driver.Close)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/platform/computer"
|
||||
"reasonix/internal/safety/evidence"
|
||||
@@ -93,7 +94,7 @@ func TestEffectComputerUseThroughTheRealAssembly(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(fakeComputerHelperEnv, "1")
|
||||
log := filepath.Join(t.TempDir(), "native-requests.jsonl")
|
||||
log := filepath.Join(testenv.TempDir(t), "native-requests.jsonl")
|
||||
t.Setenv("REASONIX_BOOT_CUA_REQUEST_LOG", log)
|
||||
driver := computer.NewDriver(exe, os.Environ())
|
||||
t.Cleanup(driver.Close)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/pricing"
|
||||
"reasonix/internal/session/control"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
func TestEffectEstimatedUsageQuoteAfterStreamRecovery(t *testing.T) {
|
||||
for _, interrupted := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "reported_control", true: "interrupted"}[interrupted], func(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
writeFile(t, root, "fixture.txt", "fixture content")
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/session/control"
|
||||
)
|
||||
|
||||
func TestEffectGoalResumeRenewsPlannerBudget(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
for n := 1; n <= 4; n++ {
|
||||
writeFile(t, root, fmt.Sprintf("fixture-%d.txt", n), fmt.Sprintf("planner resume fixture %d", n))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/ext/hook"
|
||||
@@ -26,7 +27,7 @@ func TestEffectReadHooksLeaveCheckpointCoverage(t *testing.T) {
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
home := statedBootHome(t)
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
marker := filepath.Join(root, "hook.txt")
|
||||
writeFile(t, root, "note.txt", "original")
|
||||
kind := "hook-checkpoint-" + strings.ReplaceAll(t.Name(), "/", "-")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/ext/hook"
|
||||
@@ -73,7 +74,7 @@ func TestEffectHookWritesInvalidateWorkspaceWithoutExecutingDeniedTool(t *testin
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
home := statedBootHome(t)
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
marker := filepath.Join(root, "hook-side.txt")
|
||||
writeFile(t, root, "note.txt", "original")
|
||||
kind := "hook-workspace-" + strings.ReplaceAll(t.Name(), "/", "-")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/session/control"
|
||||
@@ -33,7 +34,7 @@ func TestEffectBackgroundJobNoLaunchFinalizesResources(t *testing.T) {
|
||||
mode = "capability"
|
||||
}
|
||||
t.Run(name+"/"+mode, func(t *testing.T) {
|
||||
home, root := statedBootHome(t), t.TempDir()
|
||||
home, root := statedBootHome(t), testenv.TempDir(t)
|
||||
child := &noLaunchProvider{}
|
||||
var ctrl *control.Controller
|
||||
parent := &browserScriptProvider{rounds: []func(string) *provider.ToolCall{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/session/control"
|
||||
@@ -107,7 +108,7 @@ func TestEffectAModQueuesAPromptThatReachesTheModelLabelledAsTheMods(t *testing.
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
res, err := BuildRuntime(t.Context(), Options{Sink: sink, SessionDir: t.TempDir()})
|
||||
res, err := BuildRuntime(t.Context(), Options{Sink: sink, SessionDir: testenv.TempDir(t)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/state/peermail"
|
||||
)
|
||||
|
||||
@@ -74,7 +75,7 @@ func TestEffectAReleasedPeerMessageStartsATurnOnAnIdleSession(t *testing.T) {
|
||||
// nothing into the prompt and does not hold the queue.
|
||||
func TestEffectPeerMessageTextIsNeverReadForFileReferences(t *testing.T) {
|
||||
rec, peersDir, ctrl, held := buildPeerSession(t, true)
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("REASONIX_HOME", filepath.Dir(peersDir))
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/runtime/agent/testutil"
|
||||
@@ -37,7 +38,7 @@ func slowShellBuild(t *testing.T, kind string, tune func(*sandbox.ShellDiscovery
|
||||
|
||||
func fakeBashOnPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
bin := t.TempDir()
|
||||
bin := testenv.TempDir(t)
|
||||
name := "bash"
|
||||
if runtime.GOOS == "windows" {
|
||||
name = "bash.exe"
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
)
|
||||
@@ -16,7 +17,7 @@ var tariffKinds atomic.Int64
|
||||
func tariffOf(t *testing.T, baseURL string, price string) bool {
|
||||
t.Helper()
|
||||
home := statedBootHome(t)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
kind := fmt.Sprintf("boot-tariff-%s-%d", strings.ReplaceAll(t.Name(), "/", "-"), tariffKinds.Add(1))
|
||||
provider.Register(kind, func(provider.Config) (provider.Provider, error) { return &peerRecorder{}, nil })
|
||||
body := `
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/ext/extension"
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
// notice naming the extension, the point and the reason — and it does so once.
|
||||
func TestMissingSidecarReachesTheSinkAsATypedNotice(t *testing.T) {
|
||||
sink := ¬iceSink{}
|
||||
stage, err := startExtensions(context.Background(), Options{}, config.RootsForHome(t.TempDir()), t.TempDir(), nil, sink)
|
||||
stage, err := startExtensions(context.Background(), Options{}, config.RootsForHome(testenv.TempDir(t)), testenv.TempDir(t), nil, sink)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -48,7 +49,7 @@ func TestMissingSidecarReachesTheSinkAsATypedNotice(t *testing.T) {
|
||||
// sidecar missing right after an extension was installed still reaches the sink.
|
||||
func TestRebuiltDispatcherKeepsTheMissingSidecarNotice(t *testing.T) {
|
||||
sink := ¬iceSink{}
|
||||
stage, err := startExtensions(context.Background(), Options{}, config.RootsForHome(t.TempDir()), t.TempDir(), nil, sink)
|
||||
stage, err := startExtensions(context.Background(), Options{}, config.RootsForHome(testenv.TempDir(t)), testenv.TempDir(t), nil, sink)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/ablation"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/tool"
|
||||
@@ -26,7 +27,7 @@ func TestRejectedMCPCacheDiscoversWithoutPinning(t *testing.T) {
|
||||
{"always_after_handshake", true, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("REASONIX_CACHE_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_CACHE_HOME", testenv.TempDir(t))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
listed, release := make(chan struct{}), make(chan struct{})
|
||||
|
||||
@@ -10,14 +10,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/ext/skill"
|
||||
)
|
||||
|
||||
func TestSkillDiagnosticsReachRendererAcrossRediscovery(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
home := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
var raw bytes.Buffer
|
||||
var notices []event.Event
|
||||
@@ -72,8 +73,8 @@ func TestSkillDiagnosticDestinationsAtStartup(t *testing.T) {
|
||||
for _, interactive := range []bool{false, true} {
|
||||
for _, suppressed := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("renderer=%t/suppressed=%t", interactive, suppressed), func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
root := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
dir := filepath.Join(root, ".reasonix", "skills", "startup-warning")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/session/control"
|
||||
@@ -49,7 +50,7 @@ model = "x"
|
||||
// a read that succeeded counts; a read that failed, and a file outside the
|
||||
// workspace, do not.
|
||||
func TestEffectCompletedReadsFeedTheSkillPathSet(t *testing.T) {
|
||||
outside := filepath.Join(t.TempDir(), "elsewhere.go")
|
||||
outside := filepath.Join(testenv.TempDir(t), "elsewhere.go")
|
||||
if err := os.WriteFile(outside, []byte("package x\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -161,7 +162,7 @@ func TestEffectGrepRecordsOnlyAFileItWasPointedAt(t *testing.T) {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
defer fresh.Close()
|
||||
if err := fresh.Resume(saved, filepath.Join(t.TempDir(), "resumed.jsonl")); err != nil {
|
||||
if err := fresh.Resume(saved, filepath.Join(testenv.TempDir(t), "resumed.jsonl")); err != nil {
|
||||
t.Fatalf("Resume: %v", err)
|
||||
}
|
||||
if got := fresh.TouchedPaths(); !reflect.DeepEqual(got, want) {
|
||||
@@ -191,7 +192,7 @@ func TestEffectResumeResolvesSymlinksAgainstTheCurrentDisk(t *testing.T) {
|
||||
for _, m := range ctrl.History() {
|
||||
saved.Add(m)
|
||||
}
|
||||
elsewhere := t.TempDir()
|
||||
elsewhere := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(elsewhere, "a.go"), []byte("package x\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"SystemHash": "bc08dbc97c837bba",
|
||||
"ToolsHash": "8e997d80438fe9b9",
|
||||
"PrefixHash": "1c79eb3c43c306ba",
|
||||
"ToolsHash": "eb9f57582878fc4c",
|
||||
"PrefixHash": "24d8ccfe47fd333d",
|
||||
"LogRewriteVersion": 0,
|
||||
"ToolSchemaTokens": 4997
|
||||
"ToolSchemaTokens": 5059
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
{
|
||||
"name": "bash",
|
||||
"description": "Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors.",
|
||||
"description": "Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors. To search, call tool:grep (contents) or tool:glob (names) through use_capability — not grep, rg or find in the shell; the built-ins honor .gitignore and cap their output. A pipeline over another command's output (git log | grep) is the exception.",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"command": {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
{
|
||||
"Name": "bash",
|
||||
"Description": "Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors.",
|
||||
"Description": "Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. To read or modify a file use read_file / edit_file / write_file — never Get-Content, cat, sed, head/tail or a shell redirect. read_file returns up to 2000 lines per call, each prefixed with its line number, and those numbers are what edit_file targets; paging a file through the shell instead costs one round trip per chunk and yields no line anchors. To search, call tool:grep (contents) or tool:glob (names) through use_capability — not grep, rg or find in the shell; the built-ins honor .gitignore and cap their output. A pipeline over another command's output (git log | grep) is the exception.",
|
||||
"ReadOnly": false,
|
||||
"Schema": {
|
||||
"properties": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/ext/hook"
|
||||
"reasonix/internal/safety/sandbox"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
// checkout's [tools.shell] must never reach the resolver or the runtime.
|
||||
func TestUserHookRunnerIgnoresCheckoutShell(t *testing.T) {
|
||||
isolateConfigHome(t)
|
||||
checkout := t.TempDir()
|
||||
checkout := testenv.TempDir(t)
|
||||
// An installed-looking path: one where sandboxed commands write is refused
|
||||
// before it could reach the merged config this test starts from.
|
||||
evil := filepath.Join(filepath.VolumeName(os.TempDir())+string(filepath.Separator), "opt", "reasonix-test", "evil-bash")
|
||||
@@ -65,7 +66,7 @@ func TestUserHookRunnerIgnoresCheckoutShell(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("user shell is honoured", func(t *testing.T) {
|
||||
userShell := filepath.Join(t.TempDir(), "user-bash")
|
||||
userShell := filepath.Join(testenv.TempDir(t), "user-bash")
|
||||
userConfig := config.UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(userConfig), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -85,7 +86,7 @@ func TestUserHookRunnerIgnoresCheckoutShell(t *testing.T) {
|
||||
// python.exe the branch ships before the user's own interpreter.
|
||||
func TestUserHookRunnerDisablesCwdExeSearch(t *testing.T) {
|
||||
isolateConfigHome(t)
|
||||
checkout := t.TempDir()
|
||||
checkout := testenv.TempDir(t)
|
||||
if err := hook.Save(hook.ScopeGlobal, "", hook.Settings{Hooks: map[hook.Event][]hook.HookConfig{
|
||||
hook.PreToolUse: {{Match: "read_file", Command: `python C:\hooks\guard.py`}},
|
||||
}}); err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/event"
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ func TestWorkspaceBranchSummaryReadsOverlapThroughBuild(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("git wrapper uses POSIX shell")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
realGit, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
t.Skip("git unavailable")
|
||||
@@ -33,7 +34,7 @@ func TestWorkspaceBranchSummaryReadsOverlapThroughBuild(t *testing.T) {
|
||||
t.Fatalf("git %v: %v %s", args, err, out)
|
||||
}
|
||||
}
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
t.Setenv("REASONIX_STATE_HOME", home)
|
||||
ctrl, err := Build(t.Context(), Options{Home: home, WorkspaceRoot: dir, Sink: event.Discard, Stderr: io.Discard})
|
||||
@@ -41,7 +42,7 @@ func TestWorkspaceBranchSummaryReadsOverlapThroughBuild(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ctrl.Close()
|
||||
bin, signals := t.TempDir(), t.TempDir()
|
||||
bin, signals := testenv.TempDir(t), testenv.TempDir(t)
|
||||
wrapper := `#!/bin/sh
|
||||
probe=
|
||||
for arg in "$@"; do
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/platform/gitcmd"
|
||||
)
|
||||
|
||||
// A folder that carries a .git git will not open keeps an empty identity, and
|
||||
// says that git refused it rather than that there is no repository.
|
||||
func TestWorkspaceRepoSaysWhyAMarkedFolderHasNoIdentity(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(dir, ".git"), []byte("gitdir: ./nowhere\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -20,7 +21,7 @@ func TestWorkspaceRepoSaysWhyAMarkedFolderHasNoIdentity(t *testing.T) {
|
||||
if repo.Valid() || repo.Absent != gitcmd.AbsenceRefused || repo.AbsentDetail == "" {
|
||||
t.Fatalf("repo = %+v, want an empty identity that says git refused it", repo)
|
||||
}
|
||||
plain := workspaceRepo(context.Background(), gitcmd.Repo{}, t.TempDir())
|
||||
plain := workspaceRepo(context.Background(), gitcmd.Repo{}, testenv.TempDir(t))
|
||||
if plain.Valid() || plain.Absent != "" {
|
||||
t.Fatalf("a folder with no marker = %+v, want an empty identity with no absence", plain)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package fileutil
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestUnderExcludesTheRootItself(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
if Under(root, root) {
|
||||
t.Error("Under(root, root) = true; the root is not below itself")
|
||||
}
|
||||
@@ -16,7 +18,7 @@ func TestUnderExcludesTheRootItself(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAtOrUnderIncludesTheRootItself(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
for _, path := range []string{root, filepath.Join(root, "child"), filepath.Join(root, "a", "b")} {
|
||||
if !AtOrUnder(path, root) {
|
||||
t.Errorf("AtOrUnder(%q, root) = false", path)
|
||||
@@ -25,7 +27,7 @@ func TestAtOrUnderIncludesTheRootItself(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContainmentRejectsEscapes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
outside := filepath.Join(filepath.Dir(root), "sibling")
|
||||
for _, path := range []string{outside, filepath.Join(root, "..", "sibling"), filepath.Dir(root)} {
|
||||
if AtOrUnder(path, root) {
|
||||
@@ -40,7 +42,7 @@ func TestContainmentRejectsEscapes(t *testing.T) {
|
||||
// An interior ".." must be resolved before the comparison, or a path spells
|
||||
// its way inside a root it never reaches.
|
||||
func TestContainmentResolvesInteriorDotDot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
if !AtOrUnder(filepath.Join(root, "a", "..", "b"), root) {
|
||||
t.Error("a path that cleans to root/b is inside root")
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func TestContainmentResolvesInteriorDotDot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContainmentRejectsEmptySides(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
if AtOrUnder("", root) || AtOrUnder(root, "") || Under("", root) || Under(root, "") {
|
||||
t.Error("an empty side answers no question and is not containment")
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestContentVersionTracksBytesKindAndAbsence(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "file")
|
||||
path := filepath.Join(testenv.TempDir(t), "file")
|
||||
absent, err := ReadContentVersion(t.Context(), path)
|
||||
if err != nil || absent.Kind != "absent" {
|
||||
t.Fatalf("absent: %+v %v", absent, err)
|
||||
@@ -43,13 +45,13 @@ func TestContentVersionTracksBytesKindAndAbsence(t *testing.T) {
|
||||
func TestContentVersionCancellationPrecedesFilesystemAccess(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
if _, err := ReadContentVersion(ctx, filepath.Join(t.TempDir(), "missing")); !errors.Is(err, context.Canceled) {
|
||||
if _, err := ReadContentVersion(ctx, filepath.Join(testenv.TempDir(t), "missing")); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("cancelled read: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentVersionHashesSymlinkTextWithoutReadingTarget(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "link")
|
||||
path := filepath.Join(testenv.TempDir(t), "link")
|
||||
if err := os.Symlink("missing-target", path); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// A workspace opened at a drive root was listed as "\": filepath.Base answers
|
||||
@@ -25,7 +27,7 @@ func TestRootNameOfAVolumeIsTheVolume(t *testing.T) {
|
||||
|
||||
// An ordinary directory keeps the name it already had.
|
||||
func TestRootNameOfADirectoryIsItsBase(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if got, want := RootName(dir), filepath.Base(dir); got != want {
|
||||
t.Fatalf("RootName(%q) = %q, want %q", dir, got, want)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestMarkUntrackedKeepsAnExistingMarker(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
marker := filepath.Join(dir, ".gitignore")
|
||||
if err := os.WriteFile(marker, []byte("keep.me\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -22,7 +24,7 @@ func TestMarkUntrackedKeepsAnExistingMarker(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarkUntrackedIsIdempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
for range 2 {
|
||||
if err := MarkUntracked(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -34,7 +36,7 @@ func TestMarkUntrackedIsIdempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarkUntrackedLeavesNothingWhenTheWriteFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
orig := writeMarker
|
||||
writeMarker = func(*os.File) error { return errors.New("disk full") }
|
||||
err := MarkUntracked(dir)
|
||||
@@ -54,7 +56,7 @@ func TestMarkUntrackedLeavesNothingWhenTheWriteFails(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarkUntrackedDoesNotFollowASymlinkedMarker(t *testing.T) {
|
||||
dir, outside := t.TempDir(), t.TempDir()
|
||||
dir, outside := testenv.TempDir(t), testenv.TempDir(t)
|
||||
target := filepath.Join(outside, "victim")
|
||||
if err := os.Symlink(target, filepath.Join(dir, ".gitignore")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// A launcher (cmd.exe) that spawns a long-lived grandchild (ping) which inherits
|
||||
@@ -99,7 +101,7 @@ func TestStartTrackedResumesSuspendedChild(t *testing.T) {
|
||||
// wedged system tool (a busy WMI, security software holding the call) behaves.
|
||||
func slowTaskkill(t *testing.T) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
src := filepath.Join(dir, "main.go")
|
||||
if err := os.WriteFile(src, []byte("package main\nimport \"time\"\nfunc main() { time.Sleep(10 * time.Minute) }\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestCommandNamesFollowsPATHEXTOnlyOnWindows(t *testing.T) {
|
||||
@@ -23,7 +25,7 @@ func TestCommandNamesFollowsPATHEXTOnlyOnWindows(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLookPathInSkipsRelativeEntriesAndFindsTheSibling(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(dir, "mcp.cmd"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
const publishedJobMarker = "REASONIX_PUBLISHED_JOB_TASKKILL_MARKER"
|
||||
@@ -51,7 +53,7 @@ func TestPublishedJobRecorderAlias(t *testing.T) {
|
||||
|
||||
func publishedJobStub(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func assertRootWaitClosed(t *testing.T, done <-chan struct{}) {
|
||||
@@ -177,7 +179,7 @@ func TestRootWaitDoesNotRegisterBeforeStartOrForUntrackedCommands(t *testing.T)
|
||||
cancel()
|
||||
}
|
||||
if scenario == "missing_executable" {
|
||||
cmd.Path = t.TempDir() + string(os.PathSeparator) + "missing"
|
||||
cmd.Path = testenv.TempDir(t) + string(os.PathSeparator) + "missing"
|
||||
}
|
||||
tracked, err := RunCommand(ctx, cmd, RunOptions{Track: scenario != "untracked"})
|
||||
if scenario != "untracked" && err == nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
const holderEnv = "REASONIX_SCRATCH_HOLDER"
|
||||
@@ -33,7 +35,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func isolatedTemp(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
t.Setenv("TMPDIR", root)
|
||||
t.Setenv("TMP", root)
|
||||
t.Setenv("TEMP", root)
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func credHome(t *testing.T, files ...string) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
for _, rel := range files {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
const sentinel = "late-write.txt"
|
||||
@@ -17,7 +19,7 @@ const sentinel = "late-write.txt"
|
||||
// raced, because a producer driven by a sleep would sometimes lose and make
|
||||
// this test the flaky one.
|
||||
func TestContentThatOutlivesTheDeadlineIsReported(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(dir, sentinel), []byte("still running"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -34,7 +36,7 @@ func TestContentThatOutlivesTheDeadlineIsReported(t *testing.T) {
|
||||
// And the behaviour that motivated the wait: a refusal that stops is not a
|
||||
// leak. Reporting the first frame of one is what made the gate flap.
|
||||
func TestARefusalThatStopsLetsTheDirectoryQuiesce(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "settling")
|
||||
dir := filepath.Join(testenv.TempDir(t), "settling")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -58,7 +60,7 @@ func TestARefusalThatStopsLetsTheDirectoryQuiesce(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnOrdinaryTreeIsRemovedWithoutWaiting(t *testing.T) {
|
||||
root := filepath.Join(t.TempDir(), "root")
|
||||
root := filepath.Join(testenv.TempDir(t), "root")
|
||||
if err := os.MkdirAll(filepath.Join(root, "nested"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -75,7 +77,7 @@ func TestAnOrdinaryTreeIsRemovedWithoutWaiting(t *testing.T) {
|
||||
|
||||
// A directory already gone is not a failure: the test may have removed it.
|
||||
func TestARemovalOfSomethingAlreadyGoneIsNotReported(t *testing.T) {
|
||||
if err := removeDir(filepath.Join(t.TempDir(), "never-existed")); err != nil {
|
||||
if err := removeDir(filepath.Join(testenv.TempDir(t), "never-existed")); err != nil {
|
||||
t.Fatalf("removeDir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestPreviewReadsBytesAndRefusesEscape(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
data := []byte("%PDF-1.7\n\x00binary")
|
||||
if err := os.WriteFile(filepath.Join(root, "报告.pdf"), data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -23,12 +25,12 @@ func TestPreviewReadsBytesAndRefusesEscape(t *testing.T) {
|
||||
if err != nil || string(got) != string(data) {
|
||||
t.Fatalf("read = %q, %v", got, err)
|
||||
}
|
||||
for _, ref := range []Ref{{Path: "../secret"}, {Root: t.TempDir(), Path: "x"}} {
|
||||
for _, ref := range []Ref{{Path: "../secret"}, {Root: testenv.TempDir(t), Path: "x"}} {
|
||||
if _, err := s.Info(context.Background(), ref); !errors.Is(err, ErrDenied) {
|
||||
t.Fatalf("escape = %v", err)
|
||||
}
|
||||
}
|
||||
outside := t.TempDir()
|
||||
outside := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("secret"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -40,7 +42,7 @@ func TestPreviewReadsBytesAndRefusesEscape(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPreviewLimitsAndUnknownBinary(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
s := Service{Primary: root}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
@@ -64,11 +66,11 @@ func TestPreviewLimitsAndUnknownBinary(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGrantedRootAndCancellation(t *testing.T) {
|
||||
extra := t.TempDir()
|
||||
extra := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(extra, "notes.md"), []byte("# Notes"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := Service{Primary: t.TempDir(), Extra: []string{extra}}
|
||||
s := Service{Primary: testenv.TempDir(t), Extra: []string{extra}}
|
||||
ref, err := s.Resolve(filepath.Join(extra, "notes.md"))
|
||||
if err != nil || ref.Root != extra || ref.Path != "notes.md" {
|
||||
t.Fatalf("resolve = %+v, %v", ref, err)
|
||||
@@ -89,7 +91,7 @@ func TestGrantedRootAndCancellation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOfficeDocumentsPreviewAsBinaryWithinTheirLimit(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
zip := []byte("PK\x03\x04\x00binary office package")
|
||||
s := Service{Primary: root}
|
||||
for _, name := range []string{"报告.docx", "budget.XLSX"} {
|
||||
|
||||
@@ -6,18 +6,20 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func bindingHome(t *testing.T) (*ActivationStore, string) {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", home)
|
||||
t.Setenv("BINDING_FLAG", "")
|
||||
_ = os.Unsetenv("BINDING_FLAG")
|
||||
return DefaultActivationStore(), t.TempDir()
|
||||
return DefaultActivationStore(), testenv.TempDir(t)
|
||||
}
|
||||
|
||||
func writeProjectFiles(t *testing.T, root string, files map[string]string) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// An enable on a repository-declared server is the user's answer for the
|
||||
@@ -76,7 +78,7 @@ func TestEnableHoldsOnlyForTheDeclarationItWasGivenFor(t *testing.T) {
|
||||
// upgrade must not make every enabled project server ask again.
|
||||
func TestEnableSurvivesChangesTheRepositoryDoesNotControl(t *testing.T) {
|
||||
store, root := bindingHome(t)
|
||||
userBin := t.TempDir()
|
||||
userBin := testenv.TempDir(t)
|
||||
writeProjectFiles(t, userBin, map[string]string{"rx-fake-node": "v1"})
|
||||
t.Setenv("PATH", userBin)
|
||||
writeProjectFiles(t, root, map[string]string{
|
||||
@@ -136,7 +138,7 @@ func TestEnableRecordedWithoutADeclarationAsksAgain(t *testing.T) {
|
||||
// depend on where the checkout sits.
|
||||
func TestDeclarationDigestDoesNotDependOnTheCheckoutPath(t *testing.T) {
|
||||
_, first := bindingHome(t)
|
||||
second := t.TempDir()
|
||||
second := testenv.TempDir(t)
|
||||
files := map[string]string{
|
||||
".mcp.json": mcpJSON(t, map[string]any{"command": "./bin/server.sh"}),
|
||||
"bin/server.sh": "#!/bin/sh\n",
|
||||
|
||||
@@ -3,13 +3,15 @@ package config
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func trustStore(t *testing.T) (*ActivationStore, string) {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
return DefaultActivationStore(), t.TempDir()
|
||||
return DefaultActivationStore(), testenv.TempDir(t)
|
||||
}
|
||||
|
||||
// A .mcp.json is checked in, so whoever wrote the repo chose the command. It
|
||||
@@ -88,7 +90,7 @@ func TestApprovalDoesNotTravelToAnotherProject(t *testing.T) {
|
||||
if err := store.SetServerEnabled(entry, root, ActivationProject, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
elsewhere := filepath.Join(t.TempDir(), "other")
|
||||
elsewhere := filepath.Join(testenv.TempDir(t), "other")
|
||||
if enabled, _ := store.IsEnabled(entry, elsewhere); enabled {
|
||||
t.Fatal("an approval in one project started a same-named server in another")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestAttachmentLimitIsHeldAtUserScope(t *testing.T) {
|
||||
@@ -48,7 +50,7 @@ func TestAttachmentLimitUserRenderAndSaveRoundTrip(t *testing.T) {
|
||||
if strings.Contains(RenderTOMLForScope(cfg, RenderScopeProject), "max_stream_bytes") || strings.Contains(RenderTOMLProjectDelta(cfg), "max_stream_bytes") {
|
||||
t.Fatal("user-only limit leaked into project config")
|
||||
}
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
path := filepath.Join(home, "config.toml")
|
||||
if err := os.WriteFile(path, []byte("# user's comment\n[future_feature]\nkeep=42\n"), 0o600); err != nil {
|
||||
@@ -69,7 +71,7 @@ func TestAttachmentLimitUserRenderAndSaveRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAttachmentLimitStrictReadRejectsMalformedUserFile(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
if got, err := LoadAttachmentStreamLimit(); err != nil || got.Bytes != DefaultAttachmentStreamBytes || got.Source != "default" {
|
||||
t.Fatalf("missing user config: %+v %v", got, err)
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func writeClaudeJSON(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(dir, claudeJSONFile), []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -20,7 +22,7 @@ func writeClaudeJSON(t *testing.T, body string) string {
|
||||
// reading only .mcp.json found none of what a machine had configured: three
|
||||
// servers on the reporting user's machine, zero of them in a file Reasonix read.
|
||||
func TestClaudeConfigSuppliesTheServersItsOwnDefaultScopeHolds(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
path := writeClaudeJSON(t, `{
|
||||
"mcpServers": {"shared": {"type": "stdio", "command": "user-node"}},
|
||||
"projects": {
|
||||
@@ -79,13 +81,13 @@ func TestReasonixConfigurationOutranksTheCompatibilityRead(t *testing.T) {
|
||||
// The file belongs to another tool. Reasonix refusing to start over a typo in it
|
||||
// would be a worse answer than starting without it.
|
||||
func TestAnUnreadableClaudeConfigIsNotAnError(t *testing.T) {
|
||||
if got := loadClaudeMCP(filepath.Join(t.TempDir(), "absent.json"), t.TempDir()); got != nil {
|
||||
if got := loadClaudeMCP(filepath.Join(testenv.TempDir(t), "absent.json"), testenv.TempDir(t)); got != nil {
|
||||
t.Fatalf("absent file returned %+v", got)
|
||||
}
|
||||
if got := loadClaudeMCP(writeClaudeJSON(t, `{ not json`), t.TempDir()); got != nil {
|
||||
if got := loadClaudeMCP(writeClaudeJSON(t, `{ not json`), testenv.TempDir(t)); got != nil {
|
||||
t.Fatalf("malformed file returned %+v", got)
|
||||
}
|
||||
if got := loadClaudeMCP("", t.TempDir()); got != nil {
|
||||
if got := loadClaudeMCP("", testenv.TempDir(t)); got != nil {
|
||||
t.Fatalf("empty path returned %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ func TestAnUnreadableClaudeConfigIsNotAnError(t *testing.T) {
|
||||
// Claude Code writes those keys with forward slashes on Windows, where the same
|
||||
// directory is also spelled with backslashes.
|
||||
func TestTheProjectKeyIsComparedAsAPathNotAString(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
path := writeClaudeJSON(t, `{"projects": {"`+filepath.ToSlash(root)+`": {
|
||||
"mcpServers": {"here": {"type": "stdio", "command": "node"}}}}}`)
|
||||
if got := loadClaudeMCP(path, filepath.FromSlash(root)); len(got) != 1 {
|
||||
@@ -107,7 +109,7 @@ func TestTheProjectKeyIsComparedAsAPathNotAString(t *testing.T) {
|
||||
// The effect at the boundary a session actually goes through: a workspace whose
|
||||
// only MCP configuration is Claude's own default scope still comes up with it.
|
||||
func TestLoadingAWorkspaceFindsWhatClaudeConfiguredForIt(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
writeClaudeJSON(t, `{"projects": {"`+filepath.ToSlash(root)+`": {
|
||||
"mcpServers": {"unity-editor-mcp": {"type": "stdio", "command": "node", "args": ["server.js"]}}}}}`)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// The [desktop] table is written by hand, one line per field, so a field added
|
||||
@@ -114,7 +116,7 @@ func TestEveryDesktopSettingSurvivesASave(t *testing.T) {
|
||||
}
|
||||
want := set.Interface()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
path := filepath.Join(testenv.TempDir(t), "config.toml")
|
||||
if err := os.WriteFile(path, []byte(RenderTOMLForScope(cfg, RenderScopeUser)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestDisabledToolsSurviveEveryTOMLRenderScope(t *testing.T) {
|
||||
@@ -38,7 +40,7 @@ func TestDisabledToolsSurviveEveryTOMLRenderScope(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDisabledToolsSurviveMCPJSONEdits(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ".mcp.json")
|
||||
path := filepath.Join(testenv.TempDir(t), ".mcp.json")
|
||||
entry := PluginEntry{Name: "server", Command: "mcp", DisabledTools: []string{"write_file", "delete_file"}}
|
||||
for _, names := range [][]string{entry.DisabledTools, {}} {
|
||||
entry.DisabledTools = names
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func labelOf(e ProviderEntry, model string) string { return e.ModelShort(model) }
|
||||
@@ -51,7 +53,7 @@ func TestHideModelPrefixRoundTripsThroughTheConfigFile(t *testing.T) {
|
||||
if !strings.Contains(out, "hide_model_prefix = true") {
|
||||
t.Fatalf("not rendered:\n%s", out)
|
||||
}
|
||||
path := t.TempDir() + "/config.toml"
|
||||
path := testenv.TempDir(t) + "/config.toml"
|
||||
if err := os.WriteFile(path, []byte(out), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestTheModCeilingsHaveDefaultsAndAZeroIsAnExplicitChoice(t *testing.T) {
|
||||
@@ -33,7 +35,7 @@ func TestAProjectFileCannotRaiseWhatAModMayAdd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSavingTheUserFileKeepsACustomModCeiling(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -557,7 +557,7 @@ func TestAcquireConfigEditLockPathRefusesLockDirOwnedBySomeoneElse(t *testing.T)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("directory ownership is not checked on Windows")
|
||||
}
|
||||
dir := filepath.Join(t.TempDir(), "locks")
|
||||
dir := filepath.Join(testenv.TempDir(t), "locks")
|
||||
if err := os.Mkdir(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -594,7 +594,7 @@ func TestAcquireConfigEditLockPathTightensOwnLockDir(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("POSIX modes are not enforced on Windows")
|
||||
}
|
||||
dir := filepath.Join(t.TempDir(), "locks")
|
||||
dir := filepath.Join(testenv.TempDir(t), "locks")
|
||||
if err := os.Mkdir(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestOTLPExportIsOffUnlessTheUserFileSwitchesItOnAndNamesACollector(t *testing.T) {
|
||||
@@ -43,7 +45,7 @@ func TestAProjectFileCannotTurnOnOrRedirectTraceExport(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSavingTheUserFileKeepsTheTraceExportSettings(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestPeersAndDispatchDefaultOnAndRespectExplicitOptOut(t *testing.T) {
|
||||
@@ -51,7 +53,7 @@ func TestAProjectFileCannotSwitchOffWhatTheUserChose(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSavingTheUserFileKeepsEveryPeersSetting(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -91,7 +93,7 @@ func TestPeersDispatchNeedsPeersAndAProjectFileCannotGrantIt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSavingPeersSettingsWritesThemAndReadsThemBack(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
cfg := LoadForEdit(path)
|
||||
cfg.Peers = PeersConfig{Enabled: true, Dispatch: true, ReleaseProjects: []string{"/work/b"}}
|
||||
@@ -113,7 +115,7 @@ func TestSavingPeersSettingsWritesThemAndReadsThemBack(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSavingAnOptOutDoesNotRestorePeerDefaults(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
for _, values := range [][2]bool{{false, false}, {true, false}, {false, true}} {
|
||||
cfg := Default()
|
||||
@@ -151,7 +153,7 @@ func TestTheDispatchCeilingsDefaultToTheNamedValuesAndOnlyTheUserFileMovesThem(t
|
||||
}
|
||||
|
||||
func TestCeilingsTheUserSetSurviveAnUnrelatedSave(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
path := UserConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func withPackageSource(t *testing.T, src func(home string) []InstalledPackage) {
|
||||
@@ -20,7 +22,7 @@ func TestEnabledPackagesConcurrentCallersShareParses(t *testing.T) {
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
return []InstalledPackage{{Name: "ecc"}}
|
||||
})
|
||||
r := RootsForHome(t.TempDir())
|
||||
r := RootsForHome(testenv.TempDir(t))
|
||||
|
||||
const callers = 16
|
||||
var wg sync.WaitGroup
|
||||
@@ -51,7 +53,7 @@ func TestEnabledPackagesCallerNeverReadsStateOlderThanItsArrival(t *testing.T) {
|
||||
<-release
|
||||
return []InstalledPackage{{Name: "gen", Version: string(rune('0' + seen))}}
|
||||
})
|
||||
r := RootsForHome(t.TempDir())
|
||||
r := RootsForHome(testenv.TempDir(t))
|
||||
|
||||
first := make(chan []InstalledPackage, 1)
|
||||
go func() { first <- r.enabledPackages() }()
|
||||
@@ -81,7 +83,7 @@ func TestEnabledPackagesCallersDoNotShareMutableResults(t *testing.T) {
|
||||
MCPServers: map[string]PackageMCPServer{"a": {Args: []string{"x"}}},
|
||||
}}
|
||||
})
|
||||
r := RootsForHome(t.TempDir())
|
||||
r := RootsForHome(testenv.TempDir(t))
|
||||
|
||||
one := r.enabledPackages()
|
||||
one[0].SkillRoots[0] = "changed"
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// Serve authentication is user-global: a repository or the agent writing
|
||||
// inside the workspace cannot choose the launch token or the auth mode.
|
||||
func TestProjectConfigCannotSetServeAuthentication(t *testing.T) {
|
||||
isolateUserConfigHome(t)
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
body := "[serve]\nauth_mode = \"none\"\ntoken = \"attacker-known\"\nbehind_proxy = true\n"
|
||||
if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestSharePortRange(t *testing.T) {
|
||||
@@ -64,7 +66,7 @@ func TestSharePortSurvivesASaveAndKeepsTheRestOfServe(t *testing.T) {
|
||||
|
||||
func TestProjectConfigCannotPinTheSharePort(t *testing.T) {
|
||||
isolateUserConfigHome(t)
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte("[serve]\nshare_port = 41234\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// A directory another test creates and removes while the guard walks the tree
|
||||
// must not fail the walk, and a join in a stable file is still found.
|
||||
func TestStateRootJoinsSurvivesDirectoriesVanishingMidWalk(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
src := "package x\n\nfunc f() { _ = filepath.Join(userSupportDir(), \"kept\") }\n"
|
||||
if err := os.WriteFile(filepath.Join(root, "kept.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -257,7 +257,7 @@ func TestVetExtraRootComparesFileIdentityNotSpelling(t *testing.T) {
|
||||
})
|
||||
}
|
||||
// A relative link to the same directory through a second hop is also the same file.
|
||||
link := filepath.Join(t.TempDir(), "hop")
|
||||
link := filepath.Join(testenv.TempDir(t), "hop")
|
||||
if os.Symlink(filepath.Join(userHome, ".ssh"), link) == nil {
|
||||
if got := refusalOf(t, cfg, primary, link); got != RootProtected {
|
||||
t.Fatalf("a link to ~/.ssh: %s", got)
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestMissingRootNamesTheFirstGoneDirectoryByItsDisplayPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
gone := filepath.Join(dir, "gone")
|
||||
roots := []Root{
|
||||
{Kind: RootWorkspace, Path: dir, Display: dir},
|
||||
@@ -52,7 +54,7 @@ func TestMissingRootLeavesOtherStatFailuresToTheTool(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMissingRootTreatsAFileWhereADirectoryShouldBeAsMissing(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "f")
|
||||
file := filepath.Join(testenv.TempDir(t), "f")
|
||||
if err := os.WriteFile(file, nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
// The existence hint beside a command asks the disk only about plain absolute
|
||||
// words: a backup from elsewhere must not be able to make this machine probe
|
||||
// arbitrary places by climbing out of one.
|
||||
func TestAbsPathsAsksTheDiskOnlyAboutPlainWords(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
real := filepath.Join(dir, "tool")
|
||||
if err := os.WriteFile(real, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -5,21 +5,22 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func TestCompositionExtensionRuntimeCopiesDoNotShareMutableFiles(t *testing.T) {
|
||||
source := t.TempDir()
|
||||
source := testenv.TempDir(t)
|
||||
if err := os.WriteFile(filepath.Join(source, "entry"), []byte("original"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := pluginpkg.InstalledPackage{}
|
||||
p.Package.Root = source
|
||||
first, err := privateCompositionPackages(t.TempDir(), []pluginpkg.InstalledPackage{p})
|
||||
first, err := privateCompositionPackages(testenv.TempDir(t), []pluginpkg.InstalledPackage{p})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := privateCompositionPackages(t.TempDir(), []pluginpkg.InstalledPackage{p})
|
||||
second, err := privateCompositionPackages(testenv.TempDir(t), []pluginpkg.InstalledPackage{p})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sidecar
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/extension/protocol"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ func TestToolOnlyRuntimeIsDeferredWithoutCopyingPackage(t *testing.T) {
|
||||
Command: "/must-not-start",
|
||||
Tools: []pluginpkg.RuntimeTool{{Name: "echo"}},
|
||||
}
|
||||
m, _, err := StartLivePackages(t.Context(), t.TempDir(), []pluginpkg.InstalledPackage{p}, protocol.SessionContext{}, nil)
|
||||
m, _, err := StartLivePackages(t.Context(), testenv.TempDir(t), []pluginpkg.InstalledPackage{p}, protocol.SessionContext{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestPolicyGuardClassificationAndLiveReplacement(t *testing.T) {
|
||||
@@ -25,7 +27,7 @@ func TestPolicyGuardClassificationAndLiveReplacement(t *testing.T) {
|
||||
runner := NewRunner([]ResolvedHook{
|
||||
{Event: SessionStart, HookConfig: HookConfig{Command: "retained"}},
|
||||
{Event: PreToolUse, HookConfig: HookConfig{Command: "old-guard"}},
|
||||
}, t.TempDir(), spawn, nil)
|
||||
}, testenv.TempDir(t), spawn, nil)
|
||||
role := runner.ForRole("child")
|
||||
runner.ReplacePolicyGuards([]ResolvedHook{
|
||||
{Event: SessionStart, HookConfig: HookConfig{Command: "unselected"}},
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestUnevaluableHookDoesNotChangeOtherHooks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSaveRejectsAnInvalidMatcher(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
err := Save(ScopeGlobal, "", Settings{Hooks: map[Event][]HookConfig{PreToolUse: {{Match: "[", Command: "x"}}}})
|
||||
if !errors.Is(err, ErrInvalidMatcher) {
|
||||
t.Fatalf("Save error = %v, want ErrInvalidMatcher", err)
|
||||
@@ -283,7 +283,7 @@ func TestObservingHookIsOnlyRefusedWhenItsApprovedScriptChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSaveNamesTheHookWithTheInvalidMatcher(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
err := Save(ScopeGlobal, "", Settings{Hooks: map[Event][]HookConfig{PreToolUse: {{Command: "ok"}, {Match: "a(", Command: "x"}}}})
|
||||
if err == nil {
|
||||
t.Fatal("want an error")
|
||||
|
||||
@@ -5,20 +5,22 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestIssue4038MCPManifestInChineseDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
source := filepath.Join(root, "中文目录")
|
||||
writeFile(t, filepath.Join(source, ".mcp.json"), `{"mcpServers":{"local-server":{"command":"/bin/echo","args":["ok"]}}}`)
|
||||
resp := execInstall(t, NewTool(Options{ProjectRoot: root, HomeDir: t.TempDir()}), map[string]any{"source": source, "kind": "mcp"})
|
||||
resp := execInstall(t, NewTool(Options{ProjectRoot: root, HomeDir: testenv.TempDir(t)}), map[string]any{"source": source, "kind": "mcp"})
|
||||
if !resp.OK || len(resp.Actions) != 1 || resp.Actions[0].Name != "local-server" {
|
||||
t.Fatalf("Chinese directory manifest plan: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue4038MCPExecutableInChineseDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
filename, body := "server", "#!/bin/sh\nprintf ok\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
filename, body = "server.cmd", "@echo off\r\necho ok\r\n"
|
||||
@@ -28,7 +30,7 @@ func TestIssue4038MCPExecutableInChineseDirectory(t *testing.T) {
|
||||
if err := os.Chmod(source, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp := execInstall(t, NewTool(Options{ProjectRoot: root, HomeDir: t.TempDir()}), map[string]any{"source": source, "kind": "mcp"})
|
||||
resp := execInstall(t, NewTool(Options{ProjectRoot: root, HomeDir: testenv.TempDir(t)}), map[string]any{"source": source, "kind": "mcp"})
|
||||
if !resp.OK || len(resp.Actions) != 1 || resp.Actions[0].Name != "server" || resp.Actions[0].Command != source {
|
||||
t.Fatalf("Chinese directory executable plan: %+v", resp)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestIssue4038NFCNameResolvesNFDInstalledSkill(t *testing.T) {
|
||||
project := t.TempDir()
|
||||
project := testenv.TempDir(t)
|
||||
target := filepath.Join(project, ".reasonix", "skills", "cafe\u0301", "SKILL.md")
|
||||
writeFile(t, target, "---\ndescription: fixture\n---\nbody")
|
||||
tool := NewTool(Options{ProjectRoot: project, HomeDir: t.TempDir()})
|
||||
tool := NewTool(Options{ProjectRoot: project, HomeDir: testenv.TempDir(t)})
|
||||
resp := execInstall(t, tool, map[string]any{"op": "uninstall", "name": "café", "scope": "project"})
|
||||
if !resp.OK || len(resp.Actions) != 1 || resp.Actions[0].Action != "remove_skill" {
|
||||
t.Fatalf("NFC uninstall did not find NFD skill: %+v", resp)
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func TestRemovingAPluginReclaimsItsSupersededCopies(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
dir := pluginpkg.PluginsDir(home)
|
||||
mk := func(name string) string {
|
||||
p := filepath.Join(dir, name)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/skilllock"
|
||||
)
|
||||
|
||||
@@ -16,8 +17,8 @@ func sourceSkill(t *testing.T, root, body string) string {
|
||||
}
|
||||
|
||||
func TestACopyInstallRecordsItsSourceAndUninstallForgetsIt(t *testing.T) {
|
||||
home, project := t.TempDir(), t.TempDir()
|
||||
src := sourceSkill(t, t.TempDir(), "v1")
|
||||
home, project := testenv.TempDir(t), testenv.TempDir(t)
|
||||
src := sourceSkill(t, testenv.TempDir(t), "v1")
|
||||
tool := NewTool(Options{ProjectRoot: project, HomeDir: home})
|
||||
resp := execInstall(t, tool, map[string]any{"source": src, "kind": "skill", "mode": "copy", "scope": "global", "apply": true})
|
||||
if !resp.OK {
|
||||
@@ -49,8 +50,8 @@ func TestACopyInstallRecordsItsSourceAndUninstallForgetsIt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAProjectInstallIsNotRecorded(t *testing.T) {
|
||||
home, project := t.TempDir(), t.TempDir()
|
||||
src := sourceSkill(t, t.TempDir(), "v1")
|
||||
home, project := testenv.TempDir(t), testenv.TempDir(t)
|
||||
src := sourceSkill(t, testenv.TempDir(t), "v1")
|
||||
tool := NewTool(Options{ProjectRoot: project, HomeDir: home})
|
||||
if resp := execInstall(t, tool, map[string]any{"source": src, "kind": "skill", "mode": "copy", "scope": "project", "apply": true}); !resp.OK {
|
||||
t.Fatalf("project install: %+v", resp)
|
||||
@@ -63,8 +64,8 @@ func TestAProjectInstallIsNotRecorded(t *testing.T) {
|
||||
// A source that is a git checkout, or holds an in-folder link, installs as its
|
||||
// files only; the record must still read it as in step with the copy.
|
||||
func TestACopyOfAGitCheckoutWithALinkReadsAsInStep(t *testing.T) {
|
||||
home, project := t.TempDir(), t.TempDir()
|
||||
src := sourceSkill(t, t.TempDir(), "v1")
|
||||
home, project := testenv.TempDir(t), testenv.TempDir(t)
|
||||
src := sourceSkill(t, testenv.TempDir(t), "v1")
|
||||
writeFile(t, filepath.Join(src, ".git", "HEAD"), "ref: refs/heads/main")
|
||||
writeFile(t, filepath.Join(src, "notes.txt"), "n")
|
||||
if os.Symlink("notes.txt", filepath.Join(src, "alias.txt")) != nil {
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
const listBody = `{"packages":[{"slug":"a/kit","status":"active","name":"kit"}],"limit":24,"offset":0}`
|
||||
@@ -71,7 +73,7 @@ func freezeClock(t *testing.T) *time.Time {
|
||||
|
||||
func TestFirstReadWithNoCacheAndNoRegistryIsUnreachable(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
stub.srv.Close()
|
||||
page, err := c.List(context.Background(), Query{})
|
||||
if !errors.Is(err, ErrUnreachable) || page.Cache != nil {
|
||||
@@ -82,7 +84,7 @@ func TestFirstReadWithNoCacheAndNoRegistryIsUnreachable(t *testing.T) {
|
||||
func TestListFallsBackToTheLastGoodCopyAndSaysSo(t *testing.T) {
|
||||
now := freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
first, err := c.List(context.Background(), Query{})
|
||||
if err != nil || first.Cache != nil {
|
||||
t.Fatalf("first = %+v err = %v", first, err)
|
||||
@@ -115,7 +117,7 @@ func TestServerErrorsAndUnreadableAnswersFallBackToo(t *testing.T) {
|
||||
}
|
||||
okList(w, r)
|
||||
})
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -137,7 +139,7 @@ func TestClientErrorsAreNotMaskedByTheCache(t *testing.T) {
|
||||
}
|
||||
okList(w, r)
|
||||
})
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -158,7 +160,7 @@ func TestNotModifiedServesTheStoredBodyWithoutANote(t *testing.T) {
|
||||
w.Header().Set("Last-Modified", "Mon, 05 Oct 2026 00:00:00 GMT")
|
||||
okList(w, r)
|
||||
})
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -190,7 +192,7 @@ func listQuery(q Query) url.Values {
|
||||
func TestACopyOlderThanTheLimitIsNotServed(t *testing.T) {
|
||||
now := freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -204,7 +206,7 @@ func TestACopyOlderThanTheLimitIsNotServed(t *testing.T) {
|
||||
func TestCorruptFilesAreIgnored(t *testing.T) {
|
||||
freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -234,7 +236,7 @@ func TestQueriesDoNotShareACopy(t *testing.T) {
|
||||
stub := newStub(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"packages":[{"slug":"a/` + r.URL.Query().Get("q") + `","status":"active"}],"limit":24}`))
|
||||
})
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
for _, q := range []string{"alpha", "beta"} {
|
||||
if _, err := c.List(context.Background(), Query{Q: q}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -265,7 +267,7 @@ func TestDetailIsCachedPerPackage(t *testing.T) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/v1/packages/a/")
|
||||
_, _ = w.Write([]byte(`{"package":{"slug":"a/` + name + `","status":"active","latestVersion":"1"},"versions":[{"version":"1","source":"s","content_hash":"sha256:ab"}]}`))
|
||||
})
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
if _, err := c.Detail(context.Background(), "a/one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -288,7 +290,7 @@ func TestAGoneEntryDropsItsCopy(t *testing.T) {
|
||||
}
|
||||
okList(w, r)
|
||||
})
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
_, _ = c.List(context.Background(), Query{})
|
||||
gone.Store(true)
|
||||
@@ -302,7 +304,7 @@ func TestAGoneEntryDropsItsCopy(t *testing.T) {
|
||||
|
||||
func TestACancelledRequestIsNotAnsweredFromTheCache(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
_, _ = c.List(context.Background(), Query{})
|
||||
stub.srv.Close()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -314,7 +316,7 @@ func TestACancelledRequestIsNotAnsweredFromTheCache(t *testing.T) {
|
||||
|
||||
func TestACopySurvivesARestart(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
if _, err := stub.client(dir).List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -329,7 +331,7 @@ func TestOwnAndTokenRequestsNeverTouchTheCache(t *testing.T) {
|
||||
stub := newStub(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"package":{"slug":"a/kit","status":"pending"},"versions":[]}`))
|
||||
})
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
if _, err := c.Owned(context.Background(), "secret-token", "a/kit"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -353,7 +355,7 @@ func TestOwnAndTokenRequestsNeverTouchTheCache(t *testing.T) {
|
||||
|
||||
func TestConcurrentReadersAndWritersAgree(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
c := stub.client(t.TempDir())
|
||||
c := stub.client(testenv.TempDir(t))
|
||||
var wg sync.WaitGroup
|
||||
for i := range 16 {
|
||||
wg.Go(func() {
|
||||
@@ -371,7 +373,7 @@ func TestConcurrentReadersAndWritersAgree(t *testing.T) {
|
||||
func TestTheCacheStaysWithinItsLimits(t *testing.T) {
|
||||
now := freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
c.cache.maxEntries = 3
|
||||
for i, q := range []string{"a", "b", "c", "d", "e"} {
|
||||
@@ -412,7 +414,7 @@ func TestTheCacheStaysWithinItsLimits(t *testing.T) {
|
||||
func TestExpiredFilesArePrunedAndForeignFilesLeftAlone(t *testing.T) {
|
||||
freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
if _, err := c.List(context.Background(), Query{Q: "a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -436,7 +438,7 @@ func TestExpiredFilesArePrunedAndForeignFilesLeftAlone(t *testing.T) {
|
||||
|
||||
func TestFileNamesAreSafeOnEveryPlatform(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
for _, q := range []string{`../../x`, `a:b*c?"<>|`, "日本語 検索", strings.Repeat("长", 300)} {
|
||||
if _, err := c.List(context.Background(), Query{Q: q}); err != nil {
|
||||
@@ -455,7 +457,7 @@ func TestFileNamesAreSafeOnEveryPlatform(t *testing.T) {
|
||||
|
||||
func TestAnUnwritableCacheNeverBreaksARead(t *testing.T) {
|
||||
stub := newStub(t, okList)
|
||||
root := filepath.Join(t.TempDir(), "file")
|
||||
root := filepath.Join(testenv.TempDir(t), "file")
|
||||
if err := os.WriteFile(root, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -478,7 +480,7 @@ func TestNoRootMeansNoCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTheKeyIgnoresParameterOrderAndSeparatesHosts(t *testing.T) {
|
||||
d := newDiskCache(t.TempDir())
|
||||
d := newDiskCache(testenv.TempDir(t))
|
||||
a := url.Values{"limit": {"24"}, "q": {"x"}}
|
||||
b := url.Values{}
|
||||
b.Set("q", "x")
|
||||
@@ -491,7 +493,7 @@ func TestTheKeyIgnoresParameterOrderAndSeparatesHosts(t *testing.T) {
|
||||
func TestAFreshCopyIsServedWithoutTheNetworkAndWithoutALabel(t *testing.T) {
|
||||
now := freezeClock(t)
|
||||
stub := newStub(t, okList)
|
||||
c := stub.freshClient(t.TempDir())
|
||||
c := stub.freshClient(testenv.TempDir(t))
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -513,7 +515,7 @@ func TestPastTheFreshWindowTheRegistryIsAskedWithValidators(t *testing.T) {
|
||||
w.Header().Set("ETag", `W/"a"`)
|
||||
okList(w, r)
|
||||
})
|
||||
c := stub.freshClient(t.TempDir())
|
||||
c := stub.freshClient(testenv.TempDir(t))
|
||||
_, _ = c.List(context.Background(), Query{})
|
||||
*now = now.Add(cacheFreshFor + time.Second)
|
||||
page, err := c.List(context.Background(), Query{})
|
||||
@@ -532,7 +534,7 @@ func TestARepeatedReadSkipsTheFreshWindow(t *testing.T) {
|
||||
}
|
||||
okList(w, r)
|
||||
})
|
||||
c := stub.freshClient(t.TempDir())
|
||||
c := stub.freshClient(testenv.TempDir(t))
|
||||
_, _ = c.List(context.Background(), Query{})
|
||||
down.Store(true)
|
||||
page, err := c.List(WithRefresh(context.Background()), Query{})
|
||||
@@ -549,7 +551,7 @@ func TestAnswersMarkedPrivateOrNoStoreAreNotKept(t *testing.T) {
|
||||
w.Header().Set("Cache-Control", mark.Load().(string))
|
||||
okList(w, r)
|
||||
})
|
||||
dir := t.TempDir()
|
||||
dir := testenv.TempDir(t)
|
||||
c := stub.client(dir)
|
||||
if _, err := c.List(context.Background(), Query{}); err != nil || len(cacheFiles(t, dir)) != 1 {
|
||||
t.Fatalf("a public answer must be kept: %v", err)
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func TestVoteSendsTheTokenAndValueToTheRegistry(t *testing.T) {
|
||||
@@ -117,7 +119,7 @@ func TestReportInstallSendsOnlyTheSlugAndKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInstallIDIsTheMarketsOwnRandomID(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
home := testenv.TempDir(t)
|
||||
id, err := InstallID(home)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -129,7 +131,7 @@ func TestInstallIDIsTheMarketsOwnRandomID(t *testing.T) {
|
||||
if err != nil || again != id {
|
||||
t.Fatalf("second read = %q, %v; want the stored %q", again, err, id)
|
||||
}
|
||||
if other, _ := InstallID(t.TempDir()); other == id {
|
||||
if other, _ := InstallID(testenv.TempDir(t)); other == id {
|
||||
t.Fatal("two homes drew the same id")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(home, "market", "install-id"), []byte("not-an-id\n"), 0o600); err != nil {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func needNode(t *testing.T) string {
|
||||
@@ -74,7 +76,7 @@ func (h *fakeHost) waitStderr(want string) string {
|
||||
func startHost(t *testing.T, module string, cfg map[string]any) *fakeHost {
|
||||
t.Helper()
|
||||
node := needNode(t)
|
||||
script, err := WriteScript(t.TempDir())
|
||||
script, err := WriteScript(testenv.TempDir(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -82,7 +84,7 @@ func startHost(t *testing.T, module string, cfg map[string]any) *fakeHost {
|
||||
cfg = map[string]any{}
|
||||
}
|
||||
if _, ok := cfg["stateDir"]; !ok {
|
||||
cfg["stateDir"] = t.TempDir()
|
||||
cfg["stateDir"] = testenv.TempDir(t)
|
||||
}
|
||||
raw, _ := json.Marshal(cfg)
|
||||
abs, _ := filepath.Abs(module)
|
||||
@@ -176,7 +178,7 @@ func (h *fakeHost) initialize() map[string]any {
|
||||
h.t.Helper()
|
||||
res := h.call("extension/initialize", map[string]any{
|
||||
"protocolVersion": "2", "protocolId": "reasonix.extension.v2", "manifest": map[string]any{},
|
||||
"session": map[string]any{"sessionId": "s1", "workspaceRoot": h.t.TempDir(), "generation": 1},
|
||||
"session": map[string]any{"sessionId": "s1", "workspaceRoot": testenv.TempDir(h.t), "generation": 1},
|
||||
"capabilities": map[string]any{"contentRefs": true, "uiHost": "desktop", "protocolVersion": "2"},
|
||||
}, 20*time.Second)
|
||||
if res["error"] == nil {
|
||||
@@ -199,7 +201,7 @@ func (h *fakeHost) intercept(event string, payload any) map[string]any {
|
||||
|
||||
func writeMod(t *testing.T, name, src string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), name)
|
||||
p := filepath.Join(testenv.TempDir(t), name)
|
||||
if err := os.WriteFile(p, []byte(src), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func packageWith(t *testing.T, hooks string, files map[string]string) pluginpkg.InstalledPackage {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
if hooks != "" {
|
||||
if err := os.MkdirAll(filepath.Join(root, "hooks"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -41,7 +42,7 @@ func TestAPackageWithAModulesListIsAMod(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAModulePathThatLeavesThePackageOrIsNotAFileIsRefused(t *testing.T) {
|
||||
outside := filepath.Join(t.TempDir(), "evil.js")
|
||||
outside := filepath.Join(testenv.TempDir(t), "evil.js")
|
||||
if err := os.WriteFile(outside, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
)
|
||||
|
||||
func fakeProbe(version string) func(context.Context, string, []string) (string, error) {
|
||||
@@ -24,8 +26,8 @@ func TestStudioRunsItsOwnBinaryAsNodeAndChecksTheVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeOnPathIsUsedOnlyWhenAbsoluteOutsideTheProjectAndNewEnough(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
good := filepath.Join(t.TempDir(), "node")
|
||||
root := testenv.TempDir(t)
|
||||
good := filepath.Join(testenv.TempDir(t), "node")
|
||||
inside := filepath.Join(root, "bin", "node")
|
||||
for name, tc := range map[string]struct {
|
||||
path string
|
||||
@@ -59,12 +61,12 @@ func TestNodeOnPathIsUsedOnlyWhenAbsoluteOutsideTheProjectAndNewEnough(t *testin
|
||||
}
|
||||
|
||||
func TestAProjectSymlinkToNodeDoesNotHideIt(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
root := testenv.TempDir(t)
|
||||
real := filepath.Join(root, "node")
|
||||
if err := writeExecutable(real); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(t.TempDir(), "node")
|
||||
link := filepath.Join(testenv.TempDir(t), "node")
|
||||
if err := symlink(real, link); err != nil {
|
||||
t.Skip("no symlinks here:", err)
|
||||
}
|
||||
|
||||
Loaded 100 of 440 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user