refactor(goal): call entity methods through remote API

This commit is contained in:
imccyu
2026-08-07 10:47:17 +08:00
parent 2f619b1b88
commit da1ebd2b68
16 changed files with 140 additions and 204 deletions

View File

@@ -148,11 +148,17 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
invalidRejected = true
}
const rootResult = await client.api.goals.create(rootAgent.id, { objective: 'root goal' })
const rootEdit = await client.api.goals.edit(
rootAgent.id,
rootResult.ref,
{ objective: 'edited root goal' },
)
const agentContext = client.extend({ builtAgentId: scopedAgent.id })
const scopedResult = await agentContext.goals.create({ objective: 'scoped goal', maxGoalRounds: 3 })
const result = {
invalidRejected,
rootResult,
rootEdit,
scopedResult,
rootGoal: host.goals.get(rootAgent)?.objective,
scopedGoal: host.goals.get(scopedAgent)?.objective,
@@ -174,6 +180,7 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
const output = JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '{}') as {
invalidRejected: boolean
rootResult: { ref: { id: string; revision: number } }
rootEdit: { objective: string; revision: number }
scopedResult: { ref: { id: string; revision: number } }
rootGoal: string
scopedGoal: string
@@ -183,10 +190,11 @@ describe.skipIf(!requiredArtifacts)('Goal Remote built LIB chain', () => {
expect(output).toMatchObject({
invalidRejected: true,
rootResult: { ref: { revision: 1 } },
rootEdit: { objective: 'edited root goal', revision: 2 },
scopedResult: { ref: { revision: 1 } },
rootGoal: 'root goal',
rootGoal: 'edited root goal',
scopedGoal: 'scoped goal',
rootEvents: 1,
rootEvents: 2,
scopedEvents: 1,
})
expect(output.rootResult.ref.id).toMatch(/^goal-/)

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-goal/README.md
README.md: 0ea00b8bf9b07f02b5df0f7b3e7d3d9c6f109fde
README.zh.md: 70bf443118e5d2b1ce46e7bc1479bf932507b3f9
README.md: b99aaf624a7d669879ba668938ee455e3cdc68ad
README.zh.md: 3d823d013066bc912398f61c85553887e05ca3b4

View File

@@ -2,13 +2,13 @@
English | [中文](README.zh.md)
Goal surface plugin, browser half: the `GoalBar` strip is the second standalone card in the `conversation.input.dock` composer-context stack (order 10, after Todo and before Queue). The live goal arrives through `useProjection('goal')` — the host-computed whole value seeded by the history tail page and updated by `session/projection` frames — so the plugin owns no domain store, refresh chain, or event listener. The slot inject face carries only the four mutation verbs (edit / pause / resume / clear over the `goal.*` wire domain — an active goal offers the pause action, a paused one resume); each reads the CAS ref from the session's current projected value at call time and surfaces the settled RPC error inline. The strip single-flights mutations synchronously because React's pending render cannot fence same-frame clicks; after a successful clear it immediately suppresses that exact goal id while the authoritative null projection catches up. Goal creation stays on the `/goal` host command; loading, absent, completed, and successfully cleared goals render nothing.
Goal surface plugin, browser half: the `GoalBar` strip is the second standalone card in the `conversation.input.dock` composer-context stack (order 10, after Todo and before Queue). The live goal arrives through `useProjection('goal')` — the host-computed whole value seeded by the history tail page and updated by `session/projection` frames — so the plugin owns no domain store, refresh chain, or event listener. The slot inject face carries only the four mutation verbs (edit / pause / resume / clear through `ctx.api.goals` — an active goal offers the pause action, a paused one resume); each reads the CAS ref from the session's current projected value at call time and surfaces the rejected Remote error inline. The strip single-flights mutations synchronously because React's pending render cannot fence same-frame clicks; after a successful clear it immediately suppresses that exact goal id while the authoritative null projection catches up. Goal creation stays on the `/goal` host command; loading, absent, completed, and successfully cleared goals render nothing.
The `/client` export surface is the plugin body (`apply`/`inject`), the `GoalBar`/`GoalDock` components, and the injected verb face types.
## Model Experience
Indirectly, through the `goal.edit`/`goal.pause`/`goal.resume`/`goal.clear` RPCs the strip's verbs submit: each accepted mutation commits in a durable `agent/inbox/spliced` insertion, which the goal projection folds immediately, and queues a `goal/change` context message. The model sees that context only if a later pre-step admits it; discarding the queued message does not roll back the projected state. The strip itself adds no prompt content.
Indirectly, through the `goals/edit`, `goals/pause`, `goals/resume`, and `goals/clear` Remote methods the strip invokes: each accepted mutation commits in a durable `agent/inbox/spliced` insertion, which the goal projection folds immediately, and queues a `goal/change` context message. The model sees that context only if a later pre-step admits it; discarding the queued message does not roll back the projected state. The strip itself adds no prompt content.
#### KV Cache effect

View File

@@ -2,13 +2,13 @@
[English](README.md) | 中文
Goal 界面插件(浏览器端部分):`GoalBar` 条带是 `conversation.input.dock` composer 上下文堆栈中的第二张独立卡片order 10位于 Todo 之后、Queue 之前)。活值经 `useProjection('goal')` 到达——host 计算的全量值由历史尾页播种、由 `session/projection` 帧更新——因此本插件不持有领域 store、不设刷新链、不挂事件监听。slot 注入面只携带四个变更动词edit / pause / resume / clear `goal.*` 协议域——active 的 goal 提供暂停动作paused 的提供恢复);每个动词在调用时从会话当前投影值读取 CAS ref把结算后的 RPC 错误内联呈现。由于 React 的 pending 渲染无法拦住同一帧内的点击,横条会同步为变更建立 single-flight 防护;清除成功后,会立即抑制该 goal id 对应的目标显示,直到权威的 null 投影追上。goal 的创建仍归 `/goal` host 命令;加载中、无 goal、已完成和已成功清除的 goal 一律不渲染。
Goal 界面插件(浏览器端部分):`GoalBar` 条带是 `conversation.input.dock` composer 上下文堆栈中的第二张独立卡片order 10位于 Todo 之后、Queue 之前)。活值经 `useProjection('goal')` 到达——host 计算的全量值由历史尾页播种、由 `session/projection` 帧更新——因此本插件不持有领域 store、不设刷新链、不挂事件监听。slot 注入面只携带四个变更动词edit / pause / resume / clear `ctx.api.goals` 调用——active 的 goal 提供暂停动作paused 的提供恢复);每个动词在调用时从会话当前投影值读取 CAS ref将 Remote 调用的拒绝错误内联呈现。由于 React 的 pending 渲染无法拦住同一帧内的点击,横条会同步为变更建立 single-flight 防护;清除成功后,会立即抑制该 goal id 对应的目标显示,直到权威的 null 投影追上。goal 的创建仍归 `/goal` host 命令;加载中、无 goal、已完成和已成功清除的 goal 一律不渲染。
`/client` 的导出接口包括插件本体(`apply`/`inject`)、`GoalBar`/`GoalDock` 组件与注入动词面类型。
## 模型体验
间接影响:条带动词提交的 `goal.edit`/`goal.pause`/`goal.resume`/`goal.clear` RPC 每次被接受后,变更都会在持久 `agent/inbox/spliced` 插入项中提交goal 投影会立即折叠该插入项,同时将一条 `goal/change` 上下文消息排队。只有后续 pre-step 准入该上下文时,模型才会看到它;丢弃已排队的消息不会回滚投影状态。条带自身不添加任何提示词内容。
间接影响:条带通过调用 `goals/edit``goals/pause``goals/resume``goals/clear` Remote 方法提交变更;每次被接受变更都会在持久 `agent/inbox/spliced` 插入项中提交goal 投影会立即折叠该插入项,同时将一条 `goal/change` 上下文消息排队。只有后续 pre-step 准入该上下文时,模型才会看到它;丢弃已排队的消息不会回滚投影状态。条带自身不添加任何提示词内容。
#### KV Cache 影响

View File

@@ -25,6 +25,7 @@
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-remotes",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-conversation"
],
@@ -36,8 +37,8 @@
},
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-locale": "^0.0.1",
"@deepseek-ai/dsh-client-remotes": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
@@ -48,8 +49,8 @@
"react": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-remotes": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",

View File

@@ -4,19 +4,19 @@
* arrives through `useProjection('goal')` (seeded by the history tail page,
* updated by session/projection frames), so this plugin owns no store, no
* refresh chain, and no event listener. The inject face carries only the
* three mutation verbs (edit/resume/clear over the goal.* wire domain);
* four mutation verbs through the generated Goal Remote API;
* their CAS ref reads the session's current projected value at call time.
* Goal creation stays on the /goal host command.
*/
import type { ConnectionHandle, GoalRef, SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { RpcResult } from '@deepseek-ai/dsh-client-connection/client'
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
// Type-only: pulls the generated Remote API and ctx.api merge through the Client assembly boundary.
import type {} from '@deepseek-ai/dsh-client-remotes/client'
// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
import type {} from '@deepseek-ai/dsh-client-locale/client'
// Type-only: the `goal` SessionProjectionMap key merge (single source, the domain's pure outlet).
import type { GoalProjection } from '@deepseek-ai/dsh-goal/client'
import type { GoalProjection, GoalRef } from '@deepseek-ai/dsh-goal/client'
import type { GoalActionResult, GoalBarActions } from './slots.ts'
import { GoalDock } from './GoalBar.tsx'
import { en, zh, type GoalKey } from './locales.ts'
@@ -35,13 +35,32 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
/** Dictionary namespace owned by this plugin. */
const NS = 'goal'
/** Required services: slots for the dock entry, sessions for the projected ref, connection for the wire verbs, locale for the copy. */
export const inject = ['slots', 'sessions', 'connection', 'locale']
/** Required services: slots for the dock entry, sessions for the projected ref, API for Remote mutations, locale for the copy. */
export const inject = ['slots', 'sessions', 'api', 'locale']
/** Map one settled RPC result onto the strip's inline-render shape. */
function settle<T>(result: RpcResult<T>): GoalActionResult {
if (result.ok) return { ok: true }
return { ok: false, error: { code: result.error.code, message: result.error.message } }
/** Map one generated Remote call onto the strip's inline-render shape. */
async function settle(result: Promise<unknown>): Promise<GoalActionResult> {
try {
await result
return { ok: true }
} catch (error) {
const cause = error instanceof Error ? error.cause : undefined
if (isRemoteError(cause)) return { ok: false, error: { code: cause.code, message: cause.message } }
return {
ok: false,
error: {
code: 'internal',
message: error instanceof Error ? error.message : 'goal mutation failed',
},
}
}
}
function isRemoteError(value: unknown): value is { readonly code: string; readonly message: string } {
return value !== null
&& typeof value === 'object'
&& typeof (value as { code?: unknown }).code === 'string'
&& typeof (value as { message?: unknown }).message === 'string'
}
/**
@@ -51,7 +70,7 @@ function settle<T>(result: RpcResult<T>): GoalActionResult {
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-goal: dictionaries')
const { goals } = (ctx.get('connection') as ConnectionHandle).api
const { goals } = ctx.api
const sessions = ctx.sessions
@@ -77,22 +96,22 @@ export function apply(ctx: ClientContext): void {
onEdit: async (objective) => {
const ref = refOf(sessionId)
if (ref === undefined) return noCurrentGoal
return settle((await goals.edit({ sessionId, ref, objective })).result)
return settle(goals.edit(sessionId, ref, { objective }))
},
onPause: async () => {
const ref = refOf(sessionId)
if (ref === undefined) return noCurrentGoal
return settle((await goals.pause({ sessionId, ref })).result)
return settle(goals.pause(sessionId, ref))
},
onResume: async () => {
const ref = refOf(sessionId)
if (ref === undefined) return noCurrentGoal
return settle((await goals.resume({ sessionId, ref })).result)
return settle(goals.resume(sessionId, ref))
},
onClear: async () => {
const ref = refOf(sessionId)
if (ref === undefined) return noCurrentGoal
return settle((await goals.clear({ sessionId, ref })).result)
return settle(goals.clear(sessionId, ref))
},
}),
}, GoalDock))

View File

@@ -1,11 +1,11 @@
// @vitest-environment jsdom
/**
* ui-goal browser half on a real cordis Context with fake slots/connection/
* ui-goal browser half on a real cordis Context with fake slots/api/
* sessions faces: the plugin registers the GoalBar dock entry at
* conversation.input.dock, the inject face's three verbs read the CAS ref
* conversation.input.dock, the inject face's four verbs read the CAS ref
* from the session's CURRENT projected value at call time (no fence — the
* RPC's compare-and-set is the guard), a missing projection short-circuits
* to the no-current-goal error without touching the wire, and RPC errors
* Remote method's compare-and-set is the guard), a missing projection short-circuits
* to the no-current-goal error without touching the wire, and Remote errors
* map onto the inline-render result shape. Registration disposal rides the
* plugin fiber (HMR safety). The node half and the invariant companion are
* exercised over the same Context.
@@ -44,27 +44,32 @@ function makeProjection(revision = 3): GoalProjection {
}
}
/** Boot the plugin over fake faces; goals verbs record payloads and answer per the script. */
async function bench(options: { projection?: GoalProjection | null | undefined; failWith?: { code: string; message: string } } = {}) {
/** Boot the plugin over fake faces; Goal Remote methods record arguments and answer per the script. */
async function bench(options: {
projection?: GoalProjection | null | undefined
failWith?: { code: string; message: string }
rejectWith?: unknown
} = {}) {
const ctx = new Context()
const calls: { method: string; payload: unknown }[] = []
const calls: { method: string; args: unknown[] }[] = []
function answer<T>(method: string, value: T) {
return (payload: unknown) => {
calls.push({ method, payload })
return Promise.resolve({
result: options.failWith === undefined
? { ok: true as const, value }
: { ok: false as const, error: { ...options.failWith, details: {} } },
})
return (...args: unknown[]) => {
calls.push({ method, args })
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the non-Error rejection is the defensive scenario under test.
if ('rejectWith' in options) return Promise.reject(options.rejectWith)
if (options.failWith !== undefined) {
return Promise.reject(new Error(`Remote ${method} failed`, { cause: options.failWith }))
}
return Promise.resolve(value)
}
}
const ref = { id: 'g-1', revision: 3 }
ctx.provide('connection', { api: { goals: {
edit: answer('goal.edit', { ref }),
pause: answer('goal.pause', { ref }),
resume: answer('goal.resume', { ref }),
clear: answer('goal.clear', { cleared: true as const }),
} } })
ctx.provide('api', { goals: {
edit: answer('goals/edit', { ref }),
pause: answer('goals/pause', { ref }),
resume: answer('goals/resume', { ref }),
clear: answer('goals/clear', ref),
} })
await ctx.plugin(SlotsService).await()
ctx.slots.register({
name: 'root', children: { 'conversation.input.dock': { kind: 'list', scope: 'session' } },
@@ -113,12 +118,12 @@ describe('ui-goal browser plugin', () => {
expect(await verbs.onPause()).toEqual({ ok: true })
expect(await verbs.onResume()).toEqual({ ok: true })
expect(await verbs.onClear()).toEqual({ ok: true })
expect(b.calls.map(c => c.method)).toEqual(['goal.edit', 'goal.pause', 'goal.resume', 'goal.clear'])
expect(b.calls.map(c => c.method)).toEqual(['goals/edit', 'goals/pause', 'goals/resume', 'goals/clear'])
const ref = { id: 'g-1', revision: 5 }
expect(b.calls[0]?.payload).toEqual({ sessionId: 's1', ref, objective: 'New objective' })
expect(b.calls[1]?.payload).toEqual({ sessionId: 's1', ref })
expect(b.calls[2]?.payload).toEqual({ sessionId: 's1', ref })
expect(b.calls[3]?.payload).toEqual({ sessionId: 's1', ref })
expect(b.calls[0]?.args).toEqual(['s1', ref, { objective: 'New objective' }])
expect(b.calls[1]?.args).toEqual(['s1', ref])
expect(b.calls[2]?.args).toEqual(['s1', ref])
expect(b.calls[3]?.args).toEqual(['s1', ref])
})
it('a null or absent projection short-circuits every verb without touching the wire', async () => {
@@ -133,13 +138,26 @@ describe('ui-goal browser plugin', () => {
}
})
it('maps a settled RPC error onto the inline-render shape', async () => {
it('maps a Remote error onto the inline-render shape', async () => {
const b = await bench({ projection: makeProjection(), failWith: { code: 'internal', message: 'stale revision' } })
await b.fiber.await()
const verbs = b.entry()!.inject!(sid('s1'))
expect(await verbs.onEdit('x')).toEqual({ ok: false, error: { code: 'internal', message: 'stale revision' } })
})
it.each([
[new Error('connection closed'), 'connection closed'],
['connection closed', 'goal mutation failed'],
[new Error('invalid Remote failure', { cause: null }), 'invalid Remote failure'],
[new Error('invalid Remote failure', { cause: { code: 1, message: 'stale revision' } }), 'invalid Remote failure'],
[new Error('invalid Remote failure', { cause: { code: 'internal', message: 1 } }), 'invalid Remote failure'],
])('maps an unstructured rejection onto an internal error', async (rejection, message) => {
const b = await bench({ projection: makeProjection(), rejectWith: rejection })
await b.fiber.await()
const verbs = b.entry()!.inject!(sid('s1'))
expect(await verbs.onEdit('x')).toEqual({ ok: false, error: { code: 'internal', message } })
})
it('drops the dock entry when the plugin fiber unloads (HMR safety)', async () => {
const b = await bench()
await b.fiber.await()

View File

@@ -12,10 +12,10 @@
"path": "../../../vendor/cordis"
},
{
"path": "../connection"
"path": "../locale"
},
{
"path": "../locale"
"path": "../remotes"
},
{
"path": "../runtime"

View File

@@ -359,19 +359,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
jsDoc: '/**\n * Create and arm a goal. A completed goal may be replaced; every other\n * current phase must be cleared or resumed instead.\n * @param agent - owning live agent.\n * @param request - objective and optional round cap.\n * @returns the created live view.\n */',
},
{
signature: 'edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView',
signature: '@Remote(\'edit\') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView',
jsDoc: '/**\n * Edit objective and/or round cap without changing phase.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @param request - at least one replacement field.\n * @returns the edited view.\n */',
},
{
signature: 'pause(agent: Agent, ref: GoalRef): GoalView',
signature: '@Remote(\'pause\') pause(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Pause an active goal and disarm automatic continuation.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the paused view.\n */',
},
{
signature: 'resume(agent: Agent, ref: GoalRef): GoalView',
signature: '@Remote(\'resume\') resume(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Resume and arm a stopped goal, or rearm an active goal after a\n * session-start edge, while its round budget still has capacity.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the active view.\n */',
},
{
signature: 'complete(agent: Agent, ref: GoalRef): GoalView',
signature: '@Remote(\'complete\') complete(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Mark a current non-complete goal complete and disarm it.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the completed view.\n */',
},
{
@@ -379,33 +379,13 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
jsDoc: '/**\n * Mark an active goal blocked and disarm it.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @param reason - policy-owned stable code and human-readable explanation.\n * @returns the blocked view with its durable reason.\n */',
},
{
signature: 'clear(agent: Agent, ref: GoalRef): GoalRef',
signature: '@Remote(\'clear\') clear(agent: Agent, ref: GoalRef): GoalRef',
jsDoc: '/**\n * Clear the current goal while retaining a durable tombstone and history.\n * @param agent - owning live agent.\n * @param ref - expected current revision.\n * @returns the tombstone ref whose revision is one past the cleared snapshot.\n */',
},
{
signature: '@Remote(\'create\') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult',
jsDoc: '/**\n * Create one Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param request - objective and optional round cap.\n * @returns the created Goal identity.\n */',
},
{
signature: '@Remote(\'edit\') remoteExportEdit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView',
jsDoc: '/**\n * Edit one Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param ref - expected current revision.\n * @param request - replacement fields.\n * @returns the edited Goal view.\n */',
},
{
signature: '@Remote(\'pause\') remoteExportPause(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Pause one Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param ref - expected current revision.\n * @returns the paused Goal view.\n */',
},
{
signature: '@Remote(\'resume\') remoteExportResume(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Resume one Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param ref - expected current revision.\n * @returns the resumed Goal view.\n */',
},
{
signature: '@Remote(\'complete\') remoteExportComplete(agent: Agent, ref: GoalRef): GoalView',
jsDoc: '/**\n * Complete one Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param ref - expected current revision.\n * @returns the completed Goal view.\n */',
},
{
signature: '@Remote(\'clear\') remoteExportClear(agent: Agent, ref: GoalRef): GoalRef',
jsDoc: '/**\n * Clear one terminal Goal through the remote boundary.\n * @param agent - exact live Agent resolved from the wire identity.\n * @param ref - expected current revision.\n * @returns the committed clear revision.\n */',
},
],
},
{

View File

@@ -273,6 +273,7 @@ export class GoalService extends GatewayService {
* @param request - at least one replacement field.
* @returns the edited view.
*/
@Remote('edit')
edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView {
const cache = this.prepareMutation(agent)
const current = this.expectCurrent(cache, ref)
@@ -294,6 +295,7 @@ export class GoalService extends GatewayService {
* @param ref - expected current revision.
* @returns the paused view.
*/
@Remote('pause')
pause(agent: Agent, ref: GoalRef): GoalView {
return this.transition(agent, ref, 'pause', ['active'], 'paused', 'disarmed')
}
@@ -305,6 +307,7 @@ export class GoalService extends GatewayService {
* @param ref - expected current revision.
* @returns the active view.
*/
@Remote('resume')
resume(agent: Agent, ref: GoalRef): GoalView {
const cache = this.prepareMutation(agent)
const current = this.expectCurrent(cache, ref)
@@ -330,6 +333,7 @@ export class GoalService extends GatewayService {
* @param ref - expected current revision.
* @returns the completed view.
*/
@Remote('complete')
complete(agent: Agent, ref: GoalRef): GoalView {
return this.transition(
agent,
@@ -369,6 +373,7 @@ export class GoalService extends GatewayService {
* @param ref - expected current revision.
* @returns the tombstone ref whose revision is one past the cleared snapshot.
*/
@Remote('clear')
clear(agent: Agent, ref: GoalRef): GoalRef {
const cache = this.prepareMutation(agent)
const current = this.expectCurrent(cache, ref)
@@ -582,62 +587,6 @@ export class GoalService extends GatewayService {
const view = this.create(agent, request)
return { ref: { id: view.id, revision: view.revision } }
}
/**
* Edit one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param ref - expected current revision.
* @param request - replacement fields.
* @returns the edited Goal view.
*/
@Remote('edit')
remoteExportEdit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView {
return this.edit(agent, ref, request)
}
/**
* Pause one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param ref - expected current revision.
* @returns the paused Goal view.
*/
@Remote('pause')
remoteExportPause(agent: Agent, ref: GoalRef): GoalView {
return this.pause(agent, ref)
}
/**
* Resume one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param ref - expected current revision.
* @returns the resumed Goal view.
*/
@Remote('resume')
remoteExportResume(agent: Agent, ref: GoalRef): GoalView {
return this.resume(agent, ref)
}
/**
* Complete one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param ref - expected current revision.
* @returns the completed Goal view.
*/
@Remote('complete')
remoteExportComplete(agent: Agent, ref: GoalRef): GoalView {
return this.complete(agent, ref)
}
/**
* Clear one terminal Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param ref - expected current revision.
* @returns the committed clear revision.
*/
@Remote('clear')
remoteExportClear(agent: Agent, ref: GoalRef): GoalRef {
return this.clear(agent, ref)
}
}
export default GoalService

View File

@@ -245,14 +245,14 @@ describe('GoalService creation and replay', () => {
})
describe('GoalService mutations', () => {
it('exposes the supported mutation sequence through Remote wrappers', async () => {
it('adapts Remote creation and reuses business methods for later mutations', async () => {
const { ctx, agent } = await harness()
const created = ctx.goals.remoteExportCreate(agent, { objective: 'remote lifecycle' })
const edited = ctx.goals.remoteExportEdit(agent, created.ref, { objective: 'edited remotely' })
const paused = ctx.goals.remoteExportPause(agent, edited)
const resumed = ctx.goals.remoteExportResume(agent, paused)
const completed = ctx.goals.remoteExportComplete(agent, resumed)
const cleared = ctx.goals.remoteExportClear(agent, completed)
const edited = ctx.goals.edit(agent, created.ref, { objective: 'edited remotely' })
const paused = ctx.goals.pause(agent, edited)
const resumed = ctx.goals.resume(agent, paused)
const completed = ctx.goals.complete(agent, resumed)
const cleared = ctx.goals.clear(agent, completed)
expect(edited).toMatchObject({ objective: 'edited remotely', revision: 2 })
expect(paused).toMatchObject({ phase: 'paused', revision: 3 })