Skip to content

Commit 5a14441

Browse files
Tighten types
Co-Authored-By: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
1 parent 210ec8e commit 5a14441

7 files changed

Lines changed: 153 additions & 7 deletions

File tree

dotnet/src/Types.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,22 +234,18 @@ public sealed class SessionFsConfig
234234
/// <summary>
235235
/// Initial working directory for sessions (user's project directory).
236236
/// </summary>
237-
public string InitialCwd { get; set; } = string.Empty;
237+
public required string InitialCwd { get; init; }
238238

239239
/// <summary>
240240
/// Path within each session's SessionFs where the runtime stores
241241
/// session-scoped files (events, workspace, checkpoints, and temp files).
242242
/// </summary>
243-
public string SessionStatePath { get; set; } = string.Empty;
243+
public required string SessionStatePath { get; init; }
244244

245245
/// <summary>
246246
/// Path conventions used by this filesystem provider.
247-
/// Defaults to the conventions of the current operating system.
248247
/// </summary>
249-
public SessionFsSetProviderRequestConventions Conventions { get; set; }
250-
= OperatingSystem.IsWindows()
251-
? SessionFsSetProviderRequestConventions.Windows
252-
: SessionFsSetProviderRequestConventions.Posix;
248+
public required SessionFsSetProviderRequestConventions Conventions { get; init; }
253249
}
254250

255251
/// <summary>

go/client.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ import (
5353

5454
const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"
5555

56+
func validateSessionFsConfig(config *SessionFsConfig) error {
57+
if config == nil {
58+
return nil
59+
}
60+
if config.InitialCwd == "" {
61+
return errors.New("SessionFs.InitialCwd is required")
62+
}
63+
if config.SessionStatePath == "" {
64+
return errors.New("SessionFs.SessionStatePath is required")
65+
}
66+
if config.Conventions != rpc.ConventionsPosix && config.Conventions != rpc.ConventionsWindows {
67+
return errors.New("SessionFs.Conventions must be either 'posix' or 'windows'")
68+
}
69+
return nil
70+
}
71+
5672
// Client manages the connection to the Copilot CLI server and provides session management.
5773
//
5874
// The Client can either spawn a CLI server process or connect to an existing server.
@@ -193,6 +209,9 @@ func NewClient(options *ClientOptions) *Client {
193209
client.onListModels = options.OnListModels
194210
}
195211
if options.SessionFs != nil {
212+
if err := validateSessionFsConfig(options.SessionFs); err != nil {
213+
panic(err.Error())
214+
}
196215
sessionFs := *options.SessionFs
197216
opts.SessionFs = &sessionFs
198217
}

go/client_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"regexp"
1010
"sync"
1111
"testing"
12+
13+
"github.com/github/copilot-sdk/go/rpc"
1214
)
1315

1416
// This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead
@@ -223,6 +225,48 @@ func TestClient_URLParsing(t *testing.T) {
223225
})
224226
}
225227

228+
func TestClient_SessionFsConfig(t *testing.T) {
229+
t.Run("should throw error when InitialCwd is missing", func(t *testing.T) {
230+
defer func() {
231+
if r := recover(); r == nil {
232+
t.Error("Expected panic for missing SessionFs.InitialCwd")
233+
} else {
234+
matched, _ := regexp.MatchString("SessionFs.InitialCwd is required", r.(string))
235+
if !matched {
236+
t.Errorf("Expected panic message to contain 'SessionFs.InitialCwd is required', got: %v", r)
237+
}
238+
}
239+
}()
240+
241+
NewClient(&ClientOptions{
242+
SessionFs: &SessionFsConfig{
243+
SessionStatePath: "/session-state",
244+
Conventions: rpc.ConventionsPosix,
245+
},
246+
})
247+
})
248+
249+
t.Run("should throw error when SessionStatePath is missing", func(t *testing.T) {
250+
defer func() {
251+
if r := recover(); r == nil {
252+
t.Error("Expected panic for missing SessionFs.SessionStatePath")
253+
} else {
254+
matched, _ := regexp.MatchString("SessionFs.SessionStatePath is required", r.(string))
255+
if !matched {
256+
t.Errorf("Expected panic message to contain 'SessionFs.SessionStatePath is required', got: %v", r)
257+
}
258+
}
259+
}()
260+
261+
NewClient(&ClientOptions{
262+
SessionFs: &SessionFsConfig{
263+
InitialCwd: "/",
264+
Conventions: rpc.ConventionsPosix,
265+
},
266+
})
267+
})
268+
}
269+
226270
func TestClient_AuthOptions(t *testing.T) {
227271
t.Run("should accept GitHubToken option", func(t *testing.T) {
228272
client := NewClient(&ClientOptions{

nodejs/src/client.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ export class CopilotClient {
297297
);
298298
}
299299

300+
if (options.sessionFs) {
301+
this.validateSessionFsConfig(options.sessionFs);
302+
}
303+
300304
// Parse cliUrl if provided
301305
if (options.cliUrl) {
302306
const { host, port } = this.parseCliUrl(options.cliUrl);
@@ -367,6 +371,20 @@ export class CopilotClient {
367371
return { host, port };
368372
}
369373

374+
private validateSessionFsConfig(config: SessionFsConfig): void {
375+
if (!config.initialCwd) {
376+
throw new Error("sessionFs.initialCwd is required");
377+
}
378+
379+
if (!config.sessionStatePath) {
380+
throw new Error("sessionFs.sessionStatePath is required");
381+
}
382+
383+
if (config.conventions !== "windows" && config.conventions !== "posix") {
384+
throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
385+
}
386+
}
387+
370388
/**
371389
* Starts the CLI server and establishes a connection.
372390
*

nodejs/test/client.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,34 @@ describe("CopilotClient", () => {
278278
});
279279
});
280280

281+
describe("SessionFs config", () => {
282+
it("throws when initialCwd is missing", () => {
283+
expect(() => {
284+
new CopilotClient({
285+
sessionFs: {
286+
initialCwd: "",
287+
sessionStatePath: "/session-state",
288+
conventions: "posix",
289+
},
290+
logLevel: "error",
291+
});
292+
}).toThrow(/sessionFs\.initialCwd is required/);
293+
});
294+
295+
it("throws when sessionStatePath is missing", () => {
296+
expect(() => {
297+
new CopilotClient({
298+
sessionFs: {
299+
initialCwd: "/",
300+
sessionStatePath: "",
301+
conventions: "posix",
302+
},
303+
logLevel: "error",
304+
});
305+
}).toThrow(/sessionFs\.sessionStatePath is required/);
306+
});
307+
});
308+
281309
describe("Auth options", () => {
282310
it("should accept githubToken option", () => {
283311
const client = new CopilotClient({

python/copilot/client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@
6666
LogLevel = Literal["none", "error", "warning", "info", "debug", "all"]
6767

6868

69+
def _validate_session_fs_config(config: SessionFsConfig) -> None:
70+
if not config.get("initial_cwd"):
71+
raise ValueError("session_fs.initial_cwd is required")
72+
if not config.get("session_state_path"):
73+
raise ValueError("session_fs.session_state_path is required")
74+
if config.get("conventions") not in ("posix", "windows"):
75+
raise ValueError("session_fs.conventions must be either 'posix' or 'windows'")
76+
77+
6978
class TelemetryConfig(TypedDict, total=False):
7079
"""Configuration for OpenTelemetry integration with the Copilot CLI."""
7180

@@ -903,6 +912,8 @@ def __init__(
903912
self._lifecycle_handlers_lock = threading.Lock()
904913
self._rpc: ServerRpc | None = None
905914
self._negotiated_protocol_version: int | None = None
915+
if config.session_fs is not None:
916+
_validate_session_fs_config(config.session_fs)
906917
self._session_fs_config = config.session_fs
907918

908919
@property

python/test_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,36 @@ def test_is_external_server_true(self):
122122
assert client._is_external_server
123123

124124

125+
class TestSessionFsConfig:
126+
def test_missing_initial_cwd(self):
127+
with pytest.raises(ValueError, match="session_fs.initial_cwd is required"):
128+
CopilotClient(
129+
SubprocessConfig(
130+
cli_path=CLI_PATH,
131+
log_level="error",
132+
session_fs={
133+
"initial_cwd": "",
134+
"session_state_path": "/session-state",
135+
"conventions": "posix",
136+
},
137+
)
138+
)
139+
140+
def test_missing_session_state_path(self):
141+
with pytest.raises(ValueError, match="session_fs.session_state_path is required"):
142+
CopilotClient(
143+
SubprocessConfig(
144+
cli_path=CLI_PATH,
145+
log_level="error",
146+
session_fs={
147+
"initial_cwd": "/",
148+
"session_state_path": "",
149+
"conventions": "posix",
150+
},
151+
)
152+
)
153+
154+
125155
class TestAuthOptions:
126156
def test_accepts_github_token(self):
127157
client = CopilotClient(

0 commit comments

Comments
 (0)