diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ee41920..ee7f691 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,8 @@ jobs:
strategy:
matrix:
node_version:
- - 20
+ # v20 does not support snapshot testing which we use for the alpha version
+ # - 20
- 22
steps:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 41c73e3..6687a93 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,3 +4,38 @@ Thank you for considering to contribute to `github-project` 💖
Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md).
By participating you agree to abide by its terms.
+
+## Ways to contribute
+
+- **Reporting bugs** - if you find a bug, please [report it](https://github.com/copilot-extensions/preview-sdk.js/issues/new)!
+- **Suggesting features** - if you have an idea for a new feature, please [suggest it](https://github.com/copilot-extensions/preview-sdk.js/issues/new)!
+- **Contribute dreamcode** - like dreaming big? Same! Send a pull request with your beautiful API design that is so good, we just _have_ to make it real: [dreamcode.md](https://github.com/copilot-extensions/preview-sdk.js/blob/main/dreamcode.md)!
+- **Contribute code** - Yes! Please! We might even have [issues that are ready to be worked on](https://github.com/copilot-extensions/preview-sdk.js/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22pull%20request%20welcome%22)!
+
+## Development
+
+### Prerequisites
+
+- [Node.js](https://nodejs.org/) (v22.x)
+
+ We currently depend on Node 22+ for local development as we use new test APIs such as [snapshot testing](https://nodejs.org/api/test.html#snapshot-testing)! At some point we might move to a different test runner, but this works great to move fast in early aplha days.
+
+### Setup
+
+Use a codespace and you are all set: https://github.com/copilot-extensions/preview-sdk.js/codespaces.
+
+Or, if you prefer to develop locally:
+
+```
+gh repo clone copilot-extensions/preview-sdk.js
+cd preview-sdk.js
+npm install
+```
+
+### Running tests
+
+```
+npm test
+```
+
+As part of the tests, we test types for our public APIs (`npm run test:types`) and our code (`npm run test:tsc`). Run `npm run` to see all available scripts.
diff --git a/MAINTAINING.md b/MAINTAINING.md
new file mode 100644
index 0000000..135e586
--- /dev/null
+++ b/MAINTAINING.md
@@ -0,0 +1,16 @@
+# Maintaining
+
+## Merging Pull Request & releasing a new version
+
+Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release).
+The following commit message conventions determine which version is released:
+
+1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3` → `1.2.4`
+2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3` → `1.3.0`
+3. `BREAKING CHANGE:` in body: bumps breaking version, e.g. `1.2.3` → `2.0.0`
+
+Only one version number is bumped at a time, the highest version change trumps the others.
+Besides, publishing a new version to npm, semantic-release also creates a git tag and release
+on GitHub, generates changelogs from the commit messages and puts them into the release notes.
+
+If the pull request looks good but does not follow the commit conventions, update the pull request title and use the Squash & merge button, at which point you can set a custom commit message.
diff --git a/README.md b/README.md
index e7b8527..9c9f6ae 100644
--- a/README.md
+++ b/README.md
@@ -22,9 +22,27 @@ const payloadIsVerified = await verifyRequestByKeyId(
// true or false
```
+### Build a response
+
+```js
+import { createAckEvent, createDoneEvent, createTextEvent } from "@copilot-extensions/preview-sdk";
+
+export default handler(request, response) {
+ const ackEvent = createAckEvent();
+ const textEvent = createTextEvent("Hello, world!");
+ const doneEvent = createDoneEvent();
+
+ response.write(ackEvent.toString());
+ response.write(textEvent.toString());
+ response.end(doneEvent.toString());
+}
+```
+
## API
-### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
+### Verification
+
+#### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
Verify the request payload using the provided signature and key ID. The method will request the public key from GitHub's API for the given keyId and then verify the payload.
@@ -46,7 +64,7 @@ await verifyRequestByKeyId(request.body, signature, key, { token: "ghp_1234" });
await verifyRequestByKeyId(request.body, signature, key, { request });
```
-### `async fetchVerificationKeys(options)`
+#### `async fetchVerificationKeys(options)`
Fetches public keys for verifying copilot extension requests [from GitHub's API](https://api.github.com/meta/public_keys/copilot_api)
and returns them as an array. The request can be made without authentication, with a token, or with a custom [octokit request](https://github.com/octokit/request.js) instance.
@@ -64,7 +82,7 @@ const [current] = await fetchVerificationKeys({ token: "ghp_1234" });
const [current] = await fetchVerificationKeys({ request });)
```
-### `async verifyRequestPayload(rawBody, signature, keyId)`
+#### `async verifyRequestPayload(rawBody, signature, keyId)`
Verify the request payload using the provided signature and key. Note that the raw body as received by GitHub must be passed, before any parsing.
@@ -79,6 +97,111 @@ const payloadIsVerified = await verifyRequestPayload(
// true or false
```
+### Response
+
+All `create*Event()` methods return an object with a `.toString()` method, which is called automatically when a string is expected. Unfortunately that's not the case for `response.write()`, you need to call `.toString()` explicitly.
+
+#### `createAckEvent()`
+
+Acknowledge the request so that the chat UI can tell the user that the agent started generating a response.
+The `ack` event should only be sent once.
+
+```js
+import { createAckEvent } from "@copilot-extensions/preview-sdk";
+
+response.write(createAckEvent().toString());
+```
+
+#### `createTextEvent(message)`
+
+Send a text message to the chat UI. Multiple messages can be sent. The `message` argument must be a string and may include markdown.
+
+```js
+import { createTextEvent } from "@copilot-extensions/preview-sdk";
+
+response.write(createTextEvent("Hello, world!").toString());
+response.write(createTextEvent("Hello, again!").toString());
+```
+
+#### `createConfirmationEvent({ id, title, message, metadata })`
+
+Ask the user to confirm an action. The `confirmation` event should only be sent once.
+
+The `meta` data object will be sent along the user's response.
+
+See additional documentation about Copilot confirmations at https://github.com/github/copilot-partners/blob/main/docs/confirmations.md.
+
+```js
+import { createConfirmationEvent } from "@copilot-extensions/preview-sdk";
+
+response.write(
+ createConfirmationEvent({
+ id: "123",
+ title: "Are you sure?",
+ message: "This will do something.",
+ }).toString()
+);
+```
+
+#### `createReferencesEvent(references)`
+
+Send a list of references to the chat UI. The `references` argument must be an array of objects with the following properties:
+
+- `id`
+- `type`
+
+The following properties are optional
+
+- `data`: object with any properties.
+- `is_implicit`: a boolean
+- `metadata`: an object with a required `display_name` and the optional properties: `display_icon` and `display_url`
+
+Multiple `references` events can be sent.
+
+See additional documentation about Copilot references at https://github.com/github/copilot-partners/blob/main/docs/copilot-references.md.
+
+```js
+import { createReferencesEvent } from "@copilot-extensions/preview-sdk";
+
+response.write(
+ createReferencesEvent([
+ {
+ id: "123",
+ type: "issue",
+ data: {
+ number: 123,
+ },
+ is_implicit: false,
+ metadata: {
+ display_name: "My issue",
+ display_icon: "issue-opened",
+ display_url: "https://github.com/monalisa/hello-world/issues/123",
+ },
+ ]).toString()
+);
+```
+
+#### `createErrorsEvent(errors)`
+
+An array of objects with the following properties:
+
+- `type`: must be one of: `"reference"`, `"function"`, `"agent"`
+- `code`
+- `message`
+- `identifier`
+
+See additional documentation about Copilot errors at https://github.com/github/copilot-partners/blob/main/docs/copilot-errors.md.
+
+#### `createDoneEvent()`
+
+The `done` event should only be sent once, at the end of the response. No further events can be sent after the `done` event.
+
+```js
+import { createDoneEvent } from "@copilot-extensions/preview-sdk";
+
+response.write(createDoneEvent().toString());
+```
+
## Dreamcode
While implementing the lower-level functionality, we also dream big: what would our dream SDK for Coplitot extensions look like? Please have a look and share your thoughts and ideas:
diff --git a/index.d.ts b/index.d.ts
index ba08dcc..0f25568 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -34,6 +34,105 @@ interface VerifyRequestByKeyIdInterface {
): Promise;
}
+export interface CreateAckEventInterface {
+ (): ResponseEvent<"ack">
+}
+
+export interface CreateTextEventInterface {
+ (message: string): ResponseEvent<"text">
+}
+
+export type CreateConfirmationEventOptions = { id: string, title: string, message: string, metadata?: Record }
+
+export interface CreateConfirmationEventInterface {
+ (options: CreateConfirmationEventOptions): ResponseEvent<"copilot_confirmation">
+}
+export interface CreateReferencesEventInterface {
+ (references: CopilotReference[]): ResponseEvent<"copilot_references">
+}
+export interface CreateErrorsEventInterface {
+ (errors: CopilotError[]): ResponseEvent<"copilot_errors">
+}
+export interface CreateDoneEventInterface {
+ (): ResponseEvent<"done">
+}
+
+type ResponseEventType = "ack" | "done" | "text" | "copilot_references" | "copilot_confirmation" | "copilot_errors"
+type EventsWithoutEventKey = "ack" | "done" | "text"
+type ResponseEvent =
+ T extends EventsWithoutEventKey ? {
+ data: T extends "ack" ? CopilotAckResponseEventData : T extends "done" ? CopilotDoneResponseEventData : T extends "text" ? CopilotTextResponseEventData : never
+ toString: () => string
+ } : {
+ event: T
+ data: T extends "copilot_references" ? CopilotReferenceResponseEventData : T extends "copilot_confirmation" ? CopilotConfirmationResponseEventData : T extends "copilot_errors" ? CopilotErrorsResponseEventData : never
+ toString: () => string
+ }
+
+type CopilotAckResponseEventData = {
+ choices: [{
+ delta: {
+ content: "", role: "assistant"
+ }
+ }]
+}
+
+type CopilotDoneResponseEventData = {
+ choices: [{
+ finish_reason: "stop"
+ delta: {
+ content: null
+ }
+ }]
+}
+
+type CopilotTextResponseEventData = {
+ choices: [{
+ delta: {
+ content: string, role: "assistant"
+ }
+ }]
+}
+type CopilotConfirmationResponseEventData = {
+ type: 'action';
+ title: string;
+ message: string;
+ confirmation?: {
+ id: string;
+ [key: string]: any;
+ };
+}
+type CopilotErrorsResponseEventData = CopilotError[]
+type CopilotReferenceResponseEventData = CopilotReference[]
+
+type CopilotError = {
+ type: "reference" | "function" | "agent";
+ code: string;
+ message: string;
+ identifier: string;
+}
+
+interface CopilotReference {
+ type: string;
+ id: string;
+ data?: {
+ [key: string]: unknown;
+ };
+ is_implicit?: boolean;
+ metadata?: {
+ display_name: string;
+ display_icon?: string;
+ display_url?: string;
+ };
+}
+
export declare const verifyRequest: VerifyRequestInterface;
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;
+
+export declare const createAckEvent: CreateAckEventInterface;
+export declare const createConfirmationEvent: CreateConfirmationEventInterface;
+export declare const createDoneEvent: CreateDoneEventInterface;
+export declare const createErrorsEvent: CreateErrorsEventInterface;
+export declare const createReferencesEvent: CreateReferencesEventInterface;
+export declare const createTextEvent: CreateTextEventInterface;
diff --git a/index.js b/index.js
index 9d1c8dc..686156a 100644
--- a/index.js
+++ b/index.js
@@ -1,78 +1,4 @@
// @ts-check
-import { createVerify } from "node:crypto";
-
-import { request as defaultRequest } from "@octokit/request";
-import { RequestError } from "@octokit/request-error";
-
-/** @type {import('.').VerifyRequestByKeyIdInterface} */
-export async function verifyRequest(rawBody, signature, key) {
- // verify arguments
- assertValidString(rawBody, "Invalid payload");
- assertValidString(signature, "Invalid signature");
- assertValidString(key, "Invalid key");
-
- // verify signature
- try {
- return createVerify("SHA256")
- .update(rawBody)
- .verify(key, signature, "base64");
- } catch {
- return false;
- }
-}
-
-/** @type {import('.').FetchVerificationKeysInterface} */
-export async function fetchVerificationKeys(
- { token = "", request = defaultRequest } = { request: defaultRequest }
-) {
- const { data } = await request("GET /meta/public_keys/copilot_api", {
- headers: token
- ? {
- Authorization: `token ${token}`,
- }
- : {},
- });
-
- return data.public_keys;
-}
-
-/** @type {import('.').VerifyRequestByKeyIdInterface} */
-export async function verifyRequestByKeyId(
- rawBody,
- signature,
- keyId,
- requestOptions
-) {
- // verify arguments
- assertValidString(rawBody, "Invalid payload");
- assertValidString(signature, "Invalid signature");
- assertValidString(keyId, "Invalid keyId");
-
- // receive valid public keys from GitHub
- const keys = await fetchVerificationKeys(requestOptions);
-
- // verify provided key Id
- const publicKey = keys.find((key) => key.key_identifier === keyId);
-
- if (!publicKey) {
- const keyNotFoundError = Object.assign(
- new Error(
- "[@copilot-extensions/preview-sdk] No public key found matching key identifier"
- ),
- {
- keyId,
- keys,
- }
- );
- throw keyNotFoundError;
- }
-
- return verifyRequest(rawBody, signature, publicKey.key);
-}
-
-function assertValidString(value, message) {
- if (typeof value !== "string" || value.length === 0) {
- throw new Error(`[@copilot-extensions/preview-sdk] ${message}`);
- }
-}
+export * from "./lib/verification.js";
+export * from "./lib/response.js";
diff --git a/index.test-d.ts b/index.test-d.ts
index 3596d63..14413b7 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -2,6 +2,12 @@ import { expectType } from "tsd";
import { request } from "@octokit/request";
import {
+ createAckEvent,
+ createConfirmationEvent,
+ createDoneEvent,
+ createErrorsEvent,
+ createReferencesEvent,
+ createTextEvent,
fetchVerificationKeys,
verifyRequest,
verifyRequestByKeyId,
@@ -63,4 +69,161 @@ export async function fetchVerificationKeysTest() {
// accepts a request argument
await fetchVerificationKeys({ request });
-}
\ No newline at end of file
+}
+
+export function createAckEventTest() {
+ const event = createAckEvent();
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ choices: [{
+ delta: {
+ content: "", role: "assistant"
+ }
+ }]
+ }>(event.data);
+
+ // @ts-expect-error - .event is required
+ event.event
+}
+
+export function createTextEventTest() {
+ const event = createTextEvent("test");
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ choices: [{
+ delta: {
+ content: string, role: "assistant"
+ }
+ }]
+ }>(event.data);
+
+ // @ts-expect-error - .event is required
+ event.event
+}
+
+export function createConfirmationEventTest() {
+ const event = createConfirmationEvent({
+ id: "test",
+ title: "test",
+ message: "test"
+ });
+
+ // optional metadata
+ createConfirmationEvent({
+ id: "test",
+ title: "test",
+ message: "test",
+ metadata: {
+ someOtherId: "test",
+ }
+ })
+
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ type: 'action';
+ title: string;
+ message: string;
+ confirmation?: {
+ id: string;
+ [key: string]: any;
+ };
+ }>(event.data);
+
+ expectType<"copilot_confirmation">(event.event);
+}
+
+export function createReferencesEventTest() {
+ const event = createReferencesEvent([
+ {
+ type: "test.story",
+ id: "test",
+ data: {
+ file: "test.js",
+ start: "1",
+ end: "42",
+ content: "function test() {...}",
+ },
+ is_implicit: false,
+ metadata: {
+ display_name: "Lines 1-42 from test.js",
+ display_icon: "test-icon",
+ display_url:
+ "http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42",
+ },
+ },
+ ]);
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ type: string;
+ id: string;
+ data?: {
+ [key: string]: unknown;
+ };
+ is_implicit?: boolean;
+ metadata?: {
+ display_name: string;
+ display_icon?: string;
+ display_url?: string;
+ };
+ }[]>(event.data);
+
+ expectType<"copilot_references">(event.event);
+}
+
+export function createErrorsEventTest() {
+ const event = createErrorsEvent([{
+ type: "reference",
+ code: "1",
+ message: "test reference error",
+ identifier: "reference-identifier",
+ }, {
+ type: "function",
+ code: "1",
+ message: "test function error",
+ identifier: "function-identifier",
+ }, {
+ type: "agent",
+ code: "1",
+ message: "test agent error",
+ identifier: "agent-identifier",
+ }]);
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ type: "reference" | "function" | "agent";
+ code: string;
+ message: string;
+ identifier: string;
+ }[]>(event.data);
+
+ expectType<"copilot_errors">(event.event);
+}
+
+export function createDoneEventTest() {
+ const event = createDoneEvent();
+ expectType<() => string>(event.toString);
+ expectType(event.toString());
+
+ expectType<{
+ "choices": [
+ {
+ "finish_reason": "stop",
+ "delta": {
+ "content": null
+ }
+ }
+ ]
+ }>(event.data);
+
+ // @ts-expect-error - .event is required
+ event.event
+}
diff --git a/lib/response.js b/lib/response.js
new file mode 100644
index 0000000..d1344b3
--- /dev/null
+++ b/lib/response.js
@@ -0,0 +1,88 @@
+// @ts-check
+
+/** @type {import('..').CreateAckEventInterface} */
+export function createAckEvent() {
+ return {
+ data: {
+ choices: [
+ {
+ delta: { content: ``, role: "assistant" },
+ },
+ ],
+ },
+ toString() {
+ return `data: ${JSON.stringify(this.data)}\n\n`;
+ },
+ };
+}
+
+/** @type {import('..').CreateTextEventInterface} */
+export function createTextEvent(message) {
+ return {
+ data: {
+ choices: [
+ {
+ delta: { content: message, role: "assistant" },
+ },
+ ],
+ },
+ toString() {
+ return `data: ${JSON.stringify(this.data)}\n\n`;
+ },
+ };
+}
+
+/** @type {import('..').CreateConfirmationEventInterface} */
+export function createConfirmationEvent({ id, title, message, metadata }) {
+ return {
+ event: "copilot_confirmation",
+ data: {
+ type: "action",
+ title,
+ message,
+ confirmation: { id, ...metadata },
+ },
+ toString() {
+ return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
+ },
+ };
+}
+
+/** @type {import('..').CreateReferencesEventInterface} */
+export function createReferencesEvent(references) {
+ return {
+ event: "copilot_references",
+ data: references,
+ toString() {
+ return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
+ },
+ };
+}
+
+/** @type {import('..').CreateErrorsEventInterface} */
+export function createErrorsEvent(errors) {
+ return {
+ event: "copilot_errors",
+ data: errors,
+ toString() {
+ return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
+ },
+ };
+}
+
+/** @type {import('..').CreateDoneEventInterface} */
+export function createDoneEvent() {
+ return {
+ data: {
+ choices: [
+ {
+ finish_reason: "stop",
+ delta: { content: null },
+ },
+ ],
+ },
+ toString() {
+ return `data: ${JSON.stringify(this.data)}\n\ndata: [DONE]\n\n`;
+ },
+ };
+}
diff --git a/lib/verification.js b/lib/verification.js
new file mode 100644
index 0000000..63e0ca6
--- /dev/null
+++ b/lib/verification.js
@@ -0,0 +1,77 @@
+// @ts-check
+
+import { createVerify } from "node:crypto";
+
+import { request as defaultRequest } from "@octokit/request";
+
+/** @type {import('..').VerifyRequestByKeyIdInterface} */
+export async function verifyRequest(rawBody, signature, key) {
+ // verify arguments
+ assertValidString(rawBody, "Invalid payload");
+ assertValidString(signature, "Invalid signature");
+ assertValidString(key, "Invalid key");
+
+ // verify signature
+ try {
+ return createVerify("SHA256")
+ .update(rawBody)
+ .verify(key, signature, "base64");
+ } catch {
+ return false;
+ }
+}
+
+/** @type {import('..').FetchVerificationKeysInterface} */
+export async function fetchVerificationKeys(
+ { token = "", request = defaultRequest } = { request: defaultRequest }
+) {
+ const { data } = await request("GET /meta/public_keys/copilot_api", {
+ headers: token
+ ? {
+ Authorization: `token ${token}`,
+ }
+ : {},
+ });
+
+ return data.public_keys;
+}
+
+/** @type {import('..').VerifyRequestByKeyIdInterface} */
+export async function verifyRequestByKeyId(
+ rawBody,
+ signature,
+ keyId,
+ requestOptions
+) {
+ // verify arguments
+ assertValidString(rawBody, "Invalid payload");
+ assertValidString(signature, "Invalid signature");
+ assertValidString(keyId, "Invalid keyId");
+
+ // receive valid public keys from GitHub
+ const keys = await fetchVerificationKeys(requestOptions);
+
+ // verify provided key Id
+ const publicKey = keys.find((key) => key.key_identifier === keyId);
+
+ if (!publicKey) {
+ const keyNotFoundError = Object.assign(
+ new Error(
+ "[@copilot-extensions/preview-sdk] No public key found matching key identifier"
+ ),
+ {
+ keyId,
+ keys,
+ }
+ );
+ throw keyNotFoundError;
+ }
+
+ return verifyRequest(rawBody, signature, publicKey.key);
+}
+
+function assertValidString(value, message) {
+ if (typeof value !== "string" || value.length === 0) {
+ throw new Error(`[@copilot-extensions/preview-sdk] ${message}`);
+ }
+}
diff --git a/package.json b/package.json
index 480fd75..2dc99a7 100644
--- a/package.json
+++ b/package.json
@@ -25,8 +25,9 @@
"@octokit/request-error": "^6.1.4"
},
"scripts": {
- "test": "npm run test:code && npm run test:types",
- "test:code": "node --test --experimental-test-coverage",
+ "test": "npm run test:code && npm run test:tsc && npm run test:types",
+ "test:code": "node --test --experimental-test-coverage --experimental-test-snapshots",
+ "test:code:update-snapshots": "node --test --experimental-test-coverage --experimental-test-snapshots --test-update-snapshots",
"test:tsc": "tsc --allowJs --noEmit --esModuleInterop --skipLibCheck --lib es2020 index.js",
"test:types": "tsd",
"lint": "prettier --check \"*.{js,json,ts,md}\" \".github/**/*.yml\" \"test/*.ts\"",
diff --git a/test/response.test.js b/test/response.test.js
new file mode 100644
index 0000000..3f1a660
--- /dev/null
+++ b/test/response.test.js
@@ -0,0 +1,99 @@
+// @ts-check
+
+import { test, suite } from "node:test";
+
+import {
+ createAckEvent,
+ createConfirmationEvent,
+ createDoneEvent,
+ createErrorsEvent,
+ createReferencesEvent,
+ createTextEvent,
+} from "../index.js";
+
+suite("response", () => {
+ test("smoke", (t) => {
+ t.assert.equal(typeof createAckEvent, "function");
+ });
+
+ test("createAckEvent()", (t) => {
+ const event = createAckEvent();
+ t.assert.equal(undefined, event.event);
+ t.assert.snapshot(event.data);
+ t.assert.snapshot(event.toString());
+ });
+
+ test("createDoneEvent()", (t) => {
+ const event = createDoneEvent();
+ t.assert.equal(undefined, event.event);
+ t.assert.snapshot(event.data);
+ t.assert.snapshot(event.toString());
+ });
+
+ test("createConfirmationEvent()", (t) => {
+ const event = createConfirmationEvent({
+ id: "123",
+ title: "title",
+ message: "message",
+ metadata: { foo: "bar" },
+ });
+ t.assert.equal("copilot_confirmation", event.event);
+ t.assert.snapshot(event.data);
+ t.assert.snapshot(event.toString());
+ });
+
+ test("createErrorsEvent()", (t) => {
+ const referenceError = {
+ type: "reference",
+ code: "1",
+ message: "test reference error",
+ identifier: "reference-identifier",
+ };
+ const functionError = {
+ type: "function",
+ code: "1",
+ message: "test function error",
+ identifier: "function-identifier",
+ };
+ const agentError = {
+ type: "agent",
+ code: "1",
+ message: "test agent error",
+ identifier: "agent-identifier",
+ };
+ const event = createErrorsEvent([
+ referenceError,
+ functionError,
+ agentError,
+ ]);
+ t.assert.equal("copilot_errors", event.event);
+ t.assert.snapshot(event.data);
+ t.assert.snapshot(event.toString());
+ });
+
+ test("createReferencesEvent()", (t) => {
+ const event = createReferencesEvent([
+ {
+ type: "test.story",
+ id: "test",
+ data: {
+ file: "test.js",
+ start: "1",
+ end: "42",
+ content: "function test() {...}",
+ },
+ is_implicit: false,
+ metadata: {
+ display_name: "Lines 1-42 from test.js",
+ display_icon: "test-icon",
+ display_url:
+ "http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42",
+ },
+ },
+ ]);
+
+ t.assert.equal("copilot_references", event.event);
+ t.assert.snapshot(event.data);
+ t.assert.snapshot(event.toString());
+ });
+});
diff --git a/test/response.test.js.snapshot b/test/response.test.js.snapshot
new file mode 100644
index 0000000..0005d55
--- /dev/null
+++ b/test/response.test.js.snapshot
@@ -0,0 +1,101 @@
+exports[`response > createAckEvent() 1`] = `
+{
+ "choices": [
+ {
+ "delta": {
+ "content": "",
+ "role": "assistant"
+ }
+ }
+ ]
+}
+`;
+
+exports[`response > createAckEvent() 2`] = `
+"data: {\\"choices\\":[{\\"delta\\":{\\"content\\":\\"\\",\\"role\\":\\"assistant\\"}}]}\\n\\n"
+`;
+
+exports[`response > createConfirmationEvent() 1`] = `
+{
+ "type": "action",
+ "title": "title",
+ "message": "message",
+ "confirmation": {
+ "id": "123",
+ "foo": "bar"
+ }
+}
+`;
+
+exports[`response > createConfirmationEvent() 2`] = `
+"event: copilot_confirmation\\ndata: {\\"type\\":\\"action\\",\\"title\\":\\"title\\",\\"message\\":\\"message\\",\\"confirmation\\":{\\"id\\":\\"123\\",\\"foo\\":\\"bar\\"}}\\n\\n"
+`;
+
+exports[`response > createDoneEvent() 1`] = `
+{
+ "choices": [
+ {
+ "finish_reason": "stop",
+ "delta": {
+ "content": null
+ }
+ }
+ ]
+}
+`;
+
+exports[`response > createDoneEvent() 2`] = `
+"data: {\\"choices\\":[{\\"finish_reason\\":\\"stop\\",\\"delta\\":{\\"content\\":null}}]}\\n\\ndata: [DONE]\\n\\n"
+`;
+
+exports[`response > createErrorsEvent() 1`] = `
+[
+ {
+ "type": "reference",
+ "code": "1",
+ "message": "test reference error",
+ "identifier": "reference-identifier"
+ },
+ {
+ "type": "function",
+ "code": "1",
+ "message": "test function error",
+ "identifier": "function-identifier"
+ },
+ {
+ "type": "agent",
+ "code": "1",
+ "message": "test agent error",
+ "identifier": "agent-identifier"
+ }
+]
+`;
+
+exports[`response > createErrorsEvent() 2`] = `
+"event: copilot_errors\\ndata: [{\\"type\\":\\"reference\\",\\"code\\":\\"1\\",\\"message\\":\\"test reference error\\",\\"identifier\\":\\"reference-identifier\\"},{\\"type\\":\\"function\\",\\"code\\":\\"1\\",\\"message\\":\\"test function error\\",\\"identifier\\":\\"function-identifier\\"},{\\"type\\":\\"agent\\",\\"code\\":\\"1\\",\\"message\\":\\"test agent error\\",\\"identifier\\":\\"agent-identifier\\"}]\\n\\n"
+`;
+
+exports[`response > createReferencesEvent() 1`] = `
+[
+ {
+ "type": "test.story",
+ "id": "test",
+ "data": {
+ "file": "test.js",
+ "start": "1",
+ "end": "42",
+ "content": "function test() {...}"
+ },
+ "is_implicit": false,
+ "metadata": {
+ "display_name": "Lines 1-42 from test.js",
+ "display_icon": "test-icon",
+ "display_url": "http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42"
+ }
+ }
+]
+`;
+
+exports[`response > createReferencesEvent() 2`] = `
+"event: copilot_references\\ndata: [{\\"type\\":\\"test.story\\",\\"id\\":\\"test\\",\\"data\\":{\\"file\\":\\"test.js\\",\\"start\\":\\"1\\",\\"end\\":\\"42\\",\\"content\\":\\"function test() {...}\\"},\\"is_implicit\\":false,\\"metadata\\":{\\"display_name\\":\\"Lines 1-42 from test.js\\",\\"display_icon\\":\\"test-icon\\",\\"display_url\\":\\"http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42\\"}}]\\n\\n"
+`;
diff --git a/test/integration.test.js b/test/verification.test.js
similarity index 100%
rename from test/integration.test.js
rename to test/verification.test.js