Files
multica/server/pkg/agent/dim_test.go
686957e743 MUL-7271: test(server): cut slow Go test time further (follow-up to #8295) (#8318)
* test(server): reduce slow unit test waits

Co-authored-by: multica-agent <[email protected]>

* test(server): cut slow Go unit test time further

Follow-up to the first pass on MUL-7271. Test-only changes plus a few
package-private seams whose production values are unchanged.

- Re-executed race-built test binaries no longer pay the race runtime's 1s
  atexit sleep (GORACE=atexit_sleep_ms=0 for child processes only).
- pkg/agent: event-driven or shorter test-only bounds for handshake, drain
  and idle graces; redundant timeout variants merged.
- daemon/execenv: git fixtures copied from a template repo built once; tests
  that re-ran the same worktree lifecycle folded into their supersets;
  ForkLock-guarded fake-CLI writes so parallel tests cannot hit ETXTBSY.
- handler/service: cap and paging tests shrink their caps instead of seeding
  thousands of rows; lock waits probe pg_locks instead of sleeping; one
  shared pgxpool per service test binary; cheaper cron and RSA fixtures.
- wecom: the phishing-link sweep reuses one parser and derives CRLF from LF
  with a sampled cross-check; fake sockets acknowledge instead of waiting out
  the ack timeout.
- migrate: the migration corpus is parsed once; isolated DB tests run in
  parallel.

Co-authored-by: multica-agent <[email protected]>

* test(server): remove transport timeout unit tests

Co-authored-by: multica-agent <[email protected]>

* test(server): restore two non-timeout tests and make a bound load-bearing

Follow-up to the review of #8318. All test-only; no production code changes.

- daemon: restore the three read-liveness cases that the transport-timeout
  sweep took with it. They assert that a ping, a pong and an application
  frame each EXTEND the read deadline, so a healthy wakeup socket survives
  without traffic of our own — the deadline never fires on the happy path,
  which is why they are not transport-timeout tests. Verified they fail
  (all three) when the extension calls are removed from wakeup.go, and pass
  in 0.57s together. The pure "times out without peer traffic" case stays
  deleted.

- pkg/agent: restore the parent-DEADLINE half of the opaque-env redaction
  pair. codex.go treats a parent deadline and a parent cancellation as one
  ending; only the Canceled arm was left covered. The fake now writes the
  secret to stderr before reading the handshake, so the check no longer
  races our own initialize write, and the budget is 2s against a measured
  430-465ms launch. Verified it fails when the !contextEnded guard is
  removed.

- pkg/agent: TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn had its bound
  widened to a literal 5s, which the shrunk 750ms drain grace can never
  reach — the test could no longer fail. It now raises the drain grace to
  10s for its own duration and asserts under 3s: healthy runs take
  0.19-0.44s, and a broken short-circuit takes 10.19s and fails.

- wecom: gofmt the import block in relay_settle_budget_test.go.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>
2026-09-14 14:33:50 +08:00

814 lines
29 KiB
Go

package agent
import (
"context"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
// fakeDimACPScript impersonates `dim acp` for unit tests. Dim 0.3.10+
// releases its per-process session lock shortly after the owning process
// exits, so a follow-up run resumes via the standard ACP session/load. This
// fake answers session/load with a resumed session (retaining the requested
// id, matching the real server which does not echo sessionId on load). It
// records every request line to DIM_REQUESTS_FILE so tests can assert which
// RPCs were (and were not) sent.
func fakeDimACPScript() string {
return `#!/bin/sh
while IFS= read -r line; do
if [ -n "$DIM_REQUESTS_FILE" ]; then
printf '%s\n' "$line" >> "$DIM_REQUESTS_FILE"
fi
id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\).*/\1/p')
case "$line" in
*'"method":"initialize"'*)
if [ -n "$DIM_INIT_FAIL_HANG" ]; then
# Spawn a descendant BEFORE sending the error response, so the PID
# file is written before the backend's cleanup race begins. Then
# ignore TERM and hang so the process does not exit on stdin EOF —
# exercises the force-kill process-group cleanup path.
trap '' TERM
sleep 60 &
child_pid=$!
if [ -n "$DIM_CHILD_PID_FILE" ]; then
printf '%s' "$child_pid" > "$DIM_CHILD_PID_FILE"
fi
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"initialize failed"}}\n' "$id"
wait
else
printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":1,"agentInfo":{"name":"dimcode","version":"0.3.10"},"agentCapabilities":{"loadSession":true,"mcpCapabilities":{"http":true,"sse":false}}}}\n' "$id"
fi
;;
*'"method":"session/new"'*)
printf '{"jsonrpc":"2.0","id":%s,"result":{"sessionId":"ses_dim_new","models":{"availableModels":[{"modelId":"dim/model-a","name":"Model A"}],"currentModelId":"dim/model-a"}}}\n' "$id"
;;
*'"method":"session/load"'*)
# The real dim ACP server resumes the session and returns configOptions
# (no sessionId field — the id is the one the client requested). When
# DIM_LOAD_NOT_FOUND is set, emulate a session that no longer exists so
# tests can exercise the ResumeRejected path. When DIM_LOAD_HELD_N is set,
# return "held by another process" for the first N calls then succeed,
# exercising the bounded retry loop.
if [ -n "$DIM_LOAD_NOT_FOUND" ]; then
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32002,"message":"ACP session not found","data":{"sessionId":"ses_prior"}}}\n' "$id"
elif [ -n "$DIM_LOAD_HELD_N" ]; then
held_file="${DIM_REQUESTS_FILE}_held_counter"
count=0
if [ -f "$held_file" ]; then count=$(cat "$held_file"); fi
if [ "$count" -lt "$DIM_LOAD_HELD_N" ]; then
count=$((count + 1))
echo "$count" > "$held_file"
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"Session held by another process","data":{"details":"held"}}}\n' "$id"
else
printf '{"jsonrpc":"2.0","id":%s,"result":{"configOptions":[{"id":"permission","currentValue":"full-access"},{"id":"mode","currentValue":"agent"}],"models":{"currentModelId":"dim/model-a"}}}\n' "$id"
fi
elif [ -n "$DIM_LOAD_HELD_ALWAYS" ]; then
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"Session held by another process","data":{"details":"held"}}}\n' "$id"
else
printf '{"jsonrpc":"2.0","id":%s,"result":{"configOptions":[{"id":"permission","currentValue":"full-access"},{"id":"mode","currentValue":"agent"}],"models":{"currentModelId":"dim/model-a"}}}\n' "$id"
fi
;;
*'"method":"session/set_config_option"'*)
if [ -n "$DIM_CONFIG_FAIL_ONCE" ] && [ ! -f "$DIM_CONFIG_FAIL_ONCE" ]; then
printf '%s' x > "$DIM_CONFIG_FAIL_ONCE"
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"config set failed"}}\n' "$id"
elif [ -n "$DIM_CONFIG_FAIL" ]; then
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"config set failed"}}\n' "$id"
else
printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id"
fi
;;
*'"method":"session/set_model"'*)
if [ -n "$DIM_MODEL_FAIL" ]; then
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32603,"message":"set_model failed"}}\n' "$id"
else
printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id"
fi
;;
*'"method":"session/prompt"'*)
# When DIM_PROMPT_NO_STOP is set, omit stopReason so extractPromptResult
# never fires onPromptDone — exercises the bounded final-notification
# wait (the backend must still complete, not hang on a missing notify).
if [ -n "$DIM_PROMPT_NO_STOP" ]; then
printf '{"jsonrpc":"2.0","id":%s,"result":{"usage":{"inputTokens":10,"outputTokens":20}}}\n' "$id"
elif [ -n "$DIM_LATE_NOTIFICATION" ]; then
# Return the prompt response, then emit a late agent_message_chunk
# notification after a short delay — exercises the notification
# quiescence drain (the late text must survive into Result.Output).
printf '{"jsonrpc":"2.0","id":%s,"result":{"stopReason":"end_turn","usage":{"inputTokens":10,"outputTokens":20}}}\n' "$id"
sleep 0.05
printf '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"%s","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"late-answer"}}}}\n' "$DIM_SESSION_ID"
else
printf '{"jsonrpc":"2.0","id":%s,"result":{"stopReason":"end_turn","usage":{"inputTokens":10,"outputTokens":20}}}\n' "$id"
fi
;;
*'"method":"session/close"'*)
printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id"
;;
*)
printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"method not found"}}\n' "$id"
;;
esac
done
`
}
func writeFakeDimScript(t *testing.T, requestsFile string) string {
t.Helper()
dir := t.TempDir()
bin := filepath.Join(dir, "dim")
if err := os.WriteFile(bin, []byte(fakeDimACPScript()), 0o755); err != nil {
t.Fatalf("write fake dim: %v", err)
}
return bin
}
func newDimTestBackend(t *testing.T, requestsFile string) Backend {
t.Helper()
return newDimTestBackendWithEnv(t, requestsFile, nil)
}
func newDimTestBackendWithEnv(t *testing.T, requestsFile string, extra map[string]string) Backend {
t.Helper()
bin := writeFakeDimScript(t, requestsFile)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
env := map[string]string{"DIM_REQUESTS_FILE": requestsFile}
for k, v := range extra {
env[k] = v
}
b, err := New("dim", Config{
ExecutablePath: bin,
Logger: logger,
Env: env,
})
if err != nil {
t.Fatalf("New(dim) error: %v", err)
}
return b
}
func readDimRequests(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read dim requests: %v", err)
}
return string(data)
}
// TestDimSessionNew verifies the happy path: initialize → session/new →
// set_config_option(permission/mode) → prompt, with a fresh session.
func TestDimSessionNew(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackend(t, requestsFile)
ctx := context.Background()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "completed" {
t.Fatalf("expected status=completed, got %q (error=%q)", result.Status, result.Error)
}
if result.SessionID != "ses_dim_new" {
t.Fatalf("expected session id ses_dim_new, got %q", result.SessionID)
}
reqs := readDimRequests(t, requestsFile)
if strings.Contains(reqs, "session/load") {
t.Fatal("backend must not send session/load on a fresh-session run")
}
// The ACP server hardcodes read-only permission; the backend must raise
// it to full-access and pin agent mode before the prompt.
if !strings.Contains(reqs, `"configId":"permission"`) || !strings.Contains(reqs, `"value":"full-access"`) {
t.Fatal("expected set_config_option permission=full-access")
}
if !strings.Contains(reqs, `"configId":"mode"`) || !strings.Contains(reqs, `"value":"agent"`) {
t.Fatal("expected set_config_option mode=agent")
}
if !strings.Contains(reqs, "session/prompt") {
t.Fatal("expected session/prompt")
}
// The backend must send session/close during teardown so dim releases the
// per-process session lock immediately (graceful exit), enabling the next
// run to resume without delay.
if !strings.Contains(reqs, "session/close") {
t.Fatal("expected session/close during teardown")
}
}
// TestDimResumeLoadsSession verifies that when the daemon asks to resume a
// prior session, the backend resumes it via the standard ACP session/load,
// reuses the resumed session id, and re-issues set_config_option (fail-closed
// against a partially configured session — review #4). ResumeRejected must
// stay false because the resume succeeded.
func TestDimResumeLoadsSession(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackend(t, requestsFile)
ctx := context.Background()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
ResumeSessionID: "ses_prior",
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "completed" {
t.Fatalf("expected status=completed, got %q (error=%q)", result.Status, result.Error)
}
if result.SessionID != "ses_prior" {
t.Fatalf("expected the resumed session id ses_prior, got %q", result.SessionID)
}
if result.ResumeRejected {
t.Fatal("ResumeRejected must be false when session/load succeeded")
}
reqs := readDimRequests(t, requestsFile)
if !strings.Contains(reqs, "session/load") {
t.Fatal("expected a session/load when a resume was requested")
}
if strings.Contains(reqs, "session/new") {
t.Fatal("backend must not send session/new when the resume succeeded")
}
// A resumed session is re-issued set_config_option (permission/mode) to
// guard against a partially configured session being resumed — see
// review #4. So set_config_option MUST appear after a successful load.
if !strings.Contains(reqs, "set_config_option") {
t.Fatal("set_config_option must be re-applied on a resumed session (fail-closed)")
}
}
// TestDimResumeNotFound verifies that when session/load reports the prior
// session is gone (ACP -32002 session not found), the backend fails with
// ResumeRejected=true so the daemon retries on a fresh session instead of
// silently losing the conversation.
func TestDimResumeNotFound(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{"DIM_LOAD_NOT_FOUND": "1"})
ctx := context.Background()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
ResumeSessionID: "ses_prior",
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "failed" {
t.Fatalf("expected status=failed, got %q", result.Status)
}
if !result.ResumeRejected {
t.Fatal("expected ResumeRejected=true when session/load reports not found")
}
reqs := readDimRequests(t, requestsFile)
if !strings.Contains(reqs, "session/load") {
t.Fatal("expected a session/load attempt")
}
if strings.Contains(reqs, "session/new") {
t.Fatal("backend must not fall through to session/new itself; the daemon owns the fresh retry")
}
}
// TestDimCleanupKillsHangingChild verifies the deferred cleanup force-kills the
// entire process group — not just the direct child — when a child ignores stdin
// EOF and SIGTERM after an early RPC failure, so Result still closes AND no
// descendant is orphaned (review #2/#3). Without the group force-kill,
// cmd.Wait() would hang forever and the sleep descendant would survive as a
// PPID=1 orphan.
func TestDimCleanupKillsHangingChild(t *testing.T) {
t.Parallel()
dir := t.TempDir()
requestsFile := filepath.Join(dir, "requests.jsonl")
childPidFile := filepath.Join(dir, "child_pid")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_INIT_FAIL_HANG": "1",
"DIM_CHILD_PID_FILE": childPidFile,
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 20 * time.Second,
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
// Result must close within a bounded window — the group force-kill
// (bounded by dimProcessWaitTimeout) guarantees the hung child cannot
// block it.
select {
case result := <-session.Result:
if result.Status != "failed" {
t.Fatalf("expected status=failed from the initialize error, got %q", result.Status)
}
case <-time.After(dimProcessWaitTimeout + 15*time.Second):
t.Fatal("Result never closed: the hanging child was not force-killed within the cleanup timeout")
}
// The descendant (sleep 60) must be gone too — not orphaned to PPID=1.
// Give the PID file a moment to appear (the child is spawned after the
// error response is written).
deadline := time.Now().Add(5 * time.Second)
var pidStr string
for time.Now().Before(deadline) {
data, err := os.ReadFile(childPidFile)
if err == nil && len(data) > 0 {
pidStr = string(data)
break
}
time.Sleep(50 * time.Millisecond)
}
if pidStr == "" {
t.Fatal("child PID file was never written; the fake did not spawn a descendant")
}
childPid, err := strconv.Atoi(strings.TrimSpace(pidStr))
if err != nil {
t.Fatalf("invalid child PID %q: %v", pidStr, err)
}
// Verify the descendant is no longer alive. Poll briefly because the
// group SIGKILL and the kernel's process-table reaping are asynchronous
// with respect to Result delivery.
proc, err := os.FindProcess(childPid)
if err != nil {
return // already reaped
}
goneDeadline := time.Now().Add(3 * time.Second)
for time.Now().Before(goneDeadline) {
if err := proc.Signal(syscall.Signal(0)); err != nil {
return // process is gone
}
time.Sleep(50 * time.Millisecond)
}
t.Fatalf("descendant PID %d is still alive after cleanup: process group was not reaped", childPid)
}
// TestDimPromptMissingNotificationStillCompletes verifies the success path
// does not hang when the runtime returns session/prompt without a stopReason
// (so onPromptDone never fires). The bounded final-notification wait must
// fall through and let Result close (review #3 success-path race).
func TestDimPromptMissingNotificationStillCompletes(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{"DIM_PROMPT_NO_STOP": "1"})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 20 * time.Second,
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
select {
case result := <-session.Result:
// The turn completed (session/prompt returned ok); the missing
// stopReason only means no usage/stopReason was captured. Status must
// still be completed, not hang or fail.
if result.Status != "completed" {
t.Fatalf("expected status=completed, got %q (error=%q)", result.Status, result.Error)
}
case <-time.After(dimNotificationQuietTime + 15*time.Second):
t.Fatal("Result never closed: the missing prompt notification was not bounded by the quiet-time wait")
}
}
// TestDimDrainsLateFinalNotificationAfterPromptResponse verifies that a
// notification arriving just after the session/prompt response is not lost —
// the activity-based quiescence drain gives the stdout reader time to consume
// it before stdin is closed (review #3, the Dim equivalent of
// TestHermesBackendDrainsLateFinalNotificationAfterPromptResponse).
func TestDimDrainsLateFinalNotificationAfterPromptResponse(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_LATE_NOTIFICATION": "1",
"DIM_SESSION_ID": "ses_dim_new",
})
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 15 * time.Second,
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
select {
case result := <-session.Result:
if result.Status != "completed" {
t.Fatalf("expected status=completed, got %q (error=%q)", result.Status, result.Error)
}
if !strings.Contains(result.Output, "late-answer") {
t.Fatalf("late notification was lost: expected output to contain %q, got %q", "late-answer", result.Output)
}
case <-time.After(20 * time.Second):
t.Fatal("timeout waiting for result")
}
}
// TestDimConfigFailClosesSession verifies that when set_config_option fails,
// the backend sends session/close before returning the error, so a partially
// configured session is not left for the next resume to inherit without
// full-access (review #4).
func TestDimConfigFailClosesSession(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{"DIM_CONFIG_FAIL": "1"})
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 15 * time.Second,
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "failed" {
t.Fatalf("expected status=failed, got %q", result.Status)
}
reqs := readDimRequests(t, requestsFile)
// set_config_option must have been attempted.
if !strings.Contains(reqs, "set_config_option") {
t.Fatal("expected a set_config_option attempt")
}
// session/close must have been sent to clean up the partially configured
// session so it is not resumed later without full-access.
if !strings.Contains(reqs, "session/close") {
t.Fatal("expected session/close after config failure to prevent a partially configured resume")
}
}
// TestDimFreshConfigFailureWithholdsSessionID pins the ghost-pointer contract
// (GH #8116) on dim's set_config_option branch: a fresh session that died
// during setup, before any prompt went out, must not be published as a resume
// pointer.
//
// dim is the runtime where this is least obviously necessary and most clearly
// still right. It closes the session on this path and persists early enough
// that the id would in fact still load — but there is no conversation behind
// it, because no prompt was ever sent. Withholding it costs one extra
// session/new on the next turn; publishing it is a bet that every ACP runtime
// persists a never-prompted session, and qodercli does not, which is what
// wedged a conversation forever in the first place. The contract is uniform
// across backends on purpose: making it per-runtime is how the neighbouring
// resume bugs got fixed one adapter at a time while the rest stayed broken.
func TestDimFreshConfigFailureWithholdsSessionID(t *testing.T) {
t.Parallel()
dir := t.TempDir()
requestsFile := filepath.Join(dir, "requests.jsonl")
failFlag := filepath.Join(dir, "config_failed")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_CONFIG_FAIL_ONCE": failFlag,
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 20 * time.Second,
})
if err != nil {
t.Fatalf("execute: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "failed" {
t.Fatalf("expected status=failed, got %q", result.Status)
}
if result.SessionID != "" {
t.Fatalf("expected the never-prompted session id to be withheld, got %q", result.SessionID)
}
if result.ResumeRejected {
t.Fatal("a fresh session cannot be resume-rejected; the daemon must not re-run this task")
}
}
// TestDimResumeReappliesConfig pins the fail-closed config re-application on
// resume (review #4): a resumed session re-sends set_config_option instead of
// trusting whatever state the earlier run left behind. It resumes a session
// that completed a prompt — the only kind that now carries a resume pointer.
func TestDimResumeReappliesConfig(t *testing.T) {
t.Parallel()
dir := t.TempDir()
requestsFile := filepath.Join(dir, "requests.jsonl")
b := newDimTestBackend(t, requestsFile)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Run A: a fresh run that reaches session/prompt and completes, so it has
// a real conversation to hand forward.
sessionA, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 20 * time.Second,
})
if err != nil {
t.Fatalf("execute A: %v", err)
}
go func() {
for range sessionA.Messages {
}
}()
resultA := <-sessionA.Result
if resultA.Status != "completed" {
t.Fatalf("run A: expected status=completed, got %q (error=%q)", resultA.Status, resultA.Error)
}
if resultA.SessionID == "" {
t.Fatal("run A: a completed run must publish its session id")
}
// Run B: resume it.
sessionB, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 20 * time.Second,
ResumeSessionID: resultA.SessionID,
})
if err != nil {
t.Fatalf("execute B: %v", err)
}
go func() {
for range sessionB.Messages {
}
}()
resultB := <-sessionB.Result
if resultB.Status != "completed" {
t.Fatalf("run B: expected status=completed, got %q (error=%q)", resultB.Status, resultB.Error)
}
if resultB.ResumeRejected {
t.Fatal("run B: ResumeRejected should be false — the session was loadable")
}
reqs := readDimRequests(t, requestsFile)
if !strings.Contains(reqs, "session/load") {
t.Fatal("run B: expected session/load")
}
// set_config_option should appear at least 4 times: 2 in run A and 2
// re-applied in run B (permission + mode).
if count := strings.Count(reqs, "set_config_option"); count < 4 {
t.Fatalf("expected at least 4 set_config_option calls (2 fresh + 2 re-applied on resume), got %d", count)
}
}
// TestDimVersionSupported pins the fail-closed version gate (review #3): only
// a parseable semver >= 0.3.10 is accepted. Empty, non-parseable, and
// too-short versions are rejected so a runtime that cannot prove it meets the
// minimum is blocked from session resume.
func TestDimVersionSupported(t *testing.T) {
t.Parallel()
cases := []struct {
version string
want bool
}{
{"0.3.10", true},
{"0.3.9", false},
{"0.4.0", true},
{"1.0.0", true},
{"", false},
{"abc", false},
{"0.3", false},
}
for _, tc := range cases {
if got := dimVersionSupported(tc.version); got != tc.want {
t.Errorf("dimVersionSupported(%q) = %v, want %v", tc.version, got, tc.want)
}
}
}
// TestDimSetModelFailClosesSession verifies that when session/set_model fails,
// the backend sends session/close before returning the error, so a partially
// configured session (permission/mode set, model not) is not left for the next
// resume to inherit (review #5).
func TestDimSetModelFailClosesSession(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{"DIM_MODEL_FAIL": "1"})
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Model: "some-model",
Timeout: 15 * time.Second,
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "failed" {
t.Fatalf("expected status=failed, got %q (error=%q)", result.Status, result.Error)
}
reqs := readDimRequests(t, requestsFile)
// session/set_model must have been attempted with the requested model.
if !strings.Contains(reqs, "session/set_model") {
t.Fatal("expected a session/set_model attempt")
}
if !strings.Contains(reqs, `"modelId":"some-model"`) {
t.Fatal("expected session/set_model to carry the requested model id")
}
// session/close must have been sent to clean up the partially configured
// session (permission/mode set, model not) so it is not resumed later.
if !strings.Contains(reqs, "session/close") {
t.Fatal("expected session/close after set_model failure to prevent a partially configured resume")
}
// The prompt must NOT have been sent — the model switch failed, so the
// turn aborts before session/prompt.
if strings.Contains(reqs, "session/prompt") {
t.Fatal("backend must not send session/prompt after set_model failure")
}
}
// TestDimSessionLoadRetrySucceeds verifies that session/load retries on
// "held by another process" and eventually succeeds (review #4 round 4).
func TestDimSessionLoadRetrySucceeds(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_LOAD_HELD_N": "2", // held for first 2 calls, succeeds on 3rd
})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 50 * time.Second,
ResumeSessionID: "ses_prior",
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "completed" {
t.Fatalf("expected status=completed, got %q (error=%q)", result.Status, result.Error)
}
if result.ResumeRejected {
t.Fatal("ResumeRejected must be false — retry succeeded")
}
reqs := readDimRequests(t, requestsFile)
loadCount := strings.Count(reqs, "session/load")
if loadCount < 3 {
t.Fatalf("expected at least 3 session/load attempts (2 held + 1 success), got %d", loadCount)
}
if strings.Contains(reqs, "session/new") {
t.Fatal("backend must not fall through to session/new when retry succeeds")
}
}
// TestDimSessionLoadRetryExhausted verifies that after exhausting retries on
// "held by another process", the backend fails without falling through to
// session/new (review #4 round 4).
func TestDimSessionLoadRetryExhausted(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_LOAD_HELD_ALWAYS": "1",
})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 50 * time.Second,
ResumeSessionID: "ses_prior",
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
result := <-session.Result
if result.Status != "failed" && result.Status != "timeout" {
t.Fatalf("expected status=failed or timeout, got %q", result.Status)
}
reqs := readDimRequests(t, requestsFile)
loadCount := strings.Count(reqs, "session/load")
// dimSessionLoadRetryAttempts=3, so 1 initial + 3 retries = 4 attempts
if loadCount != 4 {
t.Fatalf("expected exactly 4 session/load attempts (1 + 3 retries), got %d", loadCount)
}
if strings.Contains(reqs, "session/new") {
t.Fatal("backend must not fall through to session/new when retries are exhausted")
}
}
// TestDimSessionLoadRetryCancelled verifies that cancelling the context during
// the retry delay aborts the loop without falling through to session/new
// (review #4 round 4).
func TestDimSessionLoadRetryCancelled(t *testing.T) {
t.Parallel()
requestsFile := filepath.Join(t.TempDir(), "requests.jsonl")
b := newDimTestBackendWithEnv(t, requestsFile, map[string]string{
"DIM_LOAD_HELD_ALWAYS": "1",
})
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
session, err := b.Execute(ctx, "test prompt", ExecOptions{
Cwd: t.TempDir(),
Timeout: 50 * time.Second,
ResumeSessionID: "ses_prior",
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
go func() {
for range session.Messages {
}
}()
// Wait for the first session/load request to appear, then cancel during
// the retry delay — this is deterministic rather than a fixed sleep.
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(requestsFile); err == nil {
data, _ := os.ReadFile(requestsFile)
if strings.Contains(string(data), "session/load") {
break
}
}
time.Sleep(20 * time.Millisecond)
}
cancel()
result := <-session.Result
if result.Status != "failed" && result.Status != "aborted" && result.Status != "timeout" {
t.Fatalf("expected status=failed/aborted/timeout, got %q", result.Status)
}
reqs := readDimRequests(t, requestsFile)
if strings.Contains(reqs, "session/new") {
t.Fatal("backend must not fall through to session/new when cancelled during retry")
}
}