@vellumai/assistant 0.4.37 → 0.4.41
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/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -1,631 +1,441 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type { AppDefinition } from "../memory/app-store.js";
|
|
4
|
-
import type { AppStore
|
|
4
|
+
import type { AppStore } from "../tools/apps/executors.js";
|
|
5
5
|
import {
|
|
6
6
|
executeAppCreate,
|
|
7
|
-
executeAppDelete,
|
|
8
7
|
executeAppFileEdit,
|
|
9
8
|
executeAppFileList,
|
|
10
9
|
executeAppFileRead,
|
|
11
10
|
executeAppFileWrite,
|
|
12
|
-
|
|
13
|
-
executeAppQuery,
|
|
14
|
-
executeAppUpdate,
|
|
11
|
+
resolveAppFilePath,
|
|
15
12
|
} from "../tools/apps/executors.js";
|
|
16
|
-
import type { EditEngineResult } from "../tools/shared/filesystem/edit-engine.js";
|
|
17
13
|
|
|
18
14
|
// ---------------------------------------------------------------------------
|
|
19
|
-
//
|
|
15
|
+
// Helpers
|
|
20
16
|
// ---------------------------------------------------------------------------
|
|
21
17
|
|
|
22
|
-
function
|
|
18
|
+
function makeLegacyApp(overrides?: Partial<AppDefinition>): AppDefinition {
|
|
23
19
|
return {
|
|
24
|
-
id: "app
|
|
25
|
-
name: "
|
|
26
|
-
description: "A test app",
|
|
20
|
+
id: "legacy-app",
|
|
21
|
+
name: "Legacy App",
|
|
27
22
|
schemaJson: "{}",
|
|
28
|
-
htmlDefinition: "<
|
|
29
|
-
createdAt:
|
|
30
|
-
updatedAt:
|
|
23
|
+
htmlDefinition: "<html></html>",
|
|
24
|
+
createdAt: Date.now(),
|
|
25
|
+
updatedAt: Date.now(),
|
|
31
26
|
...overrides,
|
|
32
27
|
};
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
function
|
|
30
|
+
function makeMultifileApp(overrides?: Partial<AppDefinition>): AppDefinition {
|
|
36
31
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
id: "multi-app",
|
|
33
|
+
name: "Multifile App",
|
|
34
|
+
schemaJson: "{}",
|
|
35
|
+
htmlDefinition: "",
|
|
36
|
+
formatVersion: 2,
|
|
37
|
+
createdAt: Date.now(),
|
|
38
|
+
updatedAt: Date.now(),
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds a minimal mock AppStore that tracks writes/edits/reads
|
|
45
|
+
* against an in-memory file map.
|
|
46
|
+
*/
|
|
47
|
+
function mockStore(
|
|
48
|
+
app: AppDefinition,
|
|
49
|
+
files: Record<string, string> = {},
|
|
50
|
+
): AppStore {
|
|
51
|
+
return {
|
|
52
|
+
getApp: (id: string) => (id === app.id ? app : null),
|
|
53
|
+
listApps: () => [app],
|
|
39
54
|
queryAppRecords: () => [],
|
|
40
|
-
listAppFiles: () =>
|
|
41
|
-
readAppFile: () =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
listAppFiles: () => Object.keys(files).sort(),
|
|
56
|
+
readAppFile: (_appId: string, path: string) => {
|
|
57
|
+
if (!(path in files)) throw new Error(`File not found: ${path}`);
|
|
58
|
+
return files[path];
|
|
59
|
+
},
|
|
60
|
+
createApp: () => app,
|
|
61
|
+
updateApp: () => app,
|
|
45
62
|
deleteApp: () => {},
|
|
46
|
-
writeAppFile: () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
writeAppFile: (_appId: string, path: string, content: string) => {
|
|
64
|
+
files[path] = content;
|
|
65
|
+
},
|
|
66
|
+
editAppFile: (
|
|
67
|
+
_appId: string,
|
|
68
|
+
path: string,
|
|
69
|
+
oldStr: string,
|
|
70
|
+
newStr: string,
|
|
71
|
+
_replaceAll?: boolean,
|
|
72
|
+
) => {
|
|
73
|
+
if (!(path in files)) throw new Error(`File not found: ${path}`);
|
|
74
|
+
const content = files[path];
|
|
75
|
+
if (!content.includes(oldStr)) {
|
|
76
|
+
return { ok: false as const, reason: "not_found" as const };
|
|
77
|
+
}
|
|
78
|
+
const updated = content.replace(oldStr, newStr);
|
|
79
|
+
files[path] = updated;
|
|
80
|
+
return {
|
|
81
|
+
ok: true as const,
|
|
82
|
+
updatedContent: updated,
|
|
51
83
|
matchCount: 1,
|
|
52
|
-
matchMethod: "exact",
|
|
84
|
+
matchMethod: "exact" as const,
|
|
53
85
|
similarity: 1,
|
|
54
|
-
actualOld:
|
|
55
|
-
actualNew:
|
|
56
|
-
}
|
|
57
|
-
|
|
86
|
+
actualOld: oldStr,
|
|
87
|
+
actualNew: newStr,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
58
90
|
};
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
// ---------------------------------------------------------------------------
|
|
62
|
-
//
|
|
94
|
+
// resolveAppFilePath
|
|
63
95
|
// ---------------------------------------------------------------------------
|
|
64
96
|
|
|
65
|
-
describe("
|
|
66
|
-
test("
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
{ name: "My App", html: "<p>Hello</p>" },
|
|
70
|
-
store,
|
|
71
|
-
);
|
|
72
|
-
expect(result.isError).toBe(false);
|
|
73
|
-
const parsed = JSON.parse(result.content);
|
|
74
|
-
expect(parsed.name).toBe("My App");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('defaults schema_json to "{}" when not provided', async () => {
|
|
78
|
-
let capturedSchema: string | undefined;
|
|
79
|
-
const store = makeMockStore({
|
|
80
|
-
createApp: (params) => {
|
|
81
|
-
capturedSchema = params.schemaJson;
|
|
82
|
-
return makeApp({ name: params.name });
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
await executeAppCreate({ name: "App", html: "<p/>" }, store);
|
|
86
|
-
expect(capturedSchema).toBe("{}");
|
|
97
|
+
describe("resolveAppFilePath", () => {
|
|
98
|
+
test("prepends src/ for multifile app with plain path", () => {
|
|
99
|
+
const app = makeMultifileApp();
|
|
100
|
+
expect(resolveAppFilePath(app, "main.tsx")).toBe("src/main.tsx");
|
|
87
101
|
});
|
|
88
102
|
|
|
89
|
-
test("
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
capturedSchema = params.schemaJson;
|
|
94
|
-
return makeApp({ name: params.name });
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
await executeAppCreate(
|
|
98
|
-
{ name: "App", html: "<p/>", schema_json: '{"type":"object"}' },
|
|
99
|
-
store,
|
|
103
|
+
test("prepends src/ for nested path in multifile app", () => {
|
|
104
|
+
const app = makeMultifileApp();
|
|
105
|
+
expect(resolveAppFilePath(app, "components/Header.tsx")).toBe(
|
|
106
|
+
"src/components/Header.tsx",
|
|
100
107
|
);
|
|
101
|
-
expect(capturedSchema).toBe('{"type":"object"}');
|
|
102
108
|
});
|
|
103
109
|
|
|
104
|
-
test("
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
content: "opened",
|
|
108
|
-
isError: false,
|
|
109
|
-
});
|
|
110
|
-
const result = await executeAppCreate(
|
|
111
|
-
{ name: "Auto", html: "<p/>" },
|
|
112
|
-
store,
|
|
113
|
-
proxy,
|
|
114
|
-
);
|
|
115
|
-
expect(result.isError).toBe(false);
|
|
116
|
-
const parsed = JSON.parse(result.content);
|
|
117
|
-
expect(parsed.auto_opened).toBe(true);
|
|
118
|
-
expect(parsed.open_result).toBe("opened");
|
|
110
|
+
test("passes through src/ prefix unchanged for multifile app", () => {
|
|
111
|
+
const app = makeMultifileApp();
|
|
112
|
+
expect(resolveAppFilePath(app, "src/main.tsx")).toBe("src/main.tsx");
|
|
119
113
|
});
|
|
120
114
|
|
|
121
|
-
test("
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
throw new Error("no client");
|
|
125
|
-
};
|
|
126
|
-
const result = await executeAppCreate(
|
|
127
|
-
{ name: "Fail Open", html: "<p/>" },
|
|
128
|
-
store,
|
|
129
|
-
proxy,
|
|
130
|
-
);
|
|
131
|
-
expect(result.isError).toBe(false);
|
|
132
|
-
const parsed = JSON.parse(result.content);
|
|
133
|
-
expect(parsed.auto_opened).toBe(false);
|
|
134
|
-
expect(parsed.auto_open_error).toBe(
|
|
135
|
-
"Failed to auto-open app. Use app_open to open it manually.",
|
|
136
|
-
);
|
|
115
|
+
test("passes through dist/ prefix unchanged for multifile app", () => {
|
|
116
|
+
const app = makeMultifileApp();
|
|
117
|
+
expect(resolveAppFilePath(app, "dist/bundle.js")).toBe("dist/bundle.js");
|
|
137
118
|
});
|
|
138
119
|
|
|
139
|
-
test("
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
proxyCalled = true;
|
|
144
|
-
return { content: "opened", isError: false };
|
|
145
|
-
};
|
|
146
|
-
const result = await executeAppCreate(
|
|
147
|
-
{ name: "No Open", html: "<p/>", auto_open: false },
|
|
148
|
-
store,
|
|
149
|
-
proxy,
|
|
120
|
+
test("passes through records/ prefix unchanged for multifile app", () => {
|
|
121
|
+
const app = makeMultifileApp();
|
|
122
|
+
expect(resolveAppFilePath(app, "records/data.json")).toBe(
|
|
123
|
+
"records/data.json",
|
|
150
124
|
);
|
|
151
|
-
expect(proxyCalled).toBe(false);
|
|
152
|
-
expect(result.isError).toBe(false);
|
|
153
|
-
const parsed = JSON.parse(result.content);
|
|
154
|
-
expect(parsed.auto_opened).toBeUndefined();
|
|
155
125
|
});
|
|
156
126
|
|
|
157
|
-
test("
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
{ name: "No Proxy", html: "<p/>" },
|
|
161
|
-
store,
|
|
162
|
-
);
|
|
163
|
-
expect(result.isError).toBe(false);
|
|
164
|
-
const parsed = JSON.parse(result.content);
|
|
165
|
-
expect(parsed.auto_opened).toBeUndefined();
|
|
127
|
+
test("does not modify path for legacy app", () => {
|
|
128
|
+
const app = makeLegacyApp();
|
|
129
|
+
expect(resolveAppFilePath(app, "main.tsx")).toBe("main.tsx");
|
|
166
130
|
});
|
|
167
131
|
|
|
168
|
-
test("
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
createApp: (params) => {
|
|
172
|
-
capturedPages = params.pages;
|
|
173
|
-
return makeApp({ name: params.name });
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
await executeAppCreate(
|
|
177
|
-
{ name: "Multi", html: "<p/>", pages: { "settings.html": "<div/>" } },
|
|
178
|
-
store,
|
|
179
|
-
);
|
|
180
|
-
expect(capturedPages).toEqual({ "settings.html": "<div/>" });
|
|
132
|
+
test("does not modify path for legacy app (formatVersion undefined)", () => {
|
|
133
|
+
const app = makeLegacyApp({ formatVersion: undefined });
|
|
134
|
+
expect(resolveAppFilePath(app, "styles.css")).toBe("styles.css");
|
|
181
135
|
});
|
|
182
136
|
|
|
183
|
-
test("
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
createApp: (params) => {
|
|
187
|
-
capturedHtml = params.htmlDefinition;
|
|
188
|
-
return makeApp({ name: params.name });
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
await executeAppCreate({ name: "No HTML" }, store);
|
|
192
|
-
expect(capturedHtml).toBe(
|
|
193
|
-
"<!DOCTYPE html><html><head></head><body></body></html>",
|
|
194
|
-
);
|
|
137
|
+
test("does not modify path for legacy app (formatVersion 1)", () => {
|
|
138
|
+
const app = makeLegacyApp({ formatVersion: 1 });
|
|
139
|
+
expect(resolveAppFilePath(app, "index.html")).toBe("index.html");
|
|
195
140
|
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
// app_list
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
141
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
listApps: () => [
|
|
206
|
-
makeApp({
|
|
207
|
-
id: "a1",
|
|
208
|
-
name: "First",
|
|
209
|
-
description: "desc1",
|
|
210
|
-
updatedAt: 100,
|
|
211
|
-
}),
|
|
212
|
-
makeApp({ id: "a2", name: "Second", updatedAt: 200 }),
|
|
213
|
-
],
|
|
214
|
-
});
|
|
215
|
-
const result = executeAppList(store);
|
|
216
|
-
expect(result.isError).toBe(false);
|
|
217
|
-
const parsed = JSON.parse(result.content);
|
|
218
|
-
expect(parsed).toHaveLength(2);
|
|
219
|
-
expect(parsed[0]).toEqual({
|
|
220
|
-
id: "a1",
|
|
221
|
-
name: "First",
|
|
222
|
-
description: "desc1",
|
|
223
|
-
updatedAt: 100,
|
|
224
|
-
});
|
|
225
|
-
expect(parsed[1].id).toBe("a2");
|
|
226
|
-
// Should not include htmlDefinition or schemaJson
|
|
227
|
-
expect(parsed[0].htmlDefinition).toBeUndefined();
|
|
228
|
-
expect(parsed[0].schemaJson).toBeUndefined();
|
|
142
|
+
test("strips ./ prefix and prepends src/ for multifile app", () => {
|
|
143
|
+
const app = makeMultifileApp();
|
|
144
|
+
expect(resolveAppFilePath(app, "./main.tsx")).toBe("src/main.tsx");
|
|
229
145
|
});
|
|
230
146
|
|
|
231
|
-
test("
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
expect(
|
|
235
|
-
expect(
|
|
147
|
+
test("strips ./ prefix for known top-level dir in multifile app", () => {
|
|
148
|
+
const app = makeMultifileApp();
|
|
149
|
+
expect(resolveAppFilePath(app, "./src/main.tsx")).toBe("src/main.tsx");
|
|
150
|
+
expect(resolveAppFilePath(app, "./dist/bundle.js")).toBe("dist/bundle.js");
|
|
151
|
+
expect(resolveAppFilePath(app, "./records/data.json")).toBe(
|
|
152
|
+
"records/data.json",
|
|
153
|
+
);
|
|
236
154
|
});
|
|
237
155
|
});
|
|
238
156
|
|
|
239
157
|
// ---------------------------------------------------------------------------
|
|
240
|
-
//
|
|
158
|
+
// executeAppFileWrite
|
|
241
159
|
// ---------------------------------------------------------------------------
|
|
242
160
|
|
|
243
|
-
describe("
|
|
244
|
-
test("
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
expect(result.isError).toBe(false);
|
|
249
|
-
expect(JSON.parse(result.content)).toEqual(records);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("returns empty array when no records", () => {
|
|
253
|
-
const store = makeMockStore({ queryAppRecords: () => [] });
|
|
254
|
-
const result = executeAppQuery({ app_id: "app-1" }, store);
|
|
255
|
-
expect(result.isError).toBe(false);
|
|
256
|
-
expect(JSON.parse(result.content)).toEqual([]);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// app_update
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
161
|
+
describe("executeAppFileWrite", () => {
|
|
162
|
+
test("resolves plain path to src/ for multifile app", () => {
|
|
163
|
+
const files: Record<string, string> = {};
|
|
164
|
+
const app = makeMultifileApp();
|
|
165
|
+
const store = mockStore(app, files);
|
|
263
166
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
let capturedUpdates: Record<string, unknown> = {};
|
|
267
|
-
const store = makeMockStore({
|
|
268
|
-
updateApp: (_id, updates) => {
|
|
269
|
-
capturedUpdates = updates;
|
|
270
|
-
return makeApp({ id: _id, ...updates });
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
const result = executeAppUpdate(
|
|
274
|
-
{
|
|
275
|
-
app_id: "app-1",
|
|
276
|
-
name: "New Name",
|
|
277
|
-
description: "New desc",
|
|
278
|
-
schema_json: '{"a":1}',
|
|
279
|
-
html: "<div/>",
|
|
280
|
-
pages: { "about.html": "<p/>" },
|
|
281
|
-
},
|
|
167
|
+
const result = executeAppFileWrite(
|
|
168
|
+
{ app_id: app.id, path: "main.tsx", content: "export default 1;" },
|
|
282
169
|
store,
|
|
283
170
|
);
|
|
171
|
+
|
|
284
172
|
expect(result.isError).toBe(false);
|
|
285
|
-
expect(
|
|
286
|
-
|
|
287
|
-
description: "New desc",
|
|
288
|
-
schemaJson: '{"a":1}',
|
|
289
|
-
htmlDefinition: "<div/>",
|
|
290
|
-
pages: { "about.html": "<p/>" },
|
|
291
|
-
});
|
|
173
|
+
expect(JSON.parse(result.content).path).toBe("src/main.tsx");
|
|
174
|
+
expect(files["src/main.tsx"]).toBe("export default 1;");
|
|
292
175
|
});
|
|
293
176
|
|
|
294
|
-
test("
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
capturedUpdates = updates;
|
|
299
|
-
return makeApp({ id: _id, ...updates });
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
executeAppUpdate({ app_id: "app-1", name: "Only Name" }, store);
|
|
303
|
-
expect(capturedUpdates).toEqual({ name: "Only Name" });
|
|
304
|
-
// html, description, schema_json, pages should NOT be in the updates
|
|
305
|
-
expect("htmlDefinition" in capturedUpdates).toBe(false);
|
|
306
|
-
expect("description" in capturedUpdates).toBe(false);
|
|
307
|
-
expect("schemaJson" in capturedUpdates).toBe(false);
|
|
308
|
-
expect("pages" in capturedUpdates).toBe(false);
|
|
309
|
-
});
|
|
177
|
+
test("passes through src/ path unchanged for multifile app", () => {
|
|
178
|
+
const files: Record<string, string> = {};
|
|
179
|
+
const app = makeMultifileApp();
|
|
180
|
+
const store = mockStore(app, files);
|
|
310
181
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
throw new Error("App not found: bad-id");
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
expect(() => executeAppUpdate({ app_id: "bad-id" }, store)).toThrow(
|
|
318
|
-
"App not found: bad-id",
|
|
182
|
+
const result = executeAppFileWrite(
|
|
183
|
+
{ app_id: app.id, path: "src/main.tsx", content: "export default 2;" },
|
|
184
|
+
store,
|
|
319
185
|
);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
// app_delete
|
|
325
|
-
// ---------------------------------------------------------------------------
|
|
326
186
|
|
|
327
|
-
describe("executeAppDelete", () => {
|
|
328
|
-
test("deletes the app and returns confirmation", () => {
|
|
329
|
-
let deletedId: string | undefined;
|
|
330
|
-
const store = makeMockStore({
|
|
331
|
-
deleteApp: (id) => {
|
|
332
|
-
deletedId = id;
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
const result = executeAppDelete({ app_id: "app-1" }, store);
|
|
336
187
|
expect(result.isError).toBe(false);
|
|
337
|
-
expect(JSON.parse(result.content)).
|
|
338
|
-
|
|
339
|
-
appId: "app-1",
|
|
340
|
-
});
|
|
341
|
-
expect(deletedId).toBe("app-1");
|
|
188
|
+
expect(JSON.parse(result.content).path).toBe("src/main.tsx");
|
|
189
|
+
expect(files["src/main.tsx"]).toBe("export default 2;");
|
|
342
190
|
});
|
|
343
|
-
});
|
|
344
191
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
192
|
+
test("does not modify path for legacy app", () => {
|
|
193
|
+
const files: Record<string, string> = {};
|
|
194
|
+
const app = makeLegacyApp();
|
|
195
|
+
const store = mockStore(app, files);
|
|
348
196
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
});
|
|
354
|
-
const result = executeAppFileList({ app_id: "app-1" }, store);
|
|
355
|
-
expect(result.isError).toBe(false);
|
|
356
|
-
expect(JSON.parse(result.content)).toEqual([
|
|
357
|
-
"index.html",
|
|
358
|
-
"styles.css",
|
|
359
|
-
"js/app.js",
|
|
360
|
-
]);
|
|
361
|
-
});
|
|
197
|
+
const result = executeAppFileWrite(
|
|
198
|
+
{ app_id: app.id, path: "index.html", content: "<html></html>" },
|
|
199
|
+
store,
|
|
200
|
+
);
|
|
362
201
|
|
|
363
|
-
test("returns empty array when app has no files", () => {
|
|
364
|
-
const store = makeMockStore({ listAppFiles: () => [] });
|
|
365
|
-
const result = executeAppFileList({ app_id: "app-1" }, store);
|
|
366
202
|
expect(result.isError).toBe(false);
|
|
367
|
-
expect(JSON.parse(result.content)).
|
|
203
|
+
expect(JSON.parse(result.content).path).toBe("index.html");
|
|
204
|
+
expect(files["index.html"]).toBe("<html></html>");
|
|
368
205
|
});
|
|
369
206
|
});
|
|
370
207
|
|
|
371
208
|
// ---------------------------------------------------------------------------
|
|
372
|
-
//
|
|
209
|
+
// executeAppFileRead
|
|
373
210
|
// ---------------------------------------------------------------------------
|
|
374
211
|
|
|
375
212
|
describe("executeAppFileRead", () => {
|
|
376
|
-
test("
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
});
|
|
380
|
-
const result = executeAppFileRead(
|
|
381
|
-
{ app_id: "app-1", path: "index.html" },
|
|
382
|
-
store,
|
|
383
|
-
);
|
|
384
|
-
expect(result.isError).toBe(false);
|
|
385
|
-
expect(result.content).toBe(" 1\tline1\n 2\tline2\n 3\tline3");
|
|
386
|
-
});
|
|
213
|
+
test("resolves plain path to src/ for multifile app", () => {
|
|
214
|
+
const app = makeMultifileApp();
|
|
215
|
+
const store = mockStore(app, { "src/main.tsx": "line1\nline2" });
|
|
387
216
|
|
|
388
|
-
test("applies offset parameter (1-based)", () => {
|
|
389
|
-
const store = makeMockStore({
|
|
390
|
-
readAppFile: () => "a\nb\nc\nd\ne",
|
|
391
|
-
});
|
|
392
217
|
const result = executeAppFileRead(
|
|
393
|
-
{ app_id:
|
|
218
|
+
{ app_id: app.id, path: "main.tsx" },
|
|
394
219
|
store,
|
|
395
220
|
);
|
|
396
|
-
expect(result.isError).toBe(false);
|
|
397
|
-
// Lines 3, 4, 5
|
|
398
|
-
expect(result.content).toBe(" 3\tc\n 4\td\n 5\te");
|
|
399
|
-
});
|
|
400
221
|
|
|
401
|
-
test("applies limit parameter", () => {
|
|
402
|
-
const store = makeMockStore({
|
|
403
|
-
readAppFile: () => "a\nb\nc\nd\ne",
|
|
404
|
-
});
|
|
405
|
-
const result = executeAppFileRead(
|
|
406
|
-
{ app_id: "app-1", path: "f.txt", limit: 2 },
|
|
407
|
-
store,
|
|
408
|
-
);
|
|
409
222
|
expect(result.isError).toBe(false);
|
|
410
|
-
expect(result.content).
|
|
223
|
+
expect(result.content).toContain("line1");
|
|
411
224
|
});
|
|
412
225
|
|
|
413
|
-
test("
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
226
|
+
test("can read dist/ files explicitly for multifile app", () => {
|
|
227
|
+
const app = makeMultifileApp();
|
|
228
|
+
const store = mockStore(app, { "dist/bundle.js": "bundled code" });
|
|
229
|
+
|
|
417
230
|
const result = executeAppFileRead(
|
|
418
|
-
{ app_id:
|
|
231
|
+
{ app_id: app.id, path: "dist/bundle.js" },
|
|
419
232
|
store,
|
|
420
233
|
);
|
|
234
|
+
|
|
421
235
|
expect(result.isError).toBe(false);
|
|
422
|
-
expect(result.content).
|
|
236
|
+
expect(result.content).toContain("bundled code");
|
|
423
237
|
});
|
|
424
238
|
|
|
425
|
-
test("
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
239
|
+
test("does not modify path for legacy app", () => {
|
|
240
|
+
const app = makeLegacyApp();
|
|
241
|
+
const store = mockStore(app, { "index.html": "<html>hello</html>" });
|
|
242
|
+
|
|
429
243
|
const result = executeAppFileRead(
|
|
430
|
-
{ app_id:
|
|
244
|
+
{ app_id: app.id, path: "index.html" },
|
|
431
245
|
store,
|
|
432
246
|
);
|
|
433
|
-
expect(result.content).toBe(" 1\tonly");
|
|
434
|
-
});
|
|
435
247
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
readAppFile: () => {
|
|
439
|
-
throw new Error("File not found: missing.txt");
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
expect(() =>
|
|
443
|
-
executeAppFileRead({ app_id: "app-1", path: "missing.txt" }, store),
|
|
444
|
-
).toThrow("File not found: missing.txt");
|
|
248
|
+
expect(result.isError).toBe(false);
|
|
249
|
+
expect(result.content).toContain("<html>hello</html>");
|
|
445
250
|
});
|
|
446
251
|
});
|
|
447
252
|
|
|
448
253
|
// ---------------------------------------------------------------------------
|
|
449
|
-
//
|
|
254
|
+
// executeAppFileEdit
|
|
450
255
|
// ---------------------------------------------------------------------------
|
|
451
256
|
|
|
452
257
|
describe("executeAppFileEdit", () => {
|
|
453
|
-
test("
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
matchMethod: "exact" as const,
|
|
459
|
-
similarity: 1,
|
|
460
|
-
actualOld: "old",
|
|
461
|
-
actualNew: "new",
|
|
462
|
-
};
|
|
463
|
-
const store = makeMockStore({ editAppFile: () => editResult });
|
|
258
|
+
test("resolves plain path to src/ for multifile app", () => {
|
|
259
|
+
const files = { "src/main.tsx": "const x = 1;" };
|
|
260
|
+
const app = makeMultifileApp();
|
|
261
|
+
const store = mockStore(app, files);
|
|
262
|
+
|
|
464
263
|
const result = executeAppFileEdit(
|
|
465
264
|
{
|
|
466
|
-
app_id:
|
|
467
|
-
path: "
|
|
468
|
-
old_string: "
|
|
469
|
-
new_string: "
|
|
265
|
+
app_id: app.id,
|
|
266
|
+
path: "main.tsx",
|
|
267
|
+
old_string: "const x = 1;",
|
|
268
|
+
new_string: "const x = 2;",
|
|
470
269
|
},
|
|
471
270
|
store,
|
|
472
271
|
);
|
|
272
|
+
|
|
473
273
|
expect(result.isError).toBe(false);
|
|
474
|
-
expect(
|
|
274
|
+
expect(files["src/main.tsx"]).toBe("const x = 2;");
|
|
475
275
|
});
|
|
476
276
|
|
|
477
|
-
test("
|
|
478
|
-
const
|
|
277
|
+
test("does not modify path for legacy app", () => {
|
|
278
|
+
const files = { "index.html": "<p>old</p>" };
|
|
279
|
+
const app = makeLegacyApp();
|
|
280
|
+
const store = mockStore(app, files);
|
|
281
|
+
|
|
479
282
|
const result = executeAppFileEdit(
|
|
480
283
|
{
|
|
481
|
-
app_id:
|
|
284
|
+
app_id: app.id,
|
|
482
285
|
path: "index.html",
|
|
483
|
-
old_string: "",
|
|
484
|
-
new_string: "new",
|
|
286
|
+
old_string: "<p>old</p>",
|
|
287
|
+
new_string: "<p>new</p>",
|
|
485
288
|
},
|
|
486
289
|
store,
|
|
487
290
|
);
|
|
488
|
-
|
|
489
|
-
expect(
|
|
490
|
-
|
|
491
|
-
});
|
|
291
|
+
|
|
292
|
+
expect(result.isError).toBe(false);
|
|
293
|
+
expect(files["index.html"]).toBe("<p>new</p>");
|
|
492
294
|
});
|
|
295
|
+
});
|
|
493
296
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
actualOld: "",
|
|
506
|
-
actualNew: "",
|
|
507
|
-
};
|
|
508
|
-
},
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// executeAppFileList
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
describe("executeAppFileList", () => {
|
|
302
|
+
test("returns clean file paths and separate buildOutput for multifile app", () => {
|
|
303
|
+
const app = makeMultifileApp();
|
|
304
|
+
const store = mockStore(app, {
|
|
305
|
+
"src/main.tsx": "",
|
|
306
|
+
"src/components/Header.tsx": "",
|
|
307
|
+
"dist/index.html": "",
|
|
509
308
|
});
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
);
|
|
520
|
-
expect(
|
|
309
|
+
|
|
310
|
+
const result = executeAppFileList({ app_id: app.id }, store);
|
|
311
|
+
const parsed = JSON.parse(result.content) as {
|
|
312
|
+
files: string[];
|
|
313
|
+
buildOutput: string[];
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// File paths must be clean — no annotations appended
|
|
317
|
+
expect(parsed.files).toContain("src/main.tsx");
|
|
318
|
+
expect(parsed.files).toContain("src/components/Header.tsx");
|
|
319
|
+
expect(parsed.files).toContain("dist/index.html");
|
|
320
|
+
expect(
|
|
321
|
+
parsed.files.every((f: string) => !f.includes("[build output]")),
|
|
322
|
+
).toBe(true);
|
|
323
|
+
|
|
324
|
+
// Build output files listed separately
|
|
325
|
+
expect(parsed.buildOutput).toEqual(["dist/index.html"]);
|
|
521
326
|
});
|
|
522
327
|
|
|
523
|
-
test("
|
|
524
|
-
|
|
525
|
-
const store =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
ok: true,
|
|
530
|
-
updatedContent: "",
|
|
531
|
-
matchCount: 1,
|
|
532
|
-
matchMethod: "exact" as const,
|
|
533
|
-
similarity: 1,
|
|
534
|
-
actualOld: "",
|
|
535
|
-
actualNew: "",
|
|
536
|
-
};
|
|
537
|
-
},
|
|
328
|
+
test("does not annotate files for legacy app", () => {
|
|
329
|
+
const app = makeLegacyApp();
|
|
330
|
+
const store = mockStore(app, {
|
|
331
|
+
"index.html": "",
|
|
332
|
+
"styles.css": "",
|
|
538
333
|
});
|
|
539
|
-
executeAppFileEdit(
|
|
540
|
-
{
|
|
541
|
-
app_id: "app-1",
|
|
542
|
-
path: "f.txt",
|
|
543
|
-
old_string: "x",
|
|
544
|
-
new_string: "y",
|
|
545
|
-
},
|
|
546
|
-
store,
|
|
547
|
-
);
|
|
548
|
-
expect(capturedReplaceAll).toBe(false);
|
|
549
|
-
});
|
|
550
334
|
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
new_string: "y",
|
|
559
|
-
status: "updating styles",
|
|
560
|
-
},
|
|
561
|
-
store,
|
|
335
|
+
const result = executeAppFileList({ app_id: app.id }, store);
|
|
336
|
+
const parsed = JSON.parse(result.content) as string[];
|
|
337
|
+
|
|
338
|
+
expect(parsed).toContain("index.html");
|
|
339
|
+
expect(parsed).toContain("styles.css");
|
|
340
|
+
expect(parsed.every((f: string) => !f.includes("[build output]"))).toBe(
|
|
341
|
+
true,
|
|
562
342
|
);
|
|
563
|
-
expect(result.status).toBe("updating styles");
|
|
564
343
|
});
|
|
565
344
|
});
|
|
566
345
|
|
|
567
346
|
// ---------------------------------------------------------------------------
|
|
568
|
-
//
|
|
347
|
+
// executeAppCreate
|
|
569
348
|
// ---------------------------------------------------------------------------
|
|
570
349
|
|
|
571
|
-
describe("
|
|
572
|
-
test("
|
|
573
|
-
|
|
574
|
-
let
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
350
|
+
describe("executeAppCreate", () => {
|
|
351
|
+
test("flag off: creates legacy app with root index.html", async () => {
|
|
352
|
+
const files: Record<string, string> = {};
|
|
353
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
354
|
+
const app = makeLegacyApp();
|
|
355
|
+
const store: AppStore = {
|
|
356
|
+
...mockStore(app, files),
|
|
357
|
+
createApp: (params) => {
|
|
358
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
359
|
+
return app;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const result = await executeAppCreate(
|
|
364
|
+
{
|
|
365
|
+
name: "Test App",
|
|
366
|
+
html: "<html><body>Hello</body></html>",
|
|
579
367
|
},
|
|
580
|
-
});
|
|
581
|
-
const result = executeAppFileWrite(
|
|
582
|
-
{ app_id: "app-1", path: "new.html", content: "<div/>" },
|
|
583
368
|
store,
|
|
584
369
|
);
|
|
370
|
+
|
|
585
371
|
expect(result.isError).toBe(false);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
372
|
+
// Legacy path: no formatVersion set, htmlDefinition is the provided html
|
|
373
|
+
expect(createdParams?.formatVersion).toBeUndefined();
|
|
374
|
+
expect(createdParams?.htmlDefinition).toBe(
|
|
375
|
+
"<html><body>Hello</body></html>",
|
|
376
|
+
);
|
|
377
|
+
// No src/ files should be written
|
|
378
|
+
expect(files["src/index.html"]).toBeUndefined();
|
|
379
|
+
expect(files["src/main.tsx"]).toBeUndefined();
|
|
592
380
|
});
|
|
593
381
|
|
|
594
|
-
test("
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
382
|
+
test("flag on: creates multifile app with src/ scaffold", async () => {
|
|
383
|
+
const files: Record<string, string> = {};
|
|
384
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
385
|
+
const app = makeMultifileApp({ name: "New App" });
|
|
386
|
+
const store: AppStore = {
|
|
387
|
+
...mockStore(app, files),
|
|
388
|
+
createApp: (params) => {
|
|
389
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
390
|
+
return app;
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const result = await executeAppCreate(
|
|
395
|
+
{
|
|
396
|
+
name: "New App",
|
|
397
|
+
featureFlags: { multifileEnabled: true },
|
|
398
|
+
},
|
|
598
399
|
store,
|
|
599
400
|
);
|
|
600
|
-
expect(result.isError).toBe(true);
|
|
601
|
-
expect(JSON.parse(result.content)).toEqual({
|
|
602
|
-
error: "App 'missing' not found",
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
401
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
402
|
+
expect(result.isError).toBe(false);
|
|
403
|
+
// formatVersion 2 passed to createApp
|
|
404
|
+
expect(createdParams?.formatVersion).toBe(2);
|
|
405
|
+
// htmlDefinition should be empty for multifile apps
|
|
406
|
+
expect(createdParams?.htmlDefinition).toBe("");
|
|
407
|
+
// Scaffold files should be written
|
|
408
|
+
expect(files["src/index.html"]).toBeDefined();
|
|
409
|
+
expect(files["src/index.html"]).toContain("<title>New App</title>");
|
|
410
|
+
expect(files["src/index.html"]).toContain('<div id="app"></div>');
|
|
411
|
+
expect(files["src/main.tsx"]).toBeDefined();
|
|
412
|
+
expect(files["src/main.tsx"]).toContain("import { render } from 'preact'");
|
|
413
|
+
expect(files["src/main.tsx"]).toContain('{"Hello, New App!"}');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("flag on with explicit html: uses provided html as src/index.html", async () => {
|
|
417
|
+
const files: Record<string, string> = {};
|
|
418
|
+
const app = makeMultifileApp({ name: "Custom App" });
|
|
419
|
+
const store: AppStore = {
|
|
420
|
+
...mockStore(app, files),
|
|
421
|
+
createApp: () => app,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const customHtml =
|
|
425
|
+
'<!DOCTYPE html><html><head></head><body><div id="root"></div></body></html>';
|
|
426
|
+
const result = await executeAppCreate(
|
|
609
427
|
{
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
status: "adding dark mode styles",
|
|
428
|
+
name: "Custom App",
|
|
429
|
+
html: customHtml,
|
|
430
|
+
featureFlags: { multifileEnabled: true },
|
|
614
431
|
},
|
|
615
432
|
store,
|
|
616
433
|
);
|
|
617
|
-
expect(result.status).toBe("adding dark mode styles");
|
|
618
|
-
});
|
|
619
434
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
writeCalled = true;
|
|
626
|
-
},
|
|
627
|
-
});
|
|
628
|
-
executeAppFileWrite({ app_id: "bad", path: "f.txt", content: "x" }, store);
|
|
629
|
-
expect(writeCalled).toBe(false);
|
|
435
|
+
expect(result.isError).toBe(false);
|
|
436
|
+
// Explicit HTML should be used instead of scaffold
|
|
437
|
+
expect(files["src/index.html"]).toBe(customHtml);
|
|
438
|
+
// main.tsx scaffold should still be written
|
|
439
|
+
expect(files["src/main.tsx"]).toBeDefined();
|
|
630
440
|
});
|
|
631
441
|
});
|