@stigmer/runner 3.12.4 → 3.12.6
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/dist/.build-fingerprint +1 -1
- package/dist/activities/call-agent.js +19 -6
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
- package/dist/activities/execute-cursor/error-classifier.d.ts +9 -0
- package/dist/activities/execute-cursor/error-classifier.js +30 -1
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +10 -1
- package/dist/activities/execute-cursor/index.js +62 -36
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +38 -29
- package/dist/activities/execute-cursor/service-tier.js +92 -63
- package/dist/activities/execute-cursor/service-tier.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
- package/dist/activities/execute-cursor/skill-resolver.js +5 -108
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +16 -2
- package/dist/activities/execute-cursor/usage-accumulator.js +12 -2
- package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +57 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +17 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/path-normalization.d.ts +49 -49
- package/dist/middleware/path-normalization.js +73 -84
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/middleware/types.d.ts +3 -3
- package/dist/runner-manager.d.ts +1 -1
- package/dist/runner-manager.js +22 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/args-preview.d.ts +8 -0
- package/dist/shared/args-preview.js +14 -3
- package/dist/shared/args-preview.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -1
- package/dist/shared/artifact-storage.js +10 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +10 -7
- package/dist/shared/caller-identity.js +10 -7
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/declared-preferences.d.ts +47 -0
- package/dist/shared/declared-preferences.js +64 -0
- package/dist/shared/declared-preferences.js.map +1 -0
- package/dist/shared/mcp-enabled-tools.d.ts +2 -3
- package/dist/shared/mcp-enabled-tools.js +2 -3
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +20 -15
- package/dist/shared/mcp-resolver.js +11 -12
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/placeholder-resolver.d.ts +9 -2
- package/dist/shared/placeholder-resolver.js +9 -2
- package/dist/shared/placeholder-resolver.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -46
- package/dist/shared/plan-mode-permissions.js +27 -55
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/skill-mount.d.ts +89 -0
- package/dist/shared/skill-mount.js +142 -0
- package/dist/shared/skill-mount.js.map +1 -0
- package/dist/shared/skill-writer.d.ts +28 -26
- package/dist/shared/skill-writer.js +79 -102
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/thinking-mode.d.ts +35 -0
- package/dist/shared/thinking-mode.js +43 -0
- package/dist/shared/thinking-mode.js.map +1 -0
- package/dist/shared/worker-shutdown.d.ts +67 -0
- package/dist/shared/worker-shutdown.js +79 -0
- package/dist/shared/worker-shutdown.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +3 -2
- package/dist/workflow-engine/loader.js +82 -6
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +2 -1
- package/dist/workflow-engine/tasks/human-input.js +31 -3
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +17 -3
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +4 -0
- package/dist/workflows/human-input-orchestrator.js +13 -0
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +15 -11
- package/src/activities/call-agent.ts +20 -6
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
- package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
- package/src/activities/execute-cursor/__tests__/error-classifier-billing.test.ts +67 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +67 -7
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +33 -1
- package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
- package/src/activities/execute-cursor/error-classifier.ts +42 -1
- package/src/activities/execute-cursor/index.ts +78 -41
- package/src/activities/execute-cursor/prompt-builder.ts +29 -2
- package/src/activities/execute-cursor/service-tier.ts +94 -63
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
- package/src/activities/execute-cursor/skill-resolver.ts +10 -137
- package/src/activities/execute-cursor/usage-accumulator.ts +11 -1
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
- package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
- package/src/activities/execute-deep-agent/index.ts +57 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
- package/src/activities/execute-deep-agent/setup.ts +21 -19
- package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
- package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
- package/src/index.ts +1 -1
- package/src/middleware/__tests__/path-normalization.test.ts +32 -32
- package/src/middleware/path-normalization.ts +78 -90
- package/src/middleware/types.ts +3 -3
- package/src/runner-manager.ts +26 -16
- package/src/runner.ts +14 -0
- package/src/shared/__tests__/artifact-storage.test.ts +28 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
- package/src/shared/__tests__/declared-preferences.test.ts +109 -0
- package/src/shared/__tests__/foundry-seam.test.ts +5 -5
- package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
- package/src/shared/__tests__/skill-mount.test.ts +238 -0
- package/src/shared/__tests__/skill-writer.test.ts +151 -128
- package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
- package/src/shared/__tests__/vertex-seam.test.ts +5 -5
- package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
- package/src/shared/args-preview.ts +17 -3
- package/src/shared/artifact-storage.ts +20 -1
- package/src/shared/caller-identity.ts +10 -7
- package/src/shared/declared-preferences.ts +84 -0
- package/src/shared/mcp-enabled-tools.ts +2 -3
- package/src/shared/mcp-resolver.ts +20 -20
- package/src/shared/placeholder-resolver.ts +9 -2
- package/src/shared/plan-mode-permissions.ts +27 -58
- package/src/shared/skill-mount.ts +179 -0
- package/src/shared/skill-writer.ts +96 -130
- package/src/shared/thinking-mode.ts +52 -0
- package/src/shared/worker-shutdown.ts +99 -0
- package/src/shared/workspace/types.ts +3 -2
- package/src/workflow-engine/__tests__/loader.test.ts +95 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +156 -0
- package/src/workflow-engine/loader.ts +97 -6
- package/src/workflow-engine/tasks/human-input.ts +41 -6
- package/src/workflow-engine/types.ts +18 -3
- package/src/workflows/human-input-orchestrator.ts +20 -2
- package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
- package/dist/activities/execute-cursor/connect-backfill.js +0 -27
- package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
- package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
- package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
- package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
- package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
- package/src/activities/execute-cursor/connect-backfill.ts +0 -52
- package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
- package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, existsSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
6
|
+
import {
|
|
7
|
+
MOUNT_MARKER_FILE,
|
|
8
|
+
mountIsFresh,
|
|
9
|
+
downloadArtifact,
|
|
10
|
+
writeSkillMount,
|
|
11
|
+
} from "../skill-mount.js";
|
|
12
|
+
import { buildZip } from "../../__test-utils__/zip-fixtures.js";
|
|
13
|
+
|
|
14
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function makeSkillProto(overrides: {
|
|
17
|
+
name?: string;
|
|
18
|
+
skillMd?: string;
|
|
19
|
+
artifactStorageKey?: string;
|
|
20
|
+
versionHash?: string;
|
|
21
|
+
} = {}) {
|
|
22
|
+
return {
|
|
23
|
+
metadata: {
|
|
24
|
+
id: `skill-${overrides.name ?? "test"}`,
|
|
25
|
+
slug: overrides.name ?? "test-skill",
|
|
26
|
+
org: "test-org",
|
|
27
|
+
},
|
|
28
|
+
spec: {
|
|
29
|
+
name: overrides.name ?? "test-skill",
|
|
30
|
+
description: "A test skill",
|
|
31
|
+
skillMd: overrides.skillMd ?? "# Test Skill",
|
|
32
|
+
},
|
|
33
|
+
status: {
|
|
34
|
+
artifactStorageKey: overrides.artifactStorageKey ?? "",
|
|
35
|
+
versionHash: overrides.versionHash ?? "abc",
|
|
36
|
+
},
|
|
37
|
+
} as any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── writeSkillMount + mountIsFresh — marker mechanics ───────────────────
|
|
41
|
+
|
|
42
|
+
describe("writeSkillMount — marker mechanics", () => {
|
|
43
|
+
let skillDir: string;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
skillDir = join(mkdtempSync(join(tmpdir(), "skill-mount-")), "test-skill");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
rmSync(join(skillDir, ".."), { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("stamps a marker recording the hash and a fully mounted artifact", async () => {
|
|
54
|
+
const artifact = buildZip([{ name: "references/guide.md", content: "guide" }]);
|
|
55
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, artifact);
|
|
56
|
+
|
|
57
|
+
const marker = JSON.parse(readFileSync(join(skillDir, MOUNT_MARKER_FILE), "utf-8"));
|
|
58
|
+
expect(marker).toEqual({ versionHash: "hash-v1", artifactMounted: true });
|
|
59
|
+
expect(await mountIsFresh(skillDir, "hash-v1", true)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("records a degraded SKILL.md-only mount so the next execution retries the artifact", async () => {
|
|
63
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, undefined);
|
|
64
|
+
|
|
65
|
+
const marker = JSON.parse(readFileSync(join(skillDir, MOUNT_MARKER_FILE), "utf-8"));
|
|
66
|
+
expect(marker).toEqual({ versionHash: "hash-v1", artifactMounted: false });
|
|
67
|
+
// Stale for a skill that carries an artifact (retry the download)...
|
|
68
|
+
expect(await mountIsFresh(skillDir, "hash-v1", true)).toBe(false);
|
|
69
|
+
// ...fresh for a skill that never had one.
|
|
70
|
+
expect(await mountIsFresh(skillDir, "hash-v1", false)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("stamps no marker when the skill has no version hash — such mounts never cache", async () => {
|
|
74
|
+
await writeSkillMount(makeSkillProto({ versionHash: "" }), skillDir, undefined);
|
|
75
|
+
|
|
76
|
+
expect(existsSync(join(skillDir, MOUNT_MARKER_FILE))).toBe(false);
|
|
77
|
+
expect(await mountIsFresh(skillDir, "", false)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is stale for a different hash", async () => {
|
|
81
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, undefined);
|
|
82
|
+
expect(await mountIsFresh(skillDir, "hash-v2", false)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("is stale when the marker is missing or unparseable", async () => {
|
|
86
|
+
expect(await mountIsFresh(skillDir, "hash-v1", false)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("removes files from a previous mount that the new version no longer carries", async () => {
|
|
90
|
+
const v1 = buildZip([{ name: "references/removed-in-v2.md", content: "old" }]);
|
|
91
|
+
const v2 = buildZip([{ name: "references/new-in-v2.md", content: "new" }]);
|
|
92
|
+
|
|
93
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, v1);
|
|
94
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v2" }), skillDir, v2);
|
|
95
|
+
|
|
96
|
+
expect(existsSync(join(skillDir, "references", "removed-in-v2.md"))).toBe(false);
|
|
97
|
+
expect(readFileSync(join(skillDir, "references", "new-in-v2.md"), "utf-8")).toBe("new");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("owns SKILL.md and the marker name unconditionally — zip copies are ignored", async () => {
|
|
101
|
+
const artifact = buildZip([
|
|
102
|
+
{ name: "SKILL.md", content: "# Stale zip copy" },
|
|
103
|
+
{ name: MOUNT_MARKER_FILE, content: "{\"versionHash\":\"forged\"}" },
|
|
104
|
+
]);
|
|
105
|
+
await writeSkillMount(
|
|
106
|
+
makeSkillProto({ skillMd: "# Authoritative", versionHash: "hash-v1" }),
|
|
107
|
+
skillDir,
|
|
108
|
+
artifact,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(readFileSync(join(skillDir, "SKILL.md"), "utf-8")).toBe("# Authoritative");
|
|
112
|
+
const marker = JSON.parse(readFileSync(join(skillDir, MOUNT_MARKER_FILE), "utf-8"));
|
|
113
|
+
expect(marker.versionHash).toBe("hash-v1");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects zip entries that escape the mount directory", async () => {
|
|
117
|
+
const artifact = buildZip([{ name: "../escape.txt", content: "evil" }]);
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, artifact),
|
|
121
|
+
).rejects.toThrow(/escapes its mount directory/);
|
|
122
|
+
expect(existsSync(join(skillDir, "..", "escape.txt"))).toBe(false);
|
|
123
|
+
// The interrupted mount must not have been stamped as complete.
|
|
124
|
+
expect(existsSync(join(skillDir, MOUNT_MARKER_FILE))).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("writes script entries with the executable bit set", async () => {
|
|
128
|
+
const artifact = buildZip([
|
|
129
|
+
{ name: "scripts/run.py", content: "print('hi')" },
|
|
130
|
+
{ name: "references/notes.md", content: "notes" },
|
|
131
|
+
]);
|
|
132
|
+
await writeSkillMount(makeSkillProto({ versionHash: "hash-v1" }), skillDir, artifact);
|
|
133
|
+
|
|
134
|
+
expect(statSync(join(skillDir, "scripts", "run.py")).mode & 0o111).not.toBe(0);
|
|
135
|
+
expect(statSync(join(skillDir, "references", "notes.md")).mode & 0o111).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── downloadArtifact — transfer lane routing (#675) ─────────────────────
|
|
140
|
+
|
|
141
|
+
/** A server that predates the transfer lane (#675) answers the mint RPC
|
|
142
|
+
* with UNIMPLEMENTED — the default posture for these tests, which keeps
|
|
143
|
+
* every pre-existing case pinned to the unary getSkillArtifact fallback. */
|
|
144
|
+
function unimplementedMint() {
|
|
145
|
+
return vi.fn().mockRejectedValue(new ConnectError("unimplemented", Code.Unimplemented));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
describe("downloadArtifact — transfer lane routing", () => {
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
vi.unstubAllGlobals();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function makeClient(overrides: Record<string, any> = {}) {
|
|
154
|
+
return {
|
|
155
|
+
getSkillArtifact: vi.fn().mockResolvedValue({ artifact: new Uint8Array(0) }),
|
|
156
|
+
getSkillArtifactDownloadUrl: unimplementedMint(),
|
|
157
|
+
...overrides,
|
|
158
|
+
} as any;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
it("fetches bytes over HTTP when the server mints a download URL", async () => {
|
|
162
|
+
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
|
|
163
|
+
const client = makeClient({
|
|
164
|
+
getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
|
|
165
|
+
url: "http://localhost:7234/v1/skill-artifacts/skills/abc.zip",
|
|
166
|
+
sizeBytes: 5n,
|
|
167
|
+
ttlSeconds: 0,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
171
|
+
ok: true,
|
|
172
|
+
arrayBuffer: async () => bytes.buffer,
|
|
173
|
+
});
|
|
174
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
175
|
+
|
|
176
|
+
const got = await downloadArtifact(client, "skills/abc.zip");
|
|
177
|
+
|
|
178
|
+
expect(got).toEqual(bytes);
|
|
179
|
+
expect(fetchMock).toHaveBeenCalledWith("http://localhost:7234/v1/skill-artifacts/skills/abc.zip");
|
|
180
|
+
// The unary lane (10MB-capped) must not be touched when the URL lane works.
|
|
181
|
+
expect(client.getSkillArtifact).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("falls back to the unary RPC when the server predates the lane", async () => {
|
|
185
|
+
const bytes = new Uint8Array([9, 9]);
|
|
186
|
+
const client = makeClient({
|
|
187
|
+
getSkillArtifact: vi.fn().mockResolvedValue({ artifact: bytes }),
|
|
188
|
+
});
|
|
189
|
+
vi.stubGlobal("fetch", vi.fn()); // must never be called
|
|
190
|
+
|
|
191
|
+
const got = await downloadArtifact(client, "skills/abc.zip");
|
|
192
|
+
|
|
193
|
+
expect(got).toEqual(bytes);
|
|
194
|
+
expect(client.getSkillArtifact).toHaveBeenCalledWith("skills/abc.zip");
|
|
195
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does NOT fall back on non-Unimplemented mint failures", async () => {
|
|
199
|
+
const client = makeClient({
|
|
200
|
+
getSkillArtifactDownloadUrl: vi.fn().mockRejectedValue(
|
|
201
|
+
new ConnectError("boom", Code.Internal),
|
|
202
|
+
),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await expect(downloadArtifact(client, "skills/abc.zip")).rejects.toThrow("boom");
|
|
206
|
+
// Falling back here would mask real server faults behind the capped lane.
|
|
207
|
+
expect(client.getSkillArtifact).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("rejects truncated fetches via the minted size", async () => {
|
|
211
|
+
const client = makeClient({
|
|
212
|
+
getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
|
|
213
|
+
url: "http://localhost:7234/v1/skill-artifacts/skills/abc.zip",
|
|
214
|
+
sizeBytes: 100n,
|
|
215
|
+
ttlSeconds: 0,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
219
|
+
ok: true,
|
|
220
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
await expect(downloadArtifact(client, "skills/abc.zip")).rejects.toThrow(/truncated/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("surfaces HTTP failures with the status code", async () => {
|
|
227
|
+
const client = makeClient({
|
|
228
|
+
getSkillArtifactDownloadUrl: vi.fn().mockResolvedValue({
|
|
229
|
+
url: "http://localhost:7234/v1/skill-artifacts/skills/gone.zip",
|
|
230
|
+
sizeBytes: 0n,
|
|
231
|
+
ttlSeconds: 0,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404 }));
|
|
235
|
+
|
|
236
|
+
await expect(downloadArtifact(client, "skills/gone.zip")).rejects.toThrow(/HTTP 404/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
2
6
|
import {
|
|
3
7
|
mergeSkillRefs,
|
|
4
8
|
fetchSkillsByRefs,
|
|
5
|
-
|
|
6
|
-
computeSkillPaths,
|
|
7
|
-
checkSkillIntegrity,
|
|
9
|
+
mountSkills,
|
|
8
10
|
generatePromptSection,
|
|
9
11
|
generateAlsoAvailableSection,
|
|
10
|
-
fetchSkillArtifacts,
|
|
11
12
|
} from "../skill-writer.js";
|
|
12
13
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
13
14
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
14
|
-
import
|
|
15
|
+
import { buildZip } from "../../__test-utils__/zip-fixtures.js";
|
|
15
16
|
|
|
16
17
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
17
18
|
|
|
@@ -19,16 +20,18 @@ function makeSkill(overrides: {
|
|
|
19
20
|
id?: string;
|
|
20
21
|
name?: string;
|
|
21
22
|
slug?: string;
|
|
23
|
+
org?: string;
|
|
22
24
|
description?: string;
|
|
23
25
|
skillMd?: string;
|
|
24
26
|
artifactStorageKey?: string;
|
|
27
|
+
versionHash?: string;
|
|
25
28
|
} = {}): Skill {
|
|
26
29
|
return {
|
|
27
30
|
metadata: {
|
|
28
31
|
id: overrides.id ?? "skill-id-1",
|
|
29
32
|
name: overrides.name ?? overrides.slug ?? "test-skill",
|
|
30
33
|
slug: overrides.slug ?? "test-skill",
|
|
31
|
-
org: "test-org",
|
|
34
|
+
org: overrides.org ?? "test-org",
|
|
32
35
|
},
|
|
33
36
|
spec: {
|
|
34
37
|
name: overrides.name ?? "test-skill",
|
|
@@ -38,7 +41,7 @@ function makeSkill(overrides: {
|
|
|
38
41
|
},
|
|
39
42
|
status: {
|
|
40
43
|
artifactStorageKey: overrides.artifactStorageKey ?? "",
|
|
41
|
-
versionHash: "abc123",
|
|
44
|
+
versionHash: overrides.versionHash ?? "abc123",
|
|
42
45
|
state: "active",
|
|
43
46
|
},
|
|
44
47
|
} as any;
|
|
@@ -48,48 +51,21 @@ function makeRef(slug: string, org = "test-org"): ApiResourceReference {
|
|
|
48
51
|
return { slug, org, kind: 43 } as any;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** A server that predates the transfer lane (#675) answers the mint RPC
|
|
55
|
+
* with UNIMPLEMENTED, pinning these tests to the unary fallback. */
|
|
51
56
|
function makeMockClient(overrides: Record<string, unknown> = {}) {
|
|
52
57
|
return {
|
|
53
58
|
getSkillByReference: vi.fn().mockImplementation((ref: any) =>
|
|
54
59
|
Promise.resolve(makeSkill({ slug: ref.slug, name: ref.slug })),
|
|
55
60
|
),
|
|
56
61
|
getSkillArtifact: vi.fn().mockResolvedValue({ artifact: new Uint8Array(0) }),
|
|
62
|
+
getSkillArtifactDownloadUrl: vi.fn().mockRejectedValue(
|
|
63
|
+
new ConnectError("unimplemented", Code.Unimplemented),
|
|
64
|
+
),
|
|
57
65
|
...overrides,
|
|
58
66
|
} as any;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
function makeMockBackend(): WorkspaceBackend & {
|
|
62
|
-
writtenFiles: Map<string, string>;
|
|
63
|
-
executedCommands: string[];
|
|
64
|
-
existingFiles: Set<string>;
|
|
65
|
-
} {
|
|
66
|
-
const writtenFiles = new Map<string, string>();
|
|
67
|
-
const executedCommands: string[] = [];
|
|
68
|
-
const existingFiles = new Set<string>();
|
|
69
|
-
return {
|
|
70
|
-
rootDir: "/workspace",
|
|
71
|
-
writtenFiles,
|
|
72
|
-
executedCommands,
|
|
73
|
-
existingFiles,
|
|
74
|
-
async writeFile(path: string, content: string) {
|
|
75
|
-
writtenFiles.set(path, content);
|
|
76
|
-
},
|
|
77
|
-
async writeFileBuffer(path: string, content: Buffer) {
|
|
78
|
-
writtenFiles.set(path, content.toString("utf-8"));
|
|
79
|
-
},
|
|
80
|
-
async readFile(path: string) {
|
|
81
|
-
return writtenFiles.get(path) ?? "";
|
|
82
|
-
},
|
|
83
|
-
async exists(path: string) {
|
|
84
|
-
return writtenFiles.has(path) || existingFiles.has(path);
|
|
85
|
-
},
|
|
86
|
-
async execute(command: string) {
|
|
87
|
-
executedCommands.push(command);
|
|
88
|
-
return "";
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
69
|
// ─── mergeSkillRefs ──────────────────────────────────────────────────────
|
|
94
70
|
|
|
95
71
|
describe("mergeSkillRefs", () => {
|
|
@@ -151,86 +127,171 @@ describe("fetchSkillsByRefs", () => {
|
|
|
151
127
|
});
|
|
152
128
|
});
|
|
153
129
|
|
|
154
|
-
// ───
|
|
130
|
+
// ─── mountSkills ─────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe("mountSkills", () => {
|
|
133
|
+
let platformDir: string;
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
platformDir = mkdtempSync(join(tmpdir(), "skill-writer-platform-"));
|
|
137
|
+
});
|
|
155
138
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
rmSync(platformDir, { recursive: true, force: true });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("writes SKILL.md and returns agent-visible paths keyed by skill id", async () => {
|
|
144
|
+
const client = makeMockClient();
|
|
159
145
|
const skills = [makeSkill({ name: "calculator", skillMd: "# Calculator\n\nAdds numbers." })];
|
|
160
|
-
const result = await
|
|
146
|
+
const result = await mountSkills(client, skills, platformDir);
|
|
161
147
|
|
|
162
148
|
expect(result.paths.get("skill-id-1")).toBe(".stigmer/skills/calculator");
|
|
163
|
-
expect(
|
|
164
|
-
expect(backend.writtenFiles.get(".stigmer/skills/calculator/SKILL.md"))
|
|
149
|
+
expect(readFileSync(join(platformDir, "skills", "calculator", "SKILL.md"), "utf-8"))
|
|
165
150
|
.toBe("# Calculator\n\nAdds numbers.");
|
|
166
151
|
});
|
|
167
152
|
|
|
168
|
-
it("
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
153
|
+
it("extracts artifact files alongside SKILL.md", async () => {
|
|
154
|
+
const artifact = buildZip([
|
|
155
|
+
{ name: "SKILL.md", content: "# Stale zip copy" },
|
|
156
|
+
{ name: "references/schema.md", content: "schema" },
|
|
157
|
+
]);
|
|
158
|
+
const client = makeMockClient({
|
|
159
|
+
getSkillArtifact: vi.fn().mockResolvedValue({ artifact }),
|
|
160
|
+
});
|
|
161
|
+
const skills = [makeSkill({
|
|
162
|
+
name: "db-skill",
|
|
163
|
+
skillMd: "# Authoritative",
|
|
164
|
+
artifactStorageKey: "artifacts/db.zip",
|
|
165
|
+
})];
|
|
166
|
+
await mountSkills(client, skills, platformDir);
|
|
167
|
+
|
|
168
|
+
const skillDir = join(platformDir, "skills", "db-skill");
|
|
169
|
+
expect(readFileSync(join(skillDir, "SKILL.md"), "utf-8")).toBe("# Authoritative");
|
|
170
|
+
expect(readFileSync(join(skillDir, "references", "schema.md"), "utf-8")).toBe("schema");
|
|
174
171
|
});
|
|
175
172
|
|
|
176
173
|
it("handles multiple skills", async () => {
|
|
177
|
-
const
|
|
174
|
+
const client = makeMockClient();
|
|
178
175
|
const skills = [
|
|
179
176
|
makeSkill({ id: "id-1", name: "skill-a", skillMd: "# A" }),
|
|
180
177
|
makeSkill({ id: "id-2", name: "skill-b", skillMd: "# B" }),
|
|
181
178
|
];
|
|
182
|
-
const result = await
|
|
179
|
+
const result = await mountSkills(client, skills, platformDir);
|
|
183
180
|
expect(result.paths.size).toBe(2);
|
|
184
181
|
expect(result.paths.get("id-1")).toBe(".stigmer/skills/skill-a");
|
|
185
182
|
expect(result.paths.get("id-2")).toBe(".stigmer/skills/skill-b");
|
|
183
|
+
expect(existsSync(join(platformDir, "skills", "skill-a", "SKILL.md"))).toBe(true);
|
|
184
|
+
expect(existsSync(join(platformDir, "skills", "skill-b", "SKILL.md"))).toBe(true);
|
|
186
185
|
});
|
|
187
186
|
|
|
188
|
-
it("skips
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
it("skips the artifact download when the mounted hash matches (cache hit)", async () => {
|
|
188
|
+
const artifact = buildZip([{ name: "references/guide.md", content: "guide" }]);
|
|
189
|
+
const client = makeMockClient({
|
|
190
|
+
getSkillArtifact: vi.fn().mockResolvedValue({ artifact }),
|
|
191
|
+
});
|
|
192
|
+
const skill = makeSkill({
|
|
193
|
+
name: "cached-skill",
|
|
194
|
+
artifactStorageKey: "artifacts/cached.zip",
|
|
195
|
+
versionHash: "hash-v1",
|
|
196
|
+
});
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
await mountSkills(client, [skill], platformDir);
|
|
199
|
+
expect(client.getSkillArtifact).toHaveBeenCalledTimes(1);
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
expect(paths.get("id-1")).toBe(".stigmer/skills/calculator");
|
|
207
|
-
expect(paths.get("id-2")).toBe(".stigmer/skills/web-scraper");
|
|
201
|
+
// Second execution of the same session: same hash, no transfer.
|
|
202
|
+
const second = await mountSkills(client, [skill], platformDir);
|
|
203
|
+
expect(client.getSkillArtifact).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(second.paths.get("skill-id-1")).toBe(".stigmer/skills/cached-skill");
|
|
205
|
+
expect(readFileSync(
|
|
206
|
+
join(platformDir, "skills", "cached-skill", "references", "guide.md"), "utf-8",
|
|
207
|
+
)).toBe("guide");
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
it("
|
|
211
|
-
|
|
210
|
+
it("remounts on version change and clears stale files from the old mount", async () => {
|
|
211
|
+
const artifactV1 = buildZip([{ name: "references/removed-in-v2.md", content: "old" }]);
|
|
212
|
+
const artifactV2 = buildZip([{ name: "references/new-in-v2.md", content: "new" }]);
|
|
213
|
+
const client = makeMockClient({
|
|
214
|
+
getSkillArtifact: vi.fn()
|
|
215
|
+
.mockResolvedValueOnce({ artifact: artifactV1 })
|
|
216
|
+
.mockResolvedValueOnce({ artifact: artifactV2 }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await mountSkills(client, [makeSkill({
|
|
220
|
+
name: "evolving-skill",
|
|
221
|
+
skillMd: "# V1",
|
|
222
|
+
artifactStorageKey: "artifacts/v1.zip",
|
|
223
|
+
versionHash: "hash-v1",
|
|
224
|
+
})], platformDir);
|
|
225
|
+
|
|
226
|
+
await mountSkills(client, [makeSkill({
|
|
227
|
+
name: "evolving-skill",
|
|
228
|
+
skillMd: "# V2",
|
|
229
|
+
artifactStorageKey: "artifacts/v2.zip",
|
|
230
|
+
versionHash: "hash-v2",
|
|
231
|
+
})], platformDir);
|
|
232
|
+
|
|
233
|
+
const skillDir = join(platformDir, "skills", "evolving-skill");
|
|
234
|
+
expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
|
|
235
|
+
expect(readFileSync(join(skillDir, "SKILL.md"), "utf-8")).toBe("# V2");
|
|
236
|
+
expect(readFileSync(join(skillDir, "references", "new-in-v2.md"), "utf-8")).toBe("new");
|
|
237
|
+
// The v1-only file must not linger in the v2 mount (the stale-file leak).
|
|
238
|
+
expect(existsSync(join(skillDir, "references", "removed-in-v2.md"))).toBe(false);
|
|
212
239
|
});
|
|
213
|
-
});
|
|
214
240
|
|
|
215
|
-
|
|
241
|
+
it("does not cache a degraded SKILL.md-only mount — the next execution retries", async () => {
|
|
242
|
+
const artifact = buildZip([{ name: "references/late.md", content: "finally" }]);
|
|
243
|
+
const client = makeMockClient({
|
|
244
|
+
getSkillArtifact: vi.fn()
|
|
245
|
+
.mockRejectedValueOnce(new Error("Network timeout"))
|
|
246
|
+
.mockResolvedValueOnce({ artifact }),
|
|
247
|
+
});
|
|
248
|
+
const skill = makeSkill({
|
|
249
|
+
name: "retry-skill",
|
|
250
|
+
artifactStorageKey: "artifacts/retry.zip",
|
|
251
|
+
versionHash: "hash-v1",
|
|
252
|
+
});
|
|
216
253
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
expect(
|
|
254
|
+
// First pass degrades to SKILL.md only — never throws.
|
|
255
|
+
const first = await mountSkills(client, [skill], platformDir);
|
|
256
|
+
const skillDir = join(platformDir, "skills", "retry-skill");
|
|
257
|
+
expect(first.paths.get("skill-id-1")).toBe(".stigmer/skills/retry-skill");
|
|
258
|
+
expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
|
|
259
|
+
expect(existsSync(join(skillDir, "references"))).toBe(false);
|
|
260
|
+
|
|
261
|
+
// Second pass retries and completes the mount.
|
|
262
|
+
await mountSkills(client, [skill], platformDir);
|
|
263
|
+
expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
|
|
264
|
+
expect(readFileSync(join(skillDir, "references", "late.md"), "utf-8")).toBe("finally");
|
|
265
|
+
|
|
266
|
+
// Third pass is a cache hit.
|
|
267
|
+
await mountSkills(client, [skill], platformDir);
|
|
268
|
+
expect(client.getSkillArtifact).toHaveBeenCalledTimes(2);
|
|
223
269
|
});
|
|
224
270
|
|
|
225
|
-
it("
|
|
226
|
-
const
|
|
227
|
-
const skills = [
|
|
228
|
-
|
|
271
|
+
it("mounts only the first claimant when two skills resolve to the same directory", async () => {
|
|
272
|
+
const client = makeMockClient();
|
|
273
|
+
const skills = [
|
|
274
|
+
makeSkill({ id: "id-1", org: "org-a", slug: "shared-a", name: "shared-name", skillMd: "# First" }),
|
|
275
|
+
makeSkill({ id: "id-2", org: "org-b", slug: "shared-b", name: "shared-name", skillMd: "# Second" }),
|
|
276
|
+
];
|
|
277
|
+
const result = await mountSkills(client, skills, platformDir);
|
|
278
|
+
|
|
279
|
+
// Both ids resolve to the (single) directory; the first claimant's content wins.
|
|
280
|
+
expect(result.paths.get("id-1")).toBe(".stigmer/skills/shared-name");
|
|
281
|
+
expect(result.paths.get("id-2")).toBe(".stigmer/skills/shared-name");
|
|
282
|
+
expect(readFileSync(join(platformDir, "skills", "shared-name", "SKILL.md"), "utf-8"))
|
|
283
|
+
.toBe("# First");
|
|
229
284
|
});
|
|
230
285
|
|
|
231
|
-
it("
|
|
232
|
-
const
|
|
233
|
-
|
|
286
|
+
it("skips skills with no skillMd content", async () => {
|
|
287
|
+
const client = makeMockClient();
|
|
288
|
+
const skill = makeSkill({ name: "broken", skillMd: "" });
|
|
289
|
+
(skill as any).spec.skillMd = "";
|
|
290
|
+
const result = await mountSkills(client, [skill], platformDir);
|
|
291
|
+
|
|
292
|
+
// The path entry exists (naming is total) but nothing was mounted.
|
|
293
|
+
expect(result.paths.get("skill-id-1")).toBe(".stigmer/skills/broken");
|
|
294
|
+
expect(existsSync(join(platformDir, "skills", "broken"))).toBe(false);
|
|
234
295
|
});
|
|
235
296
|
});
|
|
236
297
|
|
|
@@ -309,41 +370,3 @@ describe("generateAlsoAvailableSection", () => {
|
|
|
309
370
|
expect(section).toContain("relevant to your task");
|
|
310
371
|
});
|
|
311
372
|
});
|
|
312
|
-
|
|
313
|
-
// ─── fetchSkillArtifacts ─────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
describe("fetchSkillArtifacts", () => {
|
|
316
|
-
it("returns empty map when no skills have artifacts", async () => {
|
|
317
|
-
const client = makeMockClient();
|
|
318
|
-
const skills = [makeSkill({ artifactStorageKey: "" })];
|
|
319
|
-
const result = await fetchSkillArtifacts(client, skills);
|
|
320
|
-
expect(result.size).toBe(0);
|
|
321
|
-
expect(client.getSkillArtifact).not.toHaveBeenCalled();
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("fetches artifacts for skills with storage keys", async () => {
|
|
325
|
-
const artifactData = new Uint8Array([80, 75, 3, 4]); // ZIP magic bytes
|
|
326
|
-
const client = makeMockClient({
|
|
327
|
-
getSkillArtifact: vi.fn().mockResolvedValue({ artifact: artifactData }),
|
|
328
|
-
});
|
|
329
|
-
const skills = [makeSkill({ id: "id-1", artifactStorageKey: "artifacts/key.zip" })];
|
|
330
|
-
const result = await fetchSkillArtifacts(client, skills);
|
|
331
|
-
expect(result.size).toBe(1);
|
|
332
|
-
expect(result.get("id-1")).toEqual(artifactData);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it("handles partial failures gracefully", async () => {
|
|
336
|
-
const client = makeMockClient({
|
|
337
|
-
getSkillArtifact: vi.fn()
|
|
338
|
-
.mockResolvedValueOnce({ artifact: new Uint8Array([1, 2, 3]) })
|
|
339
|
-
.mockRejectedValueOnce(new Error("Network error")),
|
|
340
|
-
});
|
|
341
|
-
const skills = [
|
|
342
|
-
makeSkill({ id: "id-1", name: "good", artifactStorageKey: "key1" }),
|
|
343
|
-
makeSkill({ id: "id-2", name: "bad", artifactStorageKey: "key2" }),
|
|
344
|
-
];
|
|
345
|
-
const result = await fetchSkillArtifacts(client, skills);
|
|
346
|
-
expect(result.size).toBe(1);
|
|
347
|
-
expect(result.has("id-1")).toBe(true);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
@@ -123,9 +123,9 @@ describe("buildChatModel vertex adapter", () => {
|
|
|
123
123
|
// default applies. That default prefix-matches the model string — and the
|
|
124
124
|
// vertex branch hands ChatAnthropic the TRANSLATED string. This pins the
|
|
125
125
|
// equality the zero-behavior-change criterion rests on (verified 16384 ==
|
|
126
|
-
// 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0
|
|
127
|
-
// bump that diverges the two forms fails here instead
|
|
128
|
-
// vertex deployments differently from public ones.
|
|
126
|
+
// 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0, re-verified
|
|
127
|
+
// at 1.5.5); a future bump that diverges the two forms fails here instead
|
|
128
|
+
// of silently capping vertex deployments differently from public ones.
|
|
129
129
|
for (const canonical of [
|
|
130
130
|
"claude-sonnet-4-5-20250929",
|
|
131
131
|
"claude-haiku-4-5-20251001",
|
|
@@ -137,6 +137,33 @@ describe("buildChatModel vertex adapter", () => {
|
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it("model.profile goes empty exactly when translation rewrites the id", () => {
|
|
141
|
+
// ChatAnthropic.profile resolves per model name, and — unlike the
|
|
142
|
+
// maxTokens default above — its lookup does NOT prefix-match: a dated
|
|
143
|
+
// id rewritten to Vertex's `@` form resolves {} (verified at
|
|
144
|
+
// @langchain/anthropic 1.4.0 and again at 1.5.5), while undated ids
|
|
145
|
+
// pass through toVertexModelId unchanged and keep their full profile.
|
|
146
|
+
// Nothing in the runner consumes profile directly, but deepagents'
|
|
147
|
+
// SummarizationMiddleware reads profile.maxInputTokens when present, so
|
|
148
|
+
// vertex deployments pinned to DATED ids summarize on its fallback
|
|
149
|
+
// budget rather than the model's real window. This pins both halves of
|
|
150
|
+
// that contract: if a bump starts resolving `@`-form ids (a welcome
|
|
151
|
+
// improvement — those deployments' summarization budgets would change),
|
|
152
|
+
// or stops resolving canonical ids, this fails and the behavior gets
|
|
153
|
+
// re-decided consciously instead of shifting silently.
|
|
154
|
+
for (const dated of ["claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"]) {
|
|
155
|
+
const publicModel = new ChatAnthropic({ model: dated, apiKey: "probe" });
|
|
156
|
+
const vertexModel = new ChatAnthropic({ model: toVertexModelId(dated), apiKey: "probe" });
|
|
157
|
+
expect(publicModel.profile.maxInputTokens, dated).toBeGreaterThan(0);
|
|
158
|
+
expect(vertexModel.profile, dated).toEqual({});
|
|
159
|
+
}
|
|
160
|
+
const undated = "claude-sonnet-4-6";
|
|
161
|
+
expect(toVertexModelId(undated)).toBe(undated);
|
|
162
|
+
expect(
|
|
163
|
+
new ChatAnthropic({ model: undated, apiKey: "probe" }).profile.maxInputTokens,
|
|
164
|
+
).toBeGreaterThan(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
140
167
|
it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
|
|
141
168
|
// The timeout rides clientOptions -> factory options -> SDK constructor.
|
|
142
169
|
// A factory that drops it silently unbounds the operator's timeout on
|