Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" --ignore-path .prettierignore",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"lint:fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\"",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
"generate": "cd ../scripts/codegen && npm run generate",
"update:protocol-version": "tsx scripts/update-protocol-version.ts",
"prepublishOnly": "npm run build",
Expand Down
13 changes: 13 additions & 0 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ export {
createSessionFsAdapter,
SYSTEM_PROMPT_SECTIONS,
} from "./types.js";
// Re-export the generated session-event types (every *Event interface and
// its corresponding *Data payload type, plus supporting unions/aliases) so
// consumers can import them directly from "@github/copilot-sdk" instead of
// reaching into the package's internal dist layout. See issue #1156.
//
// Three names from this file are also explicitly exported elsewhere in this
// module — `SessionEvent` (re-exported below from `./types.js`),
// `PermissionRequest` (re-exported below from `./types.js`), and
// `AssistantMessageEvent` (re-exported above from `./session.js`). Per the
// ECMAScript module spec, the explicit named re-exports shadow the names
// arriving via `export type *`, so the hand-authored public API surface for
// those three identifiers is preserved unchanged.
export type * from "./generated/session-events.js";
export type {
CommandContext,
CommandDefinition,
Expand Down
182 changes: 182 additions & 0 deletions nodejs/test/session-event-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* Regression test for #1156: dedicated session event data/payload types are
* importable from the package entry point (`@github/copilot-sdk` /
* `src/index.js`).
*
* Before this fix, only the aggregate `SessionEvent` discriminated union was
* re-exported. The constituent `*Event` wrapper interfaces and their `*Data`
* payload types lived in `generated/session-events.ts` and could only be
* reached via a deep import (`@github/copilot-sdk/dist/generated/...`).
*
* Most of this file exercises the *type* surface — if these imports type-check
* the public API exposes the types. A small set of runtime assertions
* additionally proves that a concrete `ToolExecutionStartData` value (the
* exact type called out in the issue) round-trips through the exports.
Comment thread
stephentoub marked this conversation as resolved.
Outdated
*/

import { describe, expect, it } from "vitest";
import type {
// The aggregate union; must still resolve via the package root.
SessionEvent,

// *Data payload types from the v0.3.0 generated session-event schema.
AssistantMessageData,
AssistantMessageDeltaData,
AssistantReasoningData,
AssistantTurnStartData,
ErrorData,
IdleData,
ResumeData,
StartData,
ToolExecutionCompleteData,
ToolExecutionPartialData,
ToolExecutionProgressData,
ToolExecutionStartData,
UserMessageData,

// *Event wrapper interfaces.
AssistantMessageEvent,
ErrorEvent,
IdleEvent,
ResumeEvent,
StartEvent,
ToolExecutionCompleteEvent,
ToolExecutionStartEvent,
UserMessageEvent,

// A sample of supporting auxiliary aliases/unions referenced by the
// *Data shapes — these must also be reachable so that consumers can
// narrow or annotate intermediate values.
UserMessageAgentMode,
UserMessageAttachment,
WorkingDirectoryContextHostType,
} from "../src/index.js";

/**
* Type-only helper: forces the compiler to resolve the supplied type
* parameter. If the type is not exported from `../src/index.js`, the file
* fails to type-check and the test never runs. There is no runtime body —
* the helper exists purely to make "is this type importable?" assertions
* compile-time checked.
*/
function assertImportable<_T>(): void {
/* no-op; compile-time check only */
}

/**
* Compile-time mutual-assignability check: passes only when `A` and `B`
* are structurally equivalent. Used below to pin the package-root
* `AssistantMessageEvent` (which is explicitly re-exported from
* `./session.js` and therefore shadows the generated `AssistantMessageEvent`
* arriving via `export type *`) to the corresponding arm of the generated
* `SessionEvent` union. If a future schema regen ever caused these two
* shapes to drift, this assertion would fail to type-check and `npm run
* typecheck` would surface it before the public API silently changed.
*/
type _AssertEqual<A, B> =
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
type _AssistantMessageEventStaysAlignedWithSessionEventUnion = _AssertEqual<
AssistantMessageEvent,
Extract<SessionEvent, { type: "assistant.message" }>
>;
const _assistantMessageEventAlignmentCheck: _AssistantMessageEventStaysAlignedWithSessionEventUnion = true;

describe("Session event type exports (#1156)", () => {
it("exposes the headline ToolExecutionStartData type with a usable shape", () => {
// This is the specific type called out in issue #1156. Build a real
// value through the public re-export so we exercise both the type
// surface and the runtime shape consumers would actually use.
const data: ToolExecutionStartData = {
toolCallId: "call-1",
toolName: "shell",
arguments: { command: "ls" },
mcpServerName: "filesystem",
mcpToolName: "list_dir",
turnId: "turn-1",
};

expect(data.toolName).toBe("shell");
expect(data.toolCallId).toBe("call-1");
expect(data.arguments?.command).toBe("ls");
expect(data.mcpServerName).toBe("filesystem");
expect(data.mcpToolName).toBe("list_dir");
expect(data.turnId).toBe("turn-1");
});

it("wraps ToolExecutionStartData inside the exported ToolExecutionStartEvent", () => {
const event: ToolExecutionStartEvent = {
id: "evt-1",
parentId: null,
timestamp: "2026-01-01T00:00:00.000Z",
type: "tool.execution_start",
data: {
toolCallId: "call-1",
toolName: "shell",
},
};

expect(event.type).toBe("tool.execution_start");
expect(event.data.toolName).toBe("shell");
expect(event.parentId).toBeNull();
});

it("narrows the aggregate SessionEvent union to a dedicated *Data type", () => {
const evt: SessionEvent = {
id: "evt-2",
parentId: null,
timestamp: "2026-01-01T00:00:01.000Z",
type: "tool.execution_start",
data: {
toolCallId: "call-2",
toolName: "shell",
},
};

if (evt.type !== "tool.execution_start") {
throw new Error("expected tool.execution_start narrowing");
}

// After narrowing, `evt.data` must satisfy `ToolExecutionStartData`.
// Annotating the local with the dedicated *Data type proves the
// re-export is wired up correctly.
const data: ToolExecutionStartData = evt.data;
expect(data.toolCallId).toBe("call-2");
expect(data.toolName).toBe("shell");
});

it("re-exports the full set of *Data and *Event types named in v0.3.0", () => {
// Compile-time checks: if any of these fail to resolve, the file
// will not type-check and the test will not be executed.
assertImportable<AssistantMessageData>();
assertImportable<AssistantMessageDeltaData>();
assertImportable<AssistantReasoningData>();
assertImportable<AssistantTurnStartData>();
assertImportable<ErrorData>();
assertImportable<IdleData>();
assertImportable<ResumeData>();
assertImportable<StartData>();
assertImportable<ToolExecutionCompleteData>();
assertImportable<ToolExecutionPartialData>();
assertImportable<ToolExecutionProgressData>();
assertImportable<ToolExecutionStartData>();
assertImportable<UserMessageData>();

assertImportable<AssistantMessageEvent>();
assertImportable<ErrorEvent>();
assertImportable<IdleEvent>();
assertImportable<ResumeEvent>();
assertImportable<StartEvent>();
assertImportable<ToolExecutionCompleteEvent>();
assertImportable<ToolExecutionStartEvent>();
assertImportable<UserMessageEvent>();

// Supporting auxiliary types referenced by the *Data shapes — these
// must round-trip through the package root too, otherwise consumers
// annotating intermediate values would still need a deep import.
assertImportable<UserMessageAgentMode>();
assertImportable<UserMessageAttachment>();
assertImportable<WorkingDirectoryContextHostType>();

expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions nodejs/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"emitDeclarationOnly": false,
"types": ["node"]
},
"include": ["src/**/*", "test/session-event-types.test.ts"],
"exclude": ["node_modules", "dist"]
}
Loading