@stigmer/runner 3.12.4 → 3.12.5
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/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/index.d.ts +10 -1
- package/dist/activities/execute-cursor/index.js +47 -29
- 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/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-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/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/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 +38 -1
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +23 -2
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/package.json +15 -11
- 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__/skill-resolver.test.ts +3 -94
- package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
- package/src/activities/execute-cursor/index.ts +63 -35
- package/src/activities/execute-cursor/prompt-builder.ts +29 -2
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
- package/src/activities/execute-cursor/skill-resolver.ts +10 -137
- 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/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/worker-shutdown.ts +99 -0
- package/src/shared/workspace/types.ts +3 -2
- package/src/workflow-engine/__tests__/loader.test.ts +51 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
- package/src/workflow-engine/loader.ts +46 -1
- package/src/workflow-engine/tasks/human-input.ts +33 -5
- 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
|
@@ -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
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This is NOT a unit test of our code (there is no production vertex code
|
|
6
6
|
* yet). It pins the exact cross-package behavior production will rely on:
|
|
7
|
-
* the REAL `ChatAnthropic` (@langchain/anthropic
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* the REAL `ChatAnthropic` (@langchain/anthropic) driving the REAL
|
|
8
|
+
* `AnthropicVertex` client (@anthropic-ai/vertex-sdk), both resolving the
|
|
9
|
+
* single override-pinned @anthropic-ai/sdk copy. If a future bump of
|
|
10
10
|
* either side changes request shaping, streaming event handling, tool-call
|
|
11
11
|
* assembly, or usage accounting across this seam, this suite fails in CI
|
|
12
|
-
* instead of production. See scripts/check-langchain-deps.sh for
|
|
13
|
-
*
|
|
12
|
+
* instead of production. See scripts/check-langchain-deps.sh for the
|
|
13
|
+
* override rationale and the single-copy invariant it guards.
|
|
14
14
|
*
|
|
15
15
|
* Determinism: zero credentials, zero network. Google auth is bypassed by
|
|
16
16
|
* injecting a fake `authClient` (the SDK's supported constructor option —
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
classifyTurnInterruption,
|
|
4
|
+
getShutdownSignalForQueue,
|
|
5
|
+
registerWorkerShutdownSignal,
|
|
6
|
+
signalWorkerShutdown,
|
|
7
|
+
unregisterWorkerShutdownSignal,
|
|
8
|
+
} from "../worker-shutdown.js";
|
|
9
|
+
|
|
10
|
+
describe("worker-shutdown signal registry", () => {
|
|
11
|
+
it("registers a live signal an activity can resolve by queue name", () => {
|
|
12
|
+
const controller = registerWorkerShutdownSignal("session:reg-1");
|
|
13
|
+
const signal = getShutdownSignalForQueue("session:reg-1");
|
|
14
|
+
expect(signal).toBe(controller.signal);
|
|
15
|
+
expect(signal?.aborted).toBe(false);
|
|
16
|
+
unregisterWorkerShutdownSignal("session:reg-1");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("aborting via signalWorkerShutdown marks the queue's signal (idempotent, unknown queues are no-ops)", () => {
|
|
20
|
+
registerWorkerShutdownSignal("session:abort-1");
|
|
21
|
+
signalWorkerShutdown("session:abort-1");
|
|
22
|
+
signalWorkerShutdown("session:abort-1");
|
|
23
|
+
expect(getShutdownSignalForQueue("session:abort-1")?.aborted).toBe(true);
|
|
24
|
+
expect(() => signalWorkerShutdown("session:never-registered")).not.toThrow();
|
|
25
|
+
unregisterWorkerShutdownSignal("session:abort-1");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("re-registering a reused queue replaces an aborted signal with a fresh one", () => {
|
|
29
|
+
// A re-created worker on a reused queue (desktop re-opens a session) must
|
|
30
|
+
// not observe the previous worker's aborted signal as its own shutdown.
|
|
31
|
+
const first = registerWorkerShutdownSignal("session:reuse-1");
|
|
32
|
+
first.abort();
|
|
33
|
+
const second = registerWorkerShutdownSignal("session:reuse-1");
|
|
34
|
+
expect(getShutdownSignalForQueue("session:reuse-1")).toBe(second.signal);
|
|
35
|
+
expect(getShutdownSignalForQueue("session:reuse-1")?.aborted).toBe(false);
|
|
36
|
+
unregisterWorkerShutdownSignal("session:reuse-1");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("unregistering removes the signal", () => {
|
|
40
|
+
registerWorkerShutdownSignal("session:gone-1");
|
|
41
|
+
unregisterWorkerShutdownSignal("session:gone-1");
|
|
42
|
+
expect(getShutdownSignalForQueue("session:gone-1")).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("classifyTurnInterruption", () => {
|
|
47
|
+
const none = {
|
|
48
|
+
heartbeatCancelled: false,
|
|
49
|
+
heartbeatWorkerShutdown: false,
|
|
50
|
+
cancellationSignalAborted: false,
|
|
51
|
+
shutdownSignalAborted: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
it("an uninterrupted turn is none", () => {
|
|
55
|
+
expect(classifyTurnInterruption(none)).toBe("none");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("GRACE-WINDOW GUARD (#776): an aborted shutdown signal alone stays none — a run that completed inside the drain grace window must not be failed", () => {
|
|
59
|
+
expect(
|
|
60
|
+
classifyTurnInterruption({ ...none, shutdownSignalAborted: true }),
|
|
61
|
+
).toBe("none");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("heartbeat cancellation without a shutdown signal is the orchestrator's pause", () => {
|
|
65
|
+
expect(
|
|
66
|
+
classifyTurnInterruption({ ...none, heartbeatCancelled: true }),
|
|
67
|
+
).toBe("pause");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("heartbeat cancellation WITH the shutdown signal aborted is a worker shutdown, not a pause (the #776 misclassification)", () => {
|
|
71
|
+
expect(
|
|
72
|
+
classifyTurnInterruption({
|
|
73
|
+
...none,
|
|
74
|
+
heartbeatCancelled: true,
|
|
75
|
+
shutdownSignalAborted: true,
|
|
76
|
+
}),
|
|
77
|
+
).toBe("worker-shutdown");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("the heartbeat's own workerShutdown flag classifies directly", () => {
|
|
81
|
+
expect(
|
|
82
|
+
classifyTurnInterruption({ ...none, heartbeatWorkerShutdown: true }),
|
|
83
|
+
).toBe("worker-shutdown");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("a delivered cancellation with the shutdown signal aborted is a worker shutdown (signal races the heartbeat tick)", () => {
|
|
87
|
+
expect(
|
|
88
|
+
classifyTurnInterruption({
|
|
89
|
+
...none,
|
|
90
|
+
cancellationSignalAborted: true,
|
|
91
|
+
shutdownSignalAborted: true,
|
|
92
|
+
}),
|
|
93
|
+
).toBe("worker-shutdown");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("a delivered cancellation alone (heartbeat timeout / infra cancel) is neither shutdown nor pause", () => {
|
|
97
|
+
expect(
|
|
98
|
+
classifyTurnInterruption({ ...none, cancellationSignalAborted: true }),
|
|
99
|
+
).toBe("none");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -34,11 +34,25 @@ export const MAX_ARGS_PREVIEW_LENGTH = 500;
|
|
|
34
34
|
* Cursor gate path must instead use {@link buildElidedArgsPreview}, which keeps
|
|
35
35
|
* the JSON valid and preserves the salient identity fields.
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Redact secret-keyed values (see {@link SENSITIVE_ARG_KEYS}), preserving every
|
|
39
|
+
* other entry verbatim. The shared first step of both preview builders, and the
|
|
40
|
+
* shape stamped as `args` on interrupt-placeholder tool calls (issue #754's
|
|
41
|
+
* header fix): full enough for the UI's path/primary-arg extraction, never
|
|
42
|
+
* carrying a secret value.
|
|
43
|
+
*/
|
|
44
|
+
export function redactSensitiveArgs(
|
|
45
|
+
args: Record<string, unknown>,
|
|
46
|
+
): Record<string, unknown> {
|
|
47
|
+
const redacted: Record<string, unknown> = {};
|
|
39
48
|
for (const [key, value] of Object.entries(args)) {
|
|
40
|
-
|
|
49
|
+
redacted[key] = SENSITIVE_ARG_KEYS.has(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
41
50
|
}
|
|
51
|
+
return redacted;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sanitizeArgsPreview(args: Record<string, unknown>): string {
|
|
55
|
+
const sanitized = redactSensitiveArgs(args);
|
|
42
56
|
try {
|
|
43
57
|
const json = JSON.stringify(sanitized);
|
|
44
58
|
return json.length > MAX_ARGS_PREVIEW_LENGTH
|
|
@@ -56,7 +56,17 @@ export interface ArtifactStorage {
|
|
|
56
56
|
exists(key: string): Promise<boolean>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* "none" is the explicit no-store posture: capture degrades to the deny-gate
|
|
61
|
+
* (file writes gated pre-execution, DD-22/DD-26) and offload is disabled —
|
|
62
|
+
* the same first-class absent-store state a proxy misconfig or unwritable
|
|
63
|
+
* local path resolves to, but chosen deliberately (ARTIFACT_STORAGE_TYPE=none)
|
|
64
|
+
* instead of reached by failure. The e2e file-gate stack boots with it, and
|
|
65
|
+
* an operator can use it to force gate-everything behavior. It is only valid
|
|
66
|
+
* where an absent store is: claimcheck (fail-hard by contract) refuses it at
|
|
67
|
+
* construction.
|
|
68
|
+
*/
|
|
69
|
+
export type ArtifactStorageType = "local" | "proxy" | "none";
|
|
60
70
|
|
|
61
71
|
// ── Local Backend ────────────────────────────────────────────────────
|
|
62
72
|
|
|
@@ -371,6 +381,7 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
|
|
|
371
381
|
const type: ArtifactStorageType =
|
|
372
382
|
envType === "proxy" ? "proxy" :
|
|
373
383
|
envType === "local" ? "local" :
|
|
384
|
+
envType === "none" ? "none" :
|
|
374
385
|
config.proxyEndpoint ? "proxy" : "local";
|
|
375
386
|
|
|
376
387
|
return {
|
|
@@ -387,6 +398,14 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
|
|
|
387
398
|
}
|
|
388
399
|
|
|
389
400
|
export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStorage {
|
|
401
|
+
if (cfg.type === "none") {
|
|
402
|
+
// Deliberate no-store posture. Throwing keeps both consumers honest:
|
|
403
|
+
// resolveUsableArtifactStorage catches and degrades to the deny-gate
|
|
404
|
+
// (the intent of "none"), while claimcheck's direct fail-hard call
|
|
405
|
+
// surfaces the config contradiction at boot instead of silently
|
|
406
|
+
// dropping Temporal payloads.
|
|
407
|
+
throw new Error("ARTIFACT_STORAGE_TYPE=none: artifact storage deliberately disabled");
|
|
408
|
+
}
|
|
390
409
|
if (cfg.type === "proxy") {
|
|
391
410
|
if (!cfg.proxyEndpoint) {
|
|
392
411
|
throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");
|