@vellumai/assistant 0.11.9-staging.2 → 0.11.9
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/Dockerfile +9 -0
- package/docker-bun-no-autoserve.js +38 -0
- package/docker-node-launcher.sh +37 -0
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +8 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +2 -1
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/accent.test.ts +118 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +83 -4
- package/node_modules/@vellumai/avatar-manifest/src/accent.ts +150 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +7 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +55 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
- package/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
- package/openapi.yaml +116 -1
- package/package.json +1 -1
- package/src/__tests__/bun-no-autoserve.test.ts +178 -0
- package/src/avatar/__tests__/avatar-manifest.test.ts +10 -0
- package/src/avatar/__tests__/avatar-store.test.ts +130 -8
- package/src/avatar/__tests__/ensure-raster.test.ts +15 -0
- package/src/avatar/avatar-accent.ts +57 -0
- package/src/avatar/avatar-manifest.ts +20 -2
- package/src/avatar/avatar-store.ts +111 -4
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -3
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/platform/sync-avatar.test.ts +3 -0
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +147 -27
- package/src/runtime/routes/avatar-routes.ts +84 -28
- package/src/runtime/routes/settings-routes.ts +1 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +60 -1
- package/src/tools/terminal/safe-env.ts +70 -1
package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts
CHANGED
|
@@ -97,3 +97,24 @@ export function isChannelBotProvider(providerKey: string): boolean {
|
|
|
97
97
|
providerKey,
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a provider key names the user grant of a brand the assistant is
|
|
103
|
+
* reached through some other way: `slack` beside `slack_channel`, `discord`
|
|
104
|
+
* beside `discord_channel`. The other half of the question
|
|
105
|
+
* `isChannelBotProvider` asks.
|
|
106
|
+
*
|
|
107
|
+
* The two halves carry the same brand name, so the grant reads as the way to
|
|
108
|
+
* connect that brand when the bot is. A surface offering people a way to
|
|
109
|
+
* connect an assistant wants the bot, and wants this to say which keys it is
|
|
110
|
+
* answering for rather than naming them itself.
|
|
111
|
+
*
|
|
112
|
+
* A channel whose bot is its own key has no such pair: `telegram` names the
|
|
113
|
+
* bot, so nothing is standing beside it to be mistaken for.
|
|
114
|
+
*/
|
|
115
|
+
export function isChannelUserIntegration(providerKey: string): boolean {
|
|
116
|
+
return Object.entries(CHANNEL_BOT_PROVIDER).some(
|
|
117
|
+
([channelId, botProviderKey]) =>
|
|
118
|
+
channelId === providerKey && botProviderKey !== providerKey,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_BOT_PROVIDER,
|
|
5
|
+
CHANNEL_IDS,
|
|
6
|
+
isChannelBotProvider,
|
|
7
|
+
isChannelUserIntegration,
|
|
8
|
+
isChannelId,
|
|
9
|
+
} from "../channels.js";
|
|
4
10
|
|
|
5
11
|
describe("isChannelId", () => {
|
|
6
12
|
test("accepts every canonical channel id", () => {
|
|
@@ -34,3 +40,36 @@ describe("isChannelId", () => {
|
|
|
34
40
|
expect(isChannelId(42)).toBe(false);
|
|
35
41
|
});
|
|
36
42
|
});
|
|
43
|
+
|
|
44
|
+
describe("isChannelUserIntegration", () => {
|
|
45
|
+
test("names the grant standing beside a bot of the same brand", () => {
|
|
46
|
+
expect(isChannelUserIntegration("slack")).toBe(true);
|
|
47
|
+
expect(isChannelUserIntegration("discord")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("excludes a channel whose bot is its own key", () => {
|
|
51
|
+
// `telegram` names the bot, so no second provider carries the brand and
|
|
52
|
+
// there is nothing to mistake for it.
|
|
53
|
+
expect(isChannelUserIntegration("telegram")).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("excludes the bots themselves and unrelated providers", () => {
|
|
57
|
+
expect(isChannelUserIntegration("slack_channel")).toBe(false);
|
|
58
|
+
expect(isChannelUserIntegration("discord_channel")).toBe(false);
|
|
59
|
+
expect(isChannelUserIntegration("google")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("is the complement of isChannelBotProvider over the map", () => {
|
|
63
|
+
// The two questions partition the brands that have both halves: a key is
|
|
64
|
+
// one or the other, never both, so a caller picking the wrong one gets an
|
|
65
|
+
// empty answer rather than a plausible wrong one.
|
|
66
|
+
for (const [channelId, botProviderKey] of Object.entries(
|
|
67
|
+
CHANNEL_BOT_PROVIDER,
|
|
68
|
+
)) {
|
|
69
|
+
expect(isChannelUserIntegration(channelId)).toBe(
|
|
70
|
+
channelId !== botProviderKey,
|
|
71
|
+
);
|
|
72
|
+
expect(isChannelBotProvider(botProviderKey)).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -97,3 +97,24 @@ export function isChannelBotProvider(providerKey: string): boolean {
|
|
|
97
97
|
providerKey,
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a provider key names the user grant of a brand the assistant is
|
|
103
|
+
* reached through some other way: `slack` beside `slack_channel`, `discord`
|
|
104
|
+
* beside `discord_channel`. The other half of the question
|
|
105
|
+
* `isChannelBotProvider` asks.
|
|
106
|
+
*
|
|
107
|
+
* The two halves carry the same brand name, so the grant reads as the way to
|
|
108
|
+
* connect that brand when the bot is. A surface offering people a way to
|
|
109
|
+
* connect an assistant wants the bot, and wants this to say which keys it is
|
|
110
|
+
* answering for rather than naming them itself.
|
|
111
|
+
*
|
|
112
|
+
* A channel whose bot is its own key has no such pair: `telegram` names the
|
|
113
|
+
* bot, so nothing is standing beside it to be mistaken for.
|
|
114
|
+
*/
|
|
115
|
+
export function isChannelUserIntegration(providerKey: string): boolean {
|
|
116
|
+
return Object.entries(CHANNEL_BOT_PROVIDER).some(
|
|
117
|
+
([channelId, botProviderKey]) =>
|
|
118
|
+
channelId === providerKey && botProviderKey !== providerKey,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_BOT_PROVIDER,
|
|
5
|
+
CHANNEL_IDS,
|
|
6
|
+
isChannelBotProvider,
|
|
7
|
+
isChannelUserIntegration,
|
|
8
|
+
isChannelId,
|
|
9
|
+
} from "../channels.js";
|
|
4
10
|
|
|
5
11
|
describe("isChannelId", () => {
|
|
6
12
|
test("accepts every canonical channel id", () => {
|
|
@@ -34,3 +40,36 @@ describe("isChannelId", () => {
|
|
|
34
40
|
expect(isChannelId(42)).toBe(false);
|
|
35
41
|
});
|
|
36
42
|
});
|
|
43
|
+
|
|
44
|
+
describe("isChannelUserIntegration", () => {
|
|
45
|
+
test("names the grant standing beside a bot of the same brand", () => {
|
|
46
|
+
expect(isChannelUserIntegration("slack")).toBe(true);
|
|
47
|
+
expect(isChannelUserIntegration("discord")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("excludes a channel whose bot is its own key", () => {
|
|
51
|
+
// `telegram` names the bot, so no second provider carries the brand and
|
|
52
|
+
// there is nothing to mistake for it.
|
|
53
|
+
expect(isChannelUserIntegration("telegram")).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("excludes the bots themselves and unrelated providers", () => {
|
|
57
|
+
expect(isChannelUserIntegration("slack_channel")).toBe(false);
|
|
58
|
+
expect(isChannelUserIntegration("discord_channel")).toBe(false);
|
|
59
|
+
expect(isChannelUserIntegration("google")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("is the complement of isChannelBotProvider over the map", () => {
|
|
63
|
+
// The two questions partition the brands that have both halves: a key is
|
|
64
|
+
// one or the other, never both, so a caller picking the wrong one gets an
|
|
65
|
+
// empty answer rather than a plausible wrong one.
|
|
66
|
+
for (const [channelId, botProviderKey] of Object.entries(
|
|
67
|
+
CHANNEL_BOT_PROVIDER,
|
|
68
|
+
)) {
|
|
69
|
+
expect(isChannelUserIntegration(channelId)).toBe(
|
|
70
|
+
channelId !== botProviderKey,
|
|
71
|
+
);
|
|
72
|
+
expect(isChannelBotProvider(botProviderKey)).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -97,3 +97,24 @@ export function isChannelBotProvider(providerKey: string): boolean {
|
|
|
97
97
|
providerKey,
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a provider key names the user grant of a brand the assistant is
|
|
103
|
+
* reached through some other way: `slack` beside `slack_channel`, `discord`
|
|
104
|
+
* beside `discord_channel`. The other half of the question
|
|
105
|
+
* `isChannelBotProvider` asks.
|
|
106
|
+
*
|
|
107
|
+
* The two halves carry the same brand name, so the grant reads as the way to
|
|
108
|
+
* connect that brand when the bot is. A surface offering people a way to
|
|
109
|
+
* connect an assistant wants the bot, and wants this to say which keys it is
|
|
110
|
+
* answering for rather than naming them itself.
|
|
111
|
+
*
|
|
112
|
+
* A channel whose bot is its own key has no such pair: `telegram` names the
|
|
113
|
+
* bot, so nothing is standing beside it to be mistaken for.
|
|
114
|
+
*/
|
|
115
|
+
export function isChannelUserIntegration(providerKey: string): boolean {
|
|
116
|
+
return Object.entries(CHANNEL_BOT_PROVIDER).some(
|
|
117
|
+
([channelId, botProviderKey]) =>
|
|
118
|
+
channelId === providerKey && botProviderKey !== providerKey,
|
|
119
|
+
);
|
|
120
|
+
}
|
package/openapi.yaml
CHANGED
|
@@ -2019,6 +2019,103 @@ paths:
|
|
|
2019
2019
|
- userId
|
|
2020
2020
|
- authenticated
|
|
2021
2021
|
additionalProperties: false
|
|
2022
|
+
/v1/avatar/accent:
|
|
2023
|
+
post:
|
|
2024
|
+
operationId: avatar_accent_post
|
|
2025
|
+
summary: Set avatar accent
|
|
2026
|
+
description:
|
|
2027
|
+
"Set the accent colour over the current avatar as #rrggbb, or null to return to the automatic one (the
|
|
2028
|
+
character's palette colour, or the colour read out of the uploaded image)."
|
|
2029
|
+
tags:
|
|
2030
|
+
- avatar
|
|
2031
|
+
requestBody:
|
|
2032
|
+
required: true
|
|
2033
|
+
content:
|
|
2034
|
+
application/json:
|
|
2035
|
+
schema:
|
|
2036
|
+
type: object
|
|
2037
|
+
properties:
|
|
2038
|
+
hex:
|
|
2039
|
+
anyOf:
|
|
2040
|
+
- type: string
|
|
2041
|
+
- type: "null"
|
|
2042
|
+
required:
|
|
2043
|
+
- hex
|
|
2044
|
+
responses:
|
|
2045
|
+
"200":
|
|
2046
|
+
description: Successful response
|
|
2047
|
+
content:
|
|
2048
|
+
application/json:
|
|
2049
|
+
schema:
|
|
2050
|
+
type: object
|
|
2051
|
+
properties:
|
|
2052
|
+
kind:
|
|
2053
|
+
type: string
|
|
2054
|
+
enum:
|
|
2055
|
+
- character
|
|
2056
|
+
- image
|
|
2057
|
+
- none
|
|
2058
|
+
traits:
|
|
2059
|
+
anyOf:
|
|
2060
|
+
- type: object
|
|
2061
|
+
properties:
|
|
2062
|
+
bodyShape:
|
|
2063
|
+
type: string
|
|
2064
|
+
eyeStyle:
|
|
2065
|
+
type: string
|
|
2066
|
+
color:
|
|
2067
|
+
type: string
|
|
2068
|
+
required:
|
|
2069
|
+
- bodyShape
|
|
2070
|
+
- eyeStyle
|
|
2071
|
+
- color
|
|
2072
|
+
additionalProperties: false
|
|
2073
|
+
- type: "null"
|
|
2074
|
+
source:
|
|
2075
|
+
anyOf:
|
|
2076
|
+
- type: string
|
|
2077
|
+
enum:
|
|
2078
|
+
- builder
|
|
2079
|
+
- upload
|
|
2080
|
+
- ai
|
|
2081
|
+
- type: "null"
|
|
2082
|
+
image:
|
|
2083
|
+
anyOf:
|
|
2084
|
+
- type: object
|
|
2085
|
+
properties:
|
|
2086
|
+
updatedAt:
|
|
2087
|
+
type: string
|
|
2088
|
+
etag:
|
|
2089
|
+
type: string
|
|
2090
|
+
required:
|
|
2091
|
+
- updatedAt
|
|
2092
|
+
- etag
|
|
2093
|
+
additionalProperties: false
|
|
2094
|
+
- type: "null"
|
|
2095
|
+
accent:
|
|
2096
|
+
anyOf:
|
|
2097
|
+
- type: object
|
|
2098
|
+
properties:
|
|
2099
|
+
hex:
|
|
2100
|
+
type: string
|
|
2101
|
+
source:
|
|
2102
|
+
type: string
|
|
2103
|
+
enum:
|
|
2104
|
+
- palette
|
|
2105
|
+
- derived
|
|
2106
|
+
- custom
|
|
2107
|
+
required:
|
|
2108
|
+
- hex
|
|
2109
|
+
- source
|
|
2110
|
+
additionalProperties: false
|
|
2111
|
+
- type: "null"
|
|
2112
|
+
required:
|
|
2113
|
+
- kind
|
|
2114
|
+
- traits
|
|
2115
|
+
- source
|
|
2116
|
+
- image
|
|
2117
|
+
- accent
|
|
2118
|
+
additionalProperties: false
|
|
2022
2119
|
/v1/avatar/character-components:
|
|
2023
2120
|
get:
|
|
2024
2121
|
operationId: avatar_charactercomponents_get
|
|
@@ -2390,7 +2487,7 @@ paths:
|
|
|
2390
2487
|
get:
|
|
2391
2488
|
operationId: avatar_state_get
|
|
2392
2489
|
summary: Get avatar state
|
|
2393
|
-
description: Return the authoritative avatar render mode (character, image, or none).
|
|
2490
|
+
description: Return the authoritative avatar render mode (character, image, or none) and its accent colour.
|
|
2394
2491
|
tags:
|
|
2395
2492
|
- avatar
|
|
2396
2493
|
responses:
|
|
@@ -2444,11 +2541,29 @@ paths:
|
|
|
2444
2541
|
- etag
|
|
2445
2542
|
additionalProperties: false
|
|
2446
2543
|
- type: "null"
|
|
2544
|
+
accent:
|
|
2545
|
+
anyOf:
|
|
2546
|
+
- type: object
|
|
2547
|
+
properties:
|
|
2548
|
+
hex:
|
|
2549
|
+
type: string
|
|
2550
|
+
source:
|
|
2551
|
+
type: string
|
|
2552
|
+
enum:
|
|
2553
|
+
- palette
|
|
2554
|
+
- derived
|
|
2555
|
+
- custom
|
|
2556
|
+
required:
|
|
2557
|
+
- hex
|
|
2558
|
+
- source
|
|
2559
|
+
additionalProperties: false
|
|
2560
|
+
- type: "null"
|
|
2447
2561
|
required:
|
|
2448
2562
|
- kind
|
|
2449
2563
|
- traits
|
|
2450
2564
|
- source
|
|
2451
2565
|
- image
|
|
2566
|
+
- accent
|
|
2452
2567
|
additionalProperties: false
|
|
2453
2568
|
/v1/background-tools:
|
|
2454
2569
|
get:
|
package/package.json
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Guard for `assistant/docker-bun-no-autoserve.js`, the preload that stops
|
|
15
|
+
* `bun:main` turning a CLI whose default export has a `fetch` method into a
|
|
16
|
+
* long-lived HTTP server. The image has no Node, so `bunx <pkg>` runs such a
|
|
17
|
+
* CLI under Bun via the `node` launcher the Dockerfile installs, and without
|
|
18
|
+
* the shim the process never exits.
|
|
19
|
+
*
|
|
20
|
+
* The shim recognizes the implicit call by its `bun:main` caller frame. If a
|
|
21
|
+
* Bun upgrade renames that frame the shim fails open (every server starts for
|
|
22
|
+
* real) and these fixtures catch it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Tests run with `assistant/` as the working directory.
|
|
26
|
+
const SHIM_PATH = join(process.cwd(), "docker-bun-no-autoserve.js");
|
|
27
|
+
const DOCKERFILE_PATH = join(process.cwd(), "Dockerfile");
|
|
28
|
+
const LAUNCHER_PATH = join(process.cwd(), "docker-node-launcher.sh");
|
|
29
|
+
const RUN_TIMEOUT_MS = 10_000;
|
|
30
|
+
|
|
31
|
+
let fixtureDir: string;
|
|
32
|
+
|
|
33
|
+
function writeFixture(name: string, source: string): string {
|
|
34
|
+
const path = join(fixtureDir, name);
|
|
35
|
+
writeFileSync(path, source);
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runWithShim(
|
|
40
|
+
fixturePath: string,
|
|
41
|
+
): Promise<{ exitCode: number | null; output: string }> {
|
|
42
|
+
const proc = Bun.spawn(
|
|
43
|
+
[process.execPath, `--preload=${SHIM_PATH}`, "run", fixturePath],
|
|
44
|
+
{ stdout: "pipe", stderr: "pipe", windowsHide: true },
|
|
45
|
+
);
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
proc.kill();
|
|
48
|
+
}, RUN_TIMEOUT_MS);
|
|
49
|
+
try {
|
|
50
|
+
const [stdout, stderr] = await Promise.all([
|
|
51
|
+
new Response(proc.stdout).text(),
|
|
52
|
+
new Response(proc.stderr).text(),
|
|
53
|
+
]);
|
|
54
|
+
await proc.exited;
|
|
55
|
+
return { exitCode: proc.exitCode, output: `${stdout}${stderr}` };
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("Bun auto-serve shim", () => {
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
fixtureDir = mkdtempSync(join(tmpdir(), "bun-no-autoserve-"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
rmSync(fixtureDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a default export with fetch exits instead of starting a server", async () => {
|
|
71
|
+
const fixture = writeFixture(
|
|
72
|
+
"auto-serve.js",
|
|
73
|
+
'export default { fetch: () => new Response("x") };\n',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const { exitCode, output } = await runWithShim(fixture);
|
|
77
|
+
|
|
78
|
+
expect(exitCode).toBe(0);
|
|
79
|
+
expect(output).not.toContain("Started ");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an explicit Bun.serve call still starts a real server", async () => {
|
|
83
|
+
const fixture = writeFixture(
|
|
84
|
+
"explicit-serve.js",
|
|
85
|
+
[
|
|
86
|
+
'const server = Bun.serve({ port: 0, fetch: () => new Response("x") });',
|
|
87
|
+
// Written raw so Bun's console formatting does not colorize the number.
|
|
88
|
+
'process.stdout.write("port=" + server.port + "\\n");',
|
|
89
|
+
"server.stop(true);",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const { exitCode, output } = await runWithShim(fixture);
|
|
95
|
+
|
|
96
|
+
expect(exitCode).toBe(0);
|
|
97
|
+
const port = Number(/port=(\d+)/.exec(output)?.[1]);
|
|
98
|
+
expect(port).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("the Dockerfile installs the node launcher, which preloads the shim", () => {
|
|
102
|
+
const dockerfile = readFileSync(DOCKERFILE_PATH, "utf8");
|
|
103
|
+
const launcher = readFileSync(LAUNCHER_PATH, "utf8");
|
|
104
|
+
|
|
105
|
+
expect(dockerfile).toContain(
|
|
106
|
+
"ln -s /app/assistant/docker-node-launcher.sh /usr/local/bin/node",
|
|
107
|
+
);
|
|
108
|
+
expect(launcher).toContain(
|
|
109
|
+
"shim=/app/assistant/docker-bun-no-autoserve.js",
|
|
110
|
+
);
|
|
111
|
+
expect(launcher).toContain(
|
|
112
|
+
'exec /usr/local/bin/bun --preload="$shim" "$@"',
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The launcher only falls back to Bun. A Node installed after the image was
|
|
118
|
+
// built (apt, or the persistent Kata apt root) has to win, or Node CLIs keep
|
|
119
|
+
// running under Bun semantics forever.
|
|
120
|
+
describe.skipIf(process.platform === "win32")(
|
|
121
|
+
"node launcher PATH handoff",
|
|
122
|
+
() => {
|
|
123
|
+
let binDir: string;
|
|
124
|
+
|
|
125
|
+
function writeStubNode(dir: string, marker: string): void {
|
|
126
|
+
mkdirSync(dir, { recursive: true });
|
|
127
|
+
const path = join(dir, "node");
|
|
128
|
+
writeFileSync(path, `#!/bin/sh\necho ${marker}\n`);
|
|
129
|
+
chmodSync(path, 0o755);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runLauncher(
|
|
133
|
+
path: string,
|
|
134
|
+
): Promise<{ exitCode: number | null; output: string }> {
|
|
135
|
+
const proc = Bun.spawn(["/bin/sh", LAUNCHER_PATH, "--version"], {
|
|
136
|
+
env: { PATH: path },
|
|
137
|
+
stdout: "pipe",
|
|
138
|
+
stderr: "pipe",
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
});
|
|
141
|
+
const [stdout, stderr] = await Promise.all([
|
|
142
|
+
new Response(proc.stdout).text(),
|
|
143
|
+
new Response(proc.stderr).text(),
|
|
144
|
+
]);
|
|
145
|
+
await proc.exited;
|
|
146
|
+
return { exitCode: proc.exitCode, output: `${stdout}${stderr}` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
beforeAll(() => {
|
|
150
|
+
binDir = mkdtempSync(join(tmpdir(), "node-launcher-"));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterAll(() => {
|
|
154
|
+
rmSync(binDir, { recursive: true, force: true });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("delegates to a real node found on PATH", async () => {
|
|
158
|
+
const realDir = join(binDir, "real");
|
|
159
|
+
writeStubNode(realDir, "REAL_NODE");
|
|
160
|
+
|
|
161
|
+
const { exitCode, output } = await runLauncher(realDir);
|
|
162
|
+
|
|
163
|
+
expect(exitCode).toBe(0);
|
|
164
|
+
expect(output).toContain("REAL_NODE");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("ignores Bun's own node shim dir", async () => {
|
|
168
|
+
const shimDir = join(binDir, "bun-node-deadbeef");
|
|
169
|
+
writeStubNode(shimDir, "BUN_SHIM");
|
|
170
|
+
|
|
171
|
+
const { output } = await runLauncher(shimDir);
|
|
172
|
+
|
|
173
|
+
// With no real node anywhere it falls through to the image's bun rather
|
|
174
|
+
// than running the shim, which is the unpreloaded Bun that hangs.
|
|
175
|
+
expect(output).not.toContain("BUN_SHIM");
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
);
|
|
@@ -30,6 +30,7 @@ describe("avatar-manifest", () => {
|
|
|
30
30
|
traits: VALID_TRAITS,
|
|
31
31
|
source: "builder",
|
|
32
32
|
image: null,
|
|
33
|
+
accent: null,
|
|
33
34
|
};
|
|
34
35
|
writeManifest(state, dir);
|
|
35
36
|
expect(readManifest(dir)).toEqual(state);
|
|
@@ -41,6 +42,7 @@ describe("avatar-manifest", () => {
|
|
|
41
42
|
traits: null,
|
|
42
43
|
source: "upload",
|
|
43
44
|
image: { updatedAt: "2024-01-01T00:00:00.000Z", etag: "abc123" },
|
|
45
|
+
accent: null,
|
|
44
46
|
};
|
|
45
47
|
writeManifest(state, dir);
|
|
46
48
|
expect(readManifest(dir)).toEqual(state);
|
|
@@ -63,6 +65,7 @@ describe("avatar-manifest", () => {
|
|
|
63
65
|
traits: null,
|
|
64
66
|
source: null,
|
|
65
67
|
image: null,
|
|
68
|
+
accent: null,
|
|
66
69
|
}),
|
|
67
70
|
);
|
|
68
71
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -81,6 +84,7 @@ describe("avatar-manifest", () => {
|
|
|
81
84
|
traits: null,
|
|
82
85
|
source: null,
|
|
83
86
|
image: null,
|
|
87
|
+
accent: null,
|
|
84
88
|
}),
|
|
85
89
|
);
|
|
86
90
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -102,6 +106,7 @@ describe("avatar-manifest", () => {
|
|
|
102
106
|
traits: { bodyShape: "round" },
|
|
103
107
|
source: null,
|
|
104
108
|
image: null,
|
|
109
|
+
accent: null,
|
|
105
110
|
}),
|
|
106
111
|
);
|
|
107
112
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -115,6 +120,7 @@ describe("avatar-manifest", () => {
|
|
|
115
120
|
traits: null,
|
|
116
121
|
source: null,
|
|
117
122
|
image: null,
|
|
123
|
+
accent: null,
|
|
118
124
|
}),
|
|
119
125
|
);
|
|
120
126
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -136,6 +142,7 @@ describe("avatar-manifest", () => {
|
|
|
136
142
|
traits: null,
|
|
137
143
|
source: null,
|
|
138
144
|
image: { updatedAt: "2024-01-01T00:00:00.000Z", etag: "" },
|
|
145
|
+
accent: null,
|
|
139
146
|
}),
|
|
140
147
|
);
|
|
141
148
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -149,6 +156,7 @@ describe("avatar-manifest", () => {
|
|
|
149
156
|
traits: null,
|
|
150
157
|
source: null,
|
|
151
158
|
image: { etag: "abc123" },
|
|
159
|
+
accent: null,
|
|
152
160
|
}),
|
|
153
161
|
);
|
|
154
162
|
expect(readManifest(dir)).toBeNull();
|
|
@@ -160,6 +168,7 @@ describe("avatar-manifest", () => {
|
|
|
160
168
|
traits: null,
|
|
161
169
|
source: null,
|
|
162
170
|
image: null,
|
|
171
|
+
accent: null,
|
|
163
172
|
};
|
|
164
173
|
writeFileSync(join(dir, "avatar.json"), JSON.stringify({ kind: "none" }));
|
|
165
174
|
expect(readManifest(dir)).toEqual(state);
|
|
@@ -230,6 +239,7 @@ describe("avatar-manifest", () => {
|
|
|
230
239
|
traits: null,
|
|
231
240
|
source: null,
|
|
232
241
|
image: null,
|
|
242
|
+
accent: null,
|
|
233
243
|
});
|
|
234
244
|
});
|
|
235
245
|
});
|