Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46ef241
feat(extension): Add stream envelope handler.
grypez Sep 13, 2024
93d0787
feat(extension): Use stream envelope handler.
grypez Sep 13, 2024
0654b70
test(extension): Cleanup envelope tests.
grypez Sep 13, 2024
b261307
Streamline handler logic.
grypez Sep 14, 2024
652d036
feat(extension): Leverage envelope handling for uninitialized capTp b…
grypez Sep 14, 2024
1485080
Standardize errorHandler param names.
grypez Sep 14, 2024
dc8db55
Remove EnvelopeLabel dependence.
grypez Sep 14, 2024
bc67061
Cleanup docs and type names.
grypez Sep 14, 2024
4005488
Make envelope kit test case name more descriptive.
grypez Sep 14, 2024
8768114
Promote default errorHandler to constant.
grypez Sep 14, 2024
380aef1
Rename types *Form -> *Like.
grypez Sep 14, 2024
e17fc6e
Narrow CapTpMessage type.
grypez Sep 15, 2024
92ac865
Narrow envelope interface.
grypez Sep 15, 2024
18a56ab
feat(streams): Add envelope kit.
grypez Sep 15, 2024
2278f7e
refactor(extension): Use @ocap/streams/envelope-kit.
grypez Sep 15, 2024
8bf3efe
test(streams): Remove it.each in type inference tests.
grypez Sep 16, 2024
4fe70de
refactor(extensions,streams): Move StreamEnvelopeHandler to streams p…
grypez Sep 16, 2024
65e0d12
docs(streams): Reduce instruction details.
grypez Sep 17, 2024
e95ae00
refactor(extension): Assign capTp handler instead of remaking.
grypez Sep 17, 2024
cb925b8
refactor(streams): Move generics.ts into utils/.
grypez Sep 17, 2024
e2d44ab
tests(streams): Simplify never type assertions.
grypez Sep 17, 2024
6f7c0dc
test(streams): Refactor type tests.
grypez Sep 17, 2024
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
16 changes: 16 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,21 @@ module.exports = {
],
},
},

{
// Rules for writing tests of typescript inference behavior.
// Mostly inherits rules for `files: ['**/*.test.{ts,js}']`.
files: ['**/*.types.test.ts'],
rules: {
// An explicit any type is useful for testing type narrowing.
'@typescript-eslint/no-explicit-any': 'off',
// Merely expressing an object is enough to invoke type inference.
'@typescript-eslint/no-unused-expressions': 'off',
// These sorts of tests don't generally need to run, only compile.
'vitest/expect-expect': 'off',
// Useful for `if (typeGuard(object))` statements.
'vitest/no-conditional-in-test': 'off',
},
},
],
};
22 changes: 0 additions & 22 deletions packages/extension/src/envelope.ts

This file was deleted.

241 changes: 121 additions & 120 deletions packages/extension/src/iframe-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import * as snapsUtils from '@metamask/snaps-utils';
import { delay, makePromiseKitMock } from '@ocap/test-utils';
import { vi, describe, it, expect } from 'vitest';

import { EnvelopeLabel } from './envelope.js';
import { IframeManager } from './iframe-manager.js';
import type { IframeMessage } from './message.js';
import { Command } from './message.js';
import { wrapCommand, wrapCapTp } from './stream-envelope.js';

vi.mock('@endo/promise-kit', () => makePromiseKitMock());

Expand Down Expand Up @@ -68,12 +68,17 @@ describe('IframeManager', () => {

it('creates a new iframe with the default getPort function', async () => {
vi.resetModules();
vi.doMock('@ocap/streams', () => ({
initializeMessageChannel: vi.fn(),
makeMessagePortStreamPair: vi.fn(() => ({ reader: {}, writer: {} })),
MessagePortReader: class Mock1 {},
MessagePortWriter: class Mock2 {},
}));
vi.doMock('@ocap/streams', async (importOriginal) => {
// @ts-expect-error This import is known to exist, and the linter erases the appropriate assertion.
const { makeStreamEnvelopeKit } = await importOriginal();
return {
initializeMessageChannel: vi.fn(),
makeMessagePortStreamPair: vi.fn(() => ({ reader: {}, writer: {} })),
MessagePortReader: class Mock1 {},
MessagePortWriter: class Mock2 {},
makeStreamEnvelopeKit,
};
});
const IframeManager2 = (await import('./iframe-manager.js'))
.IframeManager;

Expand Down Expand Up @@ -143,16 +148,13 @@ describe('IframeManager', () => {
const postMessage = (i: number): void => {
port2.postMessage({
done: false,
value: {
label: EnvelopeLabel.Command,
content: {
id: `foo-${i + 1}`,
message: {
type: Command.Evaluate,
data: `${i + 1}`,
},
value: wrapCommand({
id: `foo-${i + 1}`,
message: {
type: Command.Evaluate,
data: `${i + 1}`,
},
},
}),
});
};

Expand Down Expand Up @@ -181,6 +183,38 @@ describe('IframeManager', () => {
});

describe('capTp', () => {
it('calls console.warn when receiving a capTp envelope before initialization', async () => {
const id = 'foo';

vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn());
const warnSpy = vi.spyOn(console, 'warn');

const manager = new IframeManager();
vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn());

const { port1, port2 } = new MessageChannel();

await manager.create({ id, getPort: makeGetPort(port1) });

const envelope = wrapCapTp({
epoch: 0,
questionID: 'q-1',
type: 'CTP_BOOTSTRAP',
});

port2.postMessage({
done: false,
value: envelope,
});

await delay();

expect(warnSpy).toHaveBeenCalledWith(
'Stream envelope handler received an envelope with known but unexpected label',
envelope,
);
});

it('throws if called before initialization', async () => {
const mockWindow = {};
vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce(
Expand All @@ -203,26 +237,20 @@ describe('IframeManager', () => {
const id = 'frangooly';

const capTpInit = {
query: {
label: EnvelopeLabel.Command,
content: {
id: `${id}-1`,
message: {
data: null,
type: 'makeCapTp',
},
query: wrapCommand({
id: `${id}-1`,
message: {
data: null,
type: Command.CapTpInit,
},
},
response: {
label: EnvelopeLabel.Command,
content: {
id: `${id}-1`,
message: {
type: 'makeCapTp',
data: null,
},
}),
response: wrapCommand({
id: `${id}-1`,
message: {
type: Command.CapTpInit,
data: null,
},
},
}),
};

vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn());
Expand Down Expand Up @@ -277,77 +305,59 @@ describe('IframeManager', () => {
const id = 'frangooly';

const capTpInit = {
query: {
label: EnvelopeLabel.Command,
content: {
id: `${id}-1`,
message: {
data: null,
type: 'makeCapTp',
},
query: wrapCommand({
id: `${id}-1`,
message: {
data: null,
type: Command.CapTpInit,
},
},
response: {
label: EnvelopeLabel.Command,
content: {
id: `${id}-1`,
message: {
type: 'makeCapTp',
data: null,
},
}),
response: wrapCommand({
id: `${id}-1`,
message: {
type: Command.CapTpInit,
data: null,
},
},
}),
};

const greatFrangoolyBootstrap = {
query: {
label: 'capTp',
content: {
epoch: 0,
questionID: 'q-1',
type: 'CTP_BOOTSTRAP',
},
},
response: {
label: 'capTp',
content: {
type: 'CTP_RETURN',
epoch: 0,
answerID: 'q-1',
result: {
body: '{"@qclass":"slot","iface":"Alleged: TheGreatFrangooly","index":0}',
slots: ['o+1'],
},
query: wrapCapTp({
epoch: 0,
questionID: 'q-1',
type: 'CTP_BOOTSTRAP',
}),
response: wrapCapTp({
type: 'CTP_RETURN',
epoch: 0,
answerID: 'q-1',
result: {
body: '{"@qclass":"slot","iface":"Alleged: TheGreatFrangooly","index":0}',
slots: ['o+1'],
},
},
}),
};

const greatFrangoolyCall = {
query: {
label: 'capTp',
content: {
type: 'CTP_CALL',
epoch: 0,
method: {
body: '["whatIsTheGreatFrangooly",[]]',
slots: [],
},
questionID: 'q-2',
target: 'o-1',
query: wrapCapTp({
type: 'CTP_CALL',
epoch: 0,
method: {
body: '["whatIsTheGreatFrangooly",[]]',
slots: [],
},
},
response: {
label: 'capTp',
content: {
type: 'CTP_RETURN',
epoch: 0,
answerID: 'q-2',
result: {
body: '"Crowned with Chaos"',
slots: [],
},
questionID: 'q-2',
target: 'o-1',
}),
response: wrapCapTp({
type: 'CTP_RETURN',
epoch: 0,
answerID: 'q-2',
result: {
body: '"Crowned with Chaos"',
slots: [],
},
},
}),
};

vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn());
Expand Down Expand Up @@ -439,22 +449,19 @@ describe('IframeManager', () => {
const message: IframeMessage = { type: Command.Evaluate, data: '2+2' };
const response: IframeMessage = { type: Command.Evaluate, data: '4' };

// sendMessage wraps the content in a EnvelopeLabel.Command envelope
// sendMessage wraps the content in a Command envelope
const messagePromise = manager.sendMessage(id, message);
const messageId: string | undefined =
const messageId: string =
portPostMessageSpy.mock.lastCall?.[0]?.value?.content?.id;
expect(messageId).toBeTypeOf('string');

// postMessage sends the json directly, so we have to wrap it in an envelope here
port2.postMessage({
done: false,
value: {
label: EnvelopeLabel.Command,
content: {
id: messageId,
message: response,
},
},
value: wrapCommand({
id: messageId,
message: response,
}),
});

// awaiting event loop should resolve the messagePromise
Expand All @@ -464,13 +471,10 @@ describe('IframeManager', () => {
expect(portPostMessageSpy).toHaveBeenCalledOnce();
expect(portPostMessageSpy).toHaveBeenCalledWith({
done: false,
value: {
label: EnvelopeLabel.Command,
content: {
id: messageId,
message,
},
},
value: wrapCommand({
id: messageId,
message,
}),
});
});

Expand Down Expand Up @@ -502,7 +506,7 @@ describe('IframeManager', () => {
await delay(10);

expect(warnSpy).toHaveBeenCalledWith(
'Offscreen received message with unexpected format',
'Stream envelope handler received unexpected value',
'foo',
);
});
Expand All @@ -521,16 +525,13 @@ describe('IframeManager', () => {

port2.postMessage({
done: false,
value: {
label: EnvelopeLabel.Command,
content: {
id: 'foo',
message: {
type: Command.Evaluate,
data: '"bar"',
},
value: wrapCommand({
id: 'foo',
message: {
type: Command.Evaluate,
data: '"bar"',
},
},
}),
});
await delay(10);

Expand Down
Loading