diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index f9c7aaf3fd3d4..f018edf453af1 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -395,36 +395,38 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => { }), ); - it.effect("returns the imported project ID through a realpath alias", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const fileSystem = yield* FileSystem.FileSystem; - const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); - const codexHomePath = yield* makeTempDir("t3code-codex-home-"); - const workspace = yield* makeTempDir("t3code-workspace-"); - const linkParent = yield* makeTempDir("t3code-scanner-links-"); - const workspaceAlias = path.join(linkParent, "workspace-alias"); - yield* fileSystem.symlink(workspace, workspaceAlias); + it.effect.skipIf(!symlinksSupported)( + "returns the imported project ID through a realpath alias", + () => + Effect.gen(function* () { + const path = yield* Path.Path; + const fileSystem = yield* FileSystem.FileSystem; + const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); + const codexHomePath = yield* makeTempDir("t3code-codex-home-"); + const workspace = yield* makeTempDir("t3code-workspace-"); + const linkParent = yield* makeTempDir("t3code-scanner-links-"); + const workspaceAlias = path.join(linkParent, "workspace-alias"); + yield* fileSystem.symlink(workspace, workspaceAlias); - yield* writeTranscript({ - filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), - contents: claudeSessionLine(workspaceAlias), - mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), - }); + yield* writeTranscript({ + filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), + contents: claudeSessionLine(workspaceAlias), + mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), + }); - const result = yield* runScan({ - claudeHomePath, - codexHomePath, - importedWorkspaceRoots: [workspace], - }); + const result = yield* runScan({ + claudeHomePath, + codexHomePath, + importedWorkspaceRoots: [workspace], + }); - expect(result.candidates[0]).toMatchObject({ - path: workspace, - projectId: ProjectId.make("project-1"), - alreadyImported: true, - git: null, - }); - }), + expect(result.candidates[0]).toMatchObject({ + path: workspace, + projectId: ProjectId.make("project-1"), + alreadyImported: true, + git: null, + }); + }), ); it.effect("merges case aliases and preserves the persisted project path", () => @@ -980,29 +982,31 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => { }), ); - it.effect("excludes sandboxes reached through a symlink into the worktrees dir", () => - Effect.gen(function* () { - const path = yield* Path.Path; - const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); - const codexHomePath = yield* makeTempDir("t3code-codex-home-"); - const configBaseDir = yield* makeTempDir("t3code-scanner-base-"); - const linkParent = yield* makeTempDir("t3code-scanner-links-"); - const fileSystem = yield* FileSystem.FileSystem; + it.effect.skipIf(!symlinksSupported)( + "excludes sandboxes reached through a symlink into the worktrees dir", + () => + Effect.gen(function* () { + const path = yield* Path.Path; + const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); + const codexHomePath = yield* makeTempDir("t3code-codex-home-"); + const configBaseDir = yield* makeTempDir("t3code-scanner-base-"); + const linkParent = yield* makeTempDir("t3code-scanner-links-"); + const fileSystem = yield* FileSystem.FileSystem; - const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-3"); - yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true }); - const symlinkCwd = path.join(linkParent, "innocent-project"); - yield* fileSystem.symlink(worktreeCwd, symlinkCwd); - yield* writeTranscript({ - filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), - contents: claudeSessionLine(symlinkCwd), - mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), - }); + const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-3"); + yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true }); + const symlinkCwd = path.join(linkParent, "innocent-project"); + yield* fileSystem.symlink(worktreeCwd, symlinkCwd); + yield* writeTranscript({ + filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), + contents: claudeSessionLine(symlinkCwd), + mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), + }); - const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir }); + const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir }); - expect(result.candidates).toEqual([]); - }), + expect(result.candidates).toEqual([]); + }), ); it.effect("finds the cwd on a later line when the first records carry none", () => diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index 347b43dd6f44c..94b1e3ee3dd2f 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -17,6 +17,7 @@ import { UsageLimitSourceId, } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; +import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import { assert, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -340,26 +341,28 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("server settings" }).pipe(Effect.provide(layerServerSettings())), ); - it.effect("saves through a symlinked settings file without replacing the link", () => - Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const service = yield* ServerSettingsModule.ServerSettingsService; - const dotfiles = yield* fs.makeTempDirectoryScoped({ prefix: "t3-settings-dotfiles-" }); - const linkedSettingsPath = path.join(dotfiles, "settings.json"); - yield* fs.writeFileString(linkedSettingsPath, `{ "responseStreamingMode": "turn" }`); - yield* fs.remove(config.settingsPath, { force: true }); - yield* fs.symlink(linkedSettingsPath, config.settingsPath); + it.effect.skipIf(!symlinksSupported)( + "saves through a symlinked settings file without replacing the link", + () => + Effect.gen(function* () { + const config = yield* ServerConfig.ServerConfig; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const service = yield* ServerSettingsModule.ServerSettingsService; + const dotfiles = yield* fs.makeTempDirectoryScoped({ prefix: "t3-settings-dotfiles-" }); + const linkedSettingsPath = path.join(dotfiles, "settings.json"); + yield* fs.writeFileString(linkedSettingsPath, `{ "responseStreamingMode": "turn" }`); + yield* fs.remove(config.settingsPath, { force: true }); + yield* fs.symlink(linkedSettingsPath, config.settingsPath); - yield* service.updateSettings({ responseStreamingMode: "paragraph" }); + yield* service.updateSettings({ responseStreamingMode: "paragraph" }); - assert.equal(yield* fs.readLink(config.settingsPath), linkedSettingsPath); - const persisted = yield* decodeServerSettingsJson( - yield* fs.readFileString(linkedSettingsPath), - ); - assert.equal(persisted.responseStreamingMode, "paragraph"); - }).pipe(Effect.provide(layerServerSettings())), + assert.equal(yield* fs.readLink(config.settingsPath), linkedSettingsPath); + const persisted = yield* decodeServerSettingsJson( + yield* fs.readFileString(linkedSettingsPath), + ); + assert.equal(persisted.responseStreamingMode, "paragraph"); + }).pipe(Effect.provide(layerServerSettings())), ); it.effect("retries a failed settings read instead of keeping the failure", () => diff --git a/packages/provider-acp/src/server/clientPolicy.test.ts b/packages/provider-acp/src/server/clientPolicy.test.ts index 630ebc979e753..18d2c5f29af59 100644 --- a/packages/provider-acp/src/server/clientPolicy.test.ts +++ b/packages/provider-acp/src/server/clientPolicy.test.ts @@ -1,6 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; +import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -122,100 +123,104 @@ describe("acpPermissionDisposition", () => { } }); - it.effect("denies mutations through workspace symlinks that escape the writable roots", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspace = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-acp-permission-workspace-", - }); - const outside = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-acp-permission-outside-", - }); - const outsideFile = path.join(outside, "existing.ts"); - yield* fileSystem.writeFileString(outsideFile, "outside"); - yield* fileSystem.symlink(outsideFile, path.join(workspace, "linked-file.ts")); - yield* fileSystem.symlink(outside, path.join(workspace, "linked-directory")); + it.effect.skipIf(!symlinksSupported)( + "denies mutations through workspace symlinks that escape the writable roots", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workspace = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-permission-workspace-", + }); + const outside = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-permission-outside-", + }); + const outsideFile = path.join(outside, "existing.ts"); + yield* fileSystem.writeFileString(outsideFile, "outside"); + yield* fileSystem.symlink(outsideFile, path.join(workspace, "linked-file.ts")); + yield* fileSystem.symlink(outside, path.join(workspace, "linked-directory")); - const realPolicy: AcpRuntimePolicy = { - runtimeMode: "full-access", - cwd: workspace, - approvalPolicy: "never", - sandboxPolicy: { - type: "workspaceWrite", - writableRoots: [], - networkAccess: false, - }, - }; + const realPolicy: AcpRuntimePolicy = { + runtimeMode: "full-access", + cwd: workspace, + approvalPolicy: "never", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: [], + networkAccess: false, + }, + }; - assert.equal( - acpPermissionDisposition( - realPolicy, - permissionRequest("edit", [{ path: "linked-file.ts" }]), - ), - "deny", - ); - assert.equal( - acpPermissionDisposition( - realPolicy, - permissionRequest("edit", [{ path: "linked-directory/new-file.ts" }]), - ), - "deny", - "a missing leaf below an escaping directory symlink must not be auto-approved", - ); - assert.equal( - acpPermissionDisposition( - realPolicy, - permissionRequest("edit", [{ path: "linked-directory/../escaped-file.ts" }]), - ), - "deny", - "physical symlink traversal must be resolved before parent segments", - ); - }).pipe(Effect.provide(NodeServices.layer)), + assert.equal( + acpPermissionDisposition( + realPolicy, + permissionRequest("edit", [{ path: "linked-file.ts" }]), + ), + "deny", + ); + assert.equal( + acpPermissionDisposition( + realPolicy, + permissionRequest("edit", [{ path: "linked-directory/new-file.ts" }]), + ), + "deny", + "a missing leaf below an escaping directory symlink must not be auto-approved", + ); + assert.equal( + acpPermissionDisposition( + realPolicy, + permissionRequest("edit", [{ path: "linked-directory/../escaped-file.ts" }]), + ), + "deny", + "physical symlink traversal must be resolved before parent segments", + ); + }).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("allows existing and new files beneath canonical writable roots", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspace = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-acp-permission-workspace-", - }); - const workspaceLinkParent = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-acp-permission-link-parent-", - }); - const workspaceLink = path.join(workspaceLinkParent, "workspace-link"); - yield* fileSystem.makeDirectory(path.join(workspace, "src"), { recursive: true }); - yield* fileSystem.writeFileString(path.join(workspace, "src", "existing.ts"), "existing"); - yield* fileSystem.symlink(workspace, workspaceLink); + it.effect.skipIf(!symlinksSupported)( + "allows existing and new files beneath canonical writable roots", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workspace = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-permission-workspace-", + }); + const workspaceLinkParent = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-permission-link-parent-", + }); + const workspaceLink = path.join(workspaceLinkParent, "workspace-link"); + yield* fileSystem.makeDirectory(path.join(workspace, "src"), { recursive: true }); + yield* fileSystem.writeFileString(path.join(workspace, "src", "existing.ts"), "existing"); + yield* fileSystem.symlink(workspace, workspaceLink); - const realPolicy: AcpRuntimePolicy = { - runtimeMode: "full-access", - cwd: workspaceLink, - approvalPolicy: "never", - sandboxPolicy: { - type: "workspaceWrite", - writableRoots: [], - networkAccess: false, - }, - }; + const realPolicy: AcpRuntimePolicy = { + runtimeMode: "full-access", + cwd: workspaceLink, + approvalPolicy: "never", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: [], + networkAccess: false, + }, + }; - assert.equal( - acpPermissionDisposition( - realPolicy, - permissionRequest("edit", [{ path: "src/existing.ts" }]), - ), - "allow", - ); - assert.equal( - acpPermissionDisposition( - realPolicy, - permissionRequest("edit", [{ path: "src/generated/new-file.ts" }]), - ), - "allow", - "non-existent descendants of a real in-root ancestor remain writable", - ); - }).pipe(Effect.provide(NodeServices.layer)), + assert.equal( + acpPermissionDisposition( + realPolicy, + permissionRequest("edit", [{ path: "src/existing.ts" }]), + ), + "allow", + ); + assert.equal( + acpPermissionDisposition( + realPolicy, + permissionRequest("edit", [{ path: "src/generated/new-file.ts" }]), + ), + "allow", + "non-existent descendants of a real in-root ancestor remain writable", + ); + }).pipe(Effect.provide(NodeServices.layer)), ); }); diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts index 6139bc615a006..429a09efc4aa5 100644 --- a/packages/provider-opencode/src/server/usage.test.ts +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest"; import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; +import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -314,7 +315,7 @@ describe("readOpenCodeUsage", () => { }).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("never follows symlinks in the legacy OpenCode store", () => + it.effect.skipIf(!symlinksSupported)("never follows symlinks in the legacy OpenCode store", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -370,56 +371,59 @@ describe("readOpenCodeUsage", () => { import * as NodeSqlite from "node:sqlite"; describe("openCodeUsageReader", () => { - it.effect("scans canonical instance roots once, including XDG and home defaults", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-instances-test-" }); - const home = yield* fileSystem.realPath(temp); - const defaultRoot = path.join(home, ".local", "share", "opencode"); - const instanceRoot = path.join(home, "instance"); - const dataHome = path.join(home, "xdg"); - const xdgRoot = path.join(dataHome, "opencode"); - const alias = path.join(home, "default-alias"); - for (const root of [defaultRoot, instanceRoot, xdgRoot]) { - yield* fileSystem.makeDirectory(root, { recursive: true }); - } - yield* fileSystem.symlink(defaultRoot, alias); - const instances = [ - { id: "opencode", environment: {}, configured: false }, - { - id: "opencode-list", - environment: { OPENCODE_DATA_DIR: ` ${instanceRoot}, ${alias}, ${defaultRoot}, , ` }, - configured: true, - }, - { - id: "opencode-xdg", - environment: { XDG_DATA_HOME: dataHome }, - configured: true, - }, - ]; - if (openCodeUsageReader.kind !== "scan") throw new Error("Expected an OpenCode scan reader"); - const sources = yield* openCodeUsageReader - .scan({ - instances: instances.map(({ id, environment, configured }) => ({ - instanceId: ProviderInstanceId.make(id), - config: undefined, - environment, - configured, - })), - settings: DEFAULT_SERVER_SETTINGS, - windowStartMs: 0, - retentionCutoffMs: 0, - awaitRefresh: true, - }) - .pipe( - Effect.provideService(HostProcess.Environment, {}), - Effect.provideService(HostProcess.HomeDirectory, home), + it.effect.skipIf(!symlinksSupported)( + "scans canonical instance roots once, including XDG and home defaults", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-instances-test-" }); + const home = yield* fileSystem.realPath(temp); + const defaultRoot = path.join(home, ".local", "share", "opencode"); + const instanceRoot = path.join(home, "instance"); + const dataHome = path.join(home, "xdg"); + const xdgRoot = path.join(dataHome, "opencode"); + const alias = path.join(home, "default-alias"); + for (const root of [defaultRoot, instanceRoot, xdgRoot]) { + yield* fileSystem.makeDirectory(root, { recursive: true }); + } + yield* fileSystem.symlink(defaultRoot, alias); + const instances = [ + { id: "opencode", environment: {}, configured: false }, + { + id: "opencode-list", + environment: { OPENCODE_DATA_DIR: ` ${instanceRoot}, ${alias}, ${defaultRoot}, , ` }, + configured: true, + }, + { + id: "opencode-xdg", + environment: { XDG_DATA_HOME: dataHome }, + configured: true, + }, + ]; + if (openCodeUsageReader.kind !== "scan") + throw new Error("Expected an OpenCode scan reader"); + const sources = yield* openCodeUsageReader + .scan({ + instances: instances.map(({ id, environment, configured }) => ({ + instanceId: ProviderInstanceId.make(id), + config: undefined, + environment, + configured, + })), + settings: DEFAULT_SERVER_SETTINGS, + windowStartMs: 0, + retentionCutoffMs: 0, + awaitRefresh: true, + }) + .pipe( + Effect.provideService(HostProcess.Environment, {}), + Effect.provideService(HostProcess.HomeDirectory, home), + ); + assert.deepStrictEqual( + sources.map((source) => source.dir).sort(), + [defaultRoot, instanceRoot, xdgRoot].sort(), ); - assert.deepStrictEqual( - sources.map((source) => source.dir).sort(), - [defaultRoot, instanceRoot, xdgRoot].sort(), - ); - }).pipe(Effect.provide(NodeServices.layer)), + }).pipe(Effect.provide(NodeServices.layer)), ); });