Files
OpenOPC/opc/plugins/office_ui/frontend_src/App.test.tsx
T
2026-07-14 20:31:10 +08:00

177 lines
6.5 KiB
TypeScript

/**
* Structural regression test for App.tsx WS handler registrations.
*
* Guards the 5 onOrgSaved* callbacks that MUST all be registered in the
* socket-handlers object. A missing one is silent (TypeScript-level the
* callback is optional) and results in broken UX: earlier bug report
* "can't switch saved orgs" was caused by the 3 of these being absent.
*
* Also guards:
* - Toast state wiring
* - useCallback stability for client.org* calls (no inline arrows in
* <OrgTab> props)
*
* Runs with `tsx` against node:assert/strict — matches repo convention
* for zero-framework tests.
*
* Usage:
* tsx opc/plugins/office_ui/frontend_src/App.test.tsx
*/
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
const src = readFileSync(join(here, 'App.tsx'), 'utf8')
// 1. All five onOrgSaved* handlers registered in the socket-handlers object.
for (const key of [
'onOrgSavedList',
'onOrgSavedSaveAs',
'onOrgSavedCreate',
'onOrgSavedLoad',
'onOrgSavedDelete',
]) {
assert.match(
src,
new RegExp(`^\\s+${key}:\\s*\\(payload\\)`, 'm'),
`App.tsx must register "${key}:" in the socket handlers object`,
)
}
// 2. Toast state + JSX render present.
assert.match(src, /const \[orgToast, setOrgToast\] = useState/, 'orgToast state must exist')
assert.match(src, /setOrgToast\(null\)/, 'orgToast auto-clear must exist')
assert.match(src, /org-toast org-toast--/, 'org-toast JSX must render class variant')
// 3. versionAtLoad tracking wired in onOrgInfo.
assert.match(
src,
/setSavedOrgVersionAtLoad\(prev =>/,
'onOrgInfo must capture versionAtLoad via functional setState',
)
// 4. useCallback for client.org* — no inline arrows in <OrgTab> props.
assert.doesNotMatch(
src,
/onSavedOrg[A-Z][a-zA-Z]*={\(/,
'OrgTab JSX must not use inline arrow functions for onSavedOrg* props',
)
for (const name of [
'handleSavedOrgsList',
'handleSavedOrgSaveAs',
'handleSavedOrgCreate',
'handleSavedOrgLoad',
'handleSavedOrgDelete',
]) {
assert.match(
src,
new RegExp(`const ${name} = useCallback`),
`App.tsx must declare "${name}" as useCallback`,
)
}
// 5. onOrgConfigImport narrowing comment present.
assert.match(
src,
/Fires only on manual YAML import/,
'onOrgConfigImport must carry the narrowing comment',
)
// 6. project_index_push is an index-only seed. It must not hydrate chat,
// kanban, or full runtime stores; full runtime state belongs to collab_sync.
assert.match(
src,
/const isProjectIndexPush = type === 'project_index_push' \|\| syncScope === 'index'/,
'project_index_push must be detected by event type and sync_scope',
)
assert.match(
src,
/if \(isProjectIndexPush\) \{[\s\S]*?preserveExistingWhenIncomingPartial: true[\s\S]*?clientRef\.current\?\.collabSync\(syncProjectId[\s\S]*?return[\s\S]*?\}\s+const cs2 = chatStoreRef\.current/,
'project_index_push must preserve existing session detail, request full collab_sync, and return before chat/kanban hydration',
)
assert.doesNotMatch(
src,
/preserveTasksWhenIncomingEmpty: isProjectIndexPush/,
'project_index_push must not call BoardStore.initFromBackend as a partial full-sync workaround',
)
// 7. Runtime tool display has two channels: currentTool is active-only,
// displayTool is the stable "last visible command" shown while the session
// remains running. This prevents a half-second header/composer flash when
// current_tool is cleared by tool_completed.
assert.match(
src,
/function runtimeStatusClearsDisplayTool/,
'App.tsx must centralize terminal status clearing for stable displayTool',
)
assert.match(
src,
/boardRuntimePatch\.displayTool = currentTool/,
'agent_runtime_update must copy a non-empty current_tool into board displayTool',
)
assert.match(
src,
/sessionRuntimePatch\.displayTool = currentTool/,
'agent_runtime_update must copy a non-empty current_tool into session displayTool',
)
assert.match(
src,
/runtimeStatusClearsDisplayTool\(payload\.status\)/,
'agent_runtime_update must clear displayTool only on terminal or idle statuses',
)
assert.match(
src,
/toolName \? \{ displayTool: toolName \}/,
'runtime events carrying a non-empty tool_name must update stable displayTool (empty tool_name keeps the sticky last command)',
)
// 8. A streaming draft is one mounted logical turn. Completion/checkpoint
// events cannot remove it before the matching persisted final is merged.
assert.match(
src,
/const turnId = resolveCanonicalTurnId\(data\) \|\| undefined/,
'runtime deltas must use the shared canonical-turn resolver',
)
assert.match(
src,
/const startsNewCanonicalTurn = evt\.type === 'turn_started'[\s\S]*turnId !== activeDraftTurnId[\s\S]*evt\.type === 'turn_failed' \|\| startsNewCanonicalTurn/,
'only failure or a genuinely new canonical turn may clear an uncommitted draft',
)
assert.doesNotMatch(
src,
/evt\.type === 'turn_completed'[^\n]*clearDraft|evt\.type === 'checkpoint_saved'[^\n]*clearDraft/,
'turn completion and checkpoint persistence must not clear a draft ahead of its final message',
)
assert.match(
src,
/detailHasFinalForDraft[\s\S]*terminalAssistantTurnId\(message\) === draftTurnId[\s\S]*ss\.clearDraft\(detailTaskId\)/,
'session_detail may clear only after merging the matching terminal assistant turn',
)
assert.match(
src,
/cs\.addMessageFromBackend\(mapped\)[\s\S]*terminalTurnId = terminalAssistantTurnId\(mapped\)[\s\S]*terminalTurnId === activeDraftTurnId[\s\S]*clearDraft\(taskId\)/,
'session_message/chat_new_message must merge a matching terminal final before clearing its draft',
)
assert.doesNotMatch(
src,
/mapped\.sender !== 'user'\)\s*\{\s*sessionStoreRef\.current\?\.clearDraft/,
'an unrelated non-user session message must never clear an active draft',
)
// 9. Summary/full pagination and transport-local failures have independent
// lifecycle state. A locally failed Promise never reaches onAck.
assert.match(
src,
/void client\.sessionDetail\([\s\S]*?\.then\(\(payload\) => \{[\s\S]*?payload\.ok !== false[\s\S]*?detailLoading: false/,
'debounced session_detail refresh must clear loading on transport-local failure',
)
assert.match(
src,
/mergeSessionDetailHasMore\([\s\S]*?payload\.client_history_page === true[\s\S]*?fullHasMore: detailHasMore[\s\S]*?summaryHasMore: detailHasMore/,
'session_detail ACK must persist pagination state under its detail policy',
)
console.log('App.test.tsx: OK (org handlers + snapshot boundary + runtime displayTool/draft contract)')