@vellumai/assistant 0.11.10 → 0.11.11-staging.1
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/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `hasNotifiedSourceContextSince`, the probe the schedule-result
|
|
3
|
+
* producer uses to ask "did this run already notify?".
|
|
4
|
+
*
|
|
5
|
+
* Two things are worth pinning down. The `since` bound: a recurring schedule
|
|
6
|
+
* with `reuseConversation` runs every firing in the same conversation, so an
|
|
7
|
+
* unbounded probe would find run #1's notification forever and silence every
|
|
8
|
+
* run after it. And the outcome test: `emitNotificationSignal` persists the
|
|
9
|
+
* event before the pipeline can fail, so an event row alone proves an attempt,
|
|
10
|
+
* not a notification — counting it would suppress the fallback in exactly the
|
|
11
|
+
* case it exists for.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import { eq } from "drizzle-orm";
|
|
17
|
+
|
|
18
|
+
import { createDecision } from "../notifications/decisions-store.js";
|
|
19
|
+
import { createDelivery } from "../notifications/deliveries-store.js";
|
|
20
|
+
import {
|
|
21
|
+
createEvent,
|
|
22
|
+
hasNotifiedSourceContextSince,
|
|
23
|
+
} from "../notifications/events-store.js";
|
|
24
|
+
import type { NotificationDeliveryStatus } from "../notifications/types.js";
|
|
25
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
26
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
27
|
+
import {
|
|
28
|
+
notificationDecisions,
|
|
29
|
+
notificationDeliveries,
|
|
30
|
+
notificationEvents,
|
|
31
|
+
} from "../persistence/schema/index.js";
|
|
32
|
+
|
|
33
|
+
await initializeDb();
|
|
34
|
+
|
|
35
|
+
const CONVERSATION_ID = "conv-schedule-run";
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
getDb().delete(notificationDeliveries).run();
|
|
39
|
+
getDb().delete(notificationDecisions).run();
|
|
40
|
+
getDb().delete(notificationEvents).run();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How far the seeded signal got through the pipeline.
|
|
45
|
+
*
|
|
46
|
+
* - `no-verdict`: the event row exists and nothing else — the pipeline threw
|
|
47
|
+
* before the decision stage.
|
|
48
|
+
* - `declined`: the engine decided not to notify.
|
|
49
|
+
* - a delivery status: the engine said notify and dispatch recorded a delivery
|
|
50
|
+
* in that state.
|
|
51
|
+
*/
|
|
52
|
+
type SeedOutcome = "no-verdict" | "declined" | NotificationDeliveryStatus;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Seed a signal and backdate its event: `createEvent` stamps `Date.now()`, but
|
|
56
|
+
* the `since` cases turn entirely on where a row sits relative to a run's start.
|
|
57
|
+
*/
|
|
58
|
+
function seedSignalAt(
|
|
59
|
+
sourceContextId: string,
|
|
60
|
+
createdAt: number,
|
|
61
|
+
outcome: SeedOutcome = "sent",
|
|
62
|
+
): void {
|
|
63
|
+
const eventId = crypto.randomUUID();
|
|
64
|
+
createEvent({
|
|
65
|
+
id: eventId,
|
|
66
|
+
sourceEventName: "assistant.share",
|
|
67
|
+
sourceChannel: "assistant_tool",
|
|
68
|
+
sourceContextId,
|
|
69
|
+
attentionHints: {
|
|
70
|
+
requiresAction: false,
|
|
71
|
+
urgency: "low",
|
|
72
|
+
isAsyncBackground: true,
|
|
73
|
+
visibleInSourceNow: false,
|
|
74
|
+
},
|
|
75
|
+
payload: { requestedMessage: "sent by the run itself" },
|
|
76
|
+
});
|
|
77
|
+
getDb()
|
|
78
|
+
.update(notificationEvents)
|
|
79
|
+
.set({ createdAt })
|
|
80
|
+
.where(eq(notificationEvents.id, eventId))
|
|
81
|
+
.run();
|
|
82
|
+
|
|
83
|
+
if (outcome === "no-verdict") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const decision = createDecision({
|
|
87
|
+
id: crypto.randomUUID(),
|
|
88
|
+
notificationEventId: eventId,
|
|
89
|
+
shouldNotify: outcome !== "declined",
|
|
90
|
+
selectedChannels: outcome === "declined" ? [] : ["vellum"],
|
|
91
|
+
reasoningSummary: "test",
|
|
92
|
+
confidence: 1,
|
|
93
|
+
fallbackUsed: false,
|
|
94
|
+
});
|
|
95
|
+
if (outcome === "declined") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
createDelivery({
|
|
99
|
+
id: crypto.randomUUID(),
|
|
100
|
+
notificationDecisionId: decision.id,
|
|
101
|
+
channel: "vellum",
|
|
102
|
+
destination: "vellum",
|
|
103
|
+
status: outcome,
|
|
104
|
+
attempt: 1,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("hasNotifiedSourceContextSince", () => {
|
|
109
|
+
const runStartedAt = 1_700_000_000_000;
|
|
110
|
+
|
|
111
|
+
describe("the since bound", () => {
|
|
112
|
+
test("finds a notification sent during the run", () => {
|
|
113
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500);
|
|
114
|
+
|
|
115
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
116
|
+
true,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("ignores a notification from a prior run in the same conversation", () => {
|
|
121
|
+
// The reused-conversation case: yesterday's firing notified, today's did
|
|
122
|
+
// not. Today's run still owes the user a notification.
|
|
123
|
+
const todaysRunStartedAt = runStartedAt + 86_400_000;
|
|
124
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500);
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
hasNotifiedSourceContextSince(CONVERSATION_ID, todaysRunStartedAt),
|
|
128
|
+
).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("counts a notification landing exactly at the run's start", () => {
|
|
132
|
+
// The bound is inclusive: a notification stamped in the same millisecond
|
|
133
|
+
// the run began belongs to that run.
|
|
134
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt);
|
|
135
|
+
|
|
136
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("ignores a notification from a different conversation", () => {
|
|
142
|
+
seedSignalAt("some-other-conversation", runStartedAt + 500);
|
|
143
|
+
|
|
144
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
145
|
+
false,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns false when nothing was ever emitted", () => {
|
|
150
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
151
|
+
false,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("the outcome test", () => {
|
|
157
|
+
test("ignores an event whose pipeline never reached a verdict", () => {
|
|
158
|
+
// The audit row of an emit that threw before the decision stage. Nobody
|
|
159
|
+
// was told, so the run still owes its notification.
|
|
160
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500, "no-verdict");
|
|
161
|
+
|
|
162
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
163
|
+
false,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("ignores an event whose only delivery failed", () => {
|
|
168
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500, "failed");
|
|
169
|
+
|
|
170
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
171
|
+
false,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("ignores an event whose only delivery was skipped", () => {
|
|
176
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500, "skipped");
|
|
177
|
+
|
|
178
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
179
|
+
false,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("counts a delivery still pending", () => {
|
|
184
|
+
// Something is on its way; the fallback must not race it.
|
|
185
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500, "pending");
|
|
186
|
+
|
|
187
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
188
|
+
true,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("counts a decision not to notify", () => {
|
|
193
|
+
// The engine ruled on the run's own signal. The fallback covers runs that
|
|
194
|
+
// never asked; it does not appeal verdicts the user's settings produced.
|
|
195
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 500, "declined");
|
|
196
|
+
|
|
197
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
198
|
+
true,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("one delivered signal among failed ones is enough", () => {
|
|
203
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 100, "failed");
|
|
204
|
+
seedSignalAt(CONVERSATION_ID, runStartedAt + 200, "sent");
|
|
205
|
+
|
|
206
|
+
expect(hasNotifiedSourceContextSince(CONVERSATION_ID, runStartedAt)).toBe(
|
|
207
|
+
true,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prompt caching on the OpenAI Responses transport: request-wide
|
|
3
|
-
* `prompt_cache_key` emission for every
|
|
4
|
-
* implicit-mode models,
|
|
5
|
-
* explicit
|
|
6
|
-
*
|
|
3
|
+
* `prompt_cache_key` emission for every Responses model (routing affinity for
|
|
4
|
+
* implicit-mode models, including the Codex subscription endpoint; opt-in
|
|
5
|
+
* explicit mode for breakpoint-capable direct-API models), explicit
|
|
6
|
+
* `prompt_cache_options` and block-level `prompt_cache_breakpoint` anchor
|
|
7
|
+
* placement for GPT-5.6+ (turn-start / previous-turn / advancing tail,
|
|
7
8
|
* mirroring the Anthropic client), the `disableCache`
|
|
8
9
|
* explicit-mode-with-zero-markers opt-out, and the unflagged-model and Codex
|
|
9
10
|
* regression guards.
|
|
@@ -389,19 +390,52 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
|
|
|
389
390
|
expect(breakpointedItemIndexes()).toEqual([0]);
|
|
390
391
|
});
|
|
391
392
|
|
|
392
|
-
test("codex subscription endpoint
|
|
393
|
+
test("codex subscription endpoint sends prompt_cache_key but no explicit-mode params", async () => {
|
|
393
394
|
const provider = makeProvider("gpt-5.6-sol", true);
|
|
394
395
|
await provider.sendMessage([userMsg("hi")], {
|
|
395
396
|
config: { promptCacheKey: "conv-1" },
|
|
396
397
|
});
|
|
397
398
|
|
|
399
|
+
// Codex uses implicit prefix caching. The key is a supported routing
|
|
400
|
+
// field (same as the official Codex client); prompt_cache_options,
|
|
401
|
+
// breakpoints, and prompt_cache_retention are not.
|
|
402
|
+
expect(lastStreamParams?.prompt_cache_key).toBe("conv-1");
|
|
398
403
|
expect(lastStreamParams?.prompt_cache_options).toBeUndefined();
|
|
399
|
-
expect(lastStreamParams?.
|
|
404
|
+
expect(lastStreamParams?.prompt_cache_retention).toBeUndefined();
|
|
405
|
+
expect(JSON.stringify(lastStreamParams?.input)).not.toContain(
|
|
406
|
+
"prompt_cache_breakpoint",
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("codex subscription endpoint keeps prompt_cache_key stable across append-only turns", async () => {
|
|
411
|
+
const provider = makeProvider("gpt-5.6-sol", true);
|
|
412
|
+
await provider.sendMessage([userMsg("hi")], {
|
|
413
|
+
config: { promptCacheKey: "conv-1" },
|
|
414
|
+
});
|
|
415
|
+
expect(lastStreamParams?.prompt_cache_key).toBe("conv-1");
|
|
416
|
+
|
|
417
|
+
await provider.sendMessage(
|
|
418
|
+
[userMsg("hi"), assistantMsg("ok"), userMsg("again")],
|
|
419
|
+
{ config: { promptCacheKey: "conv-1" } },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(lastStreamParams?.prompt_cache_key).toBe("conv-1");
|
|
423
|
+
expect(lastStreamParams?.prompt_cache_options).toBeUndefined();
|
|
424
|
+
expect(lastStreamParams?.prompt_cache_retention).toBeUndefined();
|
|
400
425
|
expect(JSON.stringify(lastStreamParams?.input)).not.toContain(
|
|
401
426
|
"prompt_cache_breakpoint",
|
|
402
427
|
);
|
|
403
428
|
});
|
|
404
429
|
|
|
430
|
+
test("codex subscription endpoint omits prompt_cache_key when absent", async () => {
|
|
431
|
+
const provider = makeProvider("gpt-5.6-sol", true);
|
|
432
|
+
await provider.sendMessage([userMsg("hi")]);
|
|
433
|
+
|
|
434
|
+
expect(lastStreamParams?.prompt_cache_key).toBeUndefined();
|
|
435
|
+
expect(lastStreamParams?.prompt_cache_options).toBeUndefined();
|
|
436
|
+
expect(lastStreamParams?.prompt_cache_retention).toBeUndefined();
|
|
437
|
+
});
|
|
438
|
+
|
|
405
439
|
test("never mutates the caller's messages", async () => {
|
|
406
440
|
const provider = makeProvider("gpt-5.6-sol");
|
|
407
441
|
const messages = [userMsg("t1"), assistantMsg("r1"), userMsg("t2")];
|
|
@@ -148,6 +148,15 @@ mock.module("../messaging/providers/outlook/client.js", () => ({
|
|
|
148
148
|
getAutoReplySettings: mockGetAutoReplySettings,
|
|
149
149
|
updateAutoReplySettings: mockUpdateAutoReplySettings,
|
|
150
150
|
listMessagesDelta: mockListMessagesDelta,
|
|
151
|
+
toOutlookFileAttachments: (
|
|
152
|
+
attachments: Array<{ filename: string; mimeType: string; data: Buffer }>,
|
|
153
|
+
) =>
|
|
154
|
+
attachments.map((att) => ({
|
|
155
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
156
|
+
name: att.filename,
|
|
157
|
+
contentType: att.mimeType,
|
|
158
|
+
contentBytes: att.data.toString("base64"),
|
|
159
|
+
})),
|
|
151
160
|
}));
|
|
152
161
|
|
|
153
162
|
import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
|
|
@@ -19,7 +19,7 @@ function makeConfig(): AssistantConfig {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
describe("getVisibleProviderCatalog", () => {
|
|
22
|
-
test("hides Vellum-hosted GPU models unless
|
|
22
|
+
test("hides Vellum-hosted GPU models unless hosted inference is on", () => {
|
|
23
23
|
setOverridesForTesting({});
|
|
24
24
|
const hidden = getVisibleProviderCatalog(makeConfig());
|
|
25
25
|
expect(hidden.find((p) => p.id === "vellum")).toBeUndefined();
|
|
@@ -29,7 +29,7 @@ describe("getVisibleProviderCatalog", () => {
|
|
|
29
29
|
.some((m) => m.id === "qwen/qwen3-8b"),
|
|
30
30
|
).toBe(false);
|
|
31
31
|
|
|
32
|
-
setOverridesForTesting({ "
|
|
32
|
+
setOverridesForTesting({ "vellum-hosted-inference": true });
|
|
33
33
|
const visible = getVisibleProviderCatalog(makeConfig());
|
|
34
34
|
const vellum = visible.find((p) => p.id === "vellum");
|
|
35
35
|
expect(vellum).toBeDefined();
|
|
@@ -65,10 +65,18 @@ import {
|
|
|
65
65
|
} from "../persistence/conversation-crud.js";
|
|
66
66
|
import { getDb } from "../persistence/db-connection.js";
|
|
67
67
|
import { initializeDb } from "../persistence/db-init.js";
|
|
68
|
+
import {
|
|
69
|
+
clearLifecycleQuiesce,
|
|
70
|
+
setLifecycleQuiesce,
|
|
71
|
+
} from "../persistence/lifecycle-quiesce.js";
|
|
68
72
|
import { recordUsageEvent } from "../persistence/llm-usage-store.js";
|
|
69
73
|
import { rawRun } from "../persistence/raw-query.js";
|
|
70
74
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
71
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
BadRequestError,
|
|
77
|
+
ConflictError,
|
|
78
|
+
NotFoundError,
|
|
79
|
+
} from "../runtime/routes/errors.js";
|
|
72
80
|
import { ROUTES as HEARTBEAT_ROUTES } from "../runtime/routes/heartbeat-routes.js";
|
|
73
81
|
import { ROUTES as SCHEDULE_ROUTES } from "../runtime/routes/schedule-routes.js";
|
|
74
82
|
import type { RouteDefinition } from "../runtime/routes/types.js";
|
|
@@ -1670,6 +1678,27 @@ describe("plugin-sourced schedules over routes", () => {
|
|
|
1670
1678
|
|
|
1671
1679
|
expect(existsSync(RUN_MARKER)).toBe(true);
|
|
1672
1680
|
});
|
|
1681
|
+
|
|
1682
|
+
test("refuses run-now while a drain quiesce lease is active", async () => {
|
|
1683
|
+
const imperative = await createSchedule({
|
|
1684
|
+
name: "Quiesced script",
|
|
1685
|
+
cronExpression: "* * * * *",
|
|
1686
|
+
message: "",
|
|
1687
|
+
mode: "script",
|
|
1688
|
+
script: `touch ${RUN_MARKER}`,
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
setLifecycleQuiesce();
|
|
1692
|
+
try {
|
|
1693
|
+
await expect(runNow(imperative.id)).rejects.toThrow(ConflictError);
|
|
1694
|
+
await expect(runNow(imperative.id)).rejects.toThrow(
|
|
1695
|
+
"The assistant is shutting down and is not starting new schedule runs.",
|
|
1696
|
+
);
|
|
1697
|
+
expect(existsSync(RUN_MARKER)).toBe(false);
|
|
1698
|
+
} finally {
|
|
1699
|
+
clearLifecycleQuiesce();
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1673
1702
|
});
|
|
1674
1703
|
});
|
|
1675
1704
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring tests for the schedule-result notification: which schedule runs reach
|
|
3
|
+
* the producer, and with what.
|
|
4
|
+
*
|
|
5
|
+
* The producer's own gates (already notified / nothing to say / flag off) are
|
|
6
|
+
* covered in `notifications/__tests__/schedule-result-producer.test.ts`. What
|
|
7
|
+
* is checked here is the scheduler's half of the contract, which the producer
|
|
8
|
+
* cannot verify about itself: that a successful execute-mode run calls it at
|
|
9
|
+
* all, that a failed run does not, that the other modes are left alone, and
|
|
10
|
+
* that `runStartedAt` is captured before the run rather than after — the bound
|
|
11
|
+
* that keeps a reused conversation's earlier notifications from silencing
|
|
12
|
+
* later runs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
// Stub the shared runner so the execute path is observable without an LLM.
|
|
18
|
+
let runBackgroundJobShouldFail = false;
|
|
19
|
+
mock.module("../runtime/background-job-runner.js", () => ({
|
|
20
|
+
runBackgroundJob: async (opts: {
|
|
21
|
+
prompt: string;
|
|
22
|
+
groupId?: string;
|
|
23
|
+
conversationType?: "background" | "scheduled";
|
|
24
|
+
scheduleJobId?: string;
|
|
25
|
+
onConversationCreated?: (id: string) => void;
|
|
26
|
+
}) => {
|
|
27
|
+
const { createConversation } =
|
|
28
|
+
await import("../persistence/conversation-crud.js");
|
|
29
|
+
const conv = createConversation({
|
|
30
|
+
title: "(test stub)",
|
|
31
|
+
conversationType: opts.conversationType ?? "background",
|
|
32
|
+
source: "schedule",
|
|
33
|
+
...(opts.groupId ? { groupId: opts.groupId } : {}),
|
|
34
|
+
...(opts.scheduleJobId ? { scheduleJobId: opts.scheduleJobId } : {}),
|
|
35
|
+
});
|
|
36
|
+
opts.onConversationCreated?.(conv.id);
|
|
37
|
+
if (runBackgroundJobShouldFail) {
|
|
38
|
+
return {
|
|
39
|
+
conversationId: conv.id,
|
|
40
|
+
ok: false,
|
|
41
|
+
error: new Error("Simulated failure"),
|
|
42
|
+
errorKind: "exception" as const,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { conversationId: conv.id, ok: true };
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
interface CapturedCall {
|
|
50
|
+
scheduleId: string;
|
|
51
|
+
scheduleName: string;
|
|
52
|
+
conversationId: string;
|
|
53
|
+
runId: string;
|
|
54
|
+
runStartedAt: number;
|
|
55
|
+
}
|
|
56
|
+
const producerCalls: CapturedCall[] = [];
|
|
57
|
+
mock.module("../notifications/schedule-result-producer.js", () => ({
|
|
58
|
+
emitScheduleResultNotification: async (params: CapturedCall) => {
|
|
59
|
+
producerCalls.push(params);
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// Notify-mode schedules emit through the pipeline; stub it so the mode-scoping
|
|
64
|
+
// test does not need a live notification stack.
|
|
65
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
66
|
+
emitNotificationSignal: async () => ({
|
|
67
|
+
signalId: "sig-1",
|
|
68
|
+
deduplicated: false,
|
|
69
|
+
dispatched: true,
|
|
70
|
+
reason: "ok",
|
|
71
|
+
deliveryResults: [],
|
|
72
|
+
}),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
76
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
77
|
+
import { createSchedule, getScheduleRuns } from "../schedule/schedule-store.js";
|
|
78
|
+
import { runDueSchedulesOnce } from "../schedule/scheduler.js";
|
|
79
|
+
|
|
80
|
+
await initializeDb();
|
|
81
|
+
|
|
82
|
+
function getRawDb(): import("bun:sqlite").Database {
|
|
83
|
+
return (getDb() as unknown as { $client: import("bun:sqlite").Database })
|
|
84
|
+
.$client;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function forceScheduleDue(scheduleId: string): void {
|
|
88
|
+
getRawDb().run("UPDATE cron_jobs SET next_run_at = ? WHERE id = ?", [
|
|
89
|
+
Date.now() - 1000,
|
|
90
|
+
scheduleId,
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("schedule result notification wiring", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
const db = getDb();
|
|
97
|
+
db.run("DELETE FROM cron_runs");
|
|
98
|
+
db.run("DELETE FROM cron_jobs");
|
|
99
|
+
db.run("DELETE FROM messages");
|
|
100
|
+
db.run("DELETE FROM conversations");
|
|
101
|
+
producerCalls.length = 0;
|
|
102
|
+
runBackgroundJobShouldFail = false;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a successful execute-mode run reaches the producer", async () => {
|
|
106
|
+
const schedule = await createSchedule({
|
|
107
|
+
name: "Morning briefing",
|
|
108
|
+
cronExpression: "0 9 * * *",
|
|
109
|
+
message: "Summarize my inbox",
|
|
110
|
+
syntax: "cron",
|
|
111
|
+
expression: "0 9 * * *",
|
|
112
|
+
});
|
|
113
|
+
forceScheduleDue(schedule.id);
|
|
114
|
+
|
|
115
|
+
await runDueSchedulesOnce();
|
|
116
|
+
|
|
117
|
+
expect(producerCalls).toHaveLength(1);
|
|
118
|
+
expect(producerCalls[0].scheduleId).toBe(schedule.id);
|
|
119
|
+
expect(producerCalls[0].scheduleName).toBe("Morning briefing");
|
|
120
|
+
expect(producerCalls[0].conversationId).toBeTruthy();
|
|
121
|
+
|
|
122
|
+
// The producer must be handed the run the scheduler actually recorded, so
|
|
123
|
+
// its dedupe key names this firing and not some other.
|
|
124
|
+
const runs = getScheduleRuns(schedule.id);
|
|
125
|
+
expect(producerCalls[0].runId).toBe(runs[0].id);
|
|
126
|
+
expect(runs[0].conversationId).toBe(producerCalls[0].conversationId);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("captures runStartedAt before the run, not after", async () => {
|
|
130
|
+
const before = Date.now();
|
|
131
|
+
const schedule = await createSchedule({
|
|
132
|
+
name: "Morning briefing",
|
|
133
|
+
cronExpression: "0 9 * * *",
|
|
134
|
+
message: "Summarize my inbox",
|
|
135
|
+
syntax: "cron",
|
|
136
|
+
expression: "0 9 * * *",
|
|
137
|
+
});
|
|
138
|
+
forceScheduleDue(schedule.id);
|
|
139
|
+
|
|
140
|
+
await runDueSchedulesOnce();
|
|
141
|
+
|
|
142
|
+
// Stamped at or after the moment this test began and at or before the
|
|
143
|
+
// run's own reply. A timestamp taken after the run would sit past any
|
|
144
|
+
// notification the run emitted, so the probe would miss it and every
|
|
145
|
+
// well-authored schedule would notify twice.
|
|
146
|
+
const runs = getScheduleRuns(schedule.id);
|
|
147
|
+
expect(producerCalls[0].runStartedAt).toBeGreaterThanOrEqual(before);
|
|
148
|
+
expect(producerCalls[0].runStartedAt).toBeLessThanOrEqual(
|
|
149
|
+
runs[0].startedAt,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a failed run does not reach the producer", async () => {
|
|
154
|
+
// A failure has its own alerting path (the retry policy's exhaustion
|
|
155
|
+
// alert). Notifying here would pair every failure with a second message
|
|
156
|
+
// carrying whatever half-finished text the run left behind.
|
|
157
|
+
runBackgroundJobShouldFail = true;
|
|
158
|
+
const schedule = await createSchedule({
|
|
159
|
+
name: "Morning briefing",
|
|
160
|
+
cronExpression: "0 9 * * *",
|
|
161
|
+
message: "Summarize my inbox",
|
|
162
|
+
syntax: "cron",
|
|
163
|
+
expression: "0 9 * * *",
|
|
164
|
+
});
|
|
165
|
+
forceScheduleDue(schedule.id);
|
|
166
|
+
|
|
167
|
+
await runDueSchedulesOnce();
|
|
168
|
+
|
|
169
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("error");
|
|
170
|
+
expect(producerCalls).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("notify-mode schedules are left alone", async () => {
|
|
174
|
+
// Notify mode already is a notification; the fallback would duplicate it.
|
|
175
|
+
const schedule = await createSchedule({
|
|
176
|
+
name: "Drink water",
|
|
177
|
+
cronExpression: "0 9 * * *",
|
|
178
|
+
message: "Time to drink water",
|
|
179
|
+
syntax: "cron",
|
|
180
|
+
expression: "0 9 * * *",
|
|
181
|
+
mode: "notify",
|
|
182
|
+
});
|
|
183
|
+
forceScheduleDue(schedule.id);
|
|
184
|
+
|
|
185
|
+
await runDueSchedulesOnce();
|
|
186
|
+
|
|
187
|
+
expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
|
|
188
|
+
expect(producerCalls).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("script-mode schedules are left alone", async () => {
|
|
192
|
+
// No agent turn runs, so there is no reply to carry and nothing the
|
|
193
|
+
// producer could say.
|
|
194
|
+
const schedule = await createSchedule({
|
|
195
|
+
name: "Rotate logs",
|
|
196
|
+
cronExpression: "0 9 * * *",
|
|
197
|
+
message: "unused",
|
|
198
|
+
syntax: "cron",
|
|
199
|
+
expression: "0 9 * * *",
|
|
200
|
+
mode: "script",
|
|
201
|
+
script: "true",
|
|
202
|
+
});
|
|
203
|
+
forceScheduleDue(schedule.id);
|
|
204
|
+
|
|
205
|
+
await runDueSchedulesOnce();
|
|
206
|
+
|
|
207
|
+
expect(producerCalls).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
extractTarToDir,
|
|
8
|
+
SKILL_DEPENDENCY_INSTALL_ARGS,
|
|
9
|
+
SkillArchiveError,
|
|
8
10
|
writeSkillFilesToDir,
|
|
9
11
|
} from "../skills/catalog-install.js";
|
|
10
12
|
import { makeTar } from "./helpers/tar-fixtures.js";
|
|
@@ -81,6 +83,30 @@ describe("extractTarToDir", () => {
|
|
|
81
83
|
expect(foundSkillMd).toBe(true);
|
|
82
84
|
expect(readFileSync(join(tempDir, "SKILL.md"), "utf-8")).toBe("# demo\n");
|
|
83
85
|
});
|
|
86
|
+
|
|
87
|
+
test("rejects a negative octal size field instead of spinning", () => {
|
|
88
|
+
const header = Buffer.alloc(512, 0);
|
|
89
|
+
Buffer.from("SKILL.md\0", "ascii").copy(header, 0);
|
|
90
|
+
header[156] = "0".charCodeAt(0);
|
|
91
|
+
// Signed octal in the size field parses to -512 with parseInt(..., 8),
|
|
92
|
+
// which would cancel the 512-byte header advance and loop forever.
|
|
93
|
+
Buffer.from("-0000001000\0", "ascii").copy(header, 124);
|
|
94
|
+
const tar = Buffer.concat([header, Buffer.alloc(1024, 0)]);
|
|
95
|
+
|
|
96
|
+
expect(() => extractTarToDir(tar, tempDir)).toThrow(SkillArchiveError);
|
|
97
|
+
expect(() => extractTarToDir(tar, tempDir)).toThrow(
|
|
98
|
+
"malformed tar size field",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("dependency install argv suppresses lifecycle scripts", () => {
|
|
103
|
+
expect(SKILL_DEPENDENCY_INSTALL_ARGS).toEqual([
|
|
104
|
+
"install",
|
|
105
|
+
"--omit=dev",
|
|
106
|
+
"--ignore-scripts",
|
|
107
|
+
"--no-save",
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
84
110
|
});
|
|
85
111
|
|
|
86
112
|
describe("writeSkillFilesToDir", () => {
|
|
@@ -13,7 +13,9 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
13
13
|
|
|
14
14
|
// Emits `error` on the returned child so the dependency install's spawn
|
|
15
15
|
// promise rejects with the injected failure.
|
|
16
|
-
const
|
|
16
|
+
const spawnArgv: string[][] = [];
|
|
17
|
+
const mockSpawn = mock((_cmd: string, args: string[] = []) => {
|
|
18
|
+
spawnArgv.push(args);
|
|
17
19
|
const child = {
|
|
18
20
|
on(event: string, cb: (arg: unknown) => void) {
|
|
19
21
|
if (event === "error") {
|
|
@@ -30,7 +32,10 @@ mock.module("node:child_process", () => ({
|
|
|
30
32
|
}));
|
|
31
33
|
|
|
32
34
|
import { loadSkillCatalog } from "../config/skills.js";
|
|
33
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
installSkillLocally,
|
|
37
|
+
SKILL_DEPENDENCY_INSTALL_ARGS,
|
|
38
|
+
} from "../skills/catalog-install.js";
|
|
34
39
|
import { installExternalSkill } from "../skills/skillssh-registry.js";
|
|
35
40
|
import { makeTar } from "./helpers/tar-fixtures.js";
|
|
36
41
|
|
|
@@ -61,6 +66,7 @@ beforeEach(() => {
|
|
|
61
66
|
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
62
67
|
mkdirSync(join(workspaceDir, "skills"), { recursive: true });
|
|
63
68
|
mockSpawn.mockClear();
|
|
69
|
+
spawnArgv.length = 0;
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
afterEach(() => {
|
|
@@ -103,6 +109,9 @@ describe("staged skill installs", () => {
|
|
|
103
109
|
),
|
|
104
110
|
).rejects.toThrow("dependency install failed");
|
|
105
111
|
|
|
112
|
+
expect(mockSpawn).toHaveBeenCalled();
|
|
113
|
+
expect(spawnArgv[0]).toEqual([...SKILL_DEPENDENCY_INSTALL_ARGS]);
|
|
114
|
+
|
|
106
115
|
expect(
|
|
107
116
|
existsSync(join(workspaceDir, "skills", "demo-skill", "SKILL.md")),
|
|
108
117
|
).toBe(false);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PERSONALITY_AXIS_IDS,
|
|
5
|
+
PERSONALITY_DIRECTION_AXES,
|
|
6
|
+
PERSONALITY_SLIDER_DEFAULT,
|
|
7
|
+
PERSONALITY_SLIDERS_PATH,
|
|
8
|
+
} from "./personality-sliders.js";
|
|
9
|
+
|
|
10
|
+
describe("personality slider contract", () => {
|
|
11
|
+
test("sidecar path and default stay centered", () => {
|
|
12
|
+
expect(PERSONALITY_SLIDERS_PATH).toBe("data/personality-sliders.json");
|
|
13
|
+
expect(PERSONALITY_SLIDER_DEFAULT).toBe(50);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("every slider axis has a left and right direction pole", () => {
|
|
17
|
+
expect(PERSONALITY_DIRECTION_AXES.map((axis) => axis.sliderId)).toEqual(
|
|
18
|
+
Object.values(PERSONALITY_AXIS_IDS),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|