@getopencrawl/cli 0.1.0 → 0.1.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/README.md +18 -0
- package/bin/opencrawl.mjs +31 -6
- package/package.json +1 -5
- package/src/accounts.test.mjs +223 -0
- package/src/agent.mjs +95 -0
- package/src/agent.test.mjs +58 -0
- package/src/browser.test.mjs +72 -0
- package/src/capture.test.mjs +171 -0
- package/src/client.mjs +13 -5
- package/src/client.test.mjs +34 -0
- package/src/connect.test.mjs +131 -0
- package/src/context.test.mjs +32 -0
- package/src/jobs.mjs +12 -3
- package/src/jobs.test.mjs +67 -0
- package/src/mcp-conversations.test.mjs +80 -0
- package/src/monitors.test.mjs +62 -0
- package/src/product-monitor-login.test.mjs +140 -0
- package/src/product-monitors.mjs +12 -4
- package/src/product-monitors.test.mjs +69 -0
- package/src/research-agent-errors.test.mjs +110 -0
- package/src/research-agent-feedback.test.mjs +28 -0
- package/src/research-agent.mjs +4 -4
- package/src/research-agent.test.mjs +231 -0
- package/src/research-answer-contract.test.mjs +14 -0
- package/src/research-answer-view.test.mjs +27 -0
- package/src/research-completion.test.mjs +110 -0
- package/src/research-event.test.mjs +57 -0
- package/src/research-presentation.test.mjs +56 -0
- package/src/research.mjs +15 -7
- package/src/research.test.mjs +97 -0
- package/src/sessions.test.mjs +151 -0
- package/src/tasks.test.mjs +83 -0
- package/src/upload-commands.test.mjs +140 -0
- package/src/uploads.test.mjs +64 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const CLI = fileURLToPath(new URL('../bin/opencrawl.mjs', import.meta.url));
|
|
12
|
+
const jobId = 'job_' + 'a'.repeat(32);
|
|
13
|
+
async function fixture(t) {
|
|
14
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencrawl-commands-'));
|
|
15
|
+
const receipts = new Map(), calls = [], state = { jobStatus: 'completed', losePrepare: false, loseContent: false, failLookup: 0, holdJob: false };
|
|
16
|
+
const server = http.createServer(async (req, res) => {
|
|
17
|
+
const chunks = []; for await (const chunk of req) chunks.push(chunk);
|
|
18
|
+
const raw = Buffer.concat(chunks), body = req.headers['content-type']?.startsWith('application/json') ? JSON.parse(raw) : raw;
|
|
19
|
+
calls.push({ path: req.url, method: req.method, body });
|
|
20
|
+
const send = (status, value) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(value)); };
|
|
21
|
+
const job = () => ({ id: jobId, kind: 'upload', status: state.jobStatus, terminal: ['completed','failed','cancelled','discarded'].includes(state.jobStatus), indexed_documents: state.jobStatus === 'completed' ? 1 : 0, filename: 'test\u001b[2J.pdf', documents: [] });
|
|
22
|
+
if (req.url === '/api/auth/me') return send(200, { user: { id: 'usr_test' }, workspace: { id: 'ws_test' } });
|
|
23
|
+
if (req.url === '/api/v1/uploads/prepare') {
|
|
24
|
+
let receipt = receipts.get(body.request_key);
|
|
25
|
+
if (receipt && JSON.stringify(receipt.descriptor) !== JSON.stringify(body)) return send(409, { detail: { code: 'descriptor_mismatch', message: 'raw body must not escape' } });
|
|
26
|
+
if (!receipt) { receipt = { id: 'receipt_' + body.request_key.replaceAll('-', ''), request_key: body.request_key, state: 'prepared', operation: body.operation, job: null, descriptor: body }; receipt.upload_url = `/api/v1/uploads/receipts/${receipt.id}/content`; receipts.set(body.request_key, receipt); }
|
|
27
|
+
if (receipt.state === 'accepted') receipt.job = job();
|
|
28
|
+
if (state.losePrepare) { state.losePrepare = false; res.destroy(); return; }
|
|
29
|
+
return send(200, receipt);
|
|
30
|
+
}
|
|
31
|
+
if (req.url.endsWith('/content')) {
|
|
32
|
+
const receipt = [...receipts.values()].find(item => item.upload_url === req.url);
|
|
33
|
+
assert(receipt); assert(raw.includes(Buffer.from('synthetic fixture'))); receipt.state = 'accepted'; receipt.job = job();
|
|
34
|
+
if (state.loseContent) { state.loseContent = false; res.destroy(); return; }
|
|
35
|
+
return send(200, receipt);
|
|
36
|
+
}
|
|
37
|
+
if (req.url.startsWith('/api/v1/uploads/receipts/by-key/')) {
|
|
38
|
+
if (state.failLookup-- > 0) { res.destroy(); return; }
|
|
39
|
+
const receipt = receipts.get(req.url.split('/').at(-1)); if (!receipt) return send(404, { detail: 'Unavailable' });
|
|
40
|
+
if (receipt.state === 'accepted') receipt.job = job(); return send(200, receipt);
|
|
41
|
+
}
|
|
42
|
+
if (req.url === `/api/v1/jobs/${jobId}`) { if (state.holdJob) return; return send(200, job()); }
|
|
43
|
+
if (req.url === '/api/v1/search') return send(200, { results: [{ title: 'bad\u001b[2J', snippet: 'keep\nline\u001b]52;x\u0007', url: 'a\rb' }] });
|
|
44
|
+
if (req.url === '/api/v1/documents') return send(200, { id: 'doc_' + 'c'.repeat(32), title: 'synchronous' });
|
|
45
|
+
return send(404, { detail: 'Unknown route' });
|
|
46
|
+
});
|
|
47
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
48
|
+
t.after(async () => { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); await fs.rm(dir, { recursive: true, force: true }); });
|
|
49
|
+
const env = { PATH: process.env.PATH, HOME: dir, OPENCRAWL_CONFIG_DIR: path.join(dir, 'config'), OPENCRAWL_API_KEY: 'oc_synthetic_test_only', OPENCRAWL_URL: `http://127.0.0.1:${server.address().port}` };
|
|
50
|
+
const run = (args, onSpawn) => new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(process.execPath, [CLI, ...args], { env, cwd: dir }); let stdout = '', stderr = '';
|
|
52
|
+
child.stdout.on('data', data => { stdout += data; }); child.stderr.on('data', data => { stderr += data; });
|
|
53
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(Error('CLI test timeout')); }, 10000);
|
|
54
|
+
child.on('error', reject); child.on('exit', (code, signal) => { clearTimeout(timer); resolve({ code, signal, stdout, stderr }); });
|
|
55
|
+
onSpawn?.(child);
|
|
56
|
+
});
|
|
57
|
+
const file = path.join(dir, 'fixture.pdf'); await fs.writeFile(file, 'synthetic fixture');
|
|
58
|
+
return { dir, file, run, calls, state, receipts, env };
|
|
59
|
+
}
|
|
60
|
+
test('real CLI waits finitely, preserves job receipt on timeout/failure, and can later inspect completion', async t => {
|
|
61
|
+
const f = await fixture(t); f.state.jobStatus = 'queued';
|
|
62
|
+
const timed = await f.run(['ingest', f.file, '--wait', '--timeout', '1', '--json']);
|
|
63
|
+
assert.equal(timed.code, 1); const receipt = JSON.parse(timed.stdout); assert.equal(receipt.job_id, jobId); assert.equal(receipt.wait.outcome, 'timeout'); assert(timed.stderr.includes(receipt.request_key));
|
|
64
|
+
f.state.jobStatus = 'completed';
|
|
65
|
+
const completed = await f.run(['jobs', 'wait', jobId, '--timeout', '2', '--json']);
|
|
66
|
+
assert.equal(completed.code, 0); assert.equal(JSON.parse(completed.stdout).wait.outcome, 'completed');
|
|
67
|
+
f.state.jobStatus = 'failed'; const failed = await f.run(['jobs', 'wait', jobId, '--json']);
|
|
68
|
+
assert.equal(failed.code, 1); assert.equal(JSON.parse(failed.stdout).job_id, jobId); assert.equal(JSON.parse(failed.stdout).wait.outcome, 'failed');
|
|
69
|
+
assert.equal(f.calls.filter(call => call.path.endsWith('/content')).length, 1);
|
|
70
|
+
});
|
|
71
|
+
test('real dropped acceptance is looked up, explicit reuse never uploads twice, changed scope conflicts', async t => {
|
|
72
|
+
const f = await fixture(t); f.state.loseContent = true;
|
|
73
|
+
const first = await f.run(['ingest', f.file, '--json']); assert.equal(first.code, 0); const result = JSON.parse(first.stdout); assert.equal(result.job_id, jobId);
|
|
74
|
+
const repeated = await f.run(['ingest', f.file, '--request-key', result.request_key, '--json']); assert.equal(repeated.code, 0); assert.equal(JSON.parse(repeated.stdout).job_id, jobId);
|
|
75
|
+
const conflict = await f.run(['ingest', f.file, '--request-key', result.request_key, '--visibility', 'workspace', '--json']);
|
|
76
|
+
assert.equal(conflict.code, 1); assert.equal(JSON.parse(conflict.stdout).error.status, 409); assert.equal(f.calls.filter(call => call.path.endsWith('/content')).length, 1);
|
|
77
|
+
});
|
|
78
|
+
test('real lost prepare response requires explicit same-key retry and never auto-transfers', async t => {
|
|
79
|
+
const f = await fixture(t); f.state.losePrepare = true;
|
|
80
|
+
const first = await f.run(['ingest', f.file, '--json']); assert.equal(first.code, 1); const result = JSON.parse(first.stdout); assert.equal(result.outcome, 'prepared'); assert.equal(f.calls.filter(call => call.path.endsWith('/content')).length, 0);
|
|
81
|
+
const retried = await f.run(['ingest', f.file, '--request-key', result.request_key, '--wait', '--json']); assert.equal(retried.code, 0); assert.equal(JSON.parse(retried.stdout).wait.outcome, 'completed');
|
|
82
|
+
});
|
|
83
|
+
test('watch persists unknown acceptance across process restart and key rotation without resending pending bytes', async t => {
|
|
84
|
+
const f = await fixture(t); const folder = path.join(f.dir, 'exports'); await fs.mkdir(folder); await fs.writeFile(path.join(folder, 'chat.json'), JSON.stringify({ messages: [{ content: 'synthetic fixture' }] }));
|
|
85
|
+
f.state.loseContent = true; f.state.failLookup = 1;
|
|
86
|
+
const first = await f.run(['watch', folder, '--provider', 'generic', '--once', '--json']); assert.equal(first.code, 1); assert.equal(JSON.parse(first.stdout).outcomes[0].outcome, 'unconfirmed');
|
|
87
|
+
const stateFile = (await fs.readdir(path.join(f.dir, 'config'))).find(name => /^watch-.*\.json$/.test(name));
|
|
88
|
+
const saved = JSON.parse(await fs.readFile(path.join(f.dir, 'config', stateFile), 'utf8')); assert(Object.values(saved.entries)[0].pending.request_key);
|
|
89
|
+
f.env.OPENCRAWL_API_KEY = 'oc_synthetic_rotated_same_account';
|
|
90
|
+
const second = await f.run(['watch', folder, '--provider', 'generic', '--once', '--json']); assert.equal(second.code, 0); assert.equal(JSON.parse(second.stdout).outcomes[0].outcome, 'accepted');
|
|
91
|
+
const third = await f.run(['watch', folder, '--provider', 'generic', '--once', '--json']); assert.equal(third.code, 0); assert.equal(JSON.parse(third.stdout).outcomes.length, 0);
|
|
92
|
+
assert.equal(f.calls.filter(call => call.path.endsWith('/content')).length, 1); assert.equal(f.calls.filter(call => call.path.endsWith('/prepare')).length, 1);
|
|
93
|
+
});
|
|
94
|
+
test('interrupt aborts an in-flight job request locally and keeps exact ID', async t => {
|
|
95
|
+
const f = await fixture(t); f.state.holdJob = true;
|
|
96
|
+
const result = await f.run(['jobs', 'wait', jobId, '--json'], child => {
|
|
97
|
+
const poll = setInterval(() => { if (f.calls.some(call => call.path === `/api/v1/jobs/${jobId}`)) { clearInterval(poll); child.kill('SIGINT'); } }, 10);
|
|
98
|
+
child.on('exit', () => clearInterval(poll));
|
|
99
|
+
});
|
|
100
|
+
assert.equal(result.code, 1); assert.equal(result.signal, null); assert.equal(JSON.parse(result.stdout).job_id, jobId); assert.equal(JSON.parse(result.stdout).wait.outcome, 'interrupted'); assert.equal(f.calls.length, 1);
|
|
101
|
+
});
|
|
102
|
+
test('strict flags fail with usable JSON and no network; human search escapes controls while JSON preserves bytes', async t => {
|
|
103
|
+
const f = await fixture(t);
|
|
104
|
+
for (const args of [['ingest', f.file, '--timeout', '1'], ['ingest', f.file, '--wait=false'], ['ingest', f.file, '--wait', '--wait'], ['jobs', 'wait', jobId, '--retry'], ['ingest', f.file, '--surprise', 'x']]) {
|
|
105
|
+
const result = await f.run([...args, '--json']); assert.equal(result.code, 1); assert(JSON.parse(result.stdout).error);
|
|
106
|
+
}
|
|
107
|
+
assert.equal(f.calls.length, 0);
|
|
108
|
+
const human = await f.run(['search', 'test']); assert.equal(human.code, 0); assert(!human.stdout.includes('\u001b')); assert(human.stdout.includes('\\u001b')); assert(human.stdout.includes('keep\nline'));
|
|
109
|
+
const json = await f.run(['search', 'test', '--json']); assert.equal(JSON.parse(json.stdout).results[0].title, 'bad\u001b[2J');
|
|
110
|
+
const text = path.join(f.dir, 'note.txt'); await fs.writeFile(text, 'synchronous note');
|
|
111
|
+
const inline = await f.run(['import', text, '--wait', '--json']); assert.equal(inline.code, 0); assert.equal(JSON.parse(inline.stdout).id, 'doc_' + 'c'.repeat(32)); assert(!JSON.parse(inline.stdout).job_id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
test('watch treats changed explicit source or visibility as a separate intent and preserves old pending scope', async t => {
|
|
116
|
+
const f = await fixture(t); const folder = path.join(f.dir, 'exports'); await fs.mkdir(folder); await fs.writeFile(path.join(folder, 'chat.json'), '{"content":"synthetic fixture"}');
|
|
117
|
+
f.state.losePrepare = true;
|
|
118
|
+
const first = await f.run(['watch', folder, '--provider', 'generic', '--source', 'src_private', '--once', '--json']);
|
|
119
|
+
assert.equal(first.code, 1);
|
|
120
|
+
const next = await f.run(['watch', folder, '--provider', 'generic', '--source', 'src_shared', '--visibility', 'workspace', '--once', '--json']);
|
|
121
|
+
assert.equal(next.code, 0);
|
|
122
|
+
const states = await Promise.all((await fs.readdir(path.join(f.dir, 'config'))).filter(name => name.endsWith('.json')).map(name => fs.readFile(path.join(f.dir, 'config', name), 'utf8').then(JSON.parse)));
|
|
123
|
+
assert.equal(states.length, 2);
|
|
124
|
+
assert(Object.values(states.find(value => value.scope.source_id === 'src_private').entries)[0].pending);
|
|
125
|
+
assert(Object.values(states.find(value => value.scope.source_id === 'src_shared').entries)[0].accepted);
|
|
126
|
+
assert.equal(f.calls.filter(call => call.path.endsWith('/prepare')).length, 2);
|
|
127
|
+
assert.equal(f.calls.filter(call => call.path.endsWith('/content')).length, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('unscoped legacy watcher state is preserved and blocks automatic migration or new uploads', async t => {
|
|
131
|
+
const f = await fixture(t); const folder = path.join(f.dir, 'exports'); await fs.mkdir(folder); const file = path.join(folder, 'chat.json'); await fs.writeFile(file, '{"content":"synthetic fixture"}');
|
|
132
|
+
const root = await fs.realpath(folder), identity = crypto.createHash('sha256').update(f.env.OPENCRAWL_API_KEY).digest('hex').slice(0,16);
|
|
133
|
+
const name = crypto.createHash('sha256').update(`${f.env.OPENCRAWL_URL}:${identity}:${root}:generic`).digest('hex').slice(0,24);
|
|
134
|
+
const config = path.join(f.dir, 'config'); await fs.mkdir(config);
|
|
135
|
+
const legacy = path.join(config, `watch-${name}.json`), original = JSON.stringify({ [file]: '25:1' }); await fs.writeFile(legacy, original);
|
|
136
|
+
const result = await f.run(['watch', folder, '--provider', 'generic', '--once', '--json']);
|
|
137
|
+
assert.equal(result.code, 1); assert(JSON.parse(result.stdout).error.message.includes('Older watcher state'));
|
|
138
|
+
assert.equal(await fs.readFile(legacy, 'utf8'), original);
|
|
139
|
+
assert(!f.calls.some(call => call.method === 'POST')); assert.equal((await fs.readdir(config)).length, 1);
|
|
140
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { submitUpload, fileDescriptor, followUpload, uploadExitCode, requestKey } from './uploads.mjs';
|
|
8
|
+
|
|
9
|
+
const key = 'abcd1234-1234-4234-8234-abcd12345678', id = 'receipt_' + 'b'.repeat(32), jobId = 'job_' + 'a'.repeat(32);
|
|
10
|
+
const receipt = (state, overrides = {}) => ({ id, request_key: key, state, operation: 'upload', upload_url: `/api/v1/uploads/receipts/${id}/content`, job: state === 'accepted' ? { id: jobId, kind: 'upload', status: 'queued' } : null, ...overrides });
|
|
11
|
+
async function fixture(t) { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencrawl-upload-')); t.after(() => fs.rm(dir, { recursive: true, force: true })); const file = path.join(dir, 'binary.pdf'); await fs.writeFile(file, Buffer.from([1,2,0,255,127])); return file; }
|
|
12
|
+
const quiet = { stderr: () => {} };
|
|
13
|
+
test('upload hashes exact bytes then sends one multipart file and no duplicated scope fields', async t => {
|
|
14
|
+
const file = await fixture(t); const events = [];
|
|
15
|
+
const client = { request: async (route, options) => {
|
|
16
|
+
events.push(route);
|
|
17
|
+
if (route.endsWith('/prepare')) { assert.equal(options.body.sha256, crypto.createHash('sha256').update(await fs.readFile(file)).digest('hex')); assert.equal(options.body.visibility, 'workspace'); assert.equal(options.body.source_id, 'src_test'); return receipt('prepared'); }
|
|
18
|
+
assert.deepEqual([...options.body.keys()], ['file']); assert.deepEqual(Buffer.from(await options.body.get('file').arrayBuffer()), await fs.readFile(file)); return receipt('accepted');
|
|
19
|
+
} };
|
|
20
|
+
const result = await submitUpload(file, client, { 'request-key': key, source: 'src_test', visibility: 'workspace' }, { ...quiet, onIntent: async () => { events.push('persist-intent'); } });
|
|
21
|
+
assert.equal(events[0], 'persist-intent'); assert.equal(events.length, 3); assert.equal(result.job_id, jobId); assert.equal(uploadExitCode(result, false), 0);
|
|
22
|
+
});
|
|
23
|
+
test('accepted, receiving, and expired prepare receipts never resend file bytes', async t => {
|
|
24
|
+
const file = await fixture(t);
|
|
25
|
+
for (const state of ['accepted','receiving','expired']) {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const result = await submitUpload(file, { request: async route => { calls.push(route); return receipt(state); } }, { 'request-key': key }, quiet);
|
|
28
|
+
assert.deepEqual(calls, ['/api/v1/uploads/prepare']); assert.equal(result.outcome, state);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
test('lost prepare response only looks up, even when lookup says prepared', async t => {
|
|
32
|
+
const file = await fixture(t); const calls = [];
|
|
33
|
+
const result = await submitUpload(file, { request: async route => { calls.push(route); if (calls.length === 1) throw Error('dropped'); return receipt('prepared'); } }, { 'request-key': key }, quiet);
|
|
34
|
+
assert.deepEqual(calls, ['/api/v1/uploads/prepare', `/api/v1/uploads/receipts/by-key/${key}`]); assert.equal(result.outcome, 'prepared'); assert.equal(uploadExitCode(result, false), 1);
|
|
35
|
+
});
|
|
36
|
+
test('lost acceptance resolves to original job by lookup without resending', async t => {
|
|
37
|
+
const file = await fixture(t); const calls = [];
|
|
38
|
+
const result = await submitUpload(file, { request: async route => { calls.push(route); if (route.endsWith('/prepare')) return receipt('prepared'); if (route.endsWith('/content')) throw Error('dropped acceptance'); return receipt('accepted'); } }, { 'request-key': key }, quiet);
|
|
39
|
+
assert.equal(calls.length, 3); assert.equal(result.job_id, jobId); assert.equal(result.outcome, 'accepted'); assert.equal(calls.filter(route => route.endsWith('/content')).length, 1);
|
|
40
|
+
});
|
|
41
|
+
test('descriptor conflicts do not treat the old accepted job as a new successful upload', async t => {
|
|
42
|
+
const file = await fixture(t); let calls = 0;
|
|
43
|
+
const result = await submitUpload(file, { request: async () => { calls++; const error = Error('private server body'); error.status = 409; throw error; } }, { 'request-key': key }, quiet);
|
|
44
|
+
assert.equal(calls, 1); assert.equal(result.request_key, key); assert.equal(result.job_id, null); assert.equal(result.error.status, 409); assert(!JSON.stringify(result).includes('private server'));
|
|
45
|
+
});
|
|
46
|
+
test('unknown outcomes retain request key and receipt ID; foreign upload routes never receive bytes', async t => {
|
|
47
|
+
const file = await fixture(t); const result = await submitUpload(file, { request: async route => { if (route.endsWith('/prepare')) return receipt('prepared'); throw Error('network'); } }, { 'request-key': key }, quiet);
|
|
48
|
+
assert.equal(result.request_key, key); assert.equal(result.receipt_id, id); assert.equal(result.job_id, null); assert.equal(result.outcome, 'unconfirmed'); assert.equal(result.receipt.state, 'prepared');
|
|
49
|
+
let calls = 0; const foreign = await submitUpload(file, { request: async () => { calls++; return receipt('prepared', { upload_url: 'https://other.example/upload' }); } }, { 'request-key': key }, quiet);
|
|
50
|
+
assert.equal(calls, 1); assert.equal(foreign.error.code, 'invalid_upload_route');
|
|
51
|
+
});
|
|
52
|
+
test('receiving wait checks receipt then existing job with one bounded deadline', async () => {
|
|
53
|
+
let time = 0; const calls = [];
|
|
54
|
+
const result = await followUpload({ request_key: key, receipt_id: id, receipt: receipt('receiving'), job_id: null, outcome: 'receiving' }, { request: async route => { calls.push(route); return route.includes('/receipts/') ? receipt('accepted') : { id: jobId, status: 'completed', kind: 'upload' }; } }, { timeout: 3, now: () => time, sleep: async ms => { time += ms; } });
|
|
55
|
+
assert.equal(result.wait.outcome, 'completed'); assert.deepEqual(calls, [`/api/v1/uploads/receipts/by-key/${key}`, `/api/v1/jobs/${jobId}`]);
|
|
56
|
+
const timeout = await followUpload({ request_key: key, receipt: receipt('receiving'), outcome: 'receiving' }, { request: async () => { throw Error('not reached'); } }, { timeout: 1, now: () => time, sleep: async ms => { time += ms; } });
|
|
57
|
+
assert.equal(timeout.wait.outcome, 'timeout'); assert.equal(timeout.request_key, key);
|
|
58
|
+
});
|
|
59
|
+
test('file validation and failed durable intent precede all network activity', async t => {
|
|
60
|
+
const file = await fixture(t); let calls = 0; const client = { request: async () => { calls++; } };
|
|
61
|
+
await assert.rejects(submitUpload(file, client, {}, { ...quiet, onIntent: async () => { throw Error('disk full'); } }), /disk full/);
|
|
62
|
+
await assert.rejects(fileDescriptor(file, { visibility: 'public' }), /visibility/);
|
|
63
|
+
assert.throws(() => requestKey('invalid'), /UUID/); await fs.writeFile(file, ''); await assert.rejects(fileDescriptor(file), /nonempty/); assert.equal(calls, 0);
|
|
64
|
+
});
|