diff --git a/nodejs/package.json b/nodejs/package.json index ef06e3631..30c917a4c 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -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", diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index ee231d79f..a34d23712 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -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, diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts new file mode 100644 index 000000000..de21ba2ba --- /dev/null +++ b/nodejs/test/session-event-types.test.ts @@ -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 type-only imports + * compile, the public API exposes the types. The runtime assertions below only + * validate representative object shapes for those annotations; they do not + * prove that type-only exports exist at runtime. + */ + +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 = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; +type _AssistantMessageEventStaysAlignedWithSessionEventUnion = _AssertEqual< + AssistantMessageEvent, + Extract +>; +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. The annotation + // is the compile-time API-surface check; these assertions only validate + // the representative runtime object shape a consumer would 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(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + assertImportable(); + + // 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(); + assertImportable(); + assertImportable(); + + expect(true).toBe(true); + }); +}); diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json new file mode 100644 index 000000000..295748750 --- /dev/null +++ b/nodejs/tsconfig.test.json @@ -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"] +}