runtimeuse 0.2.0

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.
Files changed (120) hide show
  1. package/.env.example +4 -0
  2. package/README.md +222 -0
  3. package/dist/agent-handler.d.ts +26 -0
  4. package/dist/agent-handler.d.ts.map +1 -0
  5. package/dist/agent-handler.js +2 -0
  6. package/dist/agent-handler.js.map +1 -0
  7. package/dist/artifact-manager.d.ts +27 -0
  8. package/dist/artifact-manager.d.ts.map +1 -0
  9. package/dist/artifact-manager.js +125 -0
  10. package/dist/artifact-manager.js.map +1 -0
  11. package/dist/artifact-manager.test.d.ts +2 -0
  12. package/dist/artifact-manager.test.d.ts.map +1 -0
  13. package/dist/artifact-manager.test.js +251 -0
  14. package/dist/artifact-manager.test.js.map +1 -0
  15. package/dist/claude-handler.d.ts +3 -0
  16. package/dist/claude-handler.d.ts.map +1 -0
  17. package/dist/claude-handler.js +76 -0
  18. package/dist/claude-handler.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +87 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/command-handler.d.ts +22 -0
  24. package/dist/command-handler.d.ts.map +1 -0
  25. package/dist/command-handler.js +75 -0
  26. package/dist/command-handler.js.map +1 -0
  27. package/dist/command-handler.test.d.ts +2 -0
  28. package/dist/command-handler.test.d.ts.map +1 -0
  29. package/dist/command-handler.test.js +267 -0
  30. package/dist/command-handler.test.js.map +1 -0
  31. package/dist/constants.d.ts +3 -0
  32. package/dist/constants.d.ts.map +1 -0
  33. package/dist/constants.js +13 -0
  34. package/dist/constants.js.map +1 -0
  35. package/dist/default-handler.d.ts +3 -0
  36. package/dist/default-handler.d.ts.map +1 -0
  37. package/dist/default-handler.js +76 -0
  38. package/dist/default-handler.js.map +1 -0
  39. package/dist/download-handler.d.ts +8 -0
  40. package/dist/download-handler.d.ts.map +1 -0
  41. package/dist/download-handler.js +36 -0
  42. package/dist/download-handler.js.map +1 -0
  43. package/dist/download-handler.test.d.ts +2 -0
  44. package/dist/download-handler.test.d.ts.map +1 -0
  45. package/dist/download-handler.test.js +123 -0
  46. package/dist/download-handler.test.js.map +1 -0
  47. package/dist/index.d.ts +20 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/logger.d.ts +8 -0
  52. package/dist/logger.d.ts.map +1 -0
  53. package/dist/logger.js +14 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/openai-handler.d.ts +3 -0
  56. package/dist/openai-handler.d.ts.map +1 -0
  57. package/dist/openai-handler.js +86 -0
  58. package/dist/openai-handler.js.map +1 -0
  59. package/dist/server.d.ts +21 -0
  60. package/dist/server.d.ts.map +1 -0
  61. package/dist/server.js +52 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/session.d.ts +29 -0
  64. package/dist/session.d.ts.map +1 -0
  65. package/dist/session.js +244 -0
  66. package/dist/session.js.map +1 -0
  67. package/dist/session.test.d.ts +2 -0
  68. package/dist/session.test.d.ts.map +1 -0
  69. package/dist/session.test.js +339 -0
  70. package/dist/session.test.js.map +1 -0
  71. package/dist/storage.d.ts +3 -0
  72. package/dist/storage.d.ts.map +1 -0
  73. package/dist/storage.js +21 -0
  74. package/dist/storage.js.map +1 -0
  75. package/dist/types.d.ts +62 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +2 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/upload-tracker.d.ts +10 -0
  80. package/dist/upload-tracker.d.ts.map +1 -0
  81. package/dist/upload-tracker.js +27 -0
  82. package/dist/upload-tracker.js.map +1 -0
  83. package/dist/upload-tracker.test.d.ts +2 -0
  84. package/dist/upload-tracker.test.d.ts.map +1 -0
  85. package/dist/upload-tracker.test.js +89 -0
  86. package/dist/upload-tracker.test.js.map +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.d.ts.map +1 -0
  89. package/dist/utils.js +32 -0
  90. package/dist/utils.js.map +1 -0
  91. package/dist/utils.test.d.ts +2 -0
  92. package/dist/utils.test.d.ts.map +1 -0
  93. package/dist/utils.test.js +92 -0
  94. package/dist/utils.test.js.map +1 -0
  95. package/package.json +40 -0
  96. package/scripts/dev-publish.sh +45 -0
  97. package/src/agent-handler.ts +26 -0
  98. package/src/artifact-manager.test.ts +320 -0
  99. package/src/artifact-manager.ts +170 -0
  100. package/src/claude-handler.ts +95 -0
  101. package/src/cli.ts +107 -0
  102. package/src/command-handler.test.ts +507 -0
  103. package/src/command-handler.ts +102 -0
  104. package/src/constants.ts +12 -0
  105. package/src/download-handler.test.ts +183 -0
  106. package/src/download-handler.ts +45 -0
  107. package/src/index.ts +59 -0
  108. package/src/logger.ts +20 -0
  109. package/src/openai-handler.ts +120 -0
  110. package/src/server.ts +68 -0
  111. package/src/session.test.ts +448 -0
  112. package/src/session.ts +319 -0
  113. package/src/storage.ts +28 -0
  114. package/src/types.ts +101 -0
  115. package/src/upload-tracker.test.ts +112 -0
  116. package/src/upload-tracker.ts +30 -0
  117. package/src/utils.test.ts +120 -0
  118. package/src/utils.ts +35 -0
  119. package/tsconfig.json +20 -0
  120. package/vitest.config.ts +7 -0
package/dist/utils.js ADDED
@@ -0,0 +1,32 @@
1
+ export async function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ /**
5
+ * Recursively redact secret values from an arbitrary data structure.
6
+ * Any string that contains a secret will have that secret replaced with [REDACTED].
7
+ */
8
+ export function redactSecrets(value, secrets) {
9
+ if (secrets.length === 0)
10
+ return value;
11
+ if (typeof value === "string") {
12
+ let redacted = value;
13
+ for (const secret of secrets) {
14
+ if (secret && redacted.includes(secret)) {
15
+ redacted = redacted.replaceAll(secret, "[REDACTED]");
16
+ }
17
+ }
18
+ return redacted;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map((item) => redactSecrets(item, secrets));
22
+ }
23
+ if (value !== null && typeof value === "object") {
24
+ const result = {};
25
+ for (const [key, val] of Object.entries(value)) {
26
+ result[key] = redactSecrets(val, secrets);
27
+ }
28
+ return result;
29
+ }
30
+ return value;
31
+ }
32
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,EAAU;IACpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAI,KAAQ,EAAE,OAAiB;IAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEvC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,QAAQ,GAAW,KAAK,CAAC;QAC7B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,MAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxC,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QACD,OAAO,QAAa,CAAC;IACvB,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAM,CAAC;IAChE,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/C,MAAM,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,MAAW,CAAC;IACrB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../src/utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { redactSecrets } from "./utils.js";
3
+ describe("redactSecrets", () => {
4
+ describe("strings", () => {
5
+ it("replaces a secret within a string", () => {
6
+ expect(redactSecrets("token is abc123", ["abc123"])).toBe("token is [REDACTED]");
7
+ });
8
+ it("replaces multiple occurrences of the same secret", () => {
9
+ expect(redactSecrets("abc123 and abc123", ["abc123"])).toBe("[REDACTED] and [REDACTED]");
10
+ });
11
+ it("replaces multiple different secrets", () => {
12
+ expect(redactSecrets("key=SECRET1 pass=SECRET2", ["SECRET1", "SECRET2"])).toBe("key=[REDACTED] pass=[REDACTED]");
13
+ });
14
+ it("returns string unchanged when no secrets match", () => {
15
+ expect(redactSecrets("nothing here", ["xyz"])).toBe("nothing here");
16
+ });
17
+ it("skips empty-string secrets", () => {
18
+ expect(redactSecrets("hello", [""])).toBe("hello");
19
+ });
20
+ });
21
+ describe("arrays", () => {
22
+ it("redacts secrets inside array elements", () => {
23
+ expect(redactSecrets(["key=SECRET", "ok"], ["SECRET"])).toEqual([
24
+ "key=[REDACTED]",
25
+ "ok",
26
+ ]);
27
+ });
28
+ it("handles nested arrays", () => {
29
+ expect(redactSecrets([["SECRET"]], ["SECRET"])).toEqual([
30
+ ["[REDACTED]"],
31
+ ]);
32
+ });
33
+ });
34
+ describe("objects", () => {
35
+ it("redacts secrets in object values", () => {
36
+ expect(redactSecrets({ token: "my-SECRET-value", count: 5 }, ["SECRET"])).toEqual({ token: "my-[REDACTED]-value", count: 5 });
37
+ });
38
+ it("redacts secrets in deeply nested objects", () => {
39
+ const input = {
40
+ level1: {
41
+ level2: {
42
+ value: "contains SECRET here",
43
+ },
44
+ },
45
+ };
46
+ expect(redactSecrets(input, ["SECRET"])).toEqual({
47
+ level1: {
48
+ level2: {
49
+ value: "contains [REDACTED] here",
50
+ },
51
+ },
52
+ });
53
+ });
54
+ it("handles mixed objects with arrays", () => {
55
+ const input = {
56
+ args: ["--token=SECRET", "--verbose"],
57
+ env: { API_KEY: "SECRET" },
58
+ };
59
+ expect(redactSecrets(input, ["SECRET"])).toEqual({
60
+ args: ["--token=[REDACTED]", "--verbose"],
61
+ env: { API_KEY: "[REDACTED]" },
62
+ });
63
+ });
64
+ });
65
+ describe("non-string primitives", () => {
66
+ it("returns numbers unchanged", () => {
67
+ expect(redactSecrets(42, ["42"])).toBe(42);
68
+ });
69
+ it("returns booleans unchanged", () => {
70
+ expect(redactSecrets(true, ["true"])).toBe(true);
71
+ });
72
+ it("returns null unchanged", () => {
73
+ expect(redactSecrets(null, ["null"])).toBeNull();
74
+ });
75
+ it("returns undefined unchanged", () => {
76
+ expect(redactSecrets(undefined, ["undefined"])).toBeUndefined();
77
+ });
78
+ });
79
+ describe("edge cases", () => {
80
+ it("returns value unchanged when secrets list is empty", () => {
81
+ const input = { key: "value" };
82
+ expect(redactSecrets(input, [])).toBe(input);
83
+ });
84
+ it("handles overlapping secrets (longer secret first)", () => {
85
+ expect(redactSecrets("my-secret-key", ["my-secret-key", "secret"])).toBe("[REDACTED]");
86
+ });
87
+ it("handles overlapping secrets (shorter secret first)", () => {
88
+ expect(redactSecrets("my-secret-key", ["secret", "my-secret-key"])).toBe("my-[REDACTED]-key");
89
+ });
90
+ });
91
+ });
92
+ //# sourceMappingURL=utils.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../src/utils.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "runtimeuse",
3
+ "version": "0.2.0",
4
+ "description": "AI agent runtime with WebSocket protocol, artifact handling, and secret management",
5
+ "license": "BSL-1.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "runtimeuse": "./dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit",
23
+ "dev-publish": "bash scripts/dev-publish.sh"
24
+ },
25
+ "dependencies": {
26
+ "@anthropic-ai/claude-agent-sdk": "^0.2.73",
27
+ "@openai/agents": "^0.6.0",
28
+ "archiver": "^7.0.1",
29
+ "chokidar": "^5.0.0",
30
+ "ignore": "^7.0.5",
31
+ "ws": "^8.18.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/archiver": "^7.0.0",
35
+ "@types/node": "^22.10.0",
36
+ "@types/ws": "^8.5.13",
37
+ "typescript": "^5.7.0",
38
+ "vitest": "^4.0.18"
39
+ }
40
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ ENV_FILE="$SCRIPT_DIR/../.env"
6
+ if [ -f "$ENV_FILE" ]; then
7
+ set -a
8
+ source "$ENV_FILE"
9
+ set +a
10
+ fi
11
+
12
+ S3_BUCKET="${S3_BUCKET:?S3_BUCKET env var is required (set in .env or pass inline)}"
13
+ S3_PREFIX="${S3_PREFIX:-local-dev}"
14
+ PRESIGN_EXPIRY="${PRESIGN_EXPIRY:-3600}"
15
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
16
+ ZIP_NAME="runtimeuse-dev-${TIMESTAMP}.zip"
17
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ STAGING_DIR=$(mktemp -d)
19
+
20
+ cleanup() { rm -rf "$STAGING_DIR" "$PROJECT_DIR/$ZIP_NAME"; }
21
+ trap cleanup EXIT
22
+
23
+ echo "Building..."
24
+ npm run build --prefix "$PROJECT_DIR"
25
+
26
+ echo "Staging package..."
27
+ cp "$PROJECT_DIR/package.json" "$STAGING_DIR/"
28
+ cp -r "$PROJECT_DIR/dist" "$STAGING_DIR/dist"
29
+ (cd "$STAGING_DIR" && npm install --omit=dev --ignore-scripts)
30
+
31
+ echo "Creating zip..."
32
+ (cd "$STAGING_DIR" && zip -qr "$PROJECT_DIR/$ZIP_NAME" .)
33
+
34
+ S3_KEY="${S3_PREFIX}/${ZIP_NAME}"
35
+ echo "Uploading to s3://${S3_BUCKET}/${S3_KEY}..."
36
+ aws s3 cp "$PROJECT_DIR/$ZIP_NAME" "s3://${S3_BUCKET}/${S3_KEY}"
37
+
38
+ URL=$(aws s3 presign "s3://${S3_BUCKET}/${S3_KEY}" --expires-in "$PRESIGN_EXPIRY")
39
+
40
+ echo ""
41
+ echo "Download URL (expires in ${PRESIGN_EXPIRY}s):"
42
+ echo "$URL"
43
+ echo ""
44
+ echo "Quick start:"
45
+ echo " curl -L \"$URL\" -o runtimeuse.zip && unzip -o runtimeuse.zip -d runtimeuse && node runtimeuse/dist/cli.js & echo \"RuntimeUse WS server started (PID \$!)\""
@@ -0,0 +1,26 @@
1
+ import type { Logger } from "./logger.js";
2
+
3
+ export interface AgentInvocation {
4
+ systemPrompt: string;
5
+ userPrompt: string;
6
+ outputFormat: { type: "json_schema"; schema: Record<string, unknown> };
7
+ model: string;
8
+ secrets: string[];
9
+ env: Record<string, string>;
10
+ signal: AbortSignal;
11
+ logger: Logger;
12
+ }
13
+
14
+ export interface AgentResult {
15
+ structuredOutput: Record<string, unknown>;
16
+ metadata?: Record<string, unknown>;
17
+ }
18
+
19
+ export interface MessageSender {
20
+ sendAssistantMessage(textBlocks: string[]): void;
21
+ sendErrorMessage(error: string, metadata?: Record<string, unknown>): void;
22
+ }
23
+
24
+ export interface AgentHandler {
25
+ run(invocation: AgentInvocation, sender: MessageSender): Promise<AgentResult>;
26
+ }
@@ -0,0 +1,320 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockWatcher = {
4
+ on: vi.fn().mockReturnThis(),
5
+ close: vi.fn().mockResolvedValue(undefined),
6
+ };
7
+
8
+ vi.mock("chokidar", () => ({
9
+ default: { watch: vi.fn(() => mockWatcher) },
10
+ }));
11
+
12
+ vi.mock("./storage.js", () => ({
13
+ uploadFile: vi.fn().mockResolvedValue(true),
14
+ }));
15
+
16
+ vi.mock("fs", async () => {
17
+ const actual = await vi.importActual<typeof import("fs")>("fs");
18
+ return {
19
+ ...actual,
20
+ default: {
21
+ ...actual,
22
+ existsSync: vi.fn(() => false),
23
+ readFileSync: vi.fn(() => ""),
24
+ createWriteStream: vi.fn(),
25
+ },
26
+ };
27
+ });
28
+
29
+ import fs from "fs";
30
+ import chokidar from "chokidar";
31
+ import {
32
+ ArtifactManager,
33
+ type ArtifactManagerConfig,
34
+ } from "./artifact-manager.js";
35
+ import { UploadTracker } from "./upload-tracker.js";
36
+ import { uploadFile } from "./storage.js";
37
+
38
+ function createManager(overrides: Partial<ArtifactManagerConfig> = {}) {
39
+ const send = vi.fn();
40
+ const uploadTracker = new UploadTracker();
41
+ const config: ArtifactManagerConfig = {
42
+ artifactsDir: "/tmp/artifacts",
43
+ uploadTracker,
44
+ send,
45
+ ...overrides,
46
+ };
47
+ const manager = new ArtifactManager(config);
48
+ return { manager, send, uploadTracker };
49
+ }
50
+
51
+ function getHandler(event: string) {
52
+ const call = mockWatcher.on.mock.calls.find((c: unknown[]) => c[0] === event);
53
+ if (!call) throw new Error(`No handler for "${event}"`);
54
+ return call[1] as (path: string, stats?: any) => void;
55
+ }
56
+
57
+ const fileStats = (size = 1024) => ({ isFile: () => true, size });
58
+ const dirStats = () => ({ isFile: () => false, size: 4096 });
59
+
60
+ describe("ArtifactManager", () => {
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ mockWatcher.on.mockReturnThis();
64
+ mockWatcher.close.mockResolvedValue(undefined);
65
+ });
66
+
67
+ describe("constructor", () => {
68
+ it("creates a chokidar watcher on the artifacts directory", () => {
69
+ createManager();
70
+ expect(chokidar.watch).toHaveBeenCalledWith("/tmp/artifacts", {
71
+ awaitWriteFinish: true,
72
+ alwaysStat: true,
73
+ });
74
+ });
75
+
76
+ it("registers add and change handlers", () => {
77
+ createManager();
78
+ const events = mockWatcher.on.mock.calls.map((c: unknown[]) => c[0]);
79
+ expect(events).toContain("add");
80
+ expect(events).toContain("change");
81
+ });
82
+ });
83
+
84
+ describe("file event handling", () => {
85
+ it("sends upload request for a new file", () => {
86
+ const { send } = createManager();
87
+ getHandler("add")("/tmp/artifacts/screenshot.png", fileStats());
88
+
89
+ expect(send).toHaveBeenCalledWith({
90
+ message_type: "artifact_upload_request_message",
91
+ artifact_type: "screenshot",
92
+ filename: "screenshot.png",
93
+ filepath: "/tmp/artifacts/screenshot.png",
94
+ });
95
+ });
96
+
97
+ it("sends upload request for a changed file", () => {
98
+ const { send } = createManager();
99
+ getHandler("change")("/tmp/artifacts/video.webm", fileStats());
100
+
101
+ expect(send).toHaveBeenCalledWith(
102
+ expect.objectContaining({
103
+ artifact_type: "video",
104
+ filename: "video.webm",
105
+ }),
106
+ );
107
+ });
108
+
109
+ it.each([
110
+ ["/a/video.webm", "video"],
111
+ ["/a/shot.png", "screenshot"],
112
+ ["/a/calls.ndjson", "tool_calls"],
113
+ ["/a/script.js", "javascript"],
114
+ ["/a/script.py", "python"],
115
+ ["/a/run.sh", "shellscript"],
116
+ ["/a/data.txt", "other"],
117
+ ["/a/readme.md", "other"],
118
+ ])("maps %s to artifact type %s", (filepath, expectedType) => {
119
+ const { send } = createManager();
120
+ getHandler("add")(filepath, fileStats());
121
+ expect(send).toHaveBeenCalledWith(
122
+ expect.objectContaining({ artifact_type: expectedType }),
123
+ );
124
+ });
125
+
126
+ it("skips directories", () => {
127
+ const { send } = createManager();
128
+ getHandler("add")("/tmp/artifacts/subdir", dirStats());
129
+ expect(send).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it("skips empty files", () => {
133
+ const { send } = createManager();
134
+ getHandler("add")("/tmp/artifacts/empty.png", fileStats(0));
135
+ expect(send).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it("skips files with no stats", () => {
139
+ const { send } = createManager();
140
+ getHandler("add")("/tmp/artifacts/mystery.png", undefined);
141
+ expect(send).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+
145
+ describe("handleUploadResponse", () => {
146
+ it("uploads the file via presigned URL", async () => {
147
+ const { manager } = createManager();
148
+ getHandler("add")("/tmp/artifacts/shot.png", fileStats());
149
+
150
+ await manager.handleUploadResponse({
151
+ message_type: "artifact_upload_response_message",
152
+ filename: "shot.png",
153
+ filepath: "/tmp/artifacts/shot.png",
154
+ presigned_url: "https://s3.example.com/upload",
155
+ content_type: "image/png",
156
+ });
157
+
158
+ expect(uploadFile).toHaveBeenCalledWith(
159
+ "/tmp/artifacts/shot.png",
160
+ "https://s3.example.com/upload",
161
+ "image/png",
162
+ expect.any(Object),
163
+ );
164
+ });
165
+
166
+ it("resolves the pending request after upload", async () => {
167
+ const { manager } = createManager();
168
+ getHandler("add")("/tmp/artifacts/shot.png", fileStats());
169
+
170
+ const waitPromise = manager.waitForPendingRequests(5000);
171
+
172
+ await manager.handleUploadResponse({
173
+ message_type: "artifact_upload_response_message",
174
+ filename: "shot.png",
175
+ filepath: "/tmp/artifacts/shot.png",
176
+ presigned_url: "https://s3.example.com/upload",
177
+ content_type: "image/png",
178
+ });
179
+
180
+ await waitPromise;
181
+ });
182
+
183
+ it("swallows ENOENT errors", async () => {
184
+ const enoent = new Error("File not found");
185
+ enoent.name = "ENOENT";
186
+ vi.mocked(uploadFile).mockRejectedValueOnce(enoent);
187
+
188
+ const { manager } = createManager();
189
+ getHandler("add")("/tmp/artifacts/gone.png", fileStats());
190
+
191
+ await expect(
192
+ manager.handleUploadResponse({
193
+ message_type: "artifact_upload_response_message",
194
+ filename: "gone.png",
195
+ filepath: "/tmp/artifacts/gone.png",
196
+ presigned_url: "https://s3.example.com/upload",
197
+ content_type: "image/png",
198
+ }),
199
+ ).resolves.toBeUndefined();
200
+ });
201
+
202
+ it("rethrows non-ENOENT errors", async () => {
203
+ vi.mocked(uploadFile).mockRejectedValueOnce(new Error("network error"));
204
+
205
+ const { manager } = createManager();
206
+ getHandler("add")("/tmp/artifacts/shot.png", fileStats());
207
+
208
+ await expect(
209
+ manager.handleUploadResponse({
210
+ message_type: "artifact_upload_response_message",
211
+ filename: "shot.png",
212
+ filepath: "/tmp/artifacts/shot.png",
213
+ presigned_url: "https://s3.example.com/upload",
214
+ content_type: "image/png",
215
+ }),
216
+ ).rejects.toThrow("network error");
217
+ });
218
+ });
219
+
220
+ describe("waitForPendingRequests", () => {
221
+ it("resolves immediately when no pending requests", async () => {
222
+ const { manager } = createManager();
223
+ await manager.waitForPendingRequests(1000);
224
+ });
225
+
226
+ it("times out if requests are not completed", async () => {
227
+ const { manager } = createManager();
228
+ getHandler("add")("/tmp/artifacts/shot.png", fileStats());
229
+
230
+ const start = Date.now();
231
+ await manager.waitForPendingRequests(50);
232
+ expect(Date.now() - start).toBeGreaterThanOrEqual(40);
233
+ });
234
+ });
235
+
236
+ describe("stopWatching", () => {
237
+ it("closes the chokidar watcher", async () => {
238
+ const { manager } = createManager();
239
+ await manager.stopWatching();
240
+ expect(mockWatcher.close).toHaveBeenCalled();
241
+ });
242
+ });
243
+
244
+ describe(".artifactignore", () => {
245
+ it("skips files matching .artifactignore patterns", () => {
246
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
247
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("*.log\ntmp/\n");
248
+
249
+ const { send } = createManager();
250
+ getHandler("add")("/tmp/artifacts/debug.log", fileStats());
251
+ expect(send).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("skips files in ignored directories", () => {
255
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
256
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("tmp/\n");
257
+
258
+ const { send } = createManager();
259
+ getHandler("add")("/tmp/artifacts/tmp/output.png", fileStats());
260
+ expect(send).not.toHaveBeenCalled();
261
+ });
262
+
263
+ it("allows files not matching .artifactignore patterns", () => {
264
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
265
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("*.log\n");
266
+
267
+ const { send } = createManager();
268
+ getHandler("add")("/tmp/artifacts/screenshot.png", fileStats());
269
+ expect(send).toHaveBeenCalledWith(
270
+ expect.objectContaining({ filename: "screenshot.png" }),
271
+ );
272
+ });
273
+
274
+ it("uploads all files when no .artifactignore exists", () => {
275
+ vi.mocked(fs.existsSync).mockReturnValueOnce(false);
276
+
277
+ const { send } = createManager();
278
+ getHandler("add")("/tmp/artifacts/debug.log", fileStats());
279
+ expect(send).toHaveBeenCalled();
280
+ });
281
+
282
+ it("reloads patterns when .artifactignore is added at runtime", () => {
283
+ const { send } = createManager();
284
+
285
+ getHandler("add")("/tmp/artifacts/debug.log", fileStats());
286
+ expect(send).toHaveBeenCalledTimes(1);
287
+
288
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
289
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("*.log\n");
290
+ getHandler("add")("/tmp/artifacts/.artifactignore", fileStats());
291
+
292
+ send.mockClear();
293
+ getHandler("add")("/tmp/artifacts/debug.log", fileStats());
294
+ expect(send).not.toHaveBeenCalled();
295
+ });
296
+
297
+ it("reloads patterns when .artifactignore is modified at runtime", () => {
298
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
299
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("*.log\n");
300
+ const { send } = createManager();
301
+
302
+ getHandler("add")("/tmp/artifacts/screenshot.png", fileStats());
303
+ expect(send).toHaveBeenCalledTimes(1);
304
+
305
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
306
+ vi.mocked(fs.readFileSync).mockReturnValueOnce("*.log\n*.png\n");
307
+ getHandler("change")("/tmp/artifacts/.artifactignore", fileStats());
308
+
309
+ send.mockClear();
310
+ getHandler("add")("/tmp/artifacts/screenshot.png", fileStats());
311
+ expect(send).not.toHaveBeenCalled();
312
+ });
313
+
314
+ it("does not upload the .artifactignore file itself", () => {
315
+ const { send } = createManager();
316
+ getHandler("add")("/tmp/artifacts/.artifactignore", fileStats());
317
+ expect(send).not.toHaveBeenCalled();
318
+ });
319
+ });
320
+ });