Skip to content

Commit 9c58da6

Browse files
committed
feat: add upload cache preflight
1 parent 52d9707 commit 9c58da6

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import { test, afterEach } from 'vitest';
2+
import assert from 'node:assert/strict';
3+
import { createHash, randomUUID } from 'node:crypto';
4+
import fs from 'node:fs';
5+
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
6+
import os from 'node:os';
7+
import path from 'node:path';
8+
import { once } from 'node:events';
9+
import { uploadArtifact } from '../upload-client.ts';
10+
11+
const TEST_TOKEN = 'agent-device-upload-test-token';
12+
const tempDirs: string[] = [];
13+
14+
afterEach(async () => {
15+
for (const dir of tempDirs) {
16+
await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {});
17+
}
18+
tempDirs.length = 0;
19+
});
20+
21+
test('uploadArtifact returns preflight uploadId without uploading bytes on cache hit', async () => {
22+
const content = 'cached-apk-payload';
23+
const artifactPath = createTempFile('app.apk', content);
24+
const expectedHash = sha256(content);
25+
let uploadCalled = false;
26+
27+
const server = await startServer(async (req, res) => {
28+
if (req.method === 'POST' && req.url === '/upload/preflight') {
29+
assert.equal(req.headers.authorization, `Bearer ${TEST_TOKEN}`);
30+
assert.equal(req.headers['x-agent-device-token'], TEST_TOKEN);
31+
const body = JSON.parse((await readRequestBody(req)).toString('utf8')) as {
32+
hash: string;
33+
hashAlgorithm: string;
34+
fileName: string;
35+
sizeBytes: number;
36+
artifactType: string;
37+
};
38+
assert.equal(body.hash, expectedHash);
39+
assert.equal(body.hashAlgorithm, 'sha256');
40+
assert.equal(body.fileName, 'app.apk');
41+
assert.equal(body.sizeBytes, Buffer.byteLength(content));
42+
assert.equal(body.artifactType, 'file');
43+
sendJson(res, { ok: true, cacheHit: true, uploadId: 'upload-cached' });
44+
return;
45+
}
46+
if (req.method === 'POST' && req.url === '/upload') {
47+
uploadCalled = true;
48+
await readRequestBody(req);
49+
sendJson(res, { ok: true, uploadId: 'upload-unexpected' });
50+
return;
51+
}
52+
res.statusCode = 404;
53+
res.end('not found');
54+
});
55+
56+
try {
57+
const uploadId = await uploadArtifact({
58+
localPath: artifactPath,
59+
baseUrl: server.baseUrl,
60+
token: TEST_TOKEN,
61+
});
62+
assert.equal(uploadId, 'upload-cached');
63+
assert.equal(uploadCalled, false);
64+
} finally {
65+
await server.close();
66+
}
67+
});
68+
69+
test('uploadArtifact uploads with hash headers after preflight cache miss', async () => {
70+
const content = 'fresh-apk-payload';
71+
const artifactPath = createTempFile('app.apk', content);
72+
const expectedHash = sha256(content);
73+
const requests: string[] = [];
74+
75+
const server = await startServer(async (req, res) => {
76+
requests.push(`${req.method} ${req.url}`);
77+
if (req.method === 'POST' && req.url === '/upload/preflight') {
78+
const body = JSON.parse((await readRequestBody(req)).toString('utf8')) as {
79+
hash: string;
80+
};
81+
assert.equal(body.hash, expectedHash);
82+
sendJson(res, { ok: true, cacheHit: false });
83+
return;
84+
}
85+
if (req.method === 'POST' && req.url === '/upload') {
86+
assert.equal(req.headers['x-artifact-type'], 'file');
87+
assert.equal(req.headers['x-artifact-filename'], 'app.apk');
88+
assert.equal(req.headers['x-artifact-hash'], expectedHash);
89+
assert.equal(req.headers['x-artifact-hash-algorithm'], 'sha256');
90+
assert.equal((await readRequestBody(req)).toString('utf8'), content);
91+
sendJson(res, { ok: true, uploadId: 'upload-miss' });
92+
return;
93+
}
94+
res.statusCode = 404;
95+
res.end('not found');
96+
});
97+
98+
try {
99+
const uploadId = await uploadArtifact({
100+
localPath: artifactPath,
101+
baseUrl: server.baseUrl,
102+
token: TEST_TOKEN,
103+
});
104+
assert.equal(uploadId, 'upload-miss');
105+
assert.deepEqual(requests, ['POST /upload/preflight', 'POST /upload']);
106+
} finally {
107+
await server.close();
108+
}
109+
});
110+
111+
test('uploadArtifact falls back to upload when preflight is unsupported', async () => {
112+
const content = 'legacy-daemon-payload';
113+
const artifactPath = createTempFile('app.apk', content);
114+
const expectedHash = sha256(content);
115+
const requests: string[] = [];
116+
117+
const server = await startServer(async (req, res) => {
118+
requests.push(`${req.method} ${req.url}`);
119+
if (req.method === 'POST' && req.url === '/upload/preflight') {
120+
await readRequestBody(req);
121+
res.statusCode = 404;
122+
res.end('not found');
123+
return;
124+
}
125+
if (req.method === 'POST' && req.url === '/upload') {
126+
assert.equal(req.headers['x-artifact-hash'], expectedHash);
127+
assert.equal(req.headers['x-artifact-hash-algorithm'], 'sha256');
128+
assert.equal((await readRequestBody(req)).toString('utf8'), content);
129+
sendJson(res, { ok: true, uploadId: 'upload-legacy' });
130+
return;
131+
}
132+
res.statusCode = 404;
133+
res.end('not found');
134+
});
135+
136+
try {
137+
const uploadId = await uploadArtifact({
138+
localPath: artifactPath,
139+
baseUrl: server.baseUrl,
140+
token: TEST_TOKEN,
141+
});
142+
assert.equal(uploadId, 'upload-legacy');
143+
assert.deepEqual(requests, ['POST /upload/preflight', 'POST /upload']);
144+
} finally {
145+
await server.close();
146+
}
147+
});
148+
149+
test('uploadArtifact falls back to upload when preflight fails', async () => {
150+
const content = 'preflight-failure-payload';
151+
const artifactPath = createTempFile('app.apk', content);
152+
const expectedHash = sha256(content);
153+
const requests: string[] = [];
154+
155+
const server = await startServer(async (req, res) => {
156+
requests.push(`${req.method} ${req.url}`);
157+
if (req.method === 'POST' && req.url === '/upload/preflight') {
158+
await readRequestBody(req);
159+
res.statusCode = 503;
160+
res.end(JSON.stringify({ ok: false, error: 'cache temporarily unavailable' }));
161+
return;
162+
}
163+
if (req.method === 'POST' && req.url === '/upload') {
164+
assert.equal(req.headers['x-artifact-hash'], expectedHash);
165+
assert.equal(req.headers['x-artifact-hash-algorithm'], 'sha256');
166+
assert.equal((await readRequestBody(req)).toString('utf8'), content);
167+
sendJson(res, { ok: true, uploadId: 'upload-after-preflight-failure' });
168+
return;
169+
}
170+
res.statusCode = 404;
171+
res.end('not found');
172+
});
173+
174+
try {
175+
const uploadId = await uploadArtifact({
176+
localPath: artifactPath,
177+
baseUrl: server.baseUrl,
178+
token: TEST_TOKEN,
179+
});
180+
assert.equal(uploadId, 'upload-after-preflight-failure');
181+
assert.deepEqual(requests, ['POST /upload/preflight', 'POST /upload']);
182+
} finally {
183+
await server.close();
184+
}
185+
});
186+
187+
test('uploadArtifact skips preflight and hash headers for app bundle directories', async () => {
188+
const tempRoot = createTempDir();
189+
const appPath = path.join(tempRoot, 'Sample.app');
190+
fs.mkdirSync(appPath, { recursive: true });
191+
fs.writeFileSync(path.join(appPath, 'payload.txt'), 'app-bundle-payload');
192+
const requests: string[] = [];
193+
194+
const server = await startServer(async (req, res) => {
195+
requests.push(`${req.method} ${req.url}`);
196+
if (req.method === 'POST' && req.url === '/upload') {
197+
assert.equal(req.headers['x-artifact-type'], 'app-bundle');
198+
assert.equal(req.headers['x-artifact-filename'], 'Sample.app');
199+
assert.equal(req.headers['x-artifact-hash'], undefined);
200+
assert.equal(req.headers['x-artifact-hash-algorithm'], undefined);
201+
const body = await readRequestBody(req);
202+
assert.ok(body.length > 0);
203+
sendJson(res, { ok: true, uploadId: 'upload-app-bundle' });
204+
return;
205+
}
206+
res.statusCode = 500;
207+
res.end('unexpected request');
208+
});
209+
210+
try {
211+
const uploadId = await uploadArtifact({
212+
localPath: appPath,
213+
baseUrl: server.baseUrl,
214+
token: TEST_TOKEN,
215+
});
216+
assert.equal(uploadId, 'upload-app-bundle');
217+
assert.deepEqual(requests, ['POST /upload']);
218+
} finally {
219+
await server.close();
220+
}
221+
});
222+
223+
function createTempDir(): string {
224+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `agent-device-upload-client-${randomUUID()}-`));
225+
tempDirs.push(dir);
226+
return dir;
227+
}
228+
229+
function createTempFile(filename: string, content: string): string {
230+
const dir = createTempDir();
231+
const filePath = path.join(dir, filename);
232+
fs.writeFileSync(filePath, content);
233+
return filePath;
234+
}
235+
236+
function sha256(content: string): string {
237+
return createHash('sha256').update(content).digest('hex');
238+
}
239+
240+
async function startServer(
241+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>,
242+
): Promise<{ baseUrl: string; close: () => Promise<void> }> {
243+
const server = http.createServer((req, res) => {
244+
void handler(req, res).catch((error) => {
245+
res.statusCode = 500;
246+
res.end(error instanceof Error ? error.message : String(error));
247+
});
248+
});
249+
server.listen(0, '127.0.0.1');
250+
server.unref();
251+
await once(server, 'listening');
252+
const address = server.address();
253+
assert.ok(address && typeof address === 'object');
254+
return {
255+
baseUrl: `http://127.0.0.1:${address.port}`,
256+
close: async () => {
257+
await new Promise<void>((resolve, reject) => {
258+
server.close((error) => {
259+
if (error) reject(error);
260+
else resolve();
261+
});
262+
});
263+
},
264+
};
265+
}
266+
267+
async function readRequestBody(req: IncomingMessage): Promise<Buffer> {
268+
const chunks: Buffer[] = [];
269+
for await (const chunk of req) {
270+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
271+
}
272+
return Buffer.concat(chunks);
273+
}
274+
275+
function sendJson(res: ServerResponse, body: unknown): void {
276+
res.statusCode = 200;
277+
res.setHeader('content-type', 'application/json');
278+
res.end(JSON.stringify(body));
279+
}

0 commit comments

Comments
 (0)