P1 review finding: extractMeta timed only the literal's vm evaluation;
materializing the RESULT then read properties ordinarily on the HOST
stack, so a meta literal smuggling a getter (get name() { while(true){} })
could wedge the host outside any timeout — defeating the exact spin
isolation the worker thread exists for.
Rather than harden the evaluator (descriptor walks, AST validation),
delete the mechanism: the workflow's identity now reaches the seam as a
plain JSON field (WorkflowStartRequest.meta), carried by the tool as a
schema-validated `meta` object parameter the model fills directly. The
engine only shape-validates data (validateMeta, every violation named)
and pre-parses the body; the scanner, the vm evaluation, and the
host-side materialization are gone, and with them the hole. A body
still opening with a Claude Code-style `export const meta` statement
gets a pointed SCRIPT_PARSE message (the likeliest authoring slip; a
CC script's body stays drop-in, only its meta header moves into the
parameter). syncTimeoutMs now governs exactly one thing: the initial
synchronous slice inside the worker.
The RFC's decision section is rewritten in place (implemented-RFC
rule); the embedded-meta format moves to alternatives-considered with
the hole as the reason. Tool description, presentation (title now reads
meta.name directly — the textual sniff is gone), seam vocabulary docs,
and catalogs follow.
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { WorkflowError } from '@deepseek-ai/dsh-workflow'
|
|
import { validateMeta } from '../src/meta.ts'
|
|
|
|
/** Assert a META_INVALID throw whose message matches every given fragment. */
|
|
function expectInvalid(value: unknown, ...fragments: string[]): void {
|
|
let thrown: unknown
|
|
try {
|
|
validateMeta(value)
|
|
} catch (error: unknown) {
|
|
thrown = error
|
|
}
|
|
expect(thrown).toBeInstanceOf(WorkflowError)
|
|
expect((thrown as WorkflowError).code).toBe('META_INVALID')
|
|
for (const fragment of fragments) {
|
|
expect((thrown as WorkflowError).message).toContain(fragment)
|
|
}
|
|
}
|
|
|
|
describe('validateMeta', () => {
|
|
it('accepts a minimal meta and returns a normalized copy (no aliasing of the input)', () => {
|
|
const input = { name: 'audit', description: 'audit the repo' }
|
|
const meta = validateMeta(input)
|
|
expect(meta).toEqual({ name: 'audit', description: 'audit the repo' })
|
|
expect(meta).not.toBe(input)
|
|
input.name = 'mutated'
|
|
expect(meta.name).toBe('audit')
|
|
})
|
|
|
|
it('accepts the full shape and rebuilds phases entry by entry', () => {
|
|
const meta = validateMeta({
|
|
name: 'migrate',
|
|
description: 'migrate call sites',
|
|
whenToUse: 'large mechanical sweeps',
|
|
phases: [
|
|
{ title: 'Discover' },
|
|
{ title: 'Transform', detail: 'one agent per file', model: 'deepseek-v4-pro' },
|
|
],
|
|
})
|
|
expect(meta).toEqual({
|
|
name: 'migrate',
|
|
description: 'migrate call sites',
|
|
whenToUse: 'large mechanical sweeps',
|
|
phases: [
|
|
{ title: 'Discover' },
|
|
{ title: 'Transform', detail: 'one agent per file', model: 'deepseek-v4-pro' },
|
|
],
|
|
})
|
|
})
|
|
|
|
it('rejects non-object values loud', () => {
|
|
expectInvalid(undefined, 'meta must be an object')
|
|
expectInvalid('a string', 'meta must be an object')
|
|
expectInvalid(null, 'meta must be an object')
|
|
expectInvalid([{ name: 'x', description: 'd' }], 'meta must be an object')
|
|
})
|
|
|
|
it('rejects unknown fields by name (accepted-then-ignored is banned)', () => {
|
|
expectInvalid({ name: 'x', description: 'd', color: 'red' }, 'meta.color is not a recognized field')
|
|
})
|
|
|
|
it('rejects missing or mistyped name/description/whenToUse', () => {
|
|
expectInvalid({ description: 'd' }, 'meta.name must be a non-empty string')
|
|
expectInvalid({ name: '', description: 'd' }, 'meta.name must be a non-empty string')
|
|
expectInvalid({ name: 'x' }, 'meta.description must be a non-empty string')
|
|
expectInvalid({ name: 'x', description: 42 }, 'meta.description must be a non-empty string')
|
|
expectInvalid({ name: 'x', description: 'd', whenToUse: 3 }, 'meta.whenToUse must be a string')
|
|
})
|
|
|
|
it('rejects malformed phases, entry by entry', () => {
|
|
expectInvalid({ name: 'x', description: 'd', phases: 'Scan' }, 'meta.phases must be an array')
|
|
expectInvalid({ name: 'x', description: 'd', phases: ['Scan'] }, 'meta.phases[0] must be an object')
|
|
expectInvalid({ name: 'x', description: 'd', phases: [{ title: '' }] }, 'meta.phases[0].title must be a non-empty string')
|
|
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', order: 1 }] }, 'meta.phases[0].order is not a recognized field')
|
|
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', detail: 9 }] }, 'meta.phases[0].detail must be a string')
|
|
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', model: 9 }] }, 'meta.phases[0].model must be a string')
|
|
})
|
|
|
|
it('names EVERY violation in one throw, not just the first', () => {
|
|
expectInvalid(
|
|
{ description: 7, extra: true, phases: [{ title: 'Scan' }, 'bad'] },
|
|
'meta.extra is not a recognized field',
|
|
'meta.name must be a non-empty string',
|
|
'meta.description must be a non-empty string',
|
|
'meta.phases[1] must be an object',
|
|
)
|
|
})
|
|
})
|