Merge remote-tracking branch 'origin/master' into worktree/preset-plane-fallout-p1
Only `docs/module-graph.*` conflicted, from master's new `@deepseek-ai/dsh-message-feedback` package; generated, so taken from master and regenerated. Hook bypassed as before: the staged-pairing check hands an archived note path to `verify-translation-pairing`. The full-corpus gate passes.
This commit is contained in:
@@ -85,14 +85,14 @@ describe('dsh badge assembled snapshot', () => {
|
||||
|
||||
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
|
||||
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
|
||||
|
||||
## Markdown
|
||||
|
||||
Use this linked badge in Markdown:
|
||||
|
||||
\`\`\`markdown
|
||||
[](https://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
[](https://github.com/deepseek-ai/deepseek-harness)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
@@ -124,14 +124,14 @@ describe('dsh badge assembled snapshot', () => {
|
||||
|
||||
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
|
||||
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
|
||||
- Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
|
||||
|
||||
## Markdown
|
||||
|
||||
Use this linked badge in Markdown:
|
||||
|
||||
\`\`\`markdown
|
||||
[](https://github.com/deepseek-ai/deepseek-harness-sdk)
|
||||
[](https://github.com/deepseek-ai/deepseek-harness)
|
||||
\`\`\`
|
||||
|
||||
If attribution should not be linked, use:
|
||||
|
||||
115
apps/web/tests/message-feedback-protocol.snapshot.ts
Normal file
115
apps/web/tests/message-feedback-protocol.snapshot.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertFixtureInventory,
|
||||
compareOrRefreshGolden,
|
||||
launchWebScaffold,
|
||||
seedSession,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url))
|
||||
const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json')
|
||||
const SESSION_ID = 'message-feedback-protocol'
|
||||
const MESSAGE_ID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
interface ProtocolExchange {
|
||||
readonly endpoint: string
|
||||
readonly request: unknown
|
||||
readonly status: number
|
||||
readonly response: unknown
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
/** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */
|
||||
function createdVersion(response: unknown): string {
|
||||
if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true
|
||||
|| !isRecord(response.result.value) || response.result.value.ok !== true
|
||||
|| !isRecord(response.result.value.value)
|
||||
|| typeof response.result.value.value.version !== 'string') {
|
||||
throw new Error('messageFeedback.put did not return a successful versioned item')
|
||||
}
|
||||
return response.result.value.value.version
|
||||
}
|
||||
|
||||
/** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */
|
||||
function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string {
|
||||
return JSON.stringify(exchanges, (key, value: unknown) => {
|
||||
if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}'
|
||||
if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}'
|
||||
return value
|
||||
}, 2)
|
||||
}
|
||||
|
||||
describe('message feedback Host Remote protocol', () => {
|
||||
let scaffold: WebScaffold
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => {
|
||||
const exchanges: ProtocolExchange[] = []
|
||||
const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise<unknown> => {
|
||||
const payload = { args: { request } }
|
||||
const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request',
|
||||
rpcId,
|
||||
method: endpoint,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
const body: unknown = await response.json()
|
||||
exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body })
|
||||
return body
|
||||
}
|
||||
|
||||
await invoke('feedback-invalid', 'messageFeedback/put', {
|
||||
sessionId: SESSION_ID,
|
||||
messageId: MESSAGE_ID,
|
||||
rating: 'invalid-rating',
|
||||
ifVersion: null,
|
||||
})
|
||||
await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID })
|
||||
const created = await invoke('feedback-put', 'messageFeedback/put', {
|
||||
sessionId: SESSION_ID,
|
||||
messageId: MESSAGE_ID,
|
||||
rating: 'positive',
|
||||
note: 'Useful answer',
|
||||
ifVersion: null,
|
||||
})
|
||||
const version = createdVersion(created)
|
||||
expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
||||
await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID })
|
||||
await invoke('feedback-conflict', 'messageFeedback/put', {
|
||||
sessionId: SESSION_ID,
|
||||
messageId: MESSAGE_ID,
|
||||
rating: 'negative',
|
||||
ifVersion: null,
|
||||
})
|
||||
await invoke('feedback-delete', 'messageFeedback/delete', {
|
||||
sessionId: SESSION_ID,
|
||||
messageId: MESSAGE_ID,
|
||||
ifVersion: version,
|
||||
})
|
||||
await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID })
|
||||
|
||||
expect(exchanges.every(exchange => exchange.status === 200)).toBe(true)
|
||||
await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode)
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
[
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/put",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol",
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "invalid-rating",
|
||||
"ifVersion": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-invalid",
|
||||
"result": {
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "internal",
|
||||
"message": "typert gateway: messageFeedback/put: wire field \"request\" failed boundary validation",
|
||||
"details": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/list",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-list-empty",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/put",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol",
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "positive",
|
||||
"note": "Useful answer",
|
||||
"ifVersion": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-put",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "positive",
|
||||
"note": "Useful answer",
|
||||
"version": "{{version}}",
|
||||
"createdAt": "{{timestamp}}",
|
||||
"updatedAt": "{{timestamp}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/list",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-list-created",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"items": [
|
||||
{
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "positive",
|
||||
"note": "Useful answer",
|
||||
"version": "{{version}}",
|
||||
"createdAt": "{{timestamp}}",
|
||||
"updatedAt": "{{timestamp}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/put",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol",
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "negative",
|
||||
"ifVersion": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-conflict",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "version-conflict",
|
||||
"current": {
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"rating": "positive",
|
||||
"note": "Useful answer",
|
||||
"version": "{{version}}",
|
||||
"createdAt": "{{timestamp}}",
|
||||
"updatedAt": "{{timestamp}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/delete",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol",
|
||||
"messageId": "11111111-1111-4111-8111-111111111111",
|
||||
"ifVersion": "{{version}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-delete",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"absent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/api/messageFeedback/list",
|
||||
"request": {
|
||||
"args": {
|
||||
"request": {
|
||||
"sessionId": "message-feedback-protocol"
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": 200,
|
||||
"response": {
|
||||
"type": "server-response",
|
||||
"rpcId": "feedback-list-deleted",
|
||||
"result": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"ok": true,
|
||||
"value": {
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1786406400000,"cwd":"{{cwd}}"}
|
||||
{"type":"turn/start","seq":0,"time":1786406400001,"data":{"turn":1}}
|
||||
{"type":"user/message","seq":1,"time":1786406400002,"data":{"role":"user","content":[{"type":"text","text":"Give one useful answer."}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":2,"time":1786406400003,"data":{"turn":1,"step":1}}
|
||||
{"type":"assistant/message","seq":3,"time":1786406400004,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"A useful answer."}],"source":{"kind":"model","provider":"fixture","model":"fixture"},"id":"11111111-1111-4111-8111-111111111111"},"usage":{"inputTokens":4,"outputTokens":4}},"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":4,"time":1786406400005,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":5,"time":1786406400006,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -57,9 +57,9 @@ export function probeFreePort(): Promise<number> {
|
||||
/**
|
||||
* Drive the hero's workspace picker through the composed directory dialog
|
||||
* until the live composer unlocks. A fresh world has no Workspace, so the boot
|
||||
* lands in the locked view state (startup auto-selection has nothing to
|
||||
* lands in the Workspace-trigger view state (startup auto-selection has nothing to
|
||||
* select); every scenario that types into the composer must connect one
|
||||
* first. With nothing to list, the chip gesture raises the dialog directly —
|
||||
* first. With nothing to list, activating the textarea raises the dialog directly —
|
||||
* adding a workspace is the picker's only entry. The directory is staged here
|
||||
* and adopted through the path editor, which is idempotent across the repeated
|
||||
* connects a scenario may make; creating a folder from inside the dialog (the
|
||||
@@ -73,7 +73,7 @@ export function probeFreePort(): Promise<number> {
|
||||
*/
|
||||
export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise<void> {
|
||||
mkdirSync(join(root, name), { recursive: true })
|
||||
await page.getByRole('button', { name: 'Choose workspace' }).click()
|
||||
await page.getByRole('textbox', { name: 'Choose workspace' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'Edit path' }).click()
|
||||
@@ -97,7 +97,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
|
||||
*/
|
||||
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise<void> {
|
||||
mkdirSync(join(root, name), { recursive: true })
|
||||
await page.getByRole('button', { name: '选择工作区' }).click()
|
||||
await page.getByRole('textbox', { name: '选择工作区' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '编辑路径' }).click()
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"tests/scaffold.ts",
|
||||
"tests/scaffold-hermetic.e2e.ts",
|
||||
"tests/minimal-preset.snapshot.ts",
|
||||
"tests/message-feedback-protocol.snapshot.ts",
|
||||
"tests/live-interactions.e2e.ts",
|
||||
"tests/question-composer.e2e.ts",
|
||||
"tests/approval-composer.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user