@stigmer/runner 3.11.1-dev.20260812192248 → 3.12.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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +6 -2
- package/dist/activities/execute-cursor/index.js +16 -32
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +15 -11
- package/dist/activities/execute-cursor/prompt-builder.js +20 -10
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +34 -8
- package/dist/activities/execute-deep-agent/attachment-injector.js +111 -106
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +0 -15
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js +25 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
- package/dist/activities/execute-deep-agent/setup.js +13 -45
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +15 -2
- package/dist/middleware/path-normalization.js +39 -5
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -0
- package/dist/shared/artifact-storage.js +2 -0
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-download-urls.d.ts +62 -0
- package/dist/shared/attachment-download-urls.js +79 -0
- package/dist/shared/attachment-download-urls.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js +1 -1
- package/dist/shared/conversation-attachment.d.ts +2 -3
- package/dist/shared/conversation-attachment.js +2 -3
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +6 -2
- package/dist/shared/mcp-enabled-tools.js +6 -2
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-transport-guard.d.ts +2 -2
- package/dist/shared/mcp-transport-guard.js +2 -2
- package/dist/shared/plan-mode-permissions.d.ts +46 -10
- package/dist/shared/plan-mode-permissions.js +56 -12
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- package/dist/shared/zip-extract.d.ts +24 -6
- package/dist/shared/zip-extract.js +31 -90
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/shared/zip-structure.d.ts +61 -0
- package/dist/shared/zip-structure.js +128 -0
- package/dist/shared/zip-structure.js.map +1 -0
- package/package.json +2 -2
- package/src/__test-utils__/fake-artifact-storage.ts +10 -1
- package/src/__test-utils__/zip-fixtures.ts +206 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +62 -29
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -42
- package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
- package/src/activities/execute-cursor/index.ts +22 -37
- package/src/activities/execute-cursor/prompt-builder.ts +38 -22
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +270 -126
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +246 -37
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +78 -2
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +35 -4
- package/src/activities/execute-deep-agent/attachment-injector.ts +166 -143
- package/src/activities/execute-deep-agent/index.ts +0 -19
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +38 -16
- package/src/activities/execute-deep-agent/setup.ts +13 -68
- package/src/config.ts +6 -5
- package/src/middleware/__tests__/path-normalization.test.ts +29 -3
- package/src/middleware/path-normalization.ts +42 -5
- package/src/shared/__tests__/artifact-storage.test.ts +9 -0
- package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
- package/src/shared/__tests__/channel-attachment.test.ts +2 -2
- package/src/shared/__tests__/plan-mode-permissions.test.ts +58 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
- package/src/shared/__tests__/zip-extract.test.ts +106 -89
- package/src/shared/artifact-storage.ts +11 -0
- package/src/shared/attachment-download-urls.ts +98 -0
- package/src/shared/channel-attachment.ts +1 -1
- package/src/shared/conversation-attachment.ts +2 -3
- package/src/shared/mcp-enabled-tools.ts +6 -2
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/plan-mode-permissions.ts +59 -12
- package/src/shared/synthesized-attachment.ts +4 -4
- package/src/shared/zip-extract.ts +35 -117
- package/src/shared/zip-structure.ts +181 -0
- package/dist/shared/datastore-attachment.d.ts +0 -99
- package/dist/shared/datastore-attachment.js +0 -184
- package/dist/shared/datastore-attachment.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
- package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
- package/src/shared/datastore-attachment.ts +0 -209
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { deflateRawSync } from "node:zlib";
|
|
3
2
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
@@ -15,130 +14,42 @@ import {
|
|
|
15
14
|
} from "../attachment-injector.js";
|
|
16
15
|
import { mockWorkspaceBackend } from "../../../__test-utils__/mock-workspace.js";
|
|
17
16
|
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
17
|
+
import { buildZip, type ZipFixtureFile } from "../../../__test-utils__/zip-fixtures.js";
|
|
18
18
|
import {
|
|
19
19
|
DEEP_AGENT_VISION_PROFILE,
|
|
20
20
|
VisionBudget,
|
|
21
21
|
} from "../../../shared/attachment-vision.js";
|
|
22
22
|
|
|
23
23
|
// ── ZIP Construction Helpers ─────────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// All archives come from the shared real-shape builder (zip-fixtures.ts):
|
|
26
|
+
// local headers, payloads, central directory, EOCD — the only shape real
|
|
27
|
+
// ZIP writers produce and the shape central-directory parsing requires.
|
|
28
|
+
// The record-form helpers below keep ordinary call sites terse; tests that
|
|
29
|
+
// need streaming or declared-size shapes call buildZip directly.
|
|
24
30
|
|
|
25
31
|
function makeZip(entries: Record<string, string | Buffer>): Buffer {
|
|
26
|
-
|
|
27
|
-
const centralDir: Buffer[] = [];
|
|
28
|
-
let offset = 0;
|
|
29
|
-
|
|
30
|
-
for (const [name, content] of Object.entries(entries)) {
|
|
31
|
-
const nameBytes = Buffer.from(name, "utf-8");
|
|
32
|
-
const contentBytes = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
33
|
-
const compressed = deflateRawSync(contentBytes);
|
|
34
|
-
|
|
35
|
-
// Local file header
|
|
36
|
-
const header = Buffer.alloc(30);
|
|
37
|
-
header.writeUInt32LE(0x04034b50, 0); // signature
|
|
38
|
-
header.writeUInt16LE(20, 4); // version needed
|
|
39
|
-
header.writeUInt16LE(0, 6); // flags
|
|
40
|
-
header.writeUInt16LE(8, 8); // compression: deflate
|
|
41
|
-
header.writeUInt16LE(0, 10); // mod time
|
|
42
|
-
header.writeUInt16LE(0, 12); // mod date
|
|
43
|
-
header.writeUInt32LE(0, 14); // crc32 (skip for tests)
|
|
44
|
-
header.writeUInt32LE(compressed.length, 18); // compressed size
|
|
45
|
-
header.writeUInt32LE(contentBytes.length, 22); // uncompressed size
|
|
46
|
-
header.writeUInt16LE(nameBytes.length, 26); // filename length
|
|
47
|
-
header.writeUInt16LE(0, 28); // extra field length
|
|
48
|
-
|
|
49
|
-
parts.push(header, nameBytes, compressed);
|
|
50
|
-
|
|
51
|
-
// Central directory entry
|
|
52
|
-
const cdEntry = Buffer.alloc(46);
|
|
53
|
-
cdEntry.writeUInt32LE(0x02014b50, 0);
|
|
54
|
-
cdEntry.writeUInt16LE(20, 4);
|
|
55
|
-
cdEntry.writeUInt16LE(20, 6);
|
|
56
|
-
cdEntry.writeUInt16LE(0, 8);
|
|
57
|
-
cdEntry.writeUInt16LE(8, 10);
|
|
58
|
-
cdEntry.writeUInt16LE(0, 12);
|
|
59
|
-
cdEntry.writeUInt16LE(0, 14);
|
|
60
|
-
cdEntry.writeUInt32LE(0, 16);
|
|
61
|
-
cdEntry.writeUInt32LE(compressed.length, 20);
|
|
62
|
-
cdEntry.writeUInt32LE(contentBytes.length, 24);
|
|
63
|
-
cdEntry.writeUInt16LE(nameBytes.length, 28);
|
|
64
|
-
cdEntry.writeUInt16LE(0, 30);
|
|
65
|
-
cdEntry.writeUInt16LE(0, 32);
|
|
66
|
-
cdEntry.writeUInt16LE(0, 34);
|
|
67
|
-
cdEntry.writeUInt16LE(0, 36);
|
|
68
|
-
cdEntry.writeUInt32LE(0, 38);
|
|
69
|
-
cdEntry.writeUInt32LE(offset, 42);
|
|
70
|
-
centralDir.push(cdEntry, nameBytes);
|
|
71
|
-
|
|
72
|
-
offset += header.length + nameBytes.length + compressed.length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// End of central directory
|
|
76
|
-
const eocd = Buffer.alloc(22);
|
|
77
|
-
const cdSize = centralDir.reduce((s, b) => s + b.length, 0);
|
|
78
|
-
eocd.writeUInt32LE(0x06054b50, 0);
|
|
79
|
-
eocd.writeUInt16LE(0, 4);
|
|
80
|
-
eocd.writeUInt16LE(0, 6);
|
|
81
|
-
eocd.writeUInt16LE(Object.keys(entries).length, 8);
|
|
82
|
-
eocd.writeUInt16LE(Object.keys(entries).length, 10);
|
|
83
|
-
eocd.writeUInt32LE(cdSize, 12);
|
|
84
|
-
eocd.writeUInt32LE(offset, 16);
|
|
85
|
-
eocd.writeUInt16LE(0, 20);
|
|
86
|
-
|
|
87
|
-
return Buffer.concat([...parts, ...centralDir, eocd]);
|
|
32
|
+
return makeZipWith(entries, { method: "deflated" });
|
|
88
33
|
}
|
|
89
34
|
|
|
90
35
|
function makeStoredZip(entries: Record<string, Buffer>): Buffer {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const [name, content] of Object.entries(entries)) {
|
|
95
|
-
const nameBytes = Buffer.from(name, "utf-8");
|
|
96
|
-
|
|
97
|
-
const header = Buffer.alloc(30);
|
|
98
|
-
header.writeUInt32LE(0x04034b50, 0);
|
|
99
|
-
header.writeUInt16LE(20, 4);
|
|
100
|
-
header.writeUInt16LE(0, 6);
|
|
101
|
-
header.writeUInt16LE(0, 8); // stored (no compression)
|
|
102
|
-
header.writeUInt32LE(content.length, 18);
|
|
103
|
-
header.writeUInt32LE(content.length, 22);
|
|
104
|
-
header.writeUInt16LE(nameBytes.length, 26);
|
|
105
|
-
header.writeUInt16LE(0, 28);
|
|
106
|
-
|
|
107
|
-
parts.push(header, nameBytes, content);
|
|
108
|
-
offset += header.length + nameBytes.length + content.length;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Minimal EOCD
|
|
112
|
-
const eocd = Buffer.alloc(22);
|
|
113
|
-
eocd.writeUInt32LE(0x06054b50, 0);
|
|
114
|
-
eocd.writeUInt16LE(Object.keys(entries).length, 8);
|
|
115
|
-
eocd.writeUInt16LE(Object.keys(entries).length, 10);
|
|
116
|
-
eocd.writeUInt32LE(0, 12);
|
|
117
|
-
eocd.writeUInt32LE(offset, 16);
|
|
36
|
+
return makeZipWith(entries, { method: "stored" });
|
|
37
|
+
}
|
|
118
38
|
|
|
119
|
-
|
|
39
|
+
function makeZipWith(
|
|
40
|
+
entries: Record<string, string | Buffer>,
|
|
41
|
+
shape: Pick<ZipFixtureFile, "method" | "streaming">,
|
|
42
|
+
): Buffer {
|
|
43
|
+
const files: ZipFixtureFile[] = Object.entries(entries).map(([name, content]) => ({
|
|
44
|
+
name,
|
|
45
|
+
content: typeof content === "string" ? content : new Uint8Array(content),
|
|
46
|
+
...shape,
|
|
47
|
+
}));
|
|
48
|
+
return Buffer.from(buildZip(files));
|
|
120
49
|
}
|
|
121
50
|
|
|
122
51
|
function makeDirectoryOnlyZip(): Buffer {
|
|
123
|
-
|
|
124
|
-
const nameBytes = Buffer.from(name, "utf-8");
|
|
125
|
-
|
|
126
|
-
const header = Buffer.alloc(30);
|
|
127
|
-
header.writeUInt32LE(0x04034b50, 0);
|
|
128
|
-
header.writeUInt16LE(20, 4);
|
|
129
|
-
header.writeUInt16LE(0, 6);
|
|
130
|
-
header.writeUInt16LE(0, 8);
|
|
131
|
-
header.writeUInt32LE(0, 18);
|
|
132
|
-
header.writeUInt32LE(0, 22);
|
|
133
|
-
header.writeUInt16LE(nameBytes.length, 26);
|
|
134
|
-
header.writeUInt16LE(0, 28);
|
|
135
|
-
|
|
136
|
-
const eocd = Buffer.alloc(22);
|
|
137
|
-
eocd.writeUInt32LE(0x06054b50, 0);
|
|
138
|
-
eocd.writeUInt16LE(1, 8);
|
|
139
|
-
eocd.writeUInt16LE(1, 10);
|
|
140
|
-
|
|
141
|
-
return Buffer.concat([header, nameBytes, eocd]);
|
|
52
|
+
return Buffer.from(buildZip([{ name: "empty_dir/", content: "" }]));
|
|
142
53
|
}
|
|
143
54
|
|
|
144
55
|
function makeAttachment(overrides: Partial<{
|
|
@@ -251,22 +162,7 @@ describe("validateZipForExtraction", () => {
|
|
|
251
162
|
});
|
|
252
163
|
|
|
253
164
|
it("rejects null bytes in filenames", () => {
|
|
254
|
-
const
|
|
255
|
-
const nameBytes = Buffer.from(nameWithNull, "utf-8");
|
|
256
|
-
const content = Buffer.from("test");
|
|
257
|
-
const compressed = deflateRawSync(content);
|
|
258
|
-
|
|
259
|
-
const header = Buffer.alloc(30);
|
|
260
|
-
header.writeUInt32LE(0x04034b50, 0);
|
|
261
|
-
header.writeUInt16LE(20, 4);
|
|
262
|
-
header.writeUInt16LE(0, 6);
|
|
263
|
-
header.writeUInt16LE(8, 8);
|
|
264
|
-
header.writeUInt32LE(compressed.length, 18);
|
|
265
|
-
header.writeUInt32LE(content.length, 22);
|
|
266
|
-
header.writeUInt16LE(nameBytes.length, 26);
|
|
267
|
-
header.writeUInt16LE(0, 28);
|
|
268
|
-
|
|
269
|
-
const zip = Buffer.concat([header, nameBytes, compressed]);
|
|
165
|
+
const zip = makeZip({ "file\u0000.txt": "test" });
|
|
270
166
|
expect(() => validateZipForExtraction(zip, "null.zip"))
|
|
271
167
|
.toThrow(/null bytes/);
|
|
272
168
|
});
|
|
@@ -305,6 +201,149 @@ describe("validateZipForExtraction", () => {
|
|
|
305
201
|
});
|
|
306
202
|
});
|
|
307
203
|
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
205
|
+
// Central-directory parsing (issue #567)
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
207
|
+
// The archive shapes the old local-header walk corrupted or rejected:
|
|
208
|
+
// stored streaming entries (silent manifest truncation) and Go-default
|
|
209
|
+
// deflated streaming entries (rejected outright). Sizes come from the
|
|
210
|
+
// central directory — the format's authoritative index — and structural
|
|
211
|
+
// failures are fail-hard, unlike the skill-artifact reader's non-fatal
|
|
212
|
+
// empty return: nothing upstream vouches for an attachment.
|
|
213
|
+
|
|
214
|
+
describe("validateZipForExtraction — central-directory parsing (issue #567)", () => {
|
|
215
|
+
it("validates every entry of a stored streaming archive (no silent truncation)", () => {
|
|
216
|
+
// Method 0 + flag bit 3 + zeroed local sizes: the old walk admitted the
|
|
217
|
+
// first entry with size 0, landed mid-payload, and quietly dropped the
|
|
218
|
+
// rest of the manifest.
|
|
219
|
+
const zip = Buffer.from(buildZip([
|
|
220
|
+
{ name: "first.txt", content: "first file content", streaming: true },
|
|
221
|
+
{ name: "second.txt", content: "second file content", streaming: true },
|
|
222
|
+
]));
|
|
223
|
+
|
|
224
|
+
const result = validateZipForExtraction(zip, "streamed.zip");
|
|
225
|
+
expect(result.map((e) => e.relativePath)).toEqual(["first.txt", "second.txt"]);
|
|
226
|
+
expect(result.map((e) => e.uncompressedSize)).toEqual([
|
|
227
|
+
"first file content".length,
|
|
228
|
+
"second file content".length,
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("accepts a Go-default archive (deflated streaming entries)", () => {
|
|
233
|
+
const zip = Buffer.from(buildZip([
|
|
234
|
+
{ name: "main.go", content: "package main", method: "deflated", streaming: true },
|
|
235
|
+
{ name: "go.mod", content: "module example", method: "deflated", streaming: true },
|
|
236
|
+
]));
|
|
237
|
+
|
|
238
|
+
const result = validateZipForExtraction(zip, "go-built.zip");
|
|
239
|
+
expect(result.map((e) => e.relativePath)).toEqual(["go.mod", "main.go"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("rejects an archive with no central directory (fail-hard, unlike skill extraction)", () => {
|
|
243
|
+
const zip = Buffer.from(buildZip(
|
|
244
|
+
[{ name: "a.txt", content: "aaa" }],
|
|
245
|
+
{ omitCentralDirectory: true },
|
|
246
|
+
));
|
|
247
|
+
|
|
248
|
+
expect(() => validateZipForExtraction(zip, "truncated.zip"))
|
|
249
|
+
.toThrow(AttachmentValidationError);
|
|
250
|
+
expect(() => validateZipForExtraction(zip, "truncated.zip"))
|
|
251
|
+
.toThrow(/not a valid ZIP archive/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("rejects duplicate entry paths (a contradictory manifest)", () => {
|
|
255
|
+
const zip = Buffer.from(buildZip([
|
|
256
|
+
{ name: "dup.txt", content: "one" },
|
|
257
|
+
{ name: "dup.txt", content: "two" },
|
|
258
|
+
]));
|
|
259
|
+
|
|
260
|
+
expect(() => validateZipForExtraction(zip, "dup.zip"))
|
|
261
|
+
.toThrow(/duplicate entry/);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("locates the central directory behind a trailing archive comment", () => {
|
|
265
|
+
const zip = Buffer.from(buildZip(
|
|
266
|
+
[{ name: "a.txt", content: "aaa" }],
|
|
267
|
+
{ comment: "release archive — built by tooling" },
|
|
268
|
+
));
|
|
269
|
+
|
|
270
|
+
const result = validateZipForExtraction(zip, "commented.zip");
|
|
271
|
+
expect(result.map((e) => e.relativePath)).toEqual(["a.txt"]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("injectAttachments — central-directory extraction (issue #567)", () => {
|
|
276
|
+
let tempDir: string;
|
|
277
|
+
|
|
278
|
+
beforeEach(async () => {
|
|
279
|
+
tempDir = await mkdtemp(join(tmpdir(), "attachment-cd-"));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
async function extractArchive(zip: Uint8Array) {
|
|
283
|
+
const localFile = join(tempDir, "archive.zip");
|
|
284
|
+
await writeFile(localFile, zip);
|
|
285
|
+
const backend = mockWorkspaceBackend();
|
|
286
|
+
const result = await injectAttachments({
|
|
287
|
+
backend,
|
|
288
|
+
attachments: [makeAttachment({
|
|
289
|
+
filename: "archive.zip",
|
|
290
|
+
mountPath: ".stigmer/inputs/archive",
|
|
291
|
+
extract: true,
|
|
292
|
+
localPath: localFile,
|
|
293
|
+
})],
|
|
294
|
+
storage: makeMockStorage(),
|
|
295
|
+
isLocalMode: true,
|
|
296
|
+
});
|
|
297
|
+
return { backend, result };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
it("extracts a stored streaming archive completely, contents intact", async () => {
|
|
301
|
+
const { backend, result } = await extractArchive(buildZip([
|
|
302
|
+
{ name: "first.txt", content: "first file content", streaming: true },
|
|
303
|
+
{ name: "second.txt", content: "second file content", streaming: true },
|
|
304
|
+
]));
|
|
305
|
+
|
|
306
|
+
expect(result.map((f) => f.path).sort()).toEqual([
|
|
307
|
+
".stigmer/inputs/archive/first.txt",
|
|
308
|
+
".stigmer/inputs/archive/second.txt",
|
|
309
|
+
]);
|
|
310
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(
|
|
311
|
+
".stigmer/inputs/archive/first.txt", Buffer.from("first file content"),
|
|
312
|
+
);
|
|
313
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(
|
|
314
|
+
".stigmer/inputs/archive/second.txt", Buffer.from("second file content"),
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("extracts a Go-default deflated streaming archive", async () => {
|
|
319
|
+
const { backend, result } = await extractArchive(buildZip([
|
|
320
|
+
{ name: "src/main.go", content: "package main\n", method: "deflated", streaming: true },
|
|
321
|
+
]));
|
|
322
|
+
|
|
323
|
+
expect(result).toHaveLength(1);
|
|
324
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(
|
|
325
|
+
".stigmer/inputs/archive/src/main.go", Buffer.from("package main\n"),
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("aborts when an entry inflates past its declared size (crafted archive)", async () => {
|
|
330
|
+
await expect(extractArchive(buildZip([
|
|
331
|
+
{
|
|
332
|
+
name: "bomb.txt",
|
|
333
|
+
content: "x".repeat(4096),
|
|
334
|
+
method: "deflated",
|
|
335
|
+
declaredUncompressedSize: 16,
|
|
336
|
+
},
|
|
337
|
+
]))).rejects.toThrow(AttachmentValidationError);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("aborts when a stored entry's payload disagrees with its declared size", async () => {
|
|
341
|
+
await expect(extractArchive(buildZip([
|
|
342
|
+
{ name: "short.txt", content: "eleven byte", declaredUncompressedSize: 4096 },
|
|
343
|
+
]))).rejects.toThrow(/declare/);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
308
347
|
// ═══════════════════════════════════════════════════════════════════════
|
|
309
348
|
// injectAttachments
|
|
310
349
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1014,3 +1053,108 @@ describe("injectAttachments — vision selection", () => {
|
|
|
1014
1053
|
expect(injected[1].visionDegraded).toBe("budget_exhausted");
|
|
1015
1054
|
});
|
|
1016
1055
|
});
|
|
1056
|
+
|
|
1057
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1058
|
+
// injectAttachments — download-URL hand-off (issue #532)
|
|
1059
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1060
|
+
// The URL is strictly additive (shared/attachment-download-urls.ts): every
|
|
1061
|
+
// case also asserts the file injected exactly as it would without one.
|
|
1062
|
+
|
|
1063
|
+
describe("injectAttachments — download-URL hand-off", () => {
|
|
1064
|
+
it("mints a URL for a storage-key attachment", async () => {
|
|
1065
|
+
const content = Buffer.from("cloud data");
|
|
1066
|
+
const storage = makeMockStorage();
|
|
1067
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1068
|
+
const backend = mockWorkspaceBackend();
|
|
1069
|
+
|
|
1070
|
+
const result = await injectAttachments({
|
|
1071
|
+
backend,
|
|
1072
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1073
|
+
storage,
|
|
1074
|
+
isLocalMode: false,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
expect(result[0]).toMatchObject({
|
|
1078
|
+
path: ".stigmer/inputs/data.csv",
|
|
1079
|
+
downloadUrl: "mem://attachments/xyz/data.csv",
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it("mints a URL for an explicit-mountPath attachment (the rule ignores mount location)", async () => {
|
|
1084
|
+
const storage = makeMockStorage();
|
|
1085
|
+
await storage.upload("attachments/xyz/config.yaml", Buffer.from("a: 1"));
|
|
1086
|
+
const backend = mockWorkspaceBackend();
|
|
1087
|
+
|
|
1088
|
+
const result = await injectAttachments({
|
|
1089
|
+
backend,
|
|
1090
|
+
attachments: [makeAttachment({
|
|
1091
|
+
filename: "config.yaml",
|
|
1092
|
+
storageKey: "attachments/xyz/config.yaml",
|
|
1093
|
+
mountPath: "config/app.yaml",
|
|
1094
|
+
})],
|
|
1095
|
+
storage,
|
|
1096
|
+
isLocalMode: false,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
expect(result[0]).toMatchObject({
|
|
1100
|
+
path: "config/app.yaml",
|
|
1101
|
+
downloadUrl: "mem://attachments/xyz/config.yaml",
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("carries no URL on extracted ZIP entries — the stored object is the ZIP, not any listed file", async () => {
|
|
1106
|
+
const zip = makeZip({ "src/a.txt": "alpha", "src/b.txt": "beta" });
|
|
1107
|
+
const storage = makeMockStorage();
|
|
1108
|
+
await storage.upload("attachments/xyz/code.zip", zip);
|
|
1109
|
+
const backend = mockWorkspaceBackend();
|
|
1110
|
+
|
|
1111
|
+
const result = await injectAttachments({
|
|
1112
|
+
backend,
|
|
1113
|
+
attachments: [makeAttachment({
|
|
1114
|
+
filename: "code.zip",
|
|
1115
|
+
storageKey: "attachments/xyz/code.zip",
|
|
1116
|
+
extract: true,
|
|
1117
|
+
})],
|
|
1118
|
+
storage,
|
|
1119
|
+
isLocalMode: false,
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
expect(result.length).toBeGreaterThan(0);
|
|
1123
|
+
for (const file of result) {
|
|
1124
|
+
expect(file.downloadUrl).toBeUndefined();
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("carries no URL for a key-less local file", async () => {
|
|
1129
|
+
const localFile = join(tmpdir(), `injector-url-${Date.now()}.txt`);
|
|
1130
|
+
await writeFile(localFile, "local only");
|
|
1131
|
+
const backend = mockWorkspaceBackend();
|
|
1132
|
+
|
|
1133
|
+
const result = await injectAttachments({
|
|
1134
|
+
backend,
|
|
1135
|
+
attachments: [makeAttachment({ filename: "local.txt", storageKey: "", localPath: localFile })],
|
|
1136
|
+
storage: makeMockStorage(),
|
|
1137
|
+
isLocalMode: true,
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("degrades to no URL when the mint fails — file still injected, no throw", async () => {
|
|
1144
|
+
const content = Buffer.from("cloud data");
|
|
1145
|
+
const storage = makeMockStorage();
|
|
1146
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
1147
|
+
storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
|
|
1148
|
+
const backend = mockWorkspaceBackend();
|
|
1149
|
+
|
|
1150
|
+
const result = await injectAttachments({
|
|
1151
|
+
backend,
|
|
1152
|
+
attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
|
|
1153
|
+
storage,
|
|
1154
|
+
isLocalMode: false,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
expect(result[0].downloadUrl).toBeUndefined();
|
|
1158
|
+
expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/data.csv", content);
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
@@ -184,7 +184,6 @@ function fakeHttpRejectSetup(): SetupResult {
|
|
|
184
184
|
session: { spec: { workspaceEntries: [] } },
|
|
185
185
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-http" },
|
|
186
186
|
mcpConnection: null,
|
|
187
|
-
datastoreToolsMissing: [],
|
|
188
187
|
mergedEnvVars: {},
|
|
189
188
|
secretKeys: new Set<string>(),
|
|
190
189
|
modelName: "claude-sonnet",
|
|
@@ -220,7 +219,6 @@ function fakeMemoryRejectSetup(): SetupResult {
|
|
|
220
219
|
session: { spec: { workspaceEntries: [] } },
|
|
221
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-mem" },
|
|
222
221
|
mcpConnection: null,
|
|
223
|
-
datastoreToolsMissing: [],
|
|
224
222
|
mergedEnvVars: {},
|
|
225
223
|
secretKeys: new Set<string>(),
|
|
226
224
|
modelName: "claude-sonnet",
|
|
@@ -264,7 +264,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
264
264
|
// path shared across files would serialize parallel test workers for real.
|
|
265
265
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-approve-all" },
|
|
266
266
|
mcpConnection: null,
|
|
267
|
-
datastoreToolsMissing: [],
|
|
268
267
|
mergedEnvVars: {},
|
|
269
268
|
secretKeys: new Set<string>(),
|
|
270
269
|
modelName: "claude-sonnet",
|
|
@@ -219,7 +219,6 @@ function fakeDurableResumeSetup(): SetupResult {
|
|
|
219
219
|
// path shared across files would serialize parallel test workers for real.
|
|
220
220
|
workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-history" },
|
|
221
221
|
mcpConnection: null,
|
|
222
|
-
datastoreToolsMissing: [],
|
|
223
222
|
mergedEnvVars: {},
|
|
224
223
|
secretKeys: new Set<string>(),
|
|
225
224
|
modelName: "claude-sonnet",
|
|
@@ -12,7 +12,6 @@ import { shouldConnectMcp } from "../mcp-gate.js";
|
|
|
12
12
|
|
|
13
13
|
const nothing = {
|
|
14
14
|
mcpServerUsageCount: 0,
|
|
15
|
-
datastoreUsageCount: 0,
|
|
16
15
|
channelMessagingCount: 0,
|
|
17
16
|
conversationChannelId: undefined,
|
|
18
17
|
};
|
|
@@ -26,17 +25,13 @@ describe("shouldConnectMcp", () => {
|
|
|
26
25
|
expect(shouldConnectMcp({ ...nothing, mcpServerUsageCount: 1 })).toBe(true);
|
|
27
26
|
});
|
|
28
27
|
|
|
29
|
-
it("enters on datastore usages alone (the records attachment)", () => {
|
|
30
|
-
expect(shouldConnectMcp({ ...nothing, datastoreUsageCount: 1 })).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
28
|
it("enters on a serving proactive channel alone (the channels attachment)", () => {
|
|
34
29
|
expect(shouldConnectMcp({ ...nothing, channelMessagingCount: 1 })).toBe(true);
|
|
35
30
|
});
|
|
36
31
|
|
|
37
32
|
it("enters on a channel conversation alone (the conversation attachment)", () => {
|
|
38
|
-
// The reply-only pilot shape: no declared servers, no
|
|
39
|
-
//
|
|
33
|
+
// The reply-only pilot shape: no declared servers, no proactive
|
|
34
|
+
// channel — the escalation tool is the ONLY source.
|
|
40
35
|
expect(shouldConnectMcp({ ...nothing, conversationChannelId: "agch_1" })).toBe(true);
|
|
41
36
|
});
|
|
42
37
|
});
|