Compare commits

..
Author SHA1 Message Date
yhh 7facc49510 test: every test directory is testenv.TempDir, and repolint refuses a bare one
On Windows a removed file's directory entry outlives it until the last handle
closes, so t.TempDir()'s cleanup fails the test as "directory is not empty"
with every file already gone. testenv.TempDir retries, but nothing required it,
and 1145 bare calls in 391 test files had come back; internal/state/sessionstore
failed a different test on most full runs.

Four of its tests ran 150 times each: 41 failures in 600 with t.TempDir(), 0 in
600 with testenv.TempDir(t), and the whole package then passed 6 of 6. A full
go test ./... after the sweep has no cleanup failure; its three failing packages
(jq missing, find.exe on PATH, WSL launcher detection) fail the same at HEAD.

The new temp-dir rule reports a TempDir() call in a test that is not os-qualified.
testenv's own test is exempt, since it cannot import itself.
2026-11-10 02:12:52 -08:00
yhh e19fc12a59 perf(sessionstore): a save digests the transcript and writes its index without per-message copies
Every save re-digests the whole transcript to check it still begins with what
is on disk, and rewrites the display index. The digest marshalled each message
into a fresh slice and the index was written indented, so a save allocated in
proportion to the session. The digest now encodes each message straight into
the hash through one encoder, byte-identical to before, and the index is
written compact.

Median of four runs of BenchmarkIncrementalSave: 1000 messages 33.7 to 24.6 ms
and 3.64 to 1.53 MB per save; 5000 messages 49.3 to 42.2 ms and 18.9 to 8.7 MB;
20000 messages 136.8 to 109.7 ms and 78.4 to 37.7 MB. The digest test pins the
bytes against the format's own definition, including characters JSON escapes.
2026-11-10 01:28:36 -08:00
yhh 2ff95dbc5e feat(studio): the workbench shows the file tree beside the open file
The tree is the left column and the file the right, in the dock as well as at
full width. Choosing a file leaves the tree open, so the next one is a click
away instead of a trip back to the Files tab; any other surface still takes the
whole column and folds the tree. The explorer title no longer wraps one
character per line in the narrow column.

Docked, the list used to hide the canvas, so the content views counted
themselves hidden while it was open and a closed last file brought the list
back. Neither holds side by side, so both rules are removed and the tests that
asserted them are rewritten to the new layout.
2026-11-10 00:59:01 -08:00
yhh f4a646549c fix(tools): the bash description steers searching to tool:grep and tool:glob
grep and glob are deferred tools reached through use_capability, so the model
had no direct name to prefer and searched through the shell: 33 of 37 searches
in a three-task sample. The description now names tool:grep and tool:glob and
leaves the shell its pipelines. Over the same three tasks the built-in share of
searches went from 4 of 37 to 16 of 31.

The tool schema grows by 62 tokens and the prefix changes once, so the golden
baseline and docs/TOOL_CONTRACT.md are regenerated with it.
2026-11-10 00:58:54 -08:00
yhh 302b6732a5 feat(tools): turnprofile splits a trajectory's turns into exclusive time buckets
Reads --trajectory JSONL and assigns every millisecond of a turn to one of
user wait, compaction, tool, model first token, model generation, discarded
attempt, or host, so where a turn spends its wall-clock is read off a recorded
run. Read-only; definitions and limits are in the README.
2026-11-09 23:40:13 -08:00
440 changed files with 2412 additions and 1162 deletions

No files matched your search

+13 -12
View File
@@ -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"}}`,
+6 -4
View File
@@ -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 {
+4 -3
View File
@@ -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
+3 -1
View File
@@ -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 -5
View File
@@ -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...)...)
+2 -2
View File
@@ -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)
}
+9 -7
View File
@@ -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()
+6 -5
View File
@@ -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")
+3 -3
View File
@@ -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)
}
+3 -1
View File
@@ -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"
+5 -4
View File
@@ -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",
+2 -1
View File
@@ -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 -8
View File
@@ -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
+1 -2
View File
@@ -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 }
+6 -5
View File
@@ -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");
});
+16 -23
View File
@@ -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));
});
+5 -14
View File
@@ -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);
});
+1 -1
View File
@@ -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"
+2 -1
View File
@@ -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 := &noticeSink{}
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 := &noticeSink{}
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)
}
+3 -3
View File
@@ -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": {
+1 -1
View File
@@ -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)
}
+7 -5
View File
@@ -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)
}
+3 -1
View File
@@ -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)
}
+6 -4
View File
@@ -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)
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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 {
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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 {
+6 -4
View File
@@ -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)
}
}
+9 -7
View File
@@ -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)
+9 -7
View File
@@ -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)
}
+3 -1
View File
@@ -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)
+2 -2
View File
@@ -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)
+4 -2
View File
@@ -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)
}
+3 -1
View File
@@ -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)
}
+2 -1
View File
@@ -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)
}
+3 -1
View File
@@ -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"}},
+2 -2
View File
@@ -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 {
+25 -23
View File
@@ -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)
+4 -2
View File
@@ -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 {
+6 -4
View File
@@ -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)
}
+3 -2
View File
@@ -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)
}
+6 -4
View File
@@ -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