@chatu-ai/builder-sdk-mock 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chatu-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @chatu-ai/builder-sdk-mock
2
+
3
+ Scripted `BuilderClient` for developing UIs against [`@chatu-ai/builder-sdk`](https://www.npmjs.com/package/@chatu-ai/builder-sdk) without a backend: event replay with configurable latency and disconnect injection. Same event schema as core, so swapping in the real client needs no glue.
4
+
5
+ MIT
@@ -0,0 +1,24 @@
1
+ /**
2
+ * createMockBuilderClient —— 按脚本回放事件序列(08 §6)。
3
+ * 支持延时与断线注入;断线后 resubscribe 从 lastSeq 续传(与真实服务端语义一致),
4
+ * 因此 UI 对 mock 与真实 client 的行为观感完全相同。
5
+ */
6
+ import type { BuilderClient, BuilderEvent, FileNode, SandboxStatus } from '@chatu-ai/builder-sdk';
7
+ export interface MockScriptStep {
8
+ event: BuilderEvent;
9
+ delayMs?: number;
10
+ /** 在该步之后注入一次断线(验证 resubscribe/seq 去重) */
11
+ dropConnectionAfter?: boolean;
12
+ }
13
+ export interface MockScript {
14
+ name: string;
15
+ steps: MockScriptStep[];
16
+ /** 初始文件树(fileDiff 事件会在其上演进) */
17
+ initialTree?: FileNode[];
18
+ }
19
+ export interface MockBuilderClient extends BuilderClient {
20
+ /** 测试辅助:当前沙箱状态(随 preview 事件演进) */
21
+ readonly currentStatus: SandboxStatus;
22
+ }
23
+ export declare function createMockBuilderClient(script: MockScript): MockBuilderClient;
24
+ export * from './scenarios';
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ import { resilientStream } from '@chatu-ai/builder-sdk';
2
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
3
+ export function createMockBuilderClient(script) {
4
+ let status = { state: 'creating' };
5
+ const versions = [];
6
+ let tree = script.initialTree ?? [];
7
+ let dropped = false; // 每脚本只断线一次
8
+ function applySideEffects(ev) {
9
+ if (ev.kind === 'ack')
10
+ status = { state: ev.sandbox.state, previewUrl: ev.sandbox.previewUrl };
11
+ if (ev.kind === 'preview')
12
+ status = {
13
+ ...status,
14
+ state: ev.state === 'ready' ? 'ready' : status.state,
15
+ previewUrl: ev.url ?? status.previewUrl,
16
+ devServer: { running: ev.state === 'ready', lastError: ev.error ?? undefined },
17
+ };
18
+ if (ev.kind === 'version')
19
+ versions.unshift({
20
+ sha: ev.sha, message: ev.message, filesChanged: ev.filesChanged, createdAt: new Date().toISOString(),
21
+ });
22
+ if (ev.kind === 'fileDiff')
23
+ tree = upsertPath(tree, ev.path, ev.action === 'delete');
24
+ }
25
+ async function* replay(fromSeq) {
26
+ for (const step of script.steps) {
27
+ const ev = step.event;
28
+ if (ev.kind !== 'ack' && ev.seq <= fromSeq)
29
+ continue; // 服务端回放语义
30
+ if (step.delayMs)
31
+ await sleep(step.delayMs);
32
+ applySideEffects(ev);
33
+ yield ev;
34
+ if (step.dropConnectionAfter && !dropped) {
35
+ dropped = true;
36
+ throw new Error('mock: connection dropped');
37
+ }
38
+ }
39
+ }
40
+ const stream = (fromSeq) => resilientStream({ open: () => replay(fromSeq), reopen: seq => replay(seq) }, { baseDelayMs: 10 });
41
+ return {
42
+ get currentStatus() { return status; },
43
+ chat: {
44
+ stream: () => stream(0),
45
+ resubscribe: (_c, _x, lastSeq) => stream(lastSeq),
46
+ cancel: async () => { status = { ...status, state: 'ready' }; },
47
+ },
48
+ sandbox: {
49
+ status: async () => status,
50
+ heartbeat: async () => { },
51
+ previewToken: async () => ({ token: 'mock-token', previewUrl: `${status.previewUrl ?? ''}/?t=mock-token` }),
52
+ wake: async () => { status = { ...status, state: 'ready' }; return status; },
53
+ },
54
+ versions: {
55
+ list: async () => versions,
56
+ restore: async (_id, sha) => {
57
+ const i = versions.findIndex(v => v.sha === sha);
58
+ if (i < 0)
59
+ throw new Error(`mock: unknown version ${sha}`);
60
+ versions.splice(0, i); // 回滚 = 丢弃更新的版本
61
+ },
62
+ },
63
+ files: {
64
+ tree: async () => tree,
65
+ read: async (_id, path) => `// mock content of ${path}\n`,
66
+ downloadUrl: () => 'https://mock.invalid/download.zip',
67
+ },
68
+ };
69
+ }
70
+ /** 极简树维护:仅保证 path 出现/消失,目录按需创建 */
71
+ function upsertPath(tree, path, remove) {
72
+ const [head, ...rest] = path.split('/');
73
+ const next = tree.filter(n => n.path !== head);
74
+ if (remove && rest.length === 0)
75
+ return next;
76
+ const existing = tree.find(n => n.path === head);
77
+ if (rest.length === 0)
78
+ return [...next, { path: head, type: 'file' }];
79
+ const dir = existing?.type === 'dir' ? existing : { path: head, type: 'dir', children: [] };
80
+ return [...next, { ...dir, children: upsertPath(dir.children ?? [], rest.join('/'), remove) }];
81
+ }
82
+ export * from './scenarios';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createMockBuilderClient, scenario1, scenario2 } from './index';
3
+ async function collect(src) {
4
+ const out = [];
5
+ for await (const e of src)
6
+ out.push(e);
7
+ return out;
8
+ }
9
+ describe('mock client', () => {
10
+ it('replays scenario1 completely and updates status/versions/tree', async () => {
11
+ const client = createMockBuilderClient(structuredClone(scenario1));
12
+ const events = await collect(client.chat.stream('c1', 'make a landing page'));
13
+ expect(events.at(0)?.kind).toBe('ack');
14
+ expect(events.at(-1)).toMatchObject({ kind: 'done', state: 'completed' });
15
+ expect(client.currentStatus.state).toBe('ready');
16
+ expect(client.currentStatus.devServer?.running).toBe(true);
17
+ expect(await client.versions.list('c1')).toHaveLength(1);
18
+ const tree = await client.files.tree('c1');
19
+ expect(tree.find(n => n.path === 'app')).toBeTruthy();
20
+ });
21
+ it('scenario2 survives injected disconnect with no gaps or dupes', async () => {
22
+ const client = createMockBuilderClient(structuredClone(scenario2));
23
+ const events = await collect(client.chat.stream('c1', 'change color'));
24
+ const seqs = events.filter(e => e.kind !== 'ack').map(e => e.seq);
25
+ expect(seqs).toEqual([1, 2, 3, 4, 5, 6]); // 断线后续传,无重无漏
26
+ expect(events.at(-1)?.kind).toBe('done');
27
+ });
28
+ it('restore rolls back version list', async () => {
29
+ const client = createMockBuilderClient(structuredClone(scenario1));
30
+ await collect(client.chat.stream('c1', 'x'));
31
+ await client.versions.restore('c1', 'aaa111');
32
+ expect((await client.versions.list('c1'))[0]?.sha).toBe('aaa111');
33
+ });
34
+ });
@@ -0,0 +1,6 @@
1
+ /** 内置脚本:DoD 场景 1(生成落地页)与场景 2(增量修改+断线注入)。 */
2
+ import type { MockScript } from './index';
3
+ /** DoD 场景 1:prompt → 任务卡渐进 → 文件生成 → 预览就绪 → 版本 → done */
4
+ export declare const scenario1: MockScript;
5
+ /** DoD 场景 2:增量修改(改主色)+ 中途断线(验证重连与 seq 去重) */
6
+ export declare const scenario2: MockScript;
@@ -0,0 +1,39 @@
1
+ const X = 'xid-mock-1';
2
+ const PREVIEW = 'https://sbx-mock.n.example.com';
3
+ /** DoD 场景 1:prompt → 任务卡渐进 → 文件生成 → 预览就绪 → 版本 → done */
4
+ export const scenario1 = {
5
+ name: 'dod-1-landing-page',
6
+ steps: [
7
+ { event: { kind: 'ack', xid: X, seq: 0, sandbox: { sandboxId: 'sbx-mock', state: 'warming', previewUrl: PREVIEW } } },
8
+ { event: { kind: 'taskCard', xid: X, seq: 1, id: 'tc1', label: '初始化模板', state: 'running' }, delayMs: 300 },
9
+ { event: { kind: 'taskCard', xid: X, seq: 2, id: 'tc1', label: '初始化模板', state: 'done' }, delayMs: 500 },
10
+ { event: { kind: 'taskCard', xid: X, seq: 3, id: 'tc2', label: '生成页面', state: 'running' }, delayMs: 200 },
11
+ { event: { kind: 'message', xid: X, seq: 4, role: 'assistant', text: '正在为你创建产品落地页,包含 Hero、特性与 CTA 区块……' }, delayMs: 300 },
12
+ { event: { kind: 'fileDiff', xid: X, seq: 5, path: 'app/page.tsx', action: 'create', bytes: 4096, truncated: false, diff: '+ export default function Landing() { ... }' }, delayMs: 600 },
13
+ { event: { kind: 'fileDiff', xid: X, seq: 6, path: 'components/hero.tsx', action: 'create', bytes: 2048, truncated: false }, delayMs: 400 },
14
+ { event: { kind: 'taskCard', xid: X, seq: 7, id: 'tc2', label: '生成页面', state: 'done' }, delayMs: 200 },
15
+ { event: { kind: 'taskCard', xid: X, seq: 8, id: 'tc3', label: '启动开发服务器', state: 'running' }, delayMs: 300 },
16
+ { event: { kind: 'preview', xid: X, seq: 9, state: 'starting' }, delayMs: 800 },
17
+ { event: { kind: 'preview', xid: X, seq: 10, state: 'ready', url: PREVIEW }, delayMs: 700 },
18
+ { event: { kind: 'taskCard', xid: X, seq: 11, id: 'tc3', label: '启动开发服务器', state: 'done' } },
19
+ { event: { kind: 'version', xid: X, seq: 12, sha: 'aaa111', message: 'create landing page', filesChanged: 2 }, delayMs: 200 },
20
+ { event: { kind: 'done', xid: X, seq: 13, state: 'completed' } },
21
+ ],
22
+ };
23
+ /** DoD 场景 2:增量修改(改主色)+ 中途断线(验证重连与 seq 去重) */
24
+ export const scenario2 = {
25
+ name: 'dod-2-incremental-with-drop',
26
+ initialTree: [
27
+ { path: 'app', type: 'dir', children: [{ path: 'page.tsx', type: 'file' }] },
28
+ { path: 'components', type: 'dir', children: [{ path: 'hero.tsx', type: 'file' }] },
29
+ ],
30
+ steps: [
31
+ { event: { kind: 'ack', xid: X, seq: 0, sandbox: { sandboxId: 'sbx-mock', state: 'ready', previewUrl: PREVIEW } } },
32
+ { event: { kind: 'taskCard', xid: X, seq: 1, id: 'tc1', label: '定位样式定义', state: 'running' }, delayMs: 300 },
33
+ { event: { kind: 'taskCard', xid: X, seq: 2, id: 'tc1', label: '定位样式定义', state: 'done' }, delayMs: 400, dropConnectionAfter: true },
34
+ { event: { kind: 'fileDiff', xid: X, seq: 3, path: 'app/globals.css', action: 'modify', bytes: 512, truncated: false, diff: '- --primary: blue\n+ --primary: green' }, delayMs: 300 },
35
+ { event: { kind: 'preview', xid: X, seq: 4, state: 'ready', url: PREVIEW }, delayMs: 400 },
36
+ { event: { kind: 'version', xid: X, seq: 5, sha: 'bbb222', message: 'change primary color to green', filesChanged: 1 } },
37
+ { event: { kind: 'done', xid: X, seq: 6, state: 'completed' } },
38
+ ],
39
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@chatu-ai/builder-sdk-mock",
3
+ "version": "0.1.0",
4
+ "description": "Mock BuilderClient for @chatu-ai/builder-sdk: scripted event replay with latency & disconnect injection",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "sideEffects": false,
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/chatu-ai/chatu-builder-sdk.git",
23
+ "directory": "packages/mock"
24
+ },
25
+ "homepage": "https://github.com/chatu-ai/chatu-builder-sdk/tree/main/packages/mock#readme",
26
+ "bugs": "https://github.com/chatu-ai/chatu-builder-sdk/issues",
27
+ "keywords": [
28
+ "chatu",
29
+ "builder",
30
+ "ai",
31
+ "app-builder",
32
+ "sdk",
33
+ "mock",
34
+ "testing"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@chatu-ai/builder-sdk": "0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.7.0",
48
+ "vitest": "^3.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.json",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "vitest run"
54
+ }
55
+ }