diff --git a/.tutti/dev-app/run-local-debug.mjs b/.tutti/dev-app/run-local-debug.mjs index 0084ce4..20cc2af 100644 --- a/.tutti/dev-app/run-local-debug.mjs +++ b/.tutti/dev-app/run-local-debug.mjs @@ -48,7 +48,6 @@ const baseEnv = { GROUP_CHAT_WEB_DIST: webDist, GROUP_CHAT_SERVER_URL: baseUrl, GROUP_CHAT_TOOL_BASE_URL: baseUrl, - GROUP_CHAT_WORKSPACE_ROOT: process.env.TUTTI_WORKSPACE_ROOT || projectRoot, }; function run(command, args, options = {}) { diff --git a/apps/server/src/domains/run-file-artifacts.ts b/apps/server/src/domains/run-file-artifacts.ts index 1ae3bc6..fc31c4e 100644 --- a/apps/server/src/domains/run-file-artifacts.ts +++ b/apps/server/src/domains/run-file-artifacts.ts @@ -33,7 +33,7 @@ const BINARY_EXTENSIONS = new Map([ [".webp", "image/webp"], ]); -const INTERNAL_AGENT_WORKSPACE_ROOT_FILES = new Set([ +const INTERNAL_AGENT_WORKSPACE_FILES = new Set([ "AGENTS.MD", "BOOTSTRAP.MD", "CLAUDE.MD", @@ -76,7 +76,7 @@ export function shouldImportRunFileArtifactPath( const [firstSegment, ...rest] = relativePath.split("/"); if (!firstSegment) return true; if (INTERNAL_AGENT_WORKSPACE_DIRS.has(firstSegment.toLowerCase())) return false; - return rest.length > 0 || !INTERNAL_AGENT_WORKSPACE_ROOT_FILES.has(firstSegment.toUpperCase()); + return rest.length > 0 || !INTERNAL_AGENT_WORKSPACE_FILES.has(firstSegment.toUpperCase()); } export function formatFileReferenceMarkdown(artifact: Pick) { diff --git a/apps/server/src/runtimes/local-agent-provider.ts b/apps/server/src/runtimes/local-agent-provider.ts index d2c621b..f79a6d5 100644 --- a/apps/server/src/runtimes/local-agent-provider.ts +++ b/apps/server/src/runtimes/local-agent-provider.ts @@ -7,6 +7,8 @@ import { createDefaultLocalAgentProviderPlugins, createDefaultLocalAgentRuntime, createManagedAgentRunContextFromHeaders, + getManagedAgentInvocationCredentialFromHeaders, + isManagedAgentInvocationProviderId, type AgentEvent, type AgentRunMessage, type LocalAgentMcpServerConfig, @@ -95,8 +97,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { async listLocalAgentTargets(detectContext?: DetectContext): Promise { const env = buildLocalAgentProcessEnv(process.env, tuttiCliEnv()); + const cwd = detectContext?.cwd?.trim() || process.cwd(); const catalog = await loadTuttiAgentCatalog({ runtime: this.localAgentRuntime, + cwd, env, detectContext, commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"], @@ -112,6 +116,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { composer = await loadTuttiAgentComposerOptions({ runtime: this.localAgentRuntime, agentTargetId: agent.agentTargetId, + cwd, env, detectContext, commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"], @@ -154,13 +159,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { } async *streamReply(context: RuntimeReplyContext) { - const target = await this.resolveExactTargetForRun(context); - if (!context.managedAgentRunContext && context.managedAgentHeaders && context.runId) { - context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders(context.managedAgentHeaders, { - providerId: target.providerId, - runId: context.runId, - }); - } + const { target } = await this.prepareEffectiveRunContext( + context, + localAgentRuntimeRunId(context), + ); const command = resolveLocalAgentCommand(target.providerId); if (command) { yield* this.streamCommandBridge(context, command); @@ -173,6 +175,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { private async resolveExactTarget(agentTargetId: string, detectContext?: DetectContext) { const catalog = await loadTuttiAgentCatalog({ runtime: this.localAgentRuntime, + ...(detectContext?.cwd ? { cwd: detectContext.cwd } : {}), env: buildLocalAgentProcessEnv(process.env, tuttiCliEnv()), detectContext, commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"], @@ -199,11 +202,38 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { return target; } + private async prepareEffectiveRunContext(context: RuntimeReplyContext, runId: string) { + const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id); + const provisionalProvider = canonicalProviderId(context.runtimeProfile?.provider); + if (provisionalProvider && isManagedAgentInvocationProviderId(provisionalProvider)) { + await ensureManagedAgentRunContext(context, provisionalProvider, runId); + } + + let runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot); + context.agentDetectContext = localAgentRunDetectContext(context, runCwd); + let target = await this.resolveExactTargetForRun(context); + + if (!context.managedAgentRunContext && hasManagedAgentCredential(context)) { + await ensureManagedAgentRunContext(context, target.providerId, runId); + runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot); + context.agentDetectContext = localAgentRunDetectContext(context, runCwd); + target = await this.resolveExactTargetForRun(context); + } + + return { + target, + workspaceRoot, + runCwd, + extraAllowedDirs: localAgentAllowedDirs(runCwd, workspaceRoot), + }; + } + private async resolveUniqueLegacyProviderTarget(providerId: string | null | undefined, detectContext?: DetectContext) { const normalizedProviderId = canonicalProviderId(providerId); if (!normalizedProviderId) throw new RuntimeProviderUnsupportedError("Exact Agent target is required."); const catalog = await loadTuttiAgentCatalog({ runtime: this.localAgentRuntime, + ...(detectContext?.cwd ? { cwd: detectContext.cwd } : {}), env: buildLocalAgentProcessEnv(process.env, tuttiCliEnv()), detectContext, commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"], @@ -230,7 +260,9 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { } async compactContext(context: RuntimeReplyContext) { - const target = await this.resolveExactTargetForRun(context); + const runId = `context-compact-${context.conversation.id}-${context.participant.id}-${Date.now()}`; + const prepared = await this.prepareEffectiveRunContext(context, runId); + const { target, workspaceRoot, runCwd, extraAllowedDirs } = prepared; const provider = target.providerId; const agentTargetId = target.agentTargetId; if (resolveLocalAgentCommand(provider)) { @@ -239,7 +271,6 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { if (!this.localAgentRuntime.listProviders().some((item) => item.id === provider)) { throw new Error(`${provider} local agent provider is not registered.`); } - const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id); const sessionStore = new LocalAgentSessionStore(workspaceRoot); const previousSession = sessionStore.read(context.conversation.id); if (!previousSession @@ -252,13 +283,6 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { } const controller = new AbortController(); - const runId = `context-compact-${context.conversation.id}-${context.participant.id}-${Date.now()}`; - if (!context.managedAgentRunContext && context.managedAgentHeaders) { - context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders(context.managedAgentHeaders, { - providerId: target.providerId, - runId, - }); - } const resume = { mode: "provider" as const, ...(previousSession.providerSessionId ? { providerSessionId: previousSession.providerSessionId } : {}), @@ -271,14 +295,14 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { provider, runtimeKind: "local-agent", runtimeProvider: provider, - cwd: context.managedAgentRunContext?.cwd ?? workspaceRoot, + cwd: runCwd, prompt: "/compact", model: localAgentModelIdForAcp(context.runtimeProfile?.model ?? previousSession.model ?? "default", provider), reasoning: context.participant.reasoningEffort ?? undefined, - env: buildLocalAgentRunEnv({ ...context, runId }, workspaceRoot), + env: buildLocalAgentRunEnv({ ...context, runId }), managedAgentInvocation: context.managedAgentRunContext?.managedAgentInvocation, metadata: context.participant.speedMode ? { speedMode: context.participant.speedMode } : undefined, - extraAllowedDirs: [workspaceRoot], + extraAllowedDirs, resume, signal: controller.signal, })) { @@ -353,9 +377,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { envOverrides?: Record, ) { const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id); + const runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot); const child = spawn(command, { - cwd: workspaceRoot, - env: buildLocalAgentRunEnv(context, workspaceRoot, envOverrides), + cwd: runCwd, + env: buildLocalAgentRunEnv(context, envOverrides), shell: true, stdio: "pipe", }); @@ -411,6 +436,8 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { legacyProviderUnique: boolean, ) { const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id); + const runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot); + const extraAllowedDirs = localAgentAllowedDirs(runCwd, workspaceRoot); const controller = new AbortController(); if (context.runId) { this.processes.set(context.runId, { @@ -426,7 +453,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { const previousSession = sessionStore.read(context.conversation.id); const input = buildLocalAgentInput(context); const timeoutMs = localAgentTimeoutMs(); - const runtimeRunId = context.runId ?? `${context.conversation.id}:${context.participant.id}`; + const runtimeRunId = localAgentRuntimeRunId(context); let skillFallbackEnv: Record | undefined; let skillContext = emptyTuttiAgentSkillContext(agentTargetId, provider, runtimeRunId); if (shouldLoadGroupChatAgentSkillContext(context, input)) { @@ -435,7 +462,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { agentTargetId, provider, agentSessionId: runtimeRunId, - workspaceRoot, + cwd: runCwd, }); } catch (error) { if (provider === "codex" && isSkillLoadFailure(error)) { @@ -446,7 +473,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { agentTargetId, provider, agentSessionId: runtimeRunId, - workspaceRoot, + cwd: runCwd, envOverrides: skillFallbackEnv, }); } catch (fallbackError) { @@ -491,7 +518,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { provider, runtimeKind: "local-agent", runtimeProvider: provider, - cwd: context.managedAgentRunContext?.cwd ?? workspaceRoot, + cwd: runCwd, prompt, systemPrompt, history: buildKitHistory(context, historyLimit), @@ -499,11 +526,11 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider { reasoning: context.participant.reasoningEffort ?? undefined, ...(mcpServers ? { mcpServers } : {}), ...(skillManifest ? { skillManifest } : {}), - env: buildLocalAgentRunEnv(context, workspaceRoot, skillFallbackEnv), + env: buildLocalAgentRunEnv(context, skillFallbackEnv), managedAgentInvocation: context.managedAgentRunContext?.managedAgentInvocation, metadata: context.participant.speedMode ? { speedMode: context.participant.speedMode } : undefined, ...(timeoutMs ? { timeoutMs } : {}), - extraAllowedDirs: [workspaceRoot], + extraAllowedDirs, resume, signal: controller.signal, })) { @@ -597,6 +624,62 @@ class LocalAgentCommandError extends Error { } } +function localAgentRuntimeRunId(context: RuntimeReplyContext) { + return context.runId ?? `${context.conversation.id}:${context.participant.id}`; +} + +async function ensureManagedAgentRunContext( + context: RuntimeReplyContext, + providerId: string, + runId: string, +) { + if (context.managedAgentRunContext || !hasManagedAgentCredential(context)) return; + context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders( + context.managedAgentHeaders, + { providerId, runId }, + ); +} + +function hasManagedAgentCredential(context: RuntimeReplyContext) { + return Boolean(getManagedAgentInvocationCredentialFromHeaders(context.managedAgentHeaders)); +} + +function effectiveLocalAgentRunCwd( + context: RuntimeReplyContext, + participantWorkspaceRoot: string, +) { + return context.managedAgentRunContext?.cwd ?? participantWorkspaceRoot; +} + +function localAgentRunDetectContext( + context: RuntimeReplyContext, + runCwd: string, +): DetectContext { + const managedAgentInvocation = context.managedAgentRunContext?.managedAgentInvocation; + const credential = managedAgentInvocation?.credential; + return { + ...(context.agentDetectContext ?? {}), + cwd: runCwd, + ...(managedAgentInvocation ? { managedAgentInvocation } : {}), + ...(credential + ? { + redactionSecrets: Array.from(new Set([ + ...(context.agentDetectContext?.redactionSecrets ?? []), + credential, + ])), + } + : {}), + }; +} + +function localAgentAllowedDirs(runCwd: string, participantWorkspaceRoot: string) { + const normalizedRunCwd = resolve(runCwd); + const normalizedParticipantWorkspaceRoot = resolve(participantWorkspaceRoot); + return normalizedRunCwd === normalizedParticipantWorkspaceRoot + ? [normalizedRunCwd] + : [normalizedRunCwd, normalizedParticipantWorkspaceRoot]; +} + function localAgentTimeoutMs() { const raw = process.env.GROUP_CHAT_LOCAL_AGENT_TIMEOUT_MS; if (raw === undefined || raw.trim() === "") return undefined; @@ -606,12 +689,10 @@ function localAgentTimeoutMs() { function buildLocalAgentRunEnv( context: RuntimeReplyContext, - workspaceRoot: string, overrides?: Record, ): Record { return { ...buildLocalAgentProcessEnv(process.env, { ...tuttiCliEnv(), ...overrides }), - GROUP_CHAT_WORKSPACE: workspaceRoot, GROUP_CHAT_RUN_ID: context.runId ?? "", GROUP_CHAT_PARTICIPANT_ID: context.participant.id, GROUP_CHAT_CONVERSATION_ID: context.conversation.id, @@ -864,7 +945,7 @@ async function loadGroupChatAgentSkillContext(input: { agentTargetId: string; provider: string; agentSessionId: string; - workspaceRoot: string; + cwd: string; envOverrides?: Record; }): Promise { try { @@ -872,7 +953,7 @@ async function loadGroupChatAgentSkillContext(input: { return await loadTuttiAgentSkillContext({ agentTargetId: input.agentTargetId, agentSessionId: input.agentSessionId, - cwd: tuttiWorkspaceCwd(input.workspaceRoot), + cwd: input.cwd, env, commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"], }); @@ -896,10 +977,6 @@ function emptyTuttiAgentSkillContext( }; } -function tuttiWorkspaceCwd(fallback: string) { - return process.env.TUTTI_WORKSPACE_ROOT?.trim() || process.env.GROUP_CHAT_WORKSPACE_ROOT?.trim() || fallback; -} - function joinPromptParts(...parts: Array) { return parts .map((part) => part?.trim()) diff --git a/scripts/package-tutti-app.mjs b/scripts/package-tutti-app.mjs index 2aaa2f3..89fe65a 100644 --- a/scripts/package-tutti-app.mjs +++ b/scripts/package-tutti-app.mjs @@ -87,7 +87,6 @@ export PORT="\${TUTTI_APP_PORT:-8788}" export GROUP_CHAT_APP_VERSION="${version}" export GROUP_CHAT_WEB_DIST="$package_dir/dist" export GROUP_CHAT_HOME="\${TUTTI_APP_DATA_DIR:-$package_dir/.data}" -export GROUP_CHAT_WORKSPACE_ROOT="\${TUTTI_WORKSPACE_ROOT:-$GROUP_CHAT_HOME}" base_url="\${TUTTI_APP_BASE_URL:-http://$HOST:$PORT}" export GROUP_CHAT_SERVER_URL="$base_url" diff --git a/tests/local-agent-effective-cwd.test.mjs b/tests/local-agent-effective-cwd.test.mjs new file mode 100644 index 0000000..074b3f1 --- /dev/null +++ b/tests/local-agent-effective-cwd.test.mjs @@ -0,0 +1,254 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import test from "node:test"; + +const execFileAsync = promisify(execFile); +const providerModuleUrl = new URL("../apps/server/src/runtimes/local-agent-provider.ts", import.meta.url).href; + +test("managed and standalone runs use one effective cwd for detection, skills, runtime, and compaction", async () => { + const home = await mkdtemp(join(tmpdir(), "group-chat-effective-cwd-")); + const fakeTutti = join(home, "fake-tutti.mjs"); + const cliLog = join(home, "cli-cwds.jsonl"); + const checkScript = join(home, "check-effective-cwd.ts"); + + await Promise.all([ + mkdir(join(home, "rooms", "room-standalone", "agents", "participant-standalone"), { recursive: true }), + mkdir(join(home, "rooms", "room-managed", "agents", "participant-managed"), { recursive: true }), + ]); + + await writeFile( + fakeTutti, + `#!/usr/bin/env node + const { appendFileSync } = await import("node:fs"); + appendFileSync(${JSON.stringify(cliLog)}, JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd() }) + "\\n"); + if (process.argv.includes("list")) { + console.log(JSON.stringify({ + schemaVersion: 1, + defaultAgentTargetId: "agent-1", + agents: [{ + id: "agent-1", + provider: "codex", + name: "Primary Agent", + availability: { status: "available", reasonCode: "ready", detail: "" } + }] + })); + } else { + console.log(JSON.stringify({ + provider: "codex", + agentTargetId: "agent-1", + recommendedSystemPrompt: { content: "Use the injected workspace app skill." }, + skills: [] + })); + } + `, + ); + await chmod(fakeTutti, 0o755); + + await writeFile( + checkScript, + ` + import assert from "node:assert/strict"; + import { readFile, realpath } from "node:fs/promises"; + import { join, resolve } from "node:path"; + + process.env.GROUP_CHAT_HOME = ${JSON.stringify(home)}; + process.env.TUTTI_APP_DATA_DIR = ${JSON.stringify(home)}; + process.env.GROUP_CHAT_TUTTI_CLI = ${JSON.stringify(fakeTutti)}; + delete process.env.GROUP_CHAT_LOCAL_AGENT_COMMAND; + delete process.env.GROUP_CHAT_LOCAL_AGENT_CODEX_COMMAND; + + const credentialHeaders = { "x-tsh-managed-agent-credential": "managed-secret" }; + + function createContext(suffix, managed) { + const conversationId = \`conversation-\${suffix}\`; + const roomId = \`room-\${suffix}\`; + const participantId = \`participant-\${suffix}\`; + const content = "[Vibe Design](mention://workspace-app/vibe-design) build a page"; + const userMessage = { + id: \`message-\${suffix}\`, + conversationId, + role: "user", + senderParticipantId: null, + senderName: "User", + content, + mentions: [{ + mentionType: "reference", + referenceProviderId: "workspace-app", + referenceEntityId: "vibe-design", + displayNameSnapshot: "Vibe Design", + }], + visibility: "public", + status: "success", + branchId: null, + parentMessageId: null, + runId: null, + tokenUsage: null, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + return { + runId: \`run-\${suffix}\`, + conversation: { + id: conversationId, + roomId, + type: "group", + title: "Effective cwd", + groupSystemPrompt: "", + collaborationRules: "", + collaborationRulesVersion: 1, + replyPolicy: { mode: "mentioned", order: "sequential", maxRounds: 1, mentionFollowupRounds: 0 }, + activeBranchId: null, + pinned: false, + lastMessage: content, + lastMessageAt: "2026-07-19T00:00:00.000Z", + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }, + participant: { + id: participantId, + conversationId, + kind: "ai", + displayName: "Agent", + avatar: null, + runtimeProfileId: "local-agent:codex", + agentTargetId: "agent-1", + identityId: null, + roomInstructions: "", + status: "active", + listenMode: "passive", + sortOrder: 0, + reasoningEffort: null, + speedMode: null, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }, + identity: null, + runtimeProfile: { + id: "local-agent:codex", + kind: "local-agent", + provider: "codex", + agentTargetId: "agent-1", + model: "codex:default", + displayName: "Codex", + enabled: true, + trustedMode: false, + systemPromptMode: "prompt-prefix", + capabilities: { streaming: true, toolUse: true, reasoning: true, vision: false, resume: true }, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }, + userMessage, + recentMessages: [], + attachments: [], + ...(managed + ? { + managedAgentHeaders: credentialHeaders, + } + : {}), + }; + } + + async function main() { + const { LocalAgentRuntimeProvider } = await import(${JSON.stringify(providerModuleUrl)}); + const runtimeInputs = []; + const fakeRuntime = { + detect: async () => [], + listProviders: () => [{ id: "codex" }], + cancel: async () => undefined, + async *run(input) { + runtimeInputs.push(input); + yield { type: "text_delta", text: "ok" }; + yield { type: "done", status: "completed", sessionId: "provider-session-1" }; + }, + }; + + async function runCase(suffix, managed) { + const provider = new LocalAgentRuntimeProvider(); + provider.localAgentRuntime = fakeRuntime; + const context = createContext(suffix, managed); + for await (const _event of provider.streamReply(context)) { + // Drain the stream so the session is persisted for compaction. + } + return { provider, context, input: runtimeInputs.at(-1) }; + } + + const standalone = await runCase("standalone", false); + const standaloneWorkspace = resolve(join( + ${JSON.stringify(home)}, + "rooms", + standalone.context.conversation.roomId, + "agents", + standalone.context.participant.id, + )); + assert.equal(standalone.input.cwd, standaloneWorkspace); + assert.deepEqual(standalone.input.extraAllowedDirs, [standaloneWorkspace]); + assert.equal("GROUP_CHAT_WORKSPACE" in standalone.input.env, false); + + const managed = await runCase("managed", true); + const managedWorkspace = resolve(join( + ${JSON.stringify(home)}, + "rooms", + managed.context.conversation.roomId, + "agents", + managed.context.participant.id, + )); + const managedCwd = resolve(managed.context.managedAgentRunContext.cwd); + assert.match(managedCwd, /\\.agent-runs\\//); + assert.equal(managed.input.cwd, managedCwd); + assert.equal(managed.input.managedAgentInvocation.cwd, managedCwd); + assert.deepEqual(managed.input.extraAllowedDirs, [managedCwd, managedWorkspace]); + assert.equal("GROUP_CHAT_WORKSPACE" in managed.input.env, false); + + const cliCalls = (await readFile(${JSON.stringify(cliLog)}, "utf8")) + .trim() + .split("\\n") + .map((line) => JSON.parse(line)); + const skillCalls = cliCalls.filter((call) => !call.args.includes("list")); + const detectCalls = cliCalls.filter((call) => call.args.includes("list")); + assert.equal(detectCalls.length, 4, JSON.stringify(cliCalls)); + for (const call of detectCalls.slice(0, 2)) { + assert.equal(await realpath(call.cwd), await realpath(standalone.input.cwd)); + } + for (const call of detectCalls.slice(2)) { + assert.equal(await realpath(call.cwd), await realpath(managed.input.cwd)); + } + assert.ok(skillCalls.length >= 2, JSON.stringify(cliCalls)); + assert.equal(await realpath(skillCalls.at(-2).cwd), await realpath(standalone.input.cwd)); + assert.equal(await realpath(skillCalls.at(-1).cwd), await realpath(managed.input.cwd)); + + const compactContext = { + ...managed.context, + runId: undefined, + managedAgentRunContext: undefined, + agentDetectContext: undefined, + }; + await managed.provider.compactContext(compactContext); + const compactInput = runtimeInputs.at(-1); + const compactCwd = resolve(compactContext.managedAgentRunContext.cwd); + assert.equal(compactInput.prompt, "/compact"); + assert.equal(compactInput.cwd, compactCwd); + assert.equal(compactInput.managedAgentInvocation.cwd, compactCwd); + assert.deepEqual(compactInput.extraAllowedDirs, [compactCwd, managedWorkspace]); + assert.equal("GROUP_CHAT_WORKSPACE" in compactInput.env, false); + } + + main().catch((error) => { + console.error(error); + process.exit(1); + }); + `, + ); + + try { + await execFileAsync("pnpm", ["--filter", "@group-chat/server", "exec", "tsx", checkScript], { + cwd: new URL("..", import.meta.url), + env: { ...process.env, GROUP_CHAT_HOME: home, TUTTI_APP_DATA_DIR: home }, + }); + } finally { + await rm(dirname(checkScript), { recursive: true, force: true }); + } +}); diff --git a/tests/package-tutti-app.test.mjs b/tests/package-tutti-app.test.mjs index 53c1cf7..f8edcd5 100644 --- a/tests/package-tutti-app.test.mjs +++ b/tests/package-tutti-app.test.mjs @@ -89,7 +89,7 @@ test("bootstrap maps Tutti runtime env into Group Chat env", () => { assert.match(bootstrap, /export PORT="\$\{TUTTI_APP_PORT:-8788\}"/); assert.match(bootstrap, /export GROUP_CHAT_APP_VERSION="9\.8\.7"/); assert.match(bootstrap, /export GROUP_CHAT_HOME="\$\{TUTTI_APP_DATA_DIR:-\$package_dir\/\.data\}"/); - assert.match(bootstrap, /export GROUP_CHAT_WORKSPACE_ROOT="\$\{TUTTI_WORKSPACE_ROOT:-\$GROUP_CHAT_HOME\}"/); + assert.doesNotMatch(bootstrap, /WORKSPACE_ROOT/); assert.match(bootstrap, /node_bin="\$\{TUTTI_APP_NODE:-node\}"/); assert.match(bootstrap, /exec "\$node_bin" "\$package_dir\/server\/server\.js"/); });