@decocms/mesh-sdk 1.2.2 → 1.2.3
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/README.md +10 -10
- package/package.json +3 -2
- package/src/context/index.ts +0 -1
- package/src/context/project-context.tsx +0 -10
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-collections.ts +19 -12
- package/src/hooks/use-connection.ts +12 -1
- package/src/hooks/use-mcp-client.ts +27 -10
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +38 -2
- package/src/lib/bridge-transport.ts +6 -434
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +121 -67
- package/src/lib/default-model.ts +188 -3
- package/src/lib/mcp-oauth.ts +59 -8
- package/src/lib/query-keys.ts +19 -4
- package/src/lib/server-client-bridge.ts +4 -146
- package/src/lib/usage.test.ts +66 -0
- package/src/lib/usage.ts +26 -0
- package/src/types/ai-providers.ts +19 -1
- package/src/types/connection.ts +5 -0
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +51 -8
- package/src/types/index.ts +18 -0
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +416 -9
package/src/lib/usage.ts
CHANGED
|
@@ -14,6 +14,18 @@ export interface UsageData {
|
|
|
14
14
|
outputTokens?: number;
|
|
15
15
|
reasoningTokens?: number;
|
|
16
16
|
totalTokens?: number;
|
|
17
|
+
/**
|
|
18
|
+
* AI SDK normalizes cache token counts across providers via
|
|
19
|
+
* usage.inputTokenDetails — populated identically by the anthropic,
|
|
20
|
+
* openai, google and openrouter adapters. `cachedInputTokens` is the
|
|
21
|
+
* convenience shorthand the AI SDK also surfaces (= cacheReadTokens).
|
|
22
|
+
*/
|
|
23
|
+
cachedInputTokens?: number;
|
|
24
|
+
inputTokenDetails?: {
|
|
25
|
+
cacheReadTokens?: number;
|
|
26
|
+
cacheWriteTokens?: number;
|
|
27
|
+
noCacheTokens?: number;
|
|
28
|
+
};
|
|
17
29
|
providerMetadata?: {
|
|
18
30
|
[key: string]: unknown;
|
|
19
31
|
};
|
|
@@ -25,6 +37,10 @@ export interface UsageStats {
|
|
|
25
37
|
reasoningTokens: number;
|
|
26
38
|
totalTokens: number;
|
|
27
39
|
cost: number;
|
|
40
|
+
/** Tokens read from prompt cache (anthropic / openrouter / openai / google). */
|
|
41
|
+
cacheReadTokens: number;
|
|
42
|
+
/** Tokens written to prompt cache (Anthropic only — others auto-cache without separate billing). */
|
|
43
|
+
cacheWriteTokens: number;
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
type ProviderCostExtractor = (
|
|
@@ -124,6 +140,8 @@ export function emptyUsageStats(): UsageStats {
|
|
|
124
140
|
reasoningTokens: 0,
|
|
125
141
|
totalTokens: 0,
|
|
126
142
|
cost: 0,
|
|
143
|
+
cacheReadTokens: 0,
|
|
144
|
+
cacheWriteTokens: 0,
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
147
|
|
|
@@ -137,6 +155,12 @@ export function addUsage(
|
|
|
137
155
|
): UsageStats {
|
|
138
156
|
if (!stepUsage) return accumulated;
|
|
139
157
|
|
|
158
|
+
const cacheRead =
|
|
159
|
+
stepUsage.inputTokenDetails?.cacheReadTokens ??
|
|
160
|
+
stepUsage.cachedInputTokens ??
|
|
161
|
+
0;
|
|
162
|
+
const cacheWrite = stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
|
|
163
|
+
|
|
140
164
|
return {
|
|
141
165
|
inputTokens: accumulated.inputTokens + (stepUsage.inputTokens ?? 0),
|
|
142
166
|
outputTokens: accumulated.outputTokens + (stepUsage.outputTokens ?? 0),
|
|
@@ -144,6 +168,8 @@ export function addUsage(
|
|
|
144
168
|
accumulated.reasoningTokens + (stepUsage.reasoningTokens ?? 0),
|
|
145
169
|
totalTokens: accumulated.totalTokens + (stepUsage.totalTokens ?? 0),
|
|
146
170
|
cost: accumulated.cost + getCostFromUsage(stepUsage),
|
|
171
|
+
cacheReadTokens: accumulated.cacheReadTokens + cacheRead,
|
|
172
|
+
cacheWriteTokens: accumulated.cacheWriteTokens + cacheWrite,
|
|
147
173
|
};
|
|
148
174
|
}
|
|
149
175
|
|
|
@@ -8,6 +8,8 @@ export const PROVIDER_IDS = [
|
|
|
8
8
|
"openrouter",
|
|
9
9
|
"google",
|
|
10
10
|
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"openai-compatible",
|
|
11
13
|
] as const;
|
|
12
14
|
|
|
13
15
|
export type ProviderId = (typeof PROVIDER_IDS)[number];
|
|
@@ -45,6 +47,16 @@ export interface AiProviderModel {
|
|
|
45
47
|
capabilities: ModelCapability[];
|
|
46
48
|
limits: AiProviderModelLimits | null;
|
|
47
49
|
costs: AiProviderModelCosts | null;
|
|
50
|
+
/** When true the upstream provider has flagged this model as deprecated. */
|
|
51
|
+
deprecated?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* When true, this model can ONLY be used through the provider's
|
|
54
|
+
* `AsyncResearchProvider` capability (e.g. Gemini Deep Research via the
|
|
55
|
+
* Interactions API). It is unusable as a Thinking/Coding/Fast/Image model
|
|
56
|
+
* because `streamText` / `generateContent` will reject it. UIs should
|
|
57
|
+
* restrict it to the deep-research slot.
|
|
58
|
+
*/
|
|
59
|
+
asyncResearch?: boolean;
|
|
48
60
|
/** Client-side only — the credential key ID used to fetch this model. */
|
|
49
61
|
keyId?: string;
|
|
50
62
|
}
|
|
@@ -53,6 +65,11 @@ export interface AiProviderKey {
|
|
|
53
65
|
id: string;
|
|
54
66
|
providerId: ProviderId;
|
|
55
67
|
label: string;
|
|
68
|
+
/**
|
|
69
|
+
* Frontend preset id (e.g. "litellm", "ollama") for openai-compatible keys
|
|
70
|
+
* that were created from a branded preset card. Null otherwise.
|
|
71
|
+
*/
|
|
72
|
+
presetId: string | null;
|
|
56
73
|
createdBy: string;
|
|
57
74
|
createdAt: string;
|
|
58
75
|
}
|
|
@@ -62,7 +79,8 @@ export interface AiProviderInfo {
|
|
|
62
79
|
name: string;
|
|
63
80
|
description: string;
|
|
64
81
|
logo?: string | null;
|
|
65
|
-
supportedMethods: ("api-key" | "oauth-pkce")[];
|
|
82
|
+
supportedMethods: ("api-key" | "oauth-pkce" | "cli-activate")[];
|
|
66
83
|
supportsTopUp?: boolean;
|
|
67
84
|
supportsCredits?: boolean;
|
|
85
|
+
supportsProvision?: boolean;
|
|
68
86
|
}
|
package/src/types/connection.ts
CHANGED
|
@@ -102,6 +102,11 @@ export const ConnectionEntitySchema = z.object({
|
|
|
102
102
|
icon: z.string().nullable().describe("Icon URL for the connection"),
|
|
103
103
|
app_name: z.string().nullable().describe("Associated app name"),
|
|
104
104
|
app_id: z.string().nullable().describe("Associated app ID"),
|
|
105
|
+
slug: z
|
|
106
|
+
.string()
|
|
107
|
+
.nullable()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("URL-safe slug derived from app_name, connection_url, or title"),
|
|
105
110
|
|
|
106
111
|
connection_type: z
|
|
107
112
|
.enum(["HTTP", "SSE", "Websocket", "STDIO", "VIRTUAL"])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createDecopilotThreadStatusEvent,
|
|
4
|
+
DECOPILOT_EVENTS,
|
|
5
|
+
} from "./decopilot-events";
|
|
6
|
+
|
|
7
|
+
describe("createDecopilotThreadStatusEvent", () => {
|
|
8
|
+
test("carries virtualMcpId, createdBy, and triggerId on data", () => {
|
|
9
|
+
const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
|
|
10
|
+
virtualMcpId: "vm-1",
|
|
11
|
+
createdBy: "user-1",
|
|
12
|
+
triggerId: "trig-1",
|
|
13
|
+
});
|
|
14
|
+
expect(e.type).toBe(DECOPILOT_EVENTS.THREAD_STATUS);
|
|
15
|
+
expect(e.subject).toBe("task-1");
|
|
16
|
+
expect(e.data.status).toBe("in_progress");
|
|
17
|
+
expect(e.data.virtual_mcp_id).toBe("vm-1");
|
|
18
|
+
expect(e.data.created_by).toBe("user-1");
|
|
19
|
+
expect(e.data.trigger_id).toBe("trig-1");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("omits optional fields when not provided", () => {
|
|
23
|
+
const e = createDecopilotThreadStatusEvent("task-1", "completed");
|
|
24
|
+
expect(e.data.status).toBe("completed");
|
|
25
|
+
expect(e.data.virtual_mcp_id).toBeUndefined();
|
|
26
|
+
expect(e.data.created_by).toBeUndefined();
|
|
27
|
+
expect(e.data.trigger_id).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("preserves explicit null trigger_id (human-initiated thread)", () => {
|
|
31
|
+
const e = createDecopilotThreadStatusEvent("task-1", "completed", {
|
|
32
|
+
triggerId: null,
|
|
33
|
+
});
|
|
34
|
+
expect(e.data.trigger_id).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("works with only virtualMcpId provided (migration shape)", () => {
|
|
38
|
+
const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
|
|
39
|
+
virtualMcpId: "vm-1",
|
|
40
|
+
});
|
|
41
|
+
expect(e.data.virtual_mcp_id).toBe("vm-1");
|
|
42
|
+
expect(e.data.created_by).toBeUndefined();
|
|
43
|
+
expect(e.data.trigger_id).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("createDecopilotThreadStatusEvent — enriched fields", () => {
|
|
48
|
+
test("round-trips title, branch, createdAt, updatedAt", () => {
|
|
49
|
+
const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
|
|
50
|
+
virtualMcpId: "vm-1",
|
|
51
|
+
title: "Refactor login",
|
|
52
|
+
branch: "feature/login",
|
|
53
|
+
createdAt: "2026-05-19T00:00:00.000Z",
|
|
54
|
+
updatedAt: "2026-05-19T00:05:00.000Z",
|
|
55
|
+
});
|
|
56
|
+
expect(e.data.title).toBe("Refactor login");
|
|
57
|
+
expect(e.data.branch).toBe("feature/login");
|
|
58
|
+
expect(e.data.created_at).toBe("2026-05-19T00:00:00.000Z");
|
|
59
|
+
expect(e.data.updated_at).toBe("2026-05-19T00:05:00.000Z");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("omits the new fields when not provided", () => {
|
|
63
|
+
const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
|
|
64
|
+
virtualMcpId: "vm-1",
|
|
65
|
+
});
|
|
66
|
+
expect(e.data.title).toBeUndefined();
|
|
67
|
+
expect(e.data.branch).toBeUndefined();
|
|
68
|
+
expect(e.data.created_at).toBeUndefined();
|
|
69
|
+
expect(e.data.updated_at).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("explicit null branch is preserved", () => {
|
|
73
|
+
const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
|
|
74
|
+
branch: null,
|
|
75
|
+
});
|
|
76
|
+
expect(e.data.branch).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -66,7 +66,28 @@ export interface DecopilotFinishEvent extends BaseDecopilotEvent {
|
|
|
66
66
|
|
|
67
67
|
export interface DecopilotThreadStatusEvent extends BaseDecopilotEvent {
|
|
68
68
|
type: typeof DECOPILOT_EVENTS.THREAD_STATUS;
|
|
69
|
-
data: {
|
|
69
|
+
data: {
|
|
70
|
+
status: ThreadStatus;
|
|
71
|
+
virtual_mcp_id?: string;
|
|
72
|
+
/** User who created the thread; needed to populate filter-complete cache rows on the client. */
|
|
73
|
+
created_by?: string;
|
|
74
|
+
/** Automation trigger id; null for human-initiated, omitted when unknown. */
|
|
75
|
+
trigger_id?: string | null;
|
|
76
|
+
/** Thread title at emit time. Absent if caller didn't load the row. */
|
|
77
|
+
title?: string;
|
|
78
|
+
/** Branch this thread is pinned to (null when unpinned). Absent if caller didn't load the row. */
|
|
79
|
+
branch?: string | null;
|
|
80
|
+
/** Thread creation timestamp. Absent if caller didn't load the row. */
|
|
81
|
+
created_at?: string;
|
|
82
|
+
/** Last update timestamp; useful for the client to sort/dedupe. Absent if caller didn't load the row. */
|
|
83
|
+
updated_at?: string;
|
|
84
|
+
/** Free-form thread metadata snapshot. The chat UI keys off
|
|
85
|
+
* metadata.kind to switch between agent-thread and tool_call_run
|
|
86
|
+
* renderings (avatar, message-renderer), so the workflow that
|
|
87
|
+
* spawns those threads must include it on the first event or the
|
|
88
|
+
* row renders with the wrong icon until a refetch. */
|
|
89
|
+
metadata?: Record<string, unknown>;
|
|
90
|
+
};
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
export type DecopilotSSEEvent =
|
|
@@ -86,43 +107,65 @@ export interface DecopilotEventMap {
|
|
|
86
107
|
// ============================================================================
|
|
87
108
|
|
|
88
109
|
export function createDecopilotStepEvent(
|
|
89
|
-
|
|
110
|
+
taskId: string,
|
|
90
111
|
stepCount: number,
|
|
91
112
|
): DecopilotStepEvent {
|
|
92
113
|
return {
|
|
93
114
|
id: crypto.randomUUID(),
|
|
94
115
|
type: DECOPILOT_EVENTS.STEP,
|
|
95
116
|
source: "decopilot",
|
|
96
|
-
subject:
|
|
117
|
+
subject: taskId,
|
|
97
118
|
data: { stepCount },
|
|
98
119
|
time: new Date().toISOString(),
|
|
99
120
|
};
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
export function createDecopilotFinishEvent(
|
|
103
|
-
|
|
124
|
+
taskId: string,
|
|
104
125
|
status: ThreadStatus,
|
|
105
126
|
): DecopilotFinishEvent {
|
|
106
127
|
return {
|
|
107
128
|
id: crypto.randomUUID(),
|
|
108
129
|
type: DECOPILOT_EVENTS.FINISH,
|
|
109
130
|
source: "decopilot",
|
|
110
|
-
subject:
|
|
131
|
+
subject: taskId,
|
|
111
132
|
data: { status },
|
|
112
133
|
time: new Date().toISOString(),
|
|
113
134
|
};
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
export function createDecopilotThreadStatusEvent(
|
|
117
|
-
|
|
138
|
+
taskId: string,
|
|
118
139
|
status: ThreadStatus,
|
|
140
|
+
opts?: {
|
|
141
|
+
virtualMcpId?: string;
|
|
142
|
+
createdBy?: string;
|
|
143
|
+
triggerId?: string | null;
|
|
144
|
+
title?: string;
|
|
145
|
+
branch?: string | null;
|
|
146
|
+
createdAt?: string;
|
|
147
|
+
updatedAt?: string;
|
|
148
|
+
metadata?: Record<string, unknown>;
|
|
149
|
+
},
|
|
119
150
|
): DecopilotThreadStatusEvent {
|
|
120
151
|
return {
|
|
121
152
|
id: crypto.randomUUID(),
|
|
122
153
|
type: DECOPILOT_EVENTS.THREAD_STATUS,
|
|
123
154
|
source: "decopilot",
|
|
124
|
-
subject:
|
|
125
|
-
data: {
|
|
155
|
+
subject: taskId,
|
|
156
|
+
data: {
|
|
157
|
+
status,
|
|
158
|
+
...(opts?.virtualMcpId !== undefined && {
|
|
159
|
+
virtual_mcp_id: opts.virtualMcpId,
|
|
160
|
+
}),
|
|
161
|
+
...(opts?.createdBy !== undefined && { created_by: opts.createdBy }),
|
|
162
|
+
...(opts?.triggerId !== undefined && { trigger_id: opts.triggerId }),
|
|
163
|
+
...(opts?.title !== undefined && { title: opts.title }),
|
|
164
|
+
...(opts?.branch !== undefined && { branch: opts.branch }),
|
|
165
|
+
...(opts?.createdAt !== undefined && { created_at: opts.createdAt }),
|
|
166
|
+
...(opts?.updatedAt !== undefined && { updated_at: opts.updatedAt }),
|
|
167
|
+
...(opts?.metadata !== undefined && { metadata: opts.metadata }),
|
|
168
|
+
},
|
|
126
169
|
time: new Date().toISOString(),
|
|
127
170
|
};
|
|
128
171
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -19,10 +19,28 @@ export {
|
|
|
19
19
|
VirtualMCPEntitySchema,
|
|
20
20
|
VirtualMCPCreateDataSchema,
|
|
21
21
|
VirtualMCPUpdateDataSchema,
|
|
22
|
+
VirtualMcpUILayoutSchema,
|
|
23
|
+
VirtualMcpUILayoutTabSchema,
|
|
22
24
|
type VirtualMCPEntity,
|
|
23
25
|
type VirtualMCPCreateData,
|
|
24
26
|
type VirtualMCPUpdateData,
|
|
25
27
|
type VirtualMCPConnection,
|
|
28
|
+
type VirtualMcpUILayout,
|
|
29
|
+
type VirtualMcpUILayoutTab,
|
|
30
|
+
type VirtualMcpHomeTile,
|
|
31
|
+
getHomeTiles,
|
|
32
|
+
type GithubRepo,
|
|
33
|
+
SandboxMapSchema,
|
|
34
|
+
type SandboxMap,
|
|
35
|
+
SandboxRecordSchema,
|
|
36
|
+
type SandboxRecord,
|
|
37
|
+
type RuntimeMetadata,
|
|
38
|
+
type RuntimeEnvEntry,
|
|
39
|
+
ENV_VAR_KEY_RE,
|
|
40
|
+
parseSandboxRecord,
|
|
41
|
+
parseBranchMap,
|
|
42
|
+
normalizeSandboxMap,
|
|
43
|
+
type SandboxProviderKind,
|
|
26
44
|
} from "./virtual-mcp";
|
|
27
45
|
|
|
28
46
|
export {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, it, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
VirtualMCPEntitySchema,
|
|
4
|
+
VirtualMcpUILayoutSchema,
|
|
5
|
+
VirtualMCPUpdateDataSchema,
|
|
6
|
+
SandboxRecordSchema,
|
|
7
|
+
parseSandboxRecord,
|
|
8
|
+
parseBranchMap,
|
|
9
|
+
} from "./virtual-mcp";
|
|
10
|
+
|
|
11
|
+
describe("VirtualMcpUILayoutSchema tabs", () => {
|
|
12
|
+
it("parses a tabs array with ext-app view", () => {
|
|
13
|
+
const parsed = VirtualMcpUILayoutSchema.parse({
|
|
14
|
+
tabs: [
|
|
15
|
+
{
|
|
16
|
+
id: "analytics",
|
|
17
|
+
title: "Analytics",
|
|
18
|
+
icon: "BarChart",
|
|
19
|
+
view: { type: "ext-app", appId: "app_abc", args: { range: "7d" } },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
defaultMainView: null,
|
|
23
|
+
});
|
|
24
|
+
expect(parsed.tabs).toHaveLength(1);
|
|
25
|
+
expect(parsed.tabs?.[0]!.view.type).toBe("ext-app");
|
|
26
|
+
expect(parsed.tabs?.[0]!.view.appId).toBe("app_abc");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("accepts tabs omitted (backwards compatible)", () => {
|
|
30
|
+
const parsed = VirtualMcpUILayoutSchema.parse({
|
|
31
|
+
defaultMainView: null,
|
|
32
|
+
});
|
|
33
|
+
expect(parsed.tabs).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects a tab view with unknown type", () => {
|
|
37
|
+
const result = VirtualMcpUILayoutSchema.safeParse({
|
|
38
|
+
tabs: [
|
|
39
|
+
{
|
|
40
|
+
id: "bad",
|
|
41
|
+
title: "Bad",
|
|
42
|
+
view: { type: "mystery", appId: "app_x" },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("metadata.runtime is typed and round-trips through parse", () => {
|
|
51
|
+
const parsed = VirtualMCPEntitySchema.parse({
|
|
52
|
+
id: "x",
|
|
53
|
+
title: "x",
|
|
54
|
+
description: null,
|
|
55
|
+
icon: null,
|
|
56
|
+
created_at: "t",
|
|
57
|
+
updated_at: "t",
|
|
58
|
+
created_by: "u",
|
|
59
|
+
organization_id: "o",
|
|
60
|
+
status: "active",
|
|
61
|
+
pinned: false,
|
|
62
|
+
metadata: {
|
|
63
|
+
instructions: null,
|
|
64
|
+
runtime: { selected: "pnpm", port: "3000" },
|
|
65
|
+
},
|
|
66
|
+
connections: [],
|
|
67
|
+
});
|
|
68
|
+
expect(parsed.metadata.runtime?.selected).toBe("pnpm");
|
|
69
|
+
expect(parsed.metadata.runtime?.port).toBe("3000");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("metadata.runtime accepts null/empty values", () => {
|
|
73
|
+
const parsed = VirtualMCPEntitySchema.parse({
|
|
74
|
+
id: "x",
|
|
75
|
+
title: "x",
|
|
76
|
+
description: null,
|
|
77
|
+
icon: null,
|
|
78
|
+
created_at: "t",
|
|
79
|
+
updated_at: "t",
|
|
80
|
+
created_by: "u",
|
|
81
|
+
organization_id: "o",
|
|
82
|
+
status: "active",
|
|
83
|
+
pinned: false,
|
|
84
|
+
metadata: {
|
|
85
|
+
instructions: null,
|
|
86
|
+
runtime: { selected: null, port: null },
|
|
87
|
+
},
|
|
88
|
+
connections: [],
|
|
89
|
+
});
|
|
90
|
+
expect(parsed.metadata.runtime?.selected).toBeNull();
|
|
91
|
+
expect(parsed.metadata.runtime?.port).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("VirtualMCPUpdateDataSchema accepts metadata.runtime", () => {
|
|
95
|
+
const parsed = VirtualMCPUpdateDataSchema.parse({
|
|
96
|
+
metadata: { runtime: { selected: "bun", port: null } },
|
|
97
|
+
});
|
|
98
|
+
expect(parsed.metadata?.runtime?.selected).toBe("bun");
|
|
99
|
+
expect(parsed.metadata?.runtime?.port).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("SandboxRecord.startedWith is optional with nullable packageManager/port/path", () => {
|
|
103
|
+
const a = SandboxRecordSchema.parse({ sandboxHandle: "v", previewUrl: null });
|
|
104
|
+
expect(a.startedWith).toBeUndefined();
|
|
105
|
+
const b = SandboxRecordSchema.parse({
|
|
106
|
+
sandboxHandle: "v",
|
|
107
|
+
previewUrl: null,
|
|
108
|
+
startedWith: { packageManager: "pnpm", port: "3000", path: "apps/web" },
|
|
109
|
+
});
|
|
110
|
+
expect(b.startedWith?.packageManager).toBe("pnpm");
|
|
111
|
+
expect(b.startedWith?.port).toBe("3000");
|
|
112
|
+
expect(b.startedWith?.path).toBe("apps/web");
|
|
113
|
+
const c = SandboxRecordSchema.parse({
|
|
114
|
+
sandboxHandle: "v",
|
|
115
|
+
previewUrl: null,
|
|
116
|
+
startedWith: { packageManager: null, port: null, path: null },
|
|
117
|
+
});
|
|
118
|
+
expect(c.startedWith?.packageManager).toBeNull();
|
|
119
|
+
expect(c.startedWith?.port).toBeNull();
|
|
120
|
+
expect(c.startedWith?.path).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("parseBranchMap", () => {
|
|
124
|
+
test("parses 3-level (kind-keyed) map with canonical kinds", () => {
|
|
125
|
+
const result = parseBranchMap({
|
|
126
|
+
cluster: {
|
|
127
|
+
sandboxHandle: "v1",
|
|
128
|
+
previewUrl: null,
|
|
129
|
+
sandboxProviderKind: "cluster",
|
|
130
|
+
},
|
|
131
|
+
"user-desktop": {
|
|
132
|
+
sandboxHandle: "v2",
|
|
133
|
+
previewUrl: null,
|
|
134
|
+
sandboxProviderKind: "user-desktop",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(result["cluster"]?.sandboxHandle).toBe("v1");
|
|
138
|
+
expect(result["user-desktop"]?.sandboxHandle).toBe("v2");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns empty object for null/undefined/arrays", () => {
|
|
142
|
+
expect(parseBranchMap(null)).toEqual({});
|
|
143
|
+
expect(parseBranchMap(undefined)).toEqual({});
|
|
144
|
+
expect(parseBranchMap([])).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("skips entries under legacy/retired kind keys", () => {
|
|
148
|
+
// Migrations 092/097 rewrote/dropped every legacy and retired key; reader
|
|
149
|
+
// no longer normalizes unknown keys, it just ignores them.
|
|
150
|
+
const result = parseBranchMap({
|
|
151
|
+
docker: {
|
|
152
|
+
sandboxHandle: "v-legacy",
|
|
153
|
+
previewUrl: null,
|
|
154
|
+
sandboxProviderKind: "cluster",
|
|
155
|
+
},
|
|
156
|
+
"local-docker": {
|
|
157
|
+
sandboxHandle: "v-retired",
|
|
158
|
+
previewUrl: null,
|
|
159
|
+
sandboxProviderKind: "cluster",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
expect(result).toEqual({});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("parseSandboxRecord", () => {
|
|
167
|
+
test("accepts canonical sandboxProviderKind", () => {
|
|
168
|
+
const result = parseSandboxRecord({
|
|
169
|
+
sandboxHandle: "v1",
|
|
170
|
+
previewUrl: null,
|
|
171
|
+
sandboxProviderKind: "cluster",
|
|
172
|
+
});
|
|
173
|
+
expect(result.sandboxProviderKind).toBe("cluster");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("rejects legacy/retired kind values", () => {
|
|
177
|
+
expect(() =>
|
|
178
|
+
parseSandboxRecord({
|
|
179
|
+
sandboxHandle: "v1",
|
|
180
|
+
previewUrl: null,
|
|
181
|
+
sandboxProviderKind: "docker",
|
|
182
|
+
}),
|
|
183
|
+
).toThrow();
|
|
184
|
+
expect(() =>
|
|
185
|
+
parseSandboxRecord({
|
|
186
|
+
sandboxHandle: "v1",
|
|
187
|
+
previewUrl: null,
|
|
188
|
+
sandboxProviderKind: "local-docker",
|
|
189
|
+
}),
|
|
190
|
+
).toThrow();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("rejects rows missing `sandboxHandle` (legacy `vmId` no longer accepted)", () => {
|
|
194
|
+
expect(() =>
|
|
195
|
+
parseSandboxRecord({
|
|
196
|
+
vmId: "v-pre-rename",
|
|
197
|
+
previewUrl: null,
|
|
198
|
+
sandboxProviderKind: "cluster",
|
|
199
|
+
}),
|
|
200
|
+
).toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|