mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
32ac8e73fd
* Fix Biome lint violations and add Biome CI check Biome was not configured to respect .gitignore, so ~92% of the 13,355 reported diagnostics came from gitignored build artifacts. Enable VCS integration (useIgnoreFile), apply safe auto-fixes across the repo, fix the remaining mechanical errors by hand, downgrade judgment-call a11y / dangerouslySetInnerHTML rules to warnings, and add a 'biome ci' job to the Lint workflow so violations block PRs going forward. * Use an empty changeset (no behavior change, no release needed)
206 lines
5.7 KiB
TypeScript
206 lines
5.7 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { hydrateData } from '@workflow/core/serialization-format';
|
|
import ts from 'typescript';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { getCLIRevivers } from '../../cli/src/lib/inspect/hydration.js';
|
|
import { getWebRevivers } from '../src/lib/hydration.js';
|
|
|
|
const SERIALIZABLE_TYPES_PATH = fileURLToPath(
|
|
new URL('../../core/src/serialization/types.ts', import.meta.url)
|
|
);
|
|
|
|
function propertyNameToString(name: ts.PropertyName): string | undefined {
|
|
if (
|
|
ts.isIdentifier(name) ||
|
|
ts.isStringLiteral(name) ||
|
|
ts.isNumericLiteral(name)
|
|
) {
|
|
return name.text;
|
|
}
|
|
}
|
|
|
|
function readSerializableSpecialKeys(): string[] {
|
|
const source = ts.createSourceFile(
|
|
SERIALIZABLE_TYPES_PATH,
|
|
readFileSync(SERIALIZABLE_TYPES_PATH, 'utf8'),
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TS
|
|
);
|
|
|
|
let keys: string[] | undefined;
|
|
source.forEachChild((node) => {
|
|
if (
|
|
ts.isInterfaceDeclaration(node) &&
|
|
node.name.text === 'SerializableSpecial'
|
|
) {
|
|
keys = node.members.flatMap((member) => {
|
|
if (!ts.isPropertySignature(member)) return [];
|
|
const key = propertyNameToString(member.name);
|
|
return key ? [key] : [];
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!keys) {
|
|
throw new Error(
|
|
`Could not find SerializableSpecial in ${SERIALIZABLE_TYPES_PATH}`
|
|
);
|
|
}
|
|
return keys.sort();
|
|
}
|
|
|
|
const SERIALIZABLE_SPECIAL_KEYS = readSerializableSpecialKeys();
|
|
|
|
const SERIALIZABLE_PAYLOADS: Record<string, unknown[]> = {
|
|
AbortController: [
|
|
['AbortController', 1],
|
|
{ streamName: 2, hookToken: 3, aborted: 4 },
|
|
'abort-stream',
|
|
'hook-token',
|
|
false,
|
|
],
|
|
AbortSignal: [
|
|
['AbortSignal', 1],
|
|
{ streamName: 2, hookToken: 3, aborted: 4 },
|
|
'abort-stream',
|
|
'hook-token',
|
|
false,
|
|
],
|
|
AggregateError: [
|
|
['AggregateError', 1],
|
|
{ message: 2, errors: 3 },
|
|
'all failed',
|
|
[4],
|
|
'inner',
|
|
],
|
|
ArrayBuffer: [['ArrayBuffer', 1], '.'],
|
|
BigInt: [['BigInt', 1], '1'],
|
|
BigInt64Array: [['BigInt64Array', 1], '.'],
|
|
BigUint64Array: [['BigUint64Array', 1], '.'],
|
|
Class: [['Class', 1], { classId: 2 }, 'class//Example'],
|
|
Date: [['Date', 1], '2025-01-01T00:00:00.000Z'],
|
|
DOMException: [
|
|
['DOMException', 1],
|
|
{ message: 2, name: 3 },
|
|
'aborted',
|
|
'AbortError',
|
|
],
|
|
Error: [['Error', 1], { name: 2, message: 3 }, 'Error', 'boom'],
|
|
EvalError: [['EvalError', 1], { message: 2 }, 'boom'],
|
|
FatalError: [['FatalError', 1], { message: 2 }, 'cannot retry'],
|
|
Float32Array: [['Float32Array', 1], '.'],
|
|
Float64Array: [['Float64Array', 1], '.'],
|
|
Headers: [['Headers', 1], [2], [3, 4], 'x-test', 'value'],
|
|
HookConflictError: [
|
|
['HookConflictError', 1],
|
|
{ message: 2, token: 3, conflictingRunId: 4 },
|
|
'Hook token "approval-token" is already in use by another workflow (run "wrun_conflicting")',
|
|
'approval-token',
|
|
'wrun_conflicting',
|
|
],
|
|
Instance: [
|
|
['Instance', 1],
|
|
{ classId: 2, data: 3 },
|
|
'class//Example',
|
|
'data',
|
|
],
|
|
Int8Array: [['Int8Array', 1], '.'],
|
|
Int16Array: [['Int16Array', 1], '.'],
|
|
Int32Array: [['Int32Array', 1], '.'],
|
|
Map: [['Map', 1], [2], [3, 4], 'key', 'value'],
|
|
RangeError: [['RangeError', 1], { message: 2 }, 'boom'],
|
|
ReadableStream: [['ReadableStream', 1], { name: 2 }, 'stream-a'],
|
|
ReferenceError: [['ReferenceError', 1], { message: 2 }, 'boom'],
|
|
RegExp: [['RegExp', 1], { source: 2, flags: 3 }, 'abc', 'i'],
|
|
Request: [
|
|
['Request', 1],
|
|
{ method: 2, url: 3, headers: 4, body: 5, duplex: 6 },
|
|
'POST',
|
|
'https://example.com/request',
|
|
[7],
|
|
null,
|
|
'half',
|
|
[8, 9],
|
|
'x-test',
|
|
'value',
|
|
],
|
|
Response: [
|
|
['Response', 1],
|
|
{
|
|
type: 2,
|
|
url: 3,
|
|
status: 4,
|
|
statusText: 5,
|
|
headers: 6,
|
|
body: 7,
|
|
redirected: 8,
|
|
},
|
|
'basic',
|
|
'https://example.com/response',
|
|
200,
|
|
'OK',
|
|
[9],
|
|
null,
|
|
false,
|
|
[10, 11],
|
|
'x-test',
|
|
'value',
|
|
],
|
|
RetryableError: [
|
|
['RetryableError', 1],
|
|
{ message: 2, retryAfter: 3 },
|
|
'try again',
|
|
Date.UTC(2025, 0, 1),
|
|
],
|
|
RuntimeDecryptionError: [
|
|
['RuntimeDecryptionError', 1],
|
|
{ message: 2, context: 3 },
|
|
'decrypt failed',
|
|
{ operation: 4 },
|
|
'hydrate',
|
|
],
|
|
Set: [['Set', 1], [2], 'value'],
|
|
StepFunction: [['StepFunction', 1], { stepId: 2 }, 'step//example'],
|
|
SyntaxError: [['SyntaxError', 1], { message: 2 }, 'boom'],
|
|
TypeError: [['TypeError', 1], { message: 2 }, 'boom'],
|
|
URIError: [['URIError', 1], { message: 2 }, 'boom'],
|
|
URL: [['URL', 1], 'https://example.com/path'],
|
|
URLSearchParams: [['URLSearchParams', 1], 'a=1'],
|
|
Uint8Array: [['Uint8Array', 1], '.'],
|
|
Uint8ClampedArray: [['Uint8ClampedArray', 1], '.'],
|
|
Uint16Array: [['Uint16Array', 1], '.'],
|
|
Uint32Array: [['Uint32Array', 1], '.'],
|
|
WorkflowFunction: [
|
|
['WorkflowFunction', 1],
|
|
{ workflowId: 2 },
|
|
'workflow//example',
|
|
],
|
|
WritableStream: [['WritableStream', 1], { name: 2 }, 'stream-a'],
|
|
};
|
|
|
|
describe('serializable type reviver surfaces', () => {
|
|
it('has a fixture for every SerializableSpecial key', () => {
|
|
expect(Object.keys(SERIALIZABLE_PAYLOADS).sort()).toEqual(
|
|
SERIALIZABLE_SPECIAL_KEYS
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['web-shared', getWebRevivers()],
|
|
['CLI', getCLIRevivers()],
|
|
])('%s can deserialize every SerializableSpecial key', (_name, revivers) => {
|
|
const missing = SERIALIZABLE_SPECIAL_KEYS.filter(
|
|
(key) => typeof (revivers as Record<string, unknown>)[key] !== 'function'
|
|
);
|
|
expect(missing).toEqual([]);
|
|
|
|
for (const key of SERIALIZABLE_SPECIAL_KEYS) {
|
|
expect(() => {
|
|
hydrateData(SERIALIZABLE_PAYLOADS[key], revivers);
|
|
}, key).not.toThrow();
|
|
}
|
|
});
|
|
});
|