fix(jobs): carry the projection kind as an open string on the wire
JobView.kind was typed as the merge-extensible JobKind, so the job-controller program, which compiles only the merges it can see, generated a strict codec of bash | subagent for job.rows and job.observe. The first pwsh, pty-send, or workflow job failed frame validation on both ends and the browser dropped the whole roster. The projection now carries kind as a string; JobSpec.kind keeps the Host-side JobKind. A built-artifact smoke parses a pty-send row through the generated schemas, and the seam spec pins the open type.
This commit is contained in:
1 parent
b76bfbcc1f
commit
2c0ca45f76
6 files changed
+70
-7
No files matched your search
@@ -216,8 +216,12 @@ interface JobOutputRead {
|
||||
interface JobView {
|
||||
/** The registry-issued id (`<kind>-N`). */
|
||||
readonly id: JobId
|
||||
/** The producer kind the job was registered with. */
|
||||
readonly kind: JobKind
|
||||
/**
|
||||
* The producer kind the job was registered with: a Host-registered
|
||||
* `JobKind`, carried as an open string because a browser bundle or a Remote
|
||||
* codec sees only the `JobKindMap` merges its own program compiles.
|
||||
*/
|
||||
readonly kind: string
|
||||
/** The producer-supplied one-line label. */
|
||||
readonly label: string
|
||||
/** Owning session; absent for an unowned job, which every caller can see. */
|
||||
|
||||
@@ -216,8 +216,12 @@ interface JobOutputRead {
|
||||
interface JobView {
|
||||
/** The registry-issued id (`<kind>-N`). */
|
||||
readonly id: JobId
|
||||
/** The producer kind the job was registered with. */
|
||||
readonly kind: JobKind
|
||||
/**
|
||||
* The producer kind the job was registered with: a Host-registered
|
||||
* `JobKind`, carried as an open string because a browser bundle or a Remote
|
||||
* codec sees only the `JobKindMap` merges its own program compiles.
|
||||
*/
|
||||
readonly kind: string
|
||||
/** The producer-supplied one-line label. */
|
||||
readonly label: string
|
||||
/** Owning session; absent for an unowned job, which every caller can see. */
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Built-artifact smoke for the generated job Remote codecs. This package's
|
||||
* program compiles only the `JobKindMap` merges it can see (`bash`,
|
||||
* `subagent`), while the Host registers `pwsh`, `pty-send`, and every other
|
||||
* producer kind, so the wire projection's `kind` must reach the codec as an
|
||||
* open string rather than the literal union this program observes.
|
||||
*/
|
||||
|
||||
const artifact = join(fileURLToPath(new URL('..', import.meta.url)), 'lib/typert.host.js')
|
||||
|
||||
interface BuiltInvocation {
|
||||
readonly method: string
|
||||
readonly result: { readonly schema: { safeParse(value: unknown): { success: boolean } } }
|
||||
}
|
||||
|
||||
const row = {
|
||||
id: 'pty-send-1',
|
||||
kind: 'pty-send',
|
||||
label: 'send',
|
||||
status: 'running',
|
||||
startedAt: 1,
|
||||
output: { total: 0, earliest: 0 },
|
||||
}
|
||||
|
||||
describe.skipIf(!existsSync(artifact))('job Remote built codecs', () => {
|
||||
it('accept a row whose kind this program never compiled, and still reject a status outside the lifecycle', async () => {
|
||||
const { TYPERT } = await import(pathToFileURL(artifact).href) as { TYPERT: { invocations: readonly BuiltInvocation[] } }
|
||||
const schemaOf = (method: string): BuiltInvocation['result']['schema'] => {
|
||||
const invocation = TYPERT.invocations.find(candidate => candidate.method === method)
|
||||
if (invocation === undefined) throw new Error(`no generated job.${method} invocation`)
|
||||
return invocation.result.schema
|
||||
}
|
||||
expect(schemaOf('rows').safeParse({ type: 'rows', jobs: [row] }).success).toBe(true)
|
||||
expect(schemaOf('observe').safeParse({ type: 'opened', job: row, from: 0 }).success).toBe(true)
|
||||
expect(schemaOf('observe').safeParse({ type: 'status', job: { ...row, status: 'paused' } }).success).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -4248,7 +4248,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'JobView',
|
||||
declaration: 'export interface JobView {\n readonly id: JobId;\n readonly kind: JobKind;\n readonly label: string;\n readonly owner?: SessionId;\n readonly outputLimitBytes?: number;\n readonly status: JobStatus;\n readonly progress?: string;\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n readonly output: {\n readonly total: number;\n readonly earliest: number;\n };\n}',
|
||||
declaration: 'export interface JobView {\n readonly id: JobId;\n readonly kind: string;\n readonly label: string;\n readonly owner?: SessionId;\n readonly outputLimitBytes?: number;\n readonly status: JobStatus;\n readonly progress?: string;\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n readonly output: {\n readonly total: number;\n readonly earliest: number;\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'JsonSchemaNode',
|
||||
|
||||
@@ -56,8 +56,12 @@ export interface JobChunk {
|
||||
export interface JobView {
|
||||
/** The registry-issued id (`<kind>-N`). */
|
||||
readonly id: JobId
|
||||
/** The producer kind the job was registered with. */
|
||||
readonly kind: JobKind
|
||||
/**
|
||||
* The producer kind the job was registered with: a Host-registered
|
||||
* `JobKind`, carried as an open string because a browser bundle or a Remote
|
||||
* codec sees only the `JobKindMap` merges its own program compiles.
|
||||
*/
|
||||
readonly kind: string
|
||||
/** The producer-supplied one-line label. */
|
||||
readonly label: string
|
||||
/** Owning session; absent for an unowned job, which every caller can see. */
|
||||
|
||||
@@ -114,6 +114,15 @@ describe('JobRegistry seam', () => {
|
||||
expect([viewHasReported, specHasRecord, hooksHaveReadOutput]).toEqual([false, false, false])
|
||||
})
|
||||
|
||||
it('keeps the projection kind an open string, so a program compiling fewer JobKindMap merges accepts every Host kind', () => {
|
||||
// A browser bundle or a generated Remote codec sees only the merges its
|
||||
// own program compiles; a literal union here would reject `pwsh`,
|
||||
// `pty-send`, and every kind another package registers.
|
||||
type KindIsOpenString = string extends JobView['kind'] ? true : false
|
||||
const viewKindOpen: KindIsOpenString = true
|
||||
expect(viewKindOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('loading a second implementation throws (one jobs service per context — cordis standard)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(StubJobRegistry)
|
||||
|
||||
Reference in new issue
Block a user