mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-25 17:36:43 +08:00
cc-connect's built-in AgentSystemPrompt is ~9KB, which on its own
exceeds Windows cmd.exe's 8192-byte command-line limit when claude
is spawned with --append-system-prompt <inline content>. The
claude.exe child immediately fails with GBK stderr "命令行太长。"
(command line too long) before any user message reaches the agent.
On v1.3.3 this breaks every Feishu / IM message on Windows hosts.
Pass the merged prompt through Claude Code CLI's
--append-system-prompt-file <path> flag instead of the inline form.
Only the short file path travels on the command line, so the cap is
no longer a function of prompt size.
Common case (99% of users — no platform formatting hints, no
user-configured append_system_prompt):
* On agent construction, write the full AgentSystemPrompt() once to
<data_dir>/agent-prompts/cc-connect-system.md (default
~/.cc-connect/agent-prompts/cc-connect-system.md). claude reads
this file at every spawn; cc-connect never rewrites it unless the
cc-connect version actually changes the prompt content.
* Every spawn reuses the same file, so there is no per-spawn write
and no concurrency race (claude is a strict reader).
Edge case (Slack / Weixin / MAX with FormattingInstructions, or
project-level append_system_prompt):
* Write a per-spawn temp file under <data_dir>/agent-prompts/
holding the merged content, passed via the same flag.
* Cleaned up on session Close to avoid leaking files.
Notes for reviewers:
* claude CLI refuses --append-system-prompt and
--append-system-prompt-file together
("Cannot use both ... Please use only one"), so the merged content
must travel as a single file.
* Only the claudecode agent runtime is affected. Other agents
(codex/opencode/qoder/cursor/gemini/kimi) inject AgentSystemPrompt
via memory files (CLAUDE.md/AGENTS.md/...), which already have no
cmdline cap.
* core/interfaces.go (AgentSystemPrompt content) is unchanged from
v1.3.3; the full prompt is preserved so agent behaviour does not
regress.
Tests:
* TestEnsureSharedSystemPromptFile_WritesOnceAndReuses — verifies the
shared file is written once and skipped on identical content.
* TestEnsureSharedSystemPromptFile_RewritesOnContentChange — verifies
cc-connect upgrades refresh the shared file.
* TestEnsureSharedSystemPromptFile_EmptyDirUsesTempDir — verifies the
fallback when ccDataDir is unset.
* TestWriteTempAppendPromptFile_UniquePerCall — verifies concurrent
edge-case spawns get unique paths.
* Existing claudecode + core suites all pass (one pre-existing flake
in TestCUJ_H2_TwoPlatformsConcurrentNoBleed is unrelated to this
change and still passes when run in isolation).
E2E (Windows, MiniMax-M3 backend, Feishu platform):
* Before fix: every Feishu message produces no reply; claude.exe
fails with "命令行太长。" on stderr.
* After fix: "你好" → MiniMax intro reply; "3 分钟后帮我看看
ipconfig" → timer correctly created in data/timers/jobs.json;
"每天早上 6 点提醒我吃药" → cron correctly created in
data/crons/jobs.json. Agent uses /timer and /cron commands as
instructed by the prompt.
Fixes #1376.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
3 files changed
+322
-9
No files matched your search
@@ -59,6 +59,15 @@ type Agent struct {
|
||||
proxyLocalURL string // local URL of the proxy
|
||||
platformPrompt string // platform-specific formatting instructions
|
||||
|
||||
// ccDataDir is injected by the cc-connect host (see buildAgentOptions
|
||||
// in cmd/cc-connect/main.go). It locates the global directory where
|
||||
// we write the shared cc-connect system prompt file (issue #1376
|
||||
// workaround for Windows 8192-byte cmdline limit). The file at
|
||||
// <ccDataDir>/agent-prompts/cc-connect-system.md is written once per
|
||||
// startup and shared across all sessions that don't need per-spawn
|
||||
// customisation. Empty value falls back to os.TempDir.
|
||||
ccDataDir string
|
||||
|
||||
// spawnOpts controls OS-user isolation via run_as_user. Zero value
|
||||
// means legacy spawn as the supervisor user. See core/runas.go.
|
||||
spawnOpts core.SpawnOptions
|
||||
@@ -138,6 +147,7 @@ func New(opts map[string]any) (core.Agent, error) {
|
||||
mode = normalizePermissionMode(mode)
|
||||
systemPrompt, _ := opts["system_prompt"].(string)
|
||||
appendSystemPrompt, _ := opts["append_system_prompt"].(string)
|
||||
ccDataDir, _ := opts["cc_data_dir"].(string)
|
||||
|
||||
var pluginDirs []string
|
||||
if dir, ok := opts["plugin_dir"].(string); ok && dir != "" {
|
||||
@@ -226,6 +236,16 @@ func New(opts map[string]any) (core.Agent, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Eagerly materialise the shared cc-connect-system.md at startup so
|
||||
// the file exists on disk before the first spawn. claude reads it
|
||||
// via --append-system-prompt-file; the lazy fallback in
|
||||
// newClaudeSession still covers content drift (cc-connect upgrades)
|
||||
// and the empty-ccDataDir corner case. Failure here is non-fatal —
|
||||
// the next spawn will retry and surface the error then.
|
||||
if _, err := ensureSharedSystemPromptFile(ccDataDir, core.AgentSystemPrompt()); err != nil {
|
||||
slog.Warn("claudecode: failed to write shared system prompt file at startup; will retry on first spawn", "err", err, "cc_data_dir", ccDataDir)
|
||||
}
|
||||
|
||||
return &Agent{
|
||||
workDir: workDir,
|
||||
cliBin: cliBin,
|
||||
@@ -244,6 +264,7 @@ func New(opts map[string]any) (core.Agent, error) {
|
||||
routerURL: routerURL,
|
||||
routerAPIKey: routerAPIKey,
|
||||
spawnOpts: spawnOpts,
|
||||
ccDataDir: ccDataDir,
|
||||
|
||||
appendSystemPrompt: appendSystemPrompt,
|
||||
}, nil
|
||||
@@ -506,7 +527,7 @@ func (a *Agent) StartSession(ctx context.Context, sessionID string) (core.AgentS
|
||||
disableVerbose := a.routerURL != ""
|
||||
a.mu.Unlock()
|
||||
|
||||
return newClaudeSession(ctx, workDir, a.cliBin, a.cliExtraArgs, a.cliArgsFlag, model, effort, sessionID, mode, systemPrompt, appendSystemPrompt, tools, disTools, pluginDirs, extraEnv, platformPrompt, disableVerbose, a.spawnOpts, maxTok)
|
||||
return newClaudeSession(ctx, workDir, a.cliBin, a.cliExtraArgs, a.cliArgsFlag, model, effort, sessionID, mode, systemPrompt, appendSystemPrompt, tools, disTools, pluginDirs, extraEnv, platformPrompt, disableVerbose, a.spawnOpts, maxTok, a.ccDataDir)
|
||||
}
|
||||
|
||||
func (a *Agent) ListSessions(ctx context.Context) ([]core.AgentSessionInfo, error) {
|
||||
|
||||
+175
-8
@@ -64,6 +64,14 @@ type claudeSession struct {
|
||||
// startupWarning holds a one-time message to surface to the IM user at
|
||||
// session start (e.g. when a permission mode was silently downgraded).
|
||||
startupWarning string
|
||||
|
||||
// promptFilePath is the per-spawn temp file holding the merged
|
||||
// content for --append-system-prompt-file when this session needs
|
||||
// platform- or user-specific append text that the shared
|
||||
// cc-connect-system.md cannot represent. Removed on Close. Empty
|
||||
// when the session reuses the shared file (the common 99% case)
|
||||
// or when there is nothing to append.
|
||||
promptFilePath string
|
||||
}
|
||||
|
||||
// StartupWarning implements core.StartupWarner. Returns a non-empty string
|
||||
@@ -71,11 +79,117 @@ type claudeSession struct {
|
||||
// know about (e.g. bypassPermissions downgraded to auto under root).
|
||||
func (cs *claudeSession) StartupWarning() string { return cs.startupWarning }
|
||||
|
||||
// sharedSystemPromptRelPath is the location under ccDataDir where the
|
||||
// shared cc-connect system prompt file lives. Reused across every spawn
|
||||
// that doesn't need per-session customization (the 99% case).
|
||||
const sharedSystemPromptRelPath = "agent-prompts/cc-connect-system.md"
|
||||
|
||||
// ensureSharedSystemPromptFile lazily writes <ccDataDir>/agent-prompts/
|
||||
// cc-connect-system.md with the cc-connect default AgentSystemPrompt
|
||||
// content, returning the path. The file is the workaround for the
|
||||
// Windows 8192-byte command-line limit (issue #1376): cc-connect's
|
||||
// built-in prompt is ~9KB on its own, so passing it inline via
|
||||
// --append-system-prompt blows past the cap regardless of whether the
|
||||
// user configured any customization.
|
||||
//
|
||||
// The file is only (re)written when missing or when its content differs
|
||||
// from the current AgentSystemPrompt() — this lets cc-connect upgrades
|
||||
// refresh the prompt automatically without per-spawn overhead. claude
|
||||
// only reads the file at startup and never writes it, so there is no
|
||||
// concurrent-write race even when multiple sessions spawn at once.
|
||||
//
|
||||
// If ccDataDir is empty (unlikely in production, but possible in tests),
|
||||
// the file lands in os.TempDir.
|
||||
func ensureSharedSystemPromptFile(ccDataDir, content string) (string, error) {
|
||||
base := ccDataDir
|
||||
if base == "" {
|
||||
base = os.TempDir()
|
||||
}
|
||||
dir := filepath.Join(base, "agent-prompts")
|
||||
path := filepath.Join(base, sharedSystemPromptRelPath)
|
||||
|
||||
if existing, err := os.ReadFile(path); err == nil && string(existing) == content {
|
||||
return path, nil
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := writeFileAtomic(path, []byte(content), 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// writeTempAppendPromptFile writes the merged prompt content to a
|
||||
// per-spawn temp file under ccDataDir/agent-prompts/, returning the
|
||||
// path. Used only when the prompt has session-specific bits (platform
|
||||
// FormattingInstructions or user-configured append_system_prompt) that
|
||||
// the shared file cannot represent. A unique name from CreateTemp
|
||||
// avoids two concurrent customised sessions overwriting each other.
|
||||
//
|
||||
// The caller is responsible for removing the file on session Close.
|
||||
func writeTempAppendPromptFile(ccDataDir, content string) (string, error) {
|
||||
base := ccDataDir
|
||||
if base == "" {
|
||||
base = os.TempDir()
|
||||
}
|
||||
dir := filepath.Join(base, "agent-prompts")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.CreateTemp(dir, "cc-connect-system-*.md")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// writeFileAtomic writes data to path via a temp file + rename, so a
|
||||
// crash mid-write does not leave a half-written prompt file that the
|
||||
// next spawn would mistake for valid content.
|
||||
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
|
||||
dir := filepath.Dir(path)
|
||||
f, err := os.CreateTemp(dir, ".cc-connect-system-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := f.Name()
|
||||
if _, err := f.Write(data); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Chmod(perm); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildAppendSystemPrompt concatenates the cc-connect functionality prompt,
|
||||
// platform formatting instructions, and the user's custom append prompt into
|
||||
// the single string passed to Claude's --append-system-prompt flag. That flag
|
||||
// only honors its last occurrence (a second flag overwrites the first), so all
|
||||
// appended content must be merged here. Returns "" when nothing is to append.
|
||||
// the single string passed to Claude's --append-system-prompt-file flag.
|
||||
// That flag only honors its last occurrence (a second flag overwrites the
|
||||
// first), so all appended content must be merged here. Returns "" when
|
||||
// nothing is to append.
|
||||
func buildAppendSystemPrompt(agentPrompt, platformPrompt, userAppend string) string {
|
||||
var parts []string
|
||||
if agentPrompt != "" {
|
||||
@@ -90,7 +204,7 @@ func buildAppendSystemPrompt(agentPrompt, platformPrompt, userAppend string) str
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func newClaudeSession(ctx context.Context, workDir, cliBin string, cliExtraArgs []string, cliArgsFlag string, model, effort, sessionID, mode, systemPrompt, appendSystemPrompt string, allowedTools, disallowedTools []string, pluginDirs []string, extraEnv []string, platformPrompt string, disableVerbose bool, spawnOpts core.SpawnOptions, maxContextTokens int) (*claudeSession, error) {
|
||||
func newClaudeSession(ctx context.Context, workDir, cliBin string, cliExtraArgs []string, cliArgsFlag string, model, effort, sessionID, mode, systemPrompt, appendSystemPrompt string, allowedTools, disallowedTools []string, pluginDirs []string, extraEnv []string, platformPrompt string, disableVerbose bool, spawnOpts core.SpawnOptions, maxContextTokens int, ccDataDir string) (*claudeSession, error) {
|
||||
sessionCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// Claude Code rejects bypassPermissions when running as root.
|
||||
@@ -144,11 +258,42 @@ func newClaudeSession(ctx context.Context, workDir, cliBin string, cliExtraArgs
|
||||
}
|
||||
|
||||
// Append the cc-connect functionality prompt, platform formatting hints,
|
||||
// and the user's custom append prompt — all as a single flag. Claude's
|
||||
// --append-system-prompt only honors its last occurrence, so the pieces
|
||||
// must be concatenated here rather than passed as repeated flags.
|
||||
// and the user's custom append prompt — via Claude's
|
||||
// --append-system-prompt-file flag (not --append-system-prompt). Writing
|
||||
// to a file avoids the Windows 8192-byte command-line limit (#1376):
|
||||
// AgentSystemPrompt is ~9KB on its own and grew past the cap in v1.3.3,
|
||||
// so even users with no customization need this workaround.
|
||||
//
|
||||
// Two paths exist to keep the common case zero-overhead:
|
||||
// • 99% case (no platform formatting, no user append) — reuse the
|
||||
// shared cc-connect-system.md file written once at startup; no
|
||||
// per-spawn write, no cleanup needed.
|
||||
// • 1% edge case (Slack/Weixin/MAX platform formatting or user-set
|
||||
// append_system_prompt) — write a per-spawn temp file containing
|
||||
// the merged content, removed on Close.
|
||||
//
|
||||
// Claude only reads the file at startup and never writes it, so the
|
||||
// shared file is safe under concurrent spawns.
|
||||
var promptFilePath string
|
||||
var promptFileIsShared bool
|
||||
if appended := buildAppendSystemPrompt(core.AgentSystemPrompt(), platformPrompt, appendSystemPrompt); appended != "" {
|
||||
innerArgs = append(innerArgs, "--append-system-prompt", appended)
|
||||
if platformPrompt == "" && appendSystemPrompt == "" {
|
||||
path, err := ensureSharedSystemPromptFile(ccDataDir, appended)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("claudeSession: ensure shared prompt file: %w", err)
|
||||
}
|
||||
promptFilePath = path
|
||||
promptFileIsShared = true
|
||||
} else {
|
||||
path, err := writeTempAppendPromptFile(ccDataDir, appended)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("claudeSession: write per-spawn prompt file: %w", err)
|
||||
}
|
||||
promptFilePath = path
|
||||
}
|
||||
innerArgs = append(innerArgs, "--append-system-prompt-file", promptFilePath)
|
||||
}
|
||||
|
||||
if effort != "" {
|
||||
@@ -269,10 +414,22 @@ func newClaudeSession(ctx context.Context, workDir, cliBin string, cliExtraArgs
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
if promptFilePath != "" && !promptFileIsShared {
|
||||
_ = os.Remove(promptFilePath)
|
||||
}
|
||||
cancel()
|
||||
return nil, fmt.Errorf("claudeSession: start: %w", err)
|
||||
}
|
||||
|
||||
// Only remember the prompt path for cleanup when it is the per-spawn
|
||||
// temp variant. The shared cc-connect-system.md file is reused across
|
||||
// all sessions and must never be deleted by an individual session's
|
||||
// Close.
|
||||
var cleanupPromptPath string
|
||||
if !promptFileIsShared {
|
||||
cleanupPromptPath = promptFilePath
|
||||
}
|
||||
|
||||
cs := &claudeSession{
|
||||
cmd: cmd,
|
||||
stdin: stdin,
|
||||
@@ -284,6 +441,7 @@ func newClaudeSession(ctx context.Context, workDir, cliBin string, cliExtraArgs
|
||||
gracefulStopTimeout: 120 * time.Second,
|
||||
ccHooks: newCCPermissionHookRunner(workDir),
|
||||
startupWarning: rootDowngradeWarning,
|
||||
promptFilePath: cleanupPromptPath,
|
||||
}
|
||||
cs.setPermissionMode(mode)
|
||||
cs.sessionID.Store(sessionID)
|
||||
@@ -969,6 +1127,15 @@ func (cs *claudeSession) Alive() bool {
|
||||
}
|
||||
|
||||
func (cs *claudeSession) Close() error {
|
||||
// Best-effort cleanup of the --append-system-prompt-file temp file on
|
||||
// every exit path. The file is small (~9KB) and OS temp cleanup also
|
||||
// eventually claims it, but explicit removal keeps workdirs tidy.
|
||||
defer func() {
|
||||
if cs.promptFilePath != "" {
|
||||
_ = os.Remove(cs.promptFilePath)
|
||||
}
|
||||
}()
|
||||
|
||||
// Phase 1: Close stdin to signal EOF. Claude Code exits cleanly on
|
||||
// stdin close, running Stop hooks (e.g. claude-mem session summary).
|
||||
cs.stdinMu.Lock()
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -457,6 +459,129 @@ func TestBuildAppendSystemPrompt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureSharedSystemPromptFile_WritesOnceAndReuses covers the 99%
|
||||
// case for the #1376 workaround. The cc-connect default
|
||||
// AgentSystemPrompt is written once to <ccDataDir>/agent-prompts/
|
||||
// cc-connect-system.md and reused across spawns — no per-spawn write,
|
||||
// no cleanup. claude only reads the file, so reuse is safe under
|
||||
// concurrent spawns.
|
||||
func TestEnsureSharedSystemPromptFile_WritesOnceAndReuses(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := "## cc-connect prompt\n" + makeFiller(10*1024)
|
||||
|
||||
// First call must create the file.
|
||||
path1, err := ensureSharedSystemPromptFile(dir, content)
|
||||
if err != nil {
|
||||
t.Fatalf("first ensure: %v", err)
|
||||
}
|
||||
if !strings.HasSuffix(filepath.ToSlash(path1), "agent-prompts/cc-connect-system.md") {
|
||||
t.Errorf("path %q does not end in agent-prompts/cc-connect-system.md", path1)
|
||||
}
|
||||
got, err := os.ReadFile(path1)
|
||||
if err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if string(got) != content {
|
||||
t.Fatalf("content mismatch after first write")
|
||||
}
|
||||
stat1, err := os.Stat(path1)
|
||||
if err != nil {
|
||||
t.Fatalf("stat: %v", err)
|
||||
}
|
||||
|
||||
// Second call with identical content must NOT rewrite the file
|
||||
// (mtime stays the same). This is what gives the common case
|
||||
// zero per-spawn overhead.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
path2, err := ensureSharedSystemPromptFile(dir, content)
|
||||
if err != nil {
|
||||
t.Fatalf("second ensure: %v", err)
|
||||
}
|
||||
if path2 != path1 {
|
||||
t.Errorf("path drifted between calls: %q vs %q", path1, path2)
|
||||
}
|
||||
stat2, err := os.Stat(path2)
|
||||
if err != nil {
|
||||
t.Fatalf("stat 2: %v", err)
|
||||
}
|
||||
if !stat1.ModTime().Equal(stat2.ModTime()) {
|
||||
t.Errorf("file was rewritten despite identical content: mtime %v -> %v",
|
||||
stat1.ModTime(), stat2.ModTime())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureSharedSystemPromptFile_RewritesOnContentChange covers
|
||||
// cc-connect upgrades: when AgentSystemPrompt content changes between
|
||||
// releases, the shared file must be refreshed automatically.
|
||||
func TestEnsureSharedSystemPromptFile_RewritesOnContentChange(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if _, err := ensureSharedSystemPromptFile(dir, "v1"); err != nil {
|
||||
t.Fatalf("ensure v1: %v", err)
|
||||
}
|
||||
path, err := ensureSharedSystemPromptFile(dir, "v2 — upgraded prompt")
|
||||
if err != nil {
|
||||
t.Fatalf("ensure v2: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if string(got) != "v2 — upgraded prompt" {
|
||||
t.Fatalf("file did not refresh after content change: got %q", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureSharedSystemPromptFile_EmptyDirUsesTempDir guards the
|
||||
// degraded path where ccDataDir was not injected (e.g. older host
|
||||
// code or test harnesses) — the shared file still lands somewhere
|
||||
// writable instead of failing the spawn.
|
||||
func TestEnsureSharedSystemPromptFile_EmptyDirUsesTempDir(t *testing.T) {
|
||||
path, err := ensureSharedSystemPromptFile("", "hello")
|
||||
if err != nil {
|
||||
t.Fatalf("ensure with empty dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Remove(path) })
|
||||
if !strings.Contains(filepath.ToSlash(path), "/agent-prompts/cc-connect-system.md") {
|
||||
t.Errorf("unexpected fallback path: %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteTempAppendPromptFile_UniquePerCall covers the 1% edge case:
|
||||
// when the prompt includes per-session pieces (platform formatting or
|
||||
// user append) two concurrent spawns must each get their own file so
|
||||
// they cannot overwrite each other's content before claude reads it.
|
||||
func TestWriteTempAppendPromptFile_UniquePerCall(t *testing.T) {
|
||||
// dir is auto-cleaned by t.TempDir(), so per-file Remove is unnecessary.
|
||||
dir := t.TempDir()
|
||||
a, err := writeTempAppendPromptFile(dir, "session A")
|
||||
if err != nil {
|
||||
t.Fatalf("write A: %v", err)
|
||||
}
|
||||
b, err := writeTempAppendPromptFile(dir, "session B")
|
||||
if err != nil {
|
||||
t.Fatalf("write B: %v", err)
|
||||
}
|
||||
if a == b {
|
||||
t.Fatalf("two writeTempAppendPromptFile calls returned the same path %q "+
|
||||
"— concurrent customised sessions would overwrite each other", a)
|
||||
}
|
||||
|
||||
// Files must contain their own content (no cross-talk).
|
||||
gotA, _ := os.ReadFile(a)
|
||||
gotB, _ := os.ReadFile(b)
|
||||
if string(gotA) != "session A" || string(gotB) != "session B" {
|
||||
t.Errorf("cross-talk: A=%q B=%q", string(gotA), string(gotB))
|
||||
}
|
||||
}
|
||||
|
||||
func makeFiller(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = 'a' + byte(i%26)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func helperCommand(ctx context.Context, mode string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestHelperProcess", "--", mode)
|
||||
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
|
||||
|
||||
Reference in new issue
Block a user