@kb-labs/shared 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +32 -0
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/deploy.yml +28 -0
- package/.github/workflows/docker-build.yml +25 -0
- package/.github/workflows/drift-check.yml +10 -0
- package/.github/workflows/profiles-validate.yml +16 -0
- package/.github/workflows/release.yml +8 -0
- package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
- package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
- package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
- package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
- package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
- package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
- package/.kb/devkit/agents/release-manager/context.globs +7 -0
- package/.kb/devkit/agents/release-manager/prompt.md +27 -0
- package/.kb/devkit/agents/release-manager/runbook.md +17 -0
- package/.kb/devkit/agents/test-generator/context.globs +7 -0
- package/.kb/devkit/agents/test-generator/prompt.md +27 -0
- package/.kb/devkit/agents/test-generator/runbook.md +18 -0
- package/.vscode/settings.json +23 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +117 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/docs/DECLARATIVE-FLAGS-AND-ENV.md +622 -0
- package/docs/DOCUMENTATION.md +70 -0
- package/docs/adr/0000-template.md +52 -0
- package/docs/adr/0001-architecture-and-repository-layout.md +31 -0
- package/docs/adr/0002-plugins-and-extensibility.md +44 -0
- package/docs/adr/0003-package-and-module-boundaries.md +35 -0
- package/docs/adr/0004-versioning-and-release-policy.md +36 -0
- package/docs/adr/0005-reactive-loader-pattern.md +179 -0
- package/docs/adr/0006-declarative-flags-and-env-systems.md +376 -0
- package/eslint.config.js +27 -0
- package/kb-labs.config.json +5 -0
- package/package.json +88 -0
- package/package.json.bin +25 -0
- package/package.json.lib +30 -0
- package/packages/shared-cli-ui/CHANGELOG.md +20 -0
- package/packages/shared-cli-ui/README.md +342 -0
- package/packages/shared-cli-ui/docs/ARCHITECTURE.md +105 -0
- package/packages/shared-cli-ui/eslint.config.js +27 -0
- package/packages/shared-cli-ui/package.json +72 -0
- package/packages/shared-cli-ui/src/__tests__/artifacts-display.spec.ts +89 -0
- package/packages/shared-cli-ui/src/__tests__/format.spec.ts +44 -0
- package/packages/shared-cli-ui/src/__tests__/loader-json-mode.test.ts +119 -0
- package/packages/shared-cli-ui/src/artifacts-display.ts +266 -0
- package/packages/shared-cli-ui/src/cli-auto-discovery.ts +120 -0
- package/packages/shared-cli-ui/src/colors.ts +142 -0
- package/packages/shared-cli-ui/src/command-discovery.ts +72 -0
- package/packages/shared-cli-ui/src/command-output.ts +153 -0
- package/packages/shared-cli-ui/src/command-result.ts +267 -0
- package/packages/shared-cli-ui/src/command-runner.ts +310 -0
- package/packages/shared-cli-ui/src/command-suggestions.ts +204 -0
- package/packages/shared-cli-ui/src/debug/components/output.ts +141 -0
- package/packages/shared-cli-ui/src/debug/components/trace.ts +101 -0
- package/packages/shared-cli-ui/src/debug/components/tree.ts +88 -0
- package/packages/shared-cli-ui/src/debug/formatters/ai.ts +17 -0
- package/packages/shared-cli-ui/src/debug/formatters/human.ts +98 -0
- package/packages/shared-cli-ui/src/debug/formatters/timeline.ts +94 -0
- package/packages/shared-cli-ui/src/debug/index.ts +56 -0
- package/packages/shared-cli-ui/src/debug/types.ts +57 -0
- package/packages/shared-cli-ui/src/debug/utilities.ts +203 -0
- package/packages/shared-cli-ui/src/dynamic-command-discovery.ts +131 -0
- package/packages/shared-cli-ui/src/format.ts +412 -0
- package/packages/shared-cli-ui/src/index.ts +34 -0
- package/packages/shared-cli-ui/src/loader.ts +196 -0
- package/packages/shared-cli-ui/src/manifest-parser.ts +151 -0
- package/packages/shared-cli-ui/src/modern-format.ts +271 -0
- package/packages/shared-cli-ui/src/multi-cli-suggestions.ts +159 -0
- package/packages/shared-cli-ui/src/table.ts +134 -0
- package/packages/shared-cli-ui/src/timing-tracker.ts +68 -0
- package/packages/shared-cli-ui/src/utils/context.ts +12 -0
- package/packages/shared-cli-ui/src/utils/env.ts +164 -0
- package/packages/shared-cli-ui/src/utils/flags.ts +269 -0
- package/packages/shared-cli-ui/src/utils/path.ts +8 -0
- package/packages/shared-cli-ui/tsconfig.build.json +15 -0
- package/packages/shared-cli-ui/tsconfig.json +9 -0
- package/packages/shared-cli-ui/tsup.config.ts +11 -0
- package/packages/shared-cli-ui/vitest.config.ts +15 -0
- package/packages/shared-command-kit/CHANGELOG.md +20 -0
- package/packages/shared-command-kit/LICENSE +22 -0
- package/packages/shared-command-kit/README.md +1030 -0
- package/packages/shared-command-kit/docs/HIGH-LEVEL-API.md +89 -0
- package/packages/shared-command-kit/docs/LOW-LEVEL-API.md +105 -0
- package/packages/shared-command-kit/docs/MIGRATION-GUIDE.md +135 -0
- package/packages/shared-command-kit/eslint.config.js +27 -0
- package/packages/shared-command-kit/eslint.config.ts +14 -0
- package/packages/shared-command-kit/package.json +76 -0
- package/packages/shared-command-kit/prettierrc.json +5 -0
- package/packages/shared-command-kit/src/__tests__/define-command.spec.ts +294 -0
- package/packages/shared-command-kit/src/__tests__/define-route.test.ts +285 -0
- package/packages/shared-command-kit/src/__tests__/define-system-command.spec.ts +508 -0
- package/packages/shared-command-kit/src/__tests__/define-webhook.test.ts +156 -0
- package/packages/shared-command-kit/src/__tests__/define-websocket.test.ts +316 -0
- package/packages/shared-command-kit/src/__tests__/errors.spec.ts +45 -0
- package/packages/shared-command-kit/src/__tests__/flags.spec.ts +353 -0
- package/packages/shared-command-kit/src/__tests__/platform-api.test.ts +135 -0
- package/packages/shared-command-kit/src/__tests__/plugin-context-v3.snapshot.spec.ts +240 -0
- package/packages/shared-command-kit/src/__tests__/ws-types.test.ts +359 -0
- package/packages/shared-command-kit/src/analytics/index.ts +6 -0
- package/packages/shared-command-kit/src/analytics/with-analytics.ts +195 -0
- package/packages/shared-command-kit/src/define-action.ts +100 -0
- package/packages/shared-command-kit/src/define-command.ts +113 -0
- package/packages/shared-command-kit/src/define-route.ts +113 -0
- package/packages/shared-command-kit/src/define-system-command.ts +362 -0
- package/packages/shared-command-kit/src/define-webhook.ts +115 -0
- package/packages/shared-command-kit/src/define-websocket.ts +308 -0
- package/packages/shared-command-kit/src/errors/factory.ts +282 -0
- package/packages/shared-command-kit/src/errors/format-validation.ts +144 -0
- package/packages/shared-command-kit/src/errors/format.ts +92 -0
- package/packages/shared-command-kit/src/errors/index.ts +9 -0
- package/packages/shared-command-kit/src/errors/types.ts +32 -0
- package/packages/shared-command-kit/src/flags/define.ts +92 -0
- package/packages/shared-command-kit/src/flags/index.ts +9 -0
- package/packages/shared-command-kit/src/flags/types.ts +153 -0
- package/packages/shared-command-kit/src/flags/validate.ts +358 -0
- package/packages/shared-command-kit/src/helpers/context.ts +8 -0
- package/packages/shared-command-kit/src/helpers/flags.ts +84 -0
- package/packages/shared-command-kit/src/helpers/index.ts +42 -0
- package/packages/shared-command-kit/src/helpers/patterns.ts +464 -0
- package/packages/shared-command-kit/src/helpers/platform.ts +335 -0
- package/packages/shared-command-kit/src/helpers/use-analytics.ts +95 -0
- package/packages/shared-command-kit/src/helpers/use-cache.ts +97 -0
- package/packages/shared-command-kit/src/helpers/use-config.ts +99 -0
- package/packages/shared-command-kit/src/helpers/use-embeddings.ts +49 -0
- package/packages/shared-command-kit/src/helpers/use-llm.ts +316 -0
- package/packages/shared-command-kit/src/helpers/use-logger.ts +77 -0
- package/packages/shared-command-kit/src/helpers/use-platform.ts +111 -0
- package/packages/shared-command-kit/src/helpers/use-resource-broker.ts +106 -0
- package/packages/shared-command-kit/src/helpers/use-storage.ts +71 -0
- package/packages/shared-command-kit/src/helpers/use-vector-store.ts +49 -0
- package/packages/shared-command-kit/src/helpers/validation.ts +398 -0
- package/packages/shared-command-kit/src/index.ts +410 -0
- package/packages/shared-command-kit/src/jobs.ts +132 -0
- package/packages/shared-command-kit/src/lifecycle/define-handlers.ts +366 -0
- package/packages/shared-command-kit/src/lifecycle/index.ts +6 -0
- package/packages/shared-command-kit/src/manifest.ts +127 -0
- package/packages/shared-command-kit/src/rest/define-handler.ts +187 -0
- package/packages/shared-command-kit/src/rest/index.ts +11 -0
- package/packages/shared-command-kit/src/studio/index.ts +12 -0
- package/packages/shared-command-kit/src/validation/index.ts +6 -0
- package/packages/shared-command-kit/src/validation/schema-builders.ts +409 -0
- package/packages/shared-command-kit/src/ws-types.ts +106 -0
- package/packages/shared-command-kit/tsconfig.build.json +15 -0
- package/packages/shared-command-kit/tsconfig.json +9 -0
- package/packages/shared-command-kit/tsup.config.ts +30 -0
- package/packages/shared-command-kit/vitest.config.ts +4 -0
- package/packages/shared-http/package.json +67 -0
- package/packages/shared-http/src/__tests__/log-correlation.test.ts +81 -0
- package/packages/shared-http/src/__tests__/operation-metrics-tracker.test.ts +55 -0
- package/packages/shared-http/src/http-observability-collector.ts +363 -0
- package/packages/shared-http/src/index.ts +36 -0
- package/packages/shared-http/src/log-correlation.ts +89 -0
- package/packages/shared-http/src/operation-metrics-tracker.ts +107 -0
- package/packages/shared-http/src/register-openapi.ts +108 -0
- package/packages/shared-http/src/resolve-schema-ref.ts +75 -0
- package/packages/shared-http/src/schemas.ts +29 -0
- package/packages/shared-http/src/service-observability.ts +63 -0
- package/packages/shared-http/tsconfig.build.json +15 -0
- package/packages/shared-http/tsconfig.json +9 -0
- package/packages/shared-http/tsup.config.ts +23 -0
- package/packages/shared-http/vitest.config.ts +13 -0
- package/packages/shared-perm-presets/CHANGELOG.md +20 -0
- package/packages/shared-perm-presets/README.md +78 -0
- package/packages/shared-perm-presets/eslint.config.js +27 -0
- package/packages/shared-perm-presets/package.json +45 -0
- package/packages/shared-perm-presets/src/__tests__/combine.test.ts +403 -0
- package/packages/shared-perm-presets/src/__tests__/presets.test.ts +205 -0
- package/packages/shared-perm-presets/src/combine.ts +278 -0
- package/packages/shared-perm-presets/src/index.ts +18 -0
- package/packages/shared-perm-presets/src/presets/ci-environment.ts +34 -0
- package/packages/shared-perm-presets/src/presets/full-env.ts +16 -0
- package/packages/shared-perm-presets/src/presets/git-workflow.ts +40 -0
- package/packages/shared-perm-presets/src/presets/index.ts +8 -0
- package/packages/shared-perm-presets/src/presets/kb-platform.ts +30 -0
- package/packages/shared-perm-presets/src/presets/llm-access.ts +29 -0
- package/packages/shared-perm-presets/src/presets/minimal.ts +21 -0
- package/packages/shared-perm-presets/src/presets/npm-publish.ts +48 -0
- package/packages/shared-perm-presets/src/presets/vector-store.ts +40 -0
- package/packages/shared-perm-presets/src/types.ts +192 -0
- package/packages/shared-perm-presets/tsconfig.build.json +15 -0
- package/packages/shared-perm-presets/tsconfig.json +9 -0
- package/packages/shared-perm-presets/tsup.config.ts +8 -0
- package/packages/shared-perm-presets/vitest.config.ts +9 -0
- package/packages/shared-testing/CHANGELOG.md +20 -0
- package/packages/shared-testing/README.md +430 -0
- package/packages/shared-testing/package.json +51 -0
- package/packages/shared-testing/src/__tests__/create-test-context.test.ts +199 -0
- package/packages/shared-testing/src/__tests__/mock-cache.test.ts +174 -0
- package/packages/shared-testing/src/__tests__/mock-llm.test.ts +212 -0
- package/packages/shared-testing/src/__tests__/setup-platform.test.ts +90 -0
- package/packages/shared-testing/src/__tests__/test-command.test.ts +557 -0
- package/packages/shared-testing/src/create-test-context.ts +550 -0
- package/packages/shared-testing/src/index.ts +77 -0
- package/packages/shared-testing/src/mock-cache.ts +179 -0
- package/packages/shared-testing/src/mock-llm.ts +319 -0
- package/packages/shared-testing/src/mock-logger.ts +97 -0
- package/packages/shared-testing/src/mock-storage.ts +108 -0
- package/packages/shared-testing/src/setup-platform.ts +101 -0
- package/packages/shared-testing/src/test-command.ts +288 -0
- package/packages/shared-testing/tsconfig.build.json +15 -0
- package/packages/shared-testing/tsconfig.json +9 -0
- package/packages/shared-testing/tsup.config.ts +20 -0
- package/packages/shared-testing/vitest.config.ts +3 -0
- package/packages/shared-tool-kit/CHANGELOG.md +20 -0
- package/packages/shared-tool-kit/package.json +47 -0
- package/packages/shared-tool-kit/src/__tests__/factory.test.ts +103 -0
- package/packages/shared-tool-kit/src/__tests__/mock-tool.test.ts +95 -0
- package/packages/shared-tool-kit/src/factory.ts +126 -0
- package/packages/shared-tool-kit/src/index.ts +32 -0
- package/packages/shared-tool-kit/src/testing/index.ts +84 -0
- package/packages/shared-tool-kit/tsconfig.build.json +15 -0
- package/packages/shared-tool-kit/tsconfig.json +9 -0
- package/packages/shared-tool-kit/tsup.config.ts +21 -0
- package/pnpm-workspace.yaml +11070 -0
- package/prettierrc.json +1 -0
- package/scripts/devkit-sync.mjs +37 -0
- package/scripts/hooks/post-push +9 -0
- package/scripts/hooks/pre-commit +9 -0
- package/scripts/hooks/pre-push +9 -0
- package/tsconfig.base.json +9 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.paths.json +50 -0
- package/tsconfig.tools.json +18 -0
- package/tsup.config.bin.ts +34 -0
- package/tsup.config.cli.ts +41 -0
- package/tsup.config.dual.ts +46 -0
- package/tsup.config.ts +36 -0
- package/tsup.external.json +104 -0
- package/vitest.config.ts +48 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Unit tests for WebSocket message builders and routers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { defineMessage, MessageBuilder, MessageRouter } from '../ws-types.js';
|
|
7
|
+
import type { WSMessage, WSSender } from '@kb-labs/plugin-contracts';
|
|
8
|
+
|
|
9
|
+
describe('MessageBuilder', () => {
|
|
10
|
+
describe('create', () => {
|
|
11
|
+
it('should create message with type and payload', () => {
|
|
12
|
+
const builder = new MessageBuilder<{ name: string }>('test');
|
|
13
|
+
|
|
14
|
+
const message = builder.create({ name: 'Alice' });
|
|
15
|
+
|
|
16
|
+
expect(message.type).toBe('test');
|
|
17
|
+
expect(message.payload).toEqual({ name: 'Alice' });
|
|
18
|
+
expect(message.timestamp).toBeGreaterThan(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create message with messageId', () => {
|
|
22
|
+
const builder = new MessageBuilder<{ data: string }>('test');
|
|
23
|
+
|
|
24
|
+
const message = builder.create({ data: 'hello' }, 'msg-123');
|
|
25
|
+
|
|
26
|
+
expect(message.type).toBe('test');
|
|
27
|
+
expect(message.payload).toEqual({ data: 'hello' });
|
|
28
|
+
expect(message.messageId).toBe('msg-123');
|
|
29
|
+
expect(message.timestamp).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should create message without messageId', () => {
|
|
33
|
+
const builder = new MessageBuilder<object>('ping');
|
|
34
|
+
|
|
35
|
+
const message = builder.create({});
|
|
36
|
+
|
|
37
|
+
expect(message.type).toBe('ping');
|
|
38
|
+
expect(message.payload).toEqual({});
|
|
39
|
+
expect(message.messageId).toBeUndefined();
|
|
40
|
+
expect(message.timestamp).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should generate unique timestamps for sequential calls', () => {
|
|
44
|
+
const builder = new MessageBuilder<object>('test');
|
|
45
|
+
|
|
46
|
+
const msg1 = builder.create({});
|
|
47
|
+
const msg2 = builder.create({});
|
|
48
|
+
|
|
49
|
+
// Timestamps should be close but not necessarily different
|
|
50
|
+
// (depends on system clock resolution)
|
|
51
|
+
expect(msg1.timestamp).toBeLessThanOrEqual(msg2.timestamp);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('is', () => {
|
|
56
|
+
it('should return true for matching message type', () => {
|
|
57
|
+
const builder = new MessageBuilder<{ count: number }>('update');
|
|
58
|
+
|
|
59
|
+
const message: WSMessage = {
|
|
60
|
+
type: 'update',
|
|
61
|
+
payload: { count: 42 },
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
expect(builder.is(message)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return false for non-matching message type', () => {
|
|
69
|
+
const builder = new MessageBuilder<{ count: number }>('update');
|
|
70
|
+
|
|
71
|
+
const message: WSMessage = {
|
|
72
|
+
type: 'ping',
|
|
73
|
+
payload: {},
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(builder.is(message)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should narrow type when used in if statement', () => {
|
|
81
|
+
const UpdateMsg = new MessageBuilder<{ progress: number }>('update');
|
|
82
|
+
|
|
83
|
+
const message: WSMessage = {
|
|
84
|
+
type: 'update',
|
|
85
|
+
payload: { progress: 50 },
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (UpdateMsg.is(message)) {
|
|
90
|
+
// TypeScript should infer message.payload as { progress: number }
|
|
91
|
+
const progress: number = message.payload.progress;
|
|
92
|
+
expect(progress).toBe(50);
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error('Should match');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('defineMessage', () => {
|
|
101
|
+
it('should create MessageBuilder instance', () => {
|
|
102
|
+
const PingMsg = defineMessage<object>('ping');
|
|
103
|
+
|
|
104
|
+
expect(PingMsg).toBeInstanceOf(MessageBuilder);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should create type-safe message builder', () => {
|
|
108
|
+
interface ProgressPayload {
|
|
109
|
+
phase: string;
|
|
110
|
+
progress: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ProgressMsg = defineMessage<ProgressPayload>('progress');
|
|
114
|
+
|
|
115
|
+
const message = ProgressMsg.create({
|
|
116
|
+
phase: 'analyzing',
|
|
117
|
+
progress: 50,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(message.type).toBe('progress');
|
|
121
|
+
expect((message.payload as any).phase).toBe('analyzing');
|
|
122
|
+
expect((message.payload as any).progress).toBe(50);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should support different payload types', () => {
|
|
126
|
+
const StringMsg = defineMessage<string>('text');
|
|
127
|
+
const NumberMsg = defineMessage<number>('count');
|
|
128
|
+
const ObjectMsg = defineMessage<{ items: string[] }>('list');
|
|
129
|
+
|
|
130
|
+
const textMsg = StringMsg.create('hello');
|
|
131
|
+
const countMsg = NumberMsg.create(42);
|
|
132
|
+
const listMsg = ObjectMsg.create({ items: ['a', 'b'] });
|
|
133
|
+
|
|
134
|
+
expect(textMsg.payload).toBe('hello');
|
|
135
|
+
expect(countMsg.payload).toBe(42);
|
|
136
|
+
expect(listMsg.payload).toEqual({ items: ['a', 'b'] });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('MessageRouter', () => {
|
|
141
|
+
let mockContext: any;
|
|
142
|
+
let mockSender: WSSender;
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
mockContext = {
|
|
146
|
+
logger: {
|
|
147
|
+
info: vi.fn(),
|
|
148
|
+
error: vi.fn(),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
mockSender = {
|
|
153
|
+
send: vi.fn(),
|
|
154
|
+
broadcast: vi.fn(),
|
|
155
|
+
sendTo: vi.fn(),
|
|
156
|
+
close: vi.fn(),
|
|
157
|
+
getConnectionId: vi.fn(() => 'conn-123'),
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('on', () => {
|
|
162
|
+
it('should register message handler', () => {
|
|
163
|
+
const router = new MessageRouter();
|
|
164
|
+
const PingMsg = defineMessage<object>('ping');
|
|
165
|
+
const handler = vi.fn();
|
|
166
|
+
|
|
167
|
+
router.on(PingMsg, handler);
|
|
168
|
+
|
|
169
|
+
// Handler should be registered (tested via handle())
|
|
170
|
+
expect(router).toBeDefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should support chaining', () => {
|
|
174
|
+
const router = new MessageRouter();
|
|
175
|
+
const PingMsg = defineMessage<object>('ping');
|
|
176
|
+
const PongMsg = defineMessage<object>('pong');
|
|
177
|
+
|
|
178
|
+
const result = router.on(PingMsg, vi.fn()).on(PongMsg, vi.fn());
|
|
179
|
+
|
|
180
|
+
expect(result).toBe(router);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should register multiple handlers for different message types', () => {
|
|
184
|
+
const router = new MessageRouter();
|
|
185
|
+
const StartMsg = defineMessage<object>('start');
|
|
186
|
+
const StopMsg = defineMessage<object>('stop');
|
|
187
|
+
|
|
188
|
+
const startHandler = vi.fn();
|
|
189
|
+
const stopHandler = vi.fn();
|
|
190
|
+
|
|
191
|
+
router.on(StartMsg, startHandler).on(StopMsg, stopHandler);
|
|
192
|
+
|
|
193
|
+
// Handlers registered (tested via handle())
|
|
194
|
+
expect(router).toBeDefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('handle', () => {
|
|
199
|
+
it('should call handler for matching message type', async () => {
|
|
200
|
+
const router = new MessageRouter();
|
|
201
|
+
const PingMsg = defineMessage<object>('ping');
|
|
202
|
+
const handler = vi.fn();
|
|
203
|
+
|
|
204
|
+
router.on(PingMsg, handler);
|
|
205
|
+
|
|
206
|
+
const message: WSMessage = {
|
|
207
|
+
type: 'ping',
|
|
208
|
+
payload: {},
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const handled = await router.handle(mockContext, message, mockSender);
|
|
213
|
+
|
|
214
|
+
expect(handled).toBe(true);
|
|
215
|
+
expect(handler).toHaveBeenCalledWith(mockContext, {}, mockSender);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should return false for unhandled message type', async () => {
|
|
219
|
+
const router = new MessageRouter();
|
|
220
|
+
const PingMsg = defineMessage<object>('ping');
|
|
221
|
+
|
|
222
|
+
router.on(PingMsg, vi.fn());
|
|
223
|
+
|
|
224
|
+
const message: WSMessage = {
|
|
225
|
+
type: 'unknown',
|
|
226
|
+
payload: {},
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const handled = await router.handle(mockContext, message, mockSender);
|
|
231
|
+
|
|
232
|
+
expect(handled).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should pass payload to handler', async () => {
|
|
236
|
+
const router = new MessageRouter();
|
|
237
|
+
const UpdateMsg = defineMessage<{ progress: number }>('update');
|
|
238
|
+
const handler = vi.fn();
|
|
239
|
+
|
|
240
|
+
router.on(UpdateMsg, handler);
|
|
241
|
+
|
|
242
|
+
const message: WSMessage = {
|
|
243
|
+
type: 'update',
|
|
244
|
+
payload: { progress: 75 },
|
|
245
|
+
timestamp: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await router.handle(mockContext, message, mockSender);
|
|
249
|
+
|
|
250
|
+
expect(handler).toHaveBeenCalledWith(mockContext, { progress: 75 }, mockSender);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle async handlers', async () => {
|
|
254
|
+
const router = new MessageRouter();
|
|
255
|
+
const StartMsg = defineMessage<object>('start');
|
|
256
|
+
|
|
257
|
+
let resolved = false;
|
|
258
|
+
const asyncHandler = vi.fn(async () => {
|
|
259
|
+
await new Promise<void>((resolve) => { setTimeout(resolve, 10); });
|
|
260
|
+
resolved = true;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
router.on(StartMsg, asyncHandler);
|
|
264
|
+
|
|
265
|
+
const message: WSMessage = {
|
|
266
|
+
type: 'start',
|
|
267
|
+
payload: {},
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await router.handle(mockContext, message, mockSender);
|
|
272
|
+
|
|
273
|
+
expect(resolved).toBe(true);
|
|
274
|
+
expect(asyncHandler).toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should route to correct handler among multiple', async () => {
|
|
278
|
+
const router = new MessageRouter();
|
|
279
|
+
const PingMsg = defineMessage<object>('ping');
|
|
280
|
+
const PongMsg = defineMessage<object>('pong');
|
|
281
|
+
const StartMsg = defineMessage<object>('start');
|
|
282
|
+
|
|
283
|
+
const pingHandler = vi.fn();
|
|
284
|
+
const pongHandler = vi.fn();
|
|
285
|
+
const startHandler = vi.fn();
|
|
286
|
+
|
|
287
|
+
router.on(PingMsg, pingHandler).on(PongMsg, pongHandler).on(StartMsg, startHandler);
|
|
288
|
+
|
|
289
|
+
const pongMessage: WSMessage = {
|
|
290
|
+
type: 'pong',
|
|
291
|
+
payload: {},
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
await router.handle(mockContext, pongMessage, mockSender);
|
|
296
|
+
|
|
297
|
+
expect(pingHandler).not.toHaveBeenCalled();
|
|
298
|
+
expect(pongHandler).toHaveBeenCalled();
|
|
299
|
+
expect(startHandler).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should propagate handler errors', async () => {
|
|
303
|
+
const router = new MessageRouter();
|
|
304
|
+
const ErrorMsg = defineMessage<object>('error');
|
|
305
|
+
const error = new Error('Handler failed');
|
|
306
|
+
|
|
307
|
+
router.on(ErrorMsg, async () => {
|
|
308
|
+
throw error;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const message: WSMessage = {
|
|
312
|
+
type: 'error',
|
|
313
|
+
payload: {},
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await expect(router.handle(mockContext, message, mockSender)).rejects.toThrow(error);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('integration', () => {
|
|
322
|
+
it('should handle complete message flow', async () => {
|
|
323
|
+
const router = new MessageRouter();
|
|
324
|
+
|
|
325
|
+
// Define message types
|
|
326
|
+
const StartMsg = defineMessage<{ scope?: string }>('start');
|
|
327
|
+
const ProgressMsg = defineMessage<{ progress: number }>('progress');
|
|
328
|
+
const CompleteMsg = defineMessage<{ result: string }>('complete');
|
|
329
|
+
|
|
330
|
+
// Handlers
|
|
331
|
+
const startHandler = vi.fn(async (ctx, payload, sender) => {
|
|
332
|
+
await sender.send(ProgressMsg.create({ progress: 0 }));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const progressHandler = vi.fn();
|
|
336
|
+
const completeHandler = vi.fn();
|
|
337
|
+
|
|
338
|
+
router.on(StartMsg, startHandler).on(ProgressMsg, progressHandler).on(CompleteMsg, completeHandler);
|
|
339
|
+
|
|
340
|
+
// Handle start message
|
|
341
|
+
const startMessage: WSMessage = {
|
|
342
|
+
type: 'start',
|
|
343
|
+
payload: { scope: 'packages' },
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const handled = await router.handle(mockContext, startMessage, mockSender);
|
|
348
|
+
|
|
349
|
+
expect(handled).toBe(true);
|
|
350
|
+
expect(startHandler).toHaveBeenCalledWith(mockContext, { scope: 'packages' }, mockSender);
|
|
351
|
+
expect(mockSender.send).toHaveBeenCalledWith(
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
type: 'progress',
|
|
354
|
+
payload: { progress: 0 },
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Optional helper for wrapping operations with analytics events.
|
|
5
|
+
* You can always use ctx.platform.analytics.track() directly - this is just convenience.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { withAnalytics } from '@kb-labs/shared-command-kit';
|
|
10
|
+
*
|
|
11
|
+
* const result = await withAnalytics(ctx, 'mind.query', {
|
|
12
|
+
* started: { queryId, text, mode },
|
|
13
|
+
* completed: (result) => ({ tokensIn: result.tokensIn, tokensOut: result.tokensOut }),
|
|
14
|
+
* failed: (error) => ({ errorMessage: error.message }),
|
|
15
|
+
* }, async () => {
|
|
16
|
+
* return await executeQuery(...);
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { IAnalytics } from '@kb-labs/core-platform';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context with optional analytics
|
|
25
|
+
*/
|
|
26
|
+
export interface AnalyticsContext {
|
|
27
|
+
platform?: {
|
|
28
|
+
analytics?: IAnalytics;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analytics event configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface AnalyticsEvents<TResult> {
|
|
36
|
+
/** Event properties when operation starts */
|
|
37
|
+
started?: Record<string, unknown>;
|
|
38
|
+
/** Event properties when operation completes (can be function of result) */
|
|
39
|
+
completed?: Record<string, unknown> | ((result: TResult) => Record<string, unknown>);
|
|
40
|
+
/** Event properties when operation fails (can be function of error) */
|
|
41
|
+
failed?: Record<string, unknown> | ((error: Error) => Record<string, unknown>);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap an async operation with analytics tracking
|
|
46
|
+
*
|
|
47
|
+
* Automatically tracks:
|
|
48
|
+
* - `{eventName}.started` when operation begins
|
|
49
|
+
* - `{eventName}.completed` when operation succeeds
|
|
50
|
+
* - `{eventName}.failed` when operation fails
|
|
51
|
+
*
|
|
52
|
+
* @param ctx - Context with platform.analytics
|
|
53
|
+
* @param eventName - Base event name (e.g., 'mind.query', 'workflow.run')
|
|
54
|
+
* @param events - Event properties for started/completed/failed events
|
|
55
|
+
* @param operation - Async operation to execute
|
|
56
|
+
* @returns Result of the operation
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const result = await withAnalytics(ctx, 'mind.query', {
|
|
61
|
+
* started: { queryId: '123', text: 'search query' },
|
|
62
|
+
* completed: (result) => ({
|
|
63
|
+
* tokensIn: result.tokensIn,
|
|
64
|
+
* tokensOut: result.tokensOut,
|
|
65
|
+
* resultsCount: result.results.length,
|
|
66
|
+
* }),
|
|
67
|
+
* failed: (error) => ({
|
|
68
|
+
* errorCode: error.code,
|
|
69
|
+
* errorMessage: error.message,
|
|
70
|
+
* }),
|
|
71
|
+
* }, async () => {
|
|
72
|
+
* return await executeQuery('search query');
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export async function withAnalytics<TResult>(
|
|
77
|
+
ctx: AnalyticsContext,
|
|
78
|
+
eventName: string,
|
|
79
|
+
events: AnalyticsEvents<TResult>,
|
|
80
|
+
operation: () => Promise<TResult>
|
|
81
|
+
): Promise<TResult> {
|
|
82
|
+
const analytics = ctx.platform?.analytics;
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
|
|
85
|
+
// Track started event
|
|
86
|
+
if (analytics && events.started) {
|
|
87
|
+
await analytics.track(`${eventName}.started`, {
|
|
88
|
+
...events.started,
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Execute operation
|
|
95
|
+
const result = await operation();
|
|
96
|
+
|
|
97
|
+
// Track completed event
|
|
98
|
+
if (analytics && events.completed) {
|
|
99
|
+
const completedProps = typeof events.completed === 'function'
|
|
100
|
+
? events.completed(result)
|
|
101
|
+
: events.completed;
|
|
102
|
+
|
|
103
|
+
await analytics.track(`${eventName}.completed`, {
|
|
104
|
+
...completedProps,
|
|
105
|
+
durationMs: Date.now() - startTime,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
// Track failed event
|
|
113
|
+
if (analytics && events.failed) {
|
|
114
|
+
const failedProps = typeof events.failed === 'function'
|
|
115
|
+
? events.failed(error)
|
|
116
|
+
: events.failed;
|
|
117
|
+
|
|
118
|
+
await analytics.track(`${eventName}.failed`, {
|
|
119
|
+
...failedProps,
|
|
120
|
+
durationMs: Date.now() - startTime,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Re-throw error
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a reusable analytics wrapper for a specific event
|
|
132
|
+
*
|
|
133
|
+
* @param eventName - Base event name
|
|
134
|
+
* @param defaultEvents - Default event properties
|
|
135
|
+
* @returns Function that wraps operations with analytics
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const trackQuery = createAnalyticsWrapper('mind.query', {
|
|
140
|
+
* started: { source: 'cli' },
|
|
141
|
+
* completed: (result) => ({ resultsCount: result.results.length }),
|
|
142
|
+
* });
|
|
143
|
+
*
|
|
144
|
+
* // Use multiple times
|
|
145
|
+
* const result1 = await trackQuery(ctx, { text: 'query 1' }, async () => {...});
|
|
146
|
+
* const result2 = await trackQuery(ctx, { text: 'query 2' }, async () => {...});
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function createAnalyticsWrapper<TResult>(
|
|
150
|
+
eventName: string,
|
|
151
|
+
defaultEvents: AnalyticsEvents<TResult>
|
|
152
|
+
) {
|
|
153
|
+
return async (
|
|
154
|
+
ctx: AnalyticsContext,
|
|
155
|
+
additionalEvents: Partial<AnalyticsEvents<TResult>>,
|
|
156
|
+
operation: () => Promise<TResult>
|
|
157
|
+
): Promise<TResult> => {
|
|
158
|
+
const mergedEvents: AnalyticsEvents<TResult> = {
|
|
159
|
+
started: { ...defaultEvents.started, ...additionalEvents.started },
|
|
160
|
+
completed: additionalEvents.completed || defaultEvents.completed,
|
|
161
|
+
failed: additionalEvents.failed || defaultEvents.failed,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return withAnalytics(ctx, eventName, mergedEvents, operation);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Track a simple event (no wrapping)
|
|
170
|
+
*
|
|
171
|
+
* Convenience helper for tracking single events without wrapping an operation.
|
|
172
|
+
*
|
|
173
|
+
* @param ctx - Context with platform.analytics
|
|
174
|
+
* @param eventName - Event name
|
|
175
|
+
* @param properties - Event properties
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* await trackEvent(ctx, 'button.clicked', { buttonId: 'submit', page: 'settings' });
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export async function trackEvent(
|
|
183
|
+
ctx: AnalyticsContext,
|
|
184
|
+
eventName: string,
|
|
185
|
+
properties?: Record<string, unknown>
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const analytics = ctx.platform?.analytics;
|
|
188
|
+
|
|
189
|
+
if (analytics) {
|
|
190
|
+
await analytics.track(eventName, {
|
|
191
|
+
...properties,
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Define a workflow action handler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PluginContextV3, CommandResult, HostContext } from '@kb-labs/plugin-contracts';
|
|
6
|
+
|
|
7
|
+
export interface ActionHandler<TConfig = unknown, TInput = unknown> {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the workflow action
|
|
10
|
+
*/
|
|
11
|
+
execute(
|
|
12
|
+
context: PluginContextV3<TConfig>,
|
|
13
|
+
input: TInput
|
|
14
|
+
): Promise<CommandResult | void> | CommandResult | void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional cleanup - called after execute completes
|
|
18
|
+
*/
|
|
19
|
+
cleanup?(): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ActionDefinition<TConfig = unknown, TInput = unknown> {
|
|
23
|
+
/**
|
|
24
|
+
* Action ID (e.g., "send-email")
|
|
25
|
+
*/
|
|
26
|
+
id: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Action description
|
|
30
|
+
*/
|
|
31
|
+
description?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handler implementation
|
|
35
|
+
*/
|
|
36
|
+
handler: ActionHandler<TConfig, TInput>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional input schema validation (future: use Zod/JSON Schema)
|
|
40
|
+
*/
|
|
41
|
+
schema?: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Define a workflow action
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* export default defineAction({
|
|
50
|
+
* id: 'send-email',
|
|
51
|
+
* description: 'Send an email notification',
|
|
52
|
+
* handler: {
|
|
53
|
+
* async execute(context, input: { to: string; subject: string; body: string }) {
|
|
54
|
+
* // Access workflow context
|
|
55
|
+
* const { workflowId, runId, stepId } = context.hostContext as WorkflowHost;
|
|
56
|
+
*
|
|
57
|
+
* context.ui.info(`Sending email in workflow ${workflowId}`);
|
|
58
|
+
*
|
|
59
|
+
* // Send email...
|
|
60
|
+
*
|
|
61
|
+
* return {
|
|
62
|
+
* data: { sent: true, messageId: '123' },
|
|
63
|
+
* exitCode: 0,
|
|
64
|
+
* };
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function defineAction<TConfig = unknown, TInput = unknown>(
|
|
71
|
+
definition: ActionDefinition<TConfig, TInput>
|
|
72
|
+
): ActionHandler<TConfig, TInput> {
|
|
73
|
+
// Validate host type at runtime
|
|
74
|
+
const wrappedHandler: ActionHandler<TConfig, TInput> = {
|
|
75
|
+
execute: (context, input) => {
|
|
76
|
+
// Ensure we're running in workflow host
|
|
77
|
+
if (context.host !== 'workflow') {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Action ${definition.id} can only run in workflow host (current: ${context.host})`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Call the actual handler
|
|
84
|
+
return definition.handler.execute(context, input);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
cleanup: definition.handler.cleanup,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return wrappedHandler;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Type guard to check if host context is workflow
|
|
95
|
+
*/
|
|
96
|
+
export function isWorkflowHost(
|
|
97
|
+
hostContext: HostContext
|
|
98
|
+
): hostContext is Extract<HostContext, { host: 'workflow' }> {
|
|
99
|
+
return hostContext.host === 'workflow';
|
|
100
|
+
}
|