diff --git a/src/swarm/model.ts b/src/swarm/model.ts index afc5ba1..1ac541d 100644 --- a/src/swarm/model.ts +++ b/src/swarm/model.ts @@ -2,7 +2,7 @@ * No provider locations, credentials, commands or arbitrary navigation URLs. */ export interface Cell { id: string; parentId: string | null; depth: number; role: 'coordinator' | 'developer' | 'verifier' | 'watcher'; status: 'reserved' | 'ready' | 'retired'; purpose: string; heartbeat: string; allocationCents: number; budgetBasis: 'logical-allocation' } export interface Task { id: string; projectId: string; branch: string; status: 'ready' | 'running' | 'review' | 'verified' | 'delivered' | 'rejected' | 'completed' | 'cancelled'; owner: string | null; attempt: number; leaseExpiry: string | null; specDigest: string; candidate: { sha256: string } | null; review: { accepted: boolean; reviewerId: string; evidenceDigest: string; candidateDigest: string; specDigest: string; attempt: number } | null } -export interface Effect { key: string; kind: 'provision' | 'flow_call' | 'retire' | 'delivery'; state: 'accepted' | 'running' | 'unknown' | 'succeeded' | 'not_applied'; owner: string; taskId: string | null; scope: string; scopeId: string; ownerEpoch: number; controlEpoch: number; createdAt: string; updatedAt: string; requestDigest: string } +export interface Effect { key: string; kind: 'provision' | 'flow_call' | 'message' | 'flow_cancel' | 'retire' | 'delivery' | 'worker_wake' | 'worker_sleep'; state: 'accepted' | 'running' | 'unknown' | 'succeeded' | 'cancelled' | 'not_applied'; owner: string; taskId: string | null; scope: 'task' | 'project' | 'cleanup' | 'worker'; scopeId: string; ownerEpoch: number; controlEpoch: number; createdAt: string; updatedAt: string; requestDigest: string } export interface Change { seq: number; type: string; subject: string; observedAt: string } export interface Snapshot { schemaVersion: 1; factoryId: string; revision: number; cursor: string; observedAt: string; scope: 'local-coordinator'; buildRevision: string; @@ -54,8 +54,8 @@ export function parseSwarm(value: unknown): Swarm { }); const effects = array(state.effects, 10000).map(value => { const effect = record(value); - return { key: id(effect.key), kind: choice(effect.kind, ['provision', 'flow_call', 'retire', 'delivery']), state: choice(effect.state, ['accepted', 'running', 'unknown', 'succeeded', 'not_applied']), - owner: id(effect.owner), taskId: optionalId(effect.taskId), scope: choice(effect.scope, ['task', 'project', 'cleanup']), scopeId: id(effect.scopeId), + return { key: id(effect.key), kind: choice(effect.kind, ['provision', 'flow_call', 'message', 'flow_cancel', 'retire', 'delivery', 'worker_wake', 'worker_sleep']), state: choice(effect.state, ['accepted', 'running', 'unknown', 'succeeded', 'cancelled', 'not_applied']), + owner: id(effect.owner), taskId: optionalId(effect.taskId), scope: choice(effect.scope, ['task', 'project', 'cleanup', 'worker']), scopeId: id(effect.scopeId), ownerEpoch: num(effect.ownerEpoch), controlEpoch: num(effect.controlEpoch), createdAt: date(effect.createdAt), updatedAt: date(effect.updatedAt), requestDigest: digest(effect.requestDigest) }; }); unique(cells.map(cell => cell.id)); unique(tasks.map(task => task.id)); unique(effects.map(effect => effect.key)); diff --git a/test/swarm.test.mjs b/test/swarm.test.mjs index c236498..cc5b19a 100644 --- a/test/swarm.test.mjs +++ b/test/swarm.test.mjs @@ -54,6 +54,21 @@ test('same bare root/task/effect identities remain distinct across authorities a assert.equal(index.nodeFor({ sourceId: source.id, kind: 'task', id: 'exploration' }).source.id, source.id); } }); +test('current FACTORY steering, cancellation and worker power effects remain observable', () => { + for (const [kind, state, scope] of [ + ['message', 'succeeded', 'task'], ['flow_cancel', 'cancelled', 'task'], + ['worker_wake', 'succeeded', 'worker'], ['worker_sleep', 'unknown', 'worker'], + ]) { + const swarm = swarmPreview(); + const effect = swarm.sources[2].snapshot.snapshot.effects[0]; + Object.assign(effect, { kind, state, scope }); + const parsed = parseSwarm(swarm).sources[2].snapshot.snapshot.effects[0]; + assert.deepEqual([parsed.kind, parsed.state, parsed.scope], [kind, state, scope]); + } + const unsupported = swarmPreview(); + unsupported.sources[2].snapshot.snapshot.effects[0].kind = 'arbitrary_command'; + assert.throws(() => parseSwarm(unsupported), /Unsupported observation value/); +}); test('10,000-level delegation can be indexed, searched, selected and drawn without recursive overflow or losing focus', () => { const swarm = swarmPreview(10000), source = swarm.sources[0]; swarm.sources = [source];