@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,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/shared-testing",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Test utilities for KB Labs plugin development — mock builders, platform setup, test context",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clean": "rimraf dist",
|
|
22
|
+
"build": "tsup --config tsup.config.ts",
|
|
23
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src",
|
|
28
|
+
"lint:fix": "eslint src --fix"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@kb-labs/core-platform": "link:../../../kb-labs-core/packages/core-platform",
|
|
32
|
+
"@kb-labs/core-runtime": "link:../../../kb-labs-core/packages/core-runtime",
|
|
33
|
+
"@kb-labs/plugin-contracts": "link:../../../../infra/kb-labs-plugin/packages/plugin-contracts",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
|
|
38
|
+
"@types/node": "^24.3.3",
|
|
39
|
+
"rimraf": "^6.0.1",
|
|
40
|
+
"tsup": "^8.5.0",
|
|
41
|
+
"typescript": "^5.6.3"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.0.0",
|
|
45
|
+
"pnpm": ">=9.0.0"
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "pnpm@9.11.0",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { platform, resetPlatform } from '@kb-labs/core-runtime';
|
|
3
|
+
import { createTestContext } from '../create-test-context.js';
|
|
4
|
+
import { mockLLM } from '../mock-llm.js';
|
|
5
|
+
import { mockCache } from '../mock-cache.js';
|
|
6
|
+
|
|
7
|
+
describe('createTestContext', () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
resetPlatform();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('basic creation', () => {
|
|
13
|
+
it('should create context with default values', () => {
|
|
14
|
+
const { ctx, cleanup } = createTestContext();
|
|
15
|
+
cleanup();
|
|
16
|
+
|
|
17
|
+
expect(ctx.pluginId).toBe('test-plugin');
|
|
18
|
+
expect(ctx.pluginVersion).toBe('0.0.0');
|
|
19
|
+
expect(ctx.host).toBe('cli');
|
|
20
|
+
expect(ctx.requestId).toBe('test-trace:test-span');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should accept custom pluginId and host', () => {
|
|
24
|
+
const { ctx, cleanup } = createTestContext({
|
|
25
|
+
pluginId: 'my-plugin',
|
|
26
|
+
host: 'rest',
|
|
27
|
+
});
|
|
28
|
+
cleanup();
|
|
29
|
+
|
|
30
|
+
expect(ctx.pluginId).toBe('my-plugin');
|
|
31
|
+
expect(ctx.host).toBe('rest');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should accept custom config', () => {
|
|
35
|
+
const { ctx, cleanup } = createTestContext<{ apiKey: string }>({
|
|
36
|
+
config: { apiKey: 'test-key' },
|
|
37
|
+
});
|
|
38
|
+
cleanup();
|
|
39
|
+
|
|
40
|
+
expect(ctx.config!.apiKey).toBe('test-key');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('singleton sync', () => {
|
|
45
|
+
it('should sync platform adapters to global singleton by default', () => {
|
|
46
|
+
const llm = mockLLM().onAnyComplete().respondWith('synced');
|
|
47
|
+
const { ctx, cleanup } = createTestContext({ platform: { llm } });
|
|
48
|
+
|
|
49
|
+
// ctx.platform has the mock
|
|
50
|
+
expect(ctx.platform.llm).toBe(llm);
|
|
51
|
+
|
|
52
|
+
// Global singleton also has the mock
|
|
53
|
+
expect(platform.llm).toBe(llm);
|
|
54
|
+
|
|
55
|
+
cleanup();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should not sync when syncSingleton is false', () => {
|
|
59
|
+
const llm = mockLLM();
|
|
60
|
+
const { ctx, cleanup } = createTestContext({
|
|
61
|
+
platform: { llm },
|
|
62
|
+
syncSingleton: false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ctx.platform has the mock
|
|
66
|
+
expect(ctx.platform.llm).toBe(llm);
|
|
67
|
+
|
|
68
|
+
// Global singleton should NOT have it (it was reset before the test)
|
|
69
|
+
expect(platform.hasAdapter('llm')).toBe(false);
|
|
70
|
+
|
|
71
|
+
cleanup();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('cleanup should reset the global singleton', () => {
|
|
75
|
+
const llm = mockLLM();
|
|
76
|
+
const { cleanup } = createTestContext({ platform: { llm } });
|
|
77
|
+
|
|
78
|
+
expect(platform.hasAdapter('llm')).toBe(true);
|
|
79
|
+
|
|
80
|
+
cleanup();
|
|
81
|
+
|
|
82
|
+
expect(platform.hasAdapter('llm')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('default mocks are spies', () => {
|
|
87
|
+
it('should provide LLM with vi.fn() spies', async () => {
|
|
88
|
+
const { ctx, cleanup } = createTestContext();
|
|
89
|
+
|
|
90
|
+
await ctx.platform.llm.complete('test');
|
|
91
|
+
expect(ctx.platform.llm.complete).toHaveBeenCalledWith('test');
|
|
92
|
+
|
|
93
|
+
cleanup();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should provide cache with vi.fn() spies', async () => {
|
|
97
|
+
const { ctx, cleanup } = createTestContext();
|
|
98
|
+
|
|
99
|
+
await ctx.platform.cache.set('key', 'value');
|
|
100
|
+
expect(ctx.platform.cache.set).toHaveBeenCalledWith('key', 'value');
|
|
101
|
+
|
|
102
|
+
cleanup();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should provide UI with vi.fn() spies', () => {
|
|
106
|
+
const { ctx, cleanup } = createTestContext();
|
|
107
|
+
|
|
108
|
+
ctx.ui.info('test message');
|
|
109
|
+
expect(ctx.ui.info).toHaveBeenCalledWith('test message');
|
|
110
|
+
|
|
111
|
+
cleanup();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should provide runtime with vi.fn() spies', async () => {
|
|
115
|
+
const { ctx, cleanup } = createTestContext();
|
|
116
|
+
|
|
117
|
+
await ctx.runtime.fs.readFile('test.txt');
|
|
118
|
+
expect(ctx.runtime.fs.readFile).toHaveBeenCalledWith('test.txt');
|
|
119
|
+
|
|
120
|
+
cleanup();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should provide plugin API with vi.fn() spies', async () => {
|
|
124
|
+
const { ctx, cleanup } = createTestContext();
|
|
125
|
+
|
|
126
|
+
await ctx.api.state.set('key', 'value');
|
|
127
|
+
expect(ctx.api.state.set).toHaveBeenCalledWith('key', 'value');
|
|
128
|
+
|
|
129
|
+
cleanup();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should provide environment/workspace/snapshot APIs by default', async () => {
|
|
133
|
+
const { ctx, cleanup } = createTestContext();
|
|
134
|
+
|
|
135
|
+
const workspace = await ctx.api.workspace.materialize({ sourceRef: 'repo://main' });
|
|
136
|
+
const environment = await ctx.api.environment.create({ templateId: 'node20' });
|
|
137
|
+
const snapshot = await ctx.api.snapshot.capture({ workspaceId: workspace.workspaceId });
|
|
138
|
+
|
|
139
|
+
expect(workspace.workspaceId).toBeDefined();
|
|
140
|
+
expect(environment.environmentId).toBeDefined();
|
|
141
|
+
expect(snapshot.snapshotId).toBeDefined();
|
|
142
|
+
expect(ctx.api.workspace.materialize).toHaveBeenCalled();
|
|
143
|
+
expect(ctx.api.environment.create).toHaveBeenCalled();
|
|
144
|
+
expect(ctx.api.snapshot.capture).toHaveBeenCalled();
|
|
145
|
+
|
|
146
|
+
cleanup();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('platform overrides', () => {
|
|
151
|
+
it('should use custom LLM mock', async () => {
|
|
152
|
+
const llm = mockLLM()
|
|
153
|
+
.onComplete('generate').respondWith('custom response');
|
|
154
|
+
|
|
155
|
+
const { ctx, cleanup } = createTestContext({ platform: { llm } });
|
|
156
|
+
|
|
157
|
+
const res = await ctx.platform.llm.complete('generate commit');
|
|
158
|
+
expect(res.content).toBe('custom response');
|
|
159
|
+
|
|
160
|
+
cleanup();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should use custom cache mock', async () => {
|
|
164
|
+
const cache = mockCache({ 'pre': 'loaded' });
|
|
165
|
+
const { ctx, cleanup } = createTestContext({ platform: { cache } });
|
|
166
|
+
|
|
167
|
+
expect(await ctx.platform.cache.get('pre')).toBe('loaded');
|
|
168
|
+
|
|
169
|
+
cleanup();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('host contexts', () => {
|
|
174
|
+
it('should generate CLI host context by default', () => {
|
|
175
|
+
const { ctx, cleanup } = createTestContext({ host: 'cli' });
|
|
176
|
+
expect(ctx.hostContext).toEqual({ host: 'cli', argv: ['test'], flags: {} });
|
|
177
|
+
cleanup();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should generate REST host context', () => {
|
|
181
|
+
const { ctx, cleanup } = createTestContext({ host: 'rest' });
|
|
182
|
+
expect(ctx.hostContext.host).toBe('rest');
|
|
183
|
+
cleanup();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should generate workflow host context', () => {
|
|
187
|
+
const { ctx, cleanup } = createTestContext({ host: 'workflow' });
|
|
188
|
+
expect(ctx.hostContext.host).toBe('workflow');
|
|
189
|
+
cleanup();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should accept custom host context', () => {
|
|
193
|
+
const custom = { host: 'cli' as const, argv: ['custom'], flags: { verbose: true } };
|
|
194
|
+
const { ctx, cleanup } = createTestContext({ hostContext: custom });
|
|
195
|
+
expect(ctx.hostContext).toEqual(custom);
|
|
196
|
+
cleanup();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mockCache } from '../mock-cache.js';
|
|
3
|
+
|
|
4
|
+
describe('mockCache', () => {
|
|
5
|
+
describe('basic operations', () => {
|
|
6
|
+
it('should store and retrieve values', async () => {
|
|
7
|
+
const cache = mockCache();
|
|
8
|
+
await cache.set('key', { name: 'Alice' });
|
|
9
|
+
|
|
10
|
+
const result = await cache.get<{ name: string }>('key');
|
|
11
|
+
expect(result).toEqual({ name: 'Alice' });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return null for missing keys', async () => {
|
|
15
|
+
const cache = mockCache();
|
|
16
|
+
expect(await cache.get('missing')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should delete values', async () => {
|
|
20
|
+
const cache = mockCache();
|
|
21
|
+
await cache.set('key', 'value');
|
|
22
|
+
await cache.delete('key');
|
|
23
|
+
expect(await cache.get('key')).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should clear all values', async () => {
|
|
27
|
+
const cache = mockCache();
|
|
28
|
+
await cache.set('a', 1);
|
|
29
|
+
await cache.set('b', 2);
|
|
30
|
+
await cache.clear();
|
|
31
|
+
expect(await cache.get('a')).toBeNull();
|
|
32
|
+
expect(await cache.get('b')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should clear by prefix pattern', async () => {
|
|
36
|
+
const cache = mockCache();
|
|
37
|
+
await cache.set('user:1', 'Alice');
|
|
38
|
+
await cache.set('user:2', 'Bob');
|
|
39
|
+
await cache.set('post:1', 'Hello');
|
|
40
|
+
|
|
41
|
+
await cache.clear('user:*');
|
|
42
|
+
|
|
43
|
+
expect(await cache.get('user:1')).toBeNull();
|
|
44
|
+
expect(await cache.get('user:2')).toBeNull();
|
|
45
|
+
expect(await cache.get('post:1')).toBe('Hello');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('TTL', () => {
|
|
50
|
+
it('should expire entries after TTL', async () => {
|
|
51
|
+
const cache = mockCache();
|
|
52
|
+
await cache.set('key', 'value', 50); // 50ms TTL
|
|
53
|
+
|
|
54
|
+
// Still alive
|
|
55
|
+
expect(await cache.get('key')).toBe('value');
|
|
56
|
+
|
|
57
|
+
// Wait for expiry
|
|
58
|
+
await new Promise(r => { setTimeout(r, 100); });
|
|
59
|
+
expect(await cache.get('key')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should not expire entries without TTL', async () => {
|
|
63
|
+
const cache = mockCache();
|
|
64
|
+
await cache.set('key', 'value'); // no TTL
|
|
65
|
+
|
|
66
|
+
await new Promise(r => { setTimeout(r, 50); });
|
|
67
|
+
expect(await cache.get('key')).toBe('value');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('setIfNotExists', () => {
|
|
72
|
+
it('should set value if key does not exist', async () => {
|
|
73
|
+
const cache = mockCache();
|
|
74
|
+
const result = await cache.setIfNotExists('key', 'value');
|
|
75
|
+
expect(result).toBe(true);
|
|
76
|
+
expect(await cache.get('key')).toBe('value');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not overwrite existing value', async () => {
|
|
80
|
+
const cache = mockCache();
|
|
81
|
+
await cache.set('key', 'original');
|
|
82
|
+
const result = await cache.setIfNotExists('key', 'new');
|
|
83
|
+
expect(result).toBe(false);
|
|
84
|
+
expect(await cache.get('key')).toBe('original');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('sorted sets', () => {
|
|
89
|
+
it('should add and retrieve by score range', async () => {
|
|
90
|
+
const cache = mockCache();
|
|
91
|
+
await cache.zadd('myset', 1, 'a');
|
|
92
|
+
await cache.zadd('myset', 3, 'c');
|
|
93
|
+
await cache.zadd('myset', 2, 'b');
|
|
94
|
+
|
|
95
|
+
const result = await cache.zrangebyscore('myset', 1, 2);
|
|
96
|
+
expect(result).toEqual(['a', 'b']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should remove members', async () => {
|
|
100
|
+
const cache = mockCache();
|
|
101
|
+
await cache.zadd('myset', 1, 'a');
|
|
102
|
+
await cache.zadd('myset', 2, 'b');
|
|
103
|
+
|
|
104
|
+
await cache.zrem('myset', 'a');
|
|
105
|
+
|
|
106
|
+
const result = await cache.zrangebyscore('myset', 0, 10);
|
|
107
|
+
expect(result).toEqual(['b']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should update score for existing member', async () => {
|
|
111
|
+
const cache = mockCache();
|
|
112
|
+
await cache.zadd('myset', 1, 'a');
|
|
113
|
+
await cache.zadd('myset', 5, 'a'); // update score
|
|
114
|
+
|
|
115
|
+
const result = await cache.zrangebyscore('myset', 4, 6);
|
|
116
|
+
expect(result).toEqual(['a']);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('initial data', () => {
|
|
121
|
+
it('should accept pre-populated data', async () => {
|
|
122
|
+
const cache = mockCache({
|
|
123
|
+
'user:1': { name: 'Alice' },
|
|
124
|
+
'config': 'debug',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(await cache.get('user:1')).toEqual({ name: 'Alice' });
|
|
128
|
+
expect(await cache.get('config')).toBe('debug');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('spy tracking', () => {
|
|
133
|
+
it('all methods should be vi.fn() spies', async () => {
|
|
134
|
+
const cache = mockCache();
|
|
135
|
+
await cache.set('key', 'value');
|
|
136
|
+
await cache.get('key');
|
|
137
|
+
|
|
138
|
+
expect(cache.set).toHaveBeenCalledWith('key', 'value');
|
|
139
|
+
expect(cache.get).toHaveBeenCalledWith('key');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('internal state access', () => {
|
|
144
|
+
it('should expose .store for assertions', async () => {
|
|
145
|
+
const cache = mockCache();
|
|
146
|
+
await cache.set('key', 'value');
|
|
147
|
+
|
|
148
|
+
expect(cache.store.size).toBe(1);
|
|
149
|
+
expect(cache.store.has('key')).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should expose .sortedSets for assertions', async () => {
|
|
153
|
+
const cache = mockCache();
|
|
154
|
+
await cache.zadd('myset', 1, 'a');
|
|
155
|
+
|
|
156
|
+
expect(cache.sortedSets.size).toBe(1);
|
|
157
|
+
expect(cache.sortedSets.get('myset')).toHaveLength(1);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('reset', () => {
|
|
162
|
+
it('should clear all data and spy history', async () => {
|
|
163
|
+
const cache = mockCache();
|
|
164
|
+
await cache.set('key', 'value');
|
|
165
|
+
await cache.zadd('myset', 1, 'a');
|
|
166
|
+
|
|
167
|
+
cache.reset();
|
|
168
|
+
|
|
169
|
+
expect(cache.store.size).toBe(0);
|
|
170
|
+
expect(cache.sortedSets.size).toBe(0);
|
|
171
|
+
expect(cache.set).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mockLLM } from '../mock-llm.js';
|
|
3
|
+
|
|
4
|
+
describe('mockLLM', () => {
|
|
5
|
+
describe('basic usage', () => {
|
|
6
|
+
it('should return default response', async () => {
|
|
7
|
+
const llm = mockLLM();
|
|
8
|
+
const res = await llm.complete('hello');
|
|
9
|
+
|
|
10
|
+
expect(res.content).toBe('mock response');
|
|
11
|
+
expect(res.model).toBe('mock');
|
|
12
|
+
expect(res.usage).toEqual({ promptTokens: 0, completionTokens: 0 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should track calls with vi.fn() spy', async () => {
|
|
16
|
+
const llm = mockLLM();
|
|
17
|
+
await llm.complete('hello');
|
|
18
|
+
await llm.complete('world');
|
|
19
|
+
|
|
20
|
+
expect(llm.complete).toHaveBeenCalledTimes(2);
|
|
21
|
+
expect(llm.complete).toHaveBeenCalledWith('hello');
|
|
22
|
+
expect(llm.complete).toHaveBeenCalledWith('world');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should record calls in .calls array', async () => {
|
|
26
|
+
const llm = mockLLM();
|
|
27
|
+
await llm.complete('hello');
|
|
28
|
+
|
|
29
|
+
expect(llm.calls).toHaveLength(1);
|
|
30
|
+
expect(llm.calls[0]!.prompt).toBe('hello');
|
|
31
|
+
expect(llm.calls[0]!.response.content).toBe('mock response');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should expose lastCall', async () => {
|
|
35
|
+
const llm = mockLLM();
|
|
36
|
+
await llm.complete('first');
|
|
37
|
+
await llm.complete('second');
|
|
38
|
+
|
|
39
|
+
expect(llm.lastCall?.prompt).toBe('second');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('onComplete() with string matcher', () => {
|
|
44
|
+
it('should match substring and return specific response', async () => {
|
|
45
|
+
const llm = mockLLM()
|
|
46
|
+
.onComplete('commit').respondWith('feat: add feature')
|
|
47
|
+
.onAnyComplete().respondWith('default');
|
|
48
|
+
|
|
49
|
+
const res1 = await llm.complete('Generate commit message');
|
|
50
|
+
expect(res1.content).toBe('feat: add feature');
|
|
51
|
+
|
|
52
|
+
const res2 = await llm.complete('something else');
|
|
53
|
+
expect(res2.content).toBe('default');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('onComplete() with regex matcher', () => {
|
|
58
|
+
it('should match regex pattern', async () => {
|
|
59
|
+
const llm = mockLLM()
|
|
60
|
+
.onComplete(/explain/i).respondWith('This code does X');
|
|
61
|
+
|
|
62
|
+
const res = await llm.complete('Please explain this function');
|
|
63
|
+
expect(res.content).toBe('This code does X');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('onComplete() with function matcher', () => {
|
|
68
|
+
it('should use custom matcher function', async () => {
|
|
69
|
+
const llm = mockLLM()
|
|
70
|
+
.onComplete((p) => p.length > 100).respondWith('long prompt handled');
|
|
71
|
+
|
|
72
|
+
const shortRes = await llm.complete('short');
|
|
73
|
+
expect(shortRes.content).toBe('mock response'); // default
|
|
74
|
+
|
|
75
|
+
const longRes = await llm.complete('a'.repeat(101));
|
|
76
|
+
expect(longRes.content).toBe('long prompt handled');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('onComplete() with function response', () => {
|
|
81
|
+
it('should accept a response factory function', async () => {
|
|
82
|
+
const llm = mockLLM()
|
|
83
|
+
.onAnyComplete().respondWith((prompt) => `Echo: ${prompt}`);
|
|
84
|
+
|
|
85
|
+
const res = await llm.complete('hello');
|
|
86
|
+
expect(res.content).toBe('Echo: hello');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('onComplete() with LLMResponse object', () => {
|
|
91
|
+
it('should accept full LLMResponse', async () => {
|
|
92
|
+
const llm = mockLLM()
|
|
93
|
+
.onAnyComplete().respondWith({
|
|
94
|
+
content: 'custom',
|
|
95
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
96
|
+
model: 'gpt-4',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const res = await llm.complete('test');
|
|
100
|
+
expect(res.content).toBe('custom');
|
|
101
|
+
expect(res.usage.promptTokens).toBe(10);
|
|
102
|
+
expect(res.model).toBe('gpt-4');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('onAnyComplete()', () => {
|
|
107
|
+
it('should set default response', async () => {
|
|
108
|
+
const llm = mockLLM()
|
|
109
|
+
.onAnyComplete().respondWith('always this');
|
|
110
|
+
|
|
111
|
+
const res = await llm.complete('anything');
|
|
112
|
+
expect(res.content).toBe('always this');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('rule priority', () => {
|
|
117
|
+
it('should match first matching rule', async () => {
|
|
118
|
+
const llm = mockLLM()
|
|
119
|
+
.onComplete('commit').respondWith('first')
|
|
120
|
+
.onComplete('commit').respondWith('second');
|
|
121
|
+
|
|
122
|
+
const res = await llm.complete('commit');
|
|
123
|
+
expect(res.content).toBe('first');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('streaming', () => {
|
|
128
|
+
it('should yield configured chunks', async () => {
|
|
129
|
+
const llm = mockLLM().streaming(['hello', ' ', 'world']);
|
|
130
|
+
|
|
131
|
+
const chunks: string[] = [];
|
|
132
|
+
for await (const chunk of llm.stream('test')) {
|
|
133
|
+
chunks.push(chunk);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
expect(chunks).toEqual(['hello', ' ', 'world']);
|
|
137
|
+
expect(llm.stream).toHaveBeenCalledWith('test');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('failing', () => {
|
|
142
|
+
it('should throw on complete()', async () => {
|
|
143
|
+
const error = new Error('rate limit exceeded');
|
|
144
|
+
const llm = mockLLM().failing(error);
|
|
145
|
+
|
|
146
|
+
await expect(llm.complete('test')).rejects.toThrow('rate limit exceeded');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw on stream()', async () => {
|
|
150
|
+
const llm = mockLLM().failing(new Error('down'));
|
|
151
|
+
|
|
152
|
+
await expect(async () => {
|
|
153
|
+
for await (const _ of llm.stream('test')) { /* consume */ }
|
|
154
|
+
}).rejects.toThrow('down');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should throw on chatWithTools()', async () => {
|
|
158
|
+
const llm = mockLLM().failing(new Error('fail'));
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
llm.chatWithTools!(
|
|
162
|
+
[{ role: 'user', content: 'test' }],
|
|
163
|
+
{ tools: [] },
|
|
164
|
+
),
|
|
165
|
+
).rejects.toThrow('fail');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('chatWithTools', () => {
|
|
170
|
+
it('should return configured tool calls', async () => {
|
|
171
|
+
const llm = mockLLM().withToolCalls([
|
|
172
|
+
{ id: 'call-1', name: 'search', input: { query: 'test' } },
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const res = await llm.chatWithTools!(
|
|
176
|
+
[{ role: 'user', content: 'find something' }],
|
|
177
|
+
{ tools: [{ name: 'search', description: 'Search', inputSchema: {} }] },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(res.toolCalls).toHaveLength(1);
|
|
181
|
+
expect(res.toolCalls![0]!.name).toBe('search');
|
|
182
|
+
expect(res.toolCalls![0]!.input).toEqual({ query: 'test' });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should record tool call history', async () => {
|
|
186
|
+
const llm = mockLLM();
|
|
187
|
+
await llm.chatWithTools!(
|
|
188
|
+
[{ role: 'user', content: 'test' }],
|
|
189
|
+
{ tools: [] },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(llm.toolCalls).toHaveLength(1);
|
|
193
|
+
expect(llm.toolCalls[0]!.messages[0]!.content).toBe('test');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('resetCalls', () => {
|
|
198
|
+
it('should clear all recorded calls and spies', async () => {
|
|
199
|
+
const llm = mockLLM();
|
|
200
|
+
await llm.complete('test');
|
|
201
|
+
|
|
202
|
+
expect(llm.calls).toHaveLength(1);
|
|
203
|
+
expect(llm.complete).toHaveBeenCalledOnce();
|
|
204
|
+
|
|
205
|
+
llm.resetCalls();
|
|
206
|
+
|
|
207
|
+
expect(llm.calls).toHaveLength(0);
|
|
208
|
+
expect(llm.complete).not.toHaveBeenCalled();
|
|
209
|
+
expect(llm.lastCall).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|