@swarmclawai/swarmclaw 1.9.12 → 1.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +2 -2
- package/src/app/api/chats/[id]/context-pack/route.ts +43 -0
- package/src/app/api/chats/context-pack-route.test.ts +109 -0
- package/src/app/api/quality/architecture-health/route.ts +16 -0
- package/src/app/api/quality/release-readiness/route.ts +6 -1
- package/src/cli/index.js +2 -0
- package/src/cli/spec.js +1 -0
- package/src/components/chat/chat-header.tsx +36 -3
- package/src/components/quality/quality-workspace.tsx +155 -1
- package/src/lib/quality/architecture-health.test.ts +79 -0
- package/src/lib/quality/architecture-health.ts +451 -0
- package/src/lib/quality/release-readiness.test.ts +13 -0
- package/src/lib/quality/release-readiness.ts +36 -0
- package/src/lib/server/chats/session-context-pack.test.ts +121 -0
- package/src/lib/server/chats/session-context-pack.ts +387 -0
- package/src/lib/server/memory/memory-abstract.ts +0 -1
- package/src/lib/server/memory/temporal-decay.ts +0 -1
- package/src/lib/server/runtime/wake-mode.ts +0 -3
- package/src/lib/server/skills/skill-prompt-budget.ts +2 -2
- package/src/lib/server/workspace-context.ts +0 -3
package/README.md
CHANGED
|
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.14 Highlights
|
|
403
|
+
|
|
404
|
+
Session context-pack release: SwarmClaw now turns a live chat into a concise handoff packet with session metadata, recent visible turns, linked tasks, attachments, resume handles, and next actions.
|
|
405
|
+
|
|
406
|
+
- **Context-pack API.** `GET /api/chats/:id/context-pack` returns structured handoff JSON, and `?format=markdown` returns copyable markdown.
|
|
407
|
+
- **Chat header copy action.** Active chats with messages expose a context-pack button for quick handoff to another operator or backend.
|
|
408
|
+
- **CLI access.** `swarmclaw chats context-pack <chatId> --query format=markdown` exposes the same packet for scripts and release automation.
|
|
409
|
+
- **Smoke coverage.** Runtime tests and the browser smoke gate now verify the context-pack route and markdown response.
|
|
410
|
+
|
|
411
|
+
### v1.9.13 Highlights
|
|
412
|
+
|
|
413
|
+
Architecture health release: SwarmClaw now turns runtime ownership, dispatch, memory, startup, and quality evidence into a scored operator report.
|
|
414
|
+
|
|
415
|
+
- **Architecture Health report.** `/api/quality/architecture-health` returns a structured inventory of runtime domains, surfaces, owners, guardrails, tests, score, risks, warnings, and next actions.
|
|
416
|
+
- **Quality Center visibility.** `/quality` now shows a Runtime Ownership Map beside release readiness so operators can inspect dispatch, memory, startup, and quality coverage before shipping.
|
|
417
|
+
- **Release gate integration.** Release readiness includes architecture health when scoring the ship gate report, blocking or warning when ownership evidence is incomplete.
|
|
418
|
+
- **CLI access.** `swarmclaw operations architecture-health` exposes the same report for automation and release scripts.
|
|
419
|
+
|
|
402
420
|
### v1.9.12 Highlights
|
|
403
421
|
|
|
404
422
|
Local file-queue connector release: operators can bridge SwarmClaw to filesystem inbox, outbox, archive, and error folders without a hosted message bus.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.14",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
import { buildSessionContextPack, formatSessionContextPackMarkdown } from '@/lib/server/chats/session-context-pack'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
6
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
7
|
+
import { listTasks } from '@/lib/server/tasks/task-repository'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_RECENT_MESSAGES = 12
|
|
10
|
+
const MAX_RECENT_MESSAGES = 40
|
|
11
|
+
|
|
12
|
+
function parseMaxRecentMessages(req: Request): number {
|
|
13
|
+
const url = new URL(req.url)
|
|
14
|
+
const raw = url.searchParams.get('messages') || url.searchParams.get('maxRecentMessages')
|
|
15
|
+
const parsed = Number.parseInt(raw || '', 10)
|
|
16
|
+
if (!Number.isFinite(parsed)) return DEFAULT_RECENT_MESSAGES
|
|
17
|
+
return Math.max(1, Math.min(MAX_RECENT_MESSAGES, parsed))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
21
|
+
const { id } = await params
|
|
22
|
+
const session = getSession(id)
|
|
23
|
+
if (!session) return notFound()
|
|
24
|
+
|
|
25
|
+
const url = new URL(req.url)
|
|
26
|
+
const format = url.searchParams.get('format')
|
|
27
|
+
const pack = buildSessionContextPack({
|
|
28
|
+
session,
|
|
29
|
+
messages: getMessages(id),
|
|
30
|
+
tasks: listTasks(),
|
|
31
|
+
maxRecentMessages: parseMaxRecentMessages(req),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (format === 'markdown') {
|
|
35
|
+
return new Response(formatSessionContextPackMarkdown(pack), {
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'text/markdown; charset=utf-8',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(pack)
|
|
43
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
test('GET /api/chats/[id]/context-pack returns structured and markdown handoff context', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
status: number
|
|
9
|
+
markdownStatus: number
|
|
10
|
+
missingStatus: number
|
|
11
|
+
schemaVersion: number
|
|
12
|
+
linkedTaskCount: number
|
|
13
|
+
recentMessageCount: number
|
|
14
|
+
markdownContentType: string
|
|
15
|
+
markdownIncludesTitle: boolean
|
|
16
|
+
markdownIncludesTask: boolean
|
|
17
|
+
}>(`
|
|
18
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
19
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
20
|
+
const routeMod = await import('./src/app/api/chats/[id]/context-pack/route')
|
|
21
|
+
const storage = storageMod.default || storageMod
|
|
22
|
+
const repo = repoMod.default || repoMod
|
|
23
|
+
const route = routeMod.default || routeMod
|
|
24
|
+
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
storage.saveSessions({
|
|
27
|
+
sess_pack_1: {
|
|
28
|
+
id: 'sess_pack_1',
|
|
29
|
+
name: 'Context pack test',
|
|
30
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
31
|
+
user: 'tester',
|
|
32
|
+
provider: 'openai',
|
|
33
|
+
model: 'gpt-4o-mini',
|
|
34
|
+
claudeSessionId: null,
|
|
35
|
+
codexThreadId: 'codex-pack-thread',
|
|
36
|
+
messages: [],
|
|
37
|
+
createdAt: now,
|
|
38
|
+
lastActiveAt: now,
|
|
39
|
+
runContext: {
|
|
40
|
+
objective: 'Hand off a release session.',
|
|
41
|
+
constraints: ['Keep the pack short.'],
|
|
42
|
+
keyFacts: ['The current tag is local only.'],
|
|
43
|
+
discoveries: [],
|
|
44
|
+
failedApproaches: [],
|
|
45
|
+
currentPlan: ['Run checks'],
|
|
46
|
+
completedSteps: [],
|
|
47
|
+
blockers: [],
|
|
48
|
+
parentContext: null,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
version: 1,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
storage.saveTasks({
|
|
55
|
+
task_pack_1: {
|
|
56
|
+
id: 'task_pack_1',
|
|
57
|
+
title: 'Verify context pack',
|
|
58
|
+
description: 'Exercise API route.',
|
|
59
|
+
status: 'running',
|
|
60
|
+
sessionId: 'sess_pack_1',
|
|
61
|
+
agentId: null,
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now + 3,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
repo.appendMessage('sess_pack_1', { role: 'user', text: 'Prepare handoff.', time: now, attachedFiles: ['/tmp/handoff.md'] })
|
|
68
|
+
repo.appendMessage('sess_pack_1', { role: 'assistant', text: 'Ready.', time: now + 1 })
|
|
69
|
+
|
|
70
|
+
const response = await route.GET(
|
|
71
|
+
new Request('http://local/api/chats/sess_pack_1/context-pack?messages=1'),
|
|
72
|
+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
|
|
73
|
+
)
|
|
74
|
+
const payload = await response.json()
|
|
75
|
+
|
|
76
|
+
const markdownResponse = await route.GET(
|
|
77
|
+
new Request('http://local/api/chats/sess_pack_1/context-pack?format=markdown'),
|
|
78
|
+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
|
|
79
|
+
)
|
|
80
|
+
const markdown = await markdownResponse.text()
|
|
81
|
+
|
|
82
|
+
const missingResponse = await route.GET(
|
|
83
|
+
new Request('http://local/api/chats/missing/context-pack'),
|
|
84
|
+
{ params: Promise.resolve({ id: 'missing' }) },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
status: response.status,
|
|
89
|
+
markdownStatus: markdownResponse.status,
|
|
90
|
+
missingStatus: missingResponse.status,
|
|
91
|
+
schemaVersion: payload.schemaVersion,
|
|
92
|
+
linkedTaskCount: payload.linkedTasks.length,
|
|
93
|
+
recentMessageCount: payload.recentMessages.length,
|
|
94
|
+
markdownContentType: markdownResponse.headers.get('content-type') || '',
|
|
95
|
+
markdownIncludesTitle: markdown.includes('# Session Context Pack: Context pack test'),
|
|
96
|
+
markdownIncludesTask: markdown.includes('Verify context pack'),
|
|
97
|
+
}))
|
|
98
|
+
`, { prefix: 'swarmclaw-context-pack-route-' })
|
|
99
|
+
|
|
100
|
+
assert.equal(output.status, 200)
|
|
101
|
+
assert.equal(output.markdownStatus, 200)
|
|
102
|
+
assert.equal(output.missingStatus, 404)
|
|
103
|
+
assert.equal(output.schemaVersion, 1)
|
|
104
|
+
assert.equal(output.linkedTaskCount, 1)
|
|
105
|
+
assert.equal(output.recentMessageCount, 1)
|
|
106
|
+
assert.match(output.markdownContentType, /text\/markdown/)
|
|
107
|
+
assert.equal(output.markdownIncludesTitle, true)
|
|
108
|
+
assert.equal(output.markdownIncludesTask, true)
|
|
109
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
|
|
3
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
try {
|
|
9
|
+
return NextResponse.json(buildArchitectureHealthReport())
|
|
10
|
+
} catch (err: unknown) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: errorMessage(err) },
|
|
13
|
+
{ status: 500 },
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { evaluateEvalGate } from '@/lib/server/eval/baseline'
|
|
3
3
|
import { getOperationPulse, normalizeOperationPulseRange } from '@/lib/server/operations/operation-pulse'
|
|
4
|
+
import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
|
|
4
5
|
import { buildReleaseReadinessReport } from '@/lib/quality/release-readiness'
|
|
5
6
|
import { errorMessage } from '@/lib/shared-utils'
|
|
6
7
|
|
|
@@ -28,7 +29,11 @@ export async function GET(req: Request) {
|
|
|
28
29
|
})
|
|
29
30
|
: null
|
|
30
31
|
|
|
31
|
-
return NextResponse.json(buildReleaseReadinessReport({
|
|
32
|
+
return NextResponse.json(buildReleaseReadinessReport({
|
|
33
|
+
pulse,
|
|
34
|
+
evalGate,
|
|
35
|
+
architectureHealth: buildArchitectureHealthReport(),
|
|
36
|
+
}))
|
|
32
37
|
} catch (err: unknown) {
|
|
33
38
|
return NextResponse.json(
|
|
34
39
|
{ error: errorMessage(err) },
|
package/src/cli/index.js
CHANGED
|
@@ -211,6 +211,7 @@ const COMMAND_GROUPS = [
|
|
|
211
211
|
commands: [
|
|
212
212
|
cmd('pulse', 'GET', '/operations/pulse', 'Get Operations Pulse summary (use --query range=24h or --query range=7d)'),
|
|
213
213
|
cmd('readiness', 'GET', '/quality/release-readiness', 'Get release readiness report (use --query agentId=... and --query suite=core for eval gate coverage)'),
|
|
214
|
+
cmd('architecture-health', 'GET', '/quality/architecture-health', 'Get architecture health inventory and drift report'),
|
|
214
215
|
],
|
|
215
216
|
},
|
|
216
217
|
{
|
|
@@ -626,6 +627,7 @@ const COMMAND_GROUPS = [
|
|
|
626
627
|
cmd('clear-undo', 'POST', '/chats/:id/clear/undo', 'Restore a cleared chat via its undoToken', { expectsJsonBody: true }),
|
|
627
628
|
cmd('compact', 'POST', '/chats/:id/compact', 'Summarize and compact chat history (accepts optional keepLastN)', { expectsJsonBody: true }),
|
|
628
629
|
cmd('context-status', 'GET', '/chats/:id/context-status', 'Report token usage and context-window status for a chat'),
|
|
630
|
+
cmd('context-pack', 'GET', '/chats/:id/context-pack', 'Get a copyable session context pack for handoff'),
|
|
629
631
|
cmd('browser-status', 'GET', '/chats/:id/browser', 'Check browser status'),
|
|
630
632
|
cmd('browser-close', 'DELETE', '/chats/:id/browser', 'Close browser'),
|
|
631
633
|
cmd('mailbox', 'GET', '/chats/:id/mailbox', 'List chat mailbox envelopes'),
|
package/src/cli/spec.js
CHANGED
|
@@ -458,6 +458,7 @@ const COMMAND_GROUPS = {
|
|
|
458
458
|
'clear-undo': { description: 'Restore a cleared chat via its undoToken', method: 'POST', path: '/chats/:id/clear/undo', params: ['id'] },
|
|
459
459
|
compact: { description: 'Summarize and compact chat history', method: 'POST', path: '/chats/:id/compact', params: ['id'] },
|
|
460
460
|
'context-status': { description: 'Report token usage and context-window status', method: 'GET', path: '/chats/:id/context-status', params: ['id'] },
|
|
461
|
+
'context-pack': { description: 'Get a copyable session context pack for handoff', method: 'GET', path: '/chats/:id/context-pack', params: ['id'] },
|
|
461
462
|
mailbox: { description: 'List mailbox envelopes for a chat', method: 'GET', path: '/chats/:id/mailbox', params: ['id'] },
|
|
462
463
|
'mailbox-action': { description: 'Send/ack/clear mailbox envelopes', method: 'POST', path: '/chats/:id/mailbox', params: ['id'] },
|
|
463
464
|
queue: { description: 'List queued follow-up turns for a chat', method: 'GET', path: '/chats/:id/queue', params: ['id'] },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useMemo, useRef, type ReactNode } from 'react'
|
|
4
|
-
import { Plus } from 'lucide-react'
|
|
4
|
+
import { ClipboardList, Plus } from 'lucide-react'
|
|
5
5
|
import type { Session } from '@/types'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -108,6 +108,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
108
108
|
const connectorMeta = connector ? resolveConnectorPlatformMeta(connector.platform) : null
|
|
109
109
|
const connectorPresence = connector?.presence
|
|
110
110
|
const [copied, setCopied] = useState(false)
|
|
111
|
+
const [contextPackCopied, setContextPackCopied] = useState(false)
|
|
112
|
+
const [contextPackLoading, setContextPackLoading] = useState(false)
|
|
111
113
|
const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
|
|
112
114
|
const sourceDropdownRef = useRef<HTMLDivElement>(null)
|
|
113
115
|
const [renaming, setRenaming] = useState(false)
|
|
@@ -220,6 +222,22 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
220
222
|
})
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
const handleCopyContextPack = async () => {
|
|
226
|
+
if (contextPackLoading) return
|
|
227
|
+
setContextPackLoading(true)
|
|
228
|
+
try {
|
|
229
|
+
const markdown = await api<string>('GET', `/chats/${session.id}/context-pack?format=markdown`)
|
|
230
|
+
const copiedPack = await copyTextToClipboard(markdown)
|
|
231
|
+
if (!copiedPack) return
|
|
232
|
+
setContextPackCopied(true)
|
|
233
|
+
setTimeout(() => setContextPackCopied(false), 2000)
|
|
234
|
+
} catch {
|
|
235
|
+
// Best-effort clipboard helper; the route remains available from the CLI.
|
|
236
|
+
} finally {
|
|
237
|
+
setContextPackLoading(false)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
223
241
|
const handleDismissResumeHandle = async (e: React.MouseEvent) => {
|
|
224
242
|
e.stopPropagation()
|
|
225
243
|
try {
|
|
@@ -302,10 +320,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
302
320
|
}
|
|
303
321
|
}, [session.name, loadConnectors])
|
|
304
322
|
|
|
305
|
-
// Context bar shows for memories, source filter, task links, resume handles, browser
|
|
323
|
+
// Context bar shows for handoff packs, memories, source filter, task links, resume handles, browser
|
|
324
|
+
const hasContextPack = messageCount > 0
|
|
306
325
|
const hasMemoryLink = !!(agent && getEnabledToolIds(session).includes('memory'))
|
|
307
326
|
const hasSourceFilter = !!hasMultipleSources
|
|
308
|
-
const hasContextBar = !!(hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || browserActive)
|
|
327
|
+
const hasContextBar = !!(hasContextPack || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || browserActive)
|
|
309
328
|
|
|
310
329
|
return (
|
|
311
330
|
<>
|
|
@@ -523,6 +542,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
523
542
|
{hasContextBar && (
|
|
524
543
|
<div className="border-t border-white/[0.05] bg-black/[0.08] px-4 py-2">
|
|
525
544
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
545
|
+
{hasContextPack && (
|
|
546
|
+
<Tip label="Copy session context pack">
|
|
547
|
+
<button
|
|
548
|
+
type="button"
|
|
549
|
+
onClick={handleCopyContextPack}
|
|
550
|
+
disabled={contextPackLoading}
|
|
551
|
+
aria-label="Copy session context pack"
|
|
552
|
+
className="flex min-w-[122px] items-center justify-center gap-1 px-2.5 py-1 rounded-[8px] bg-white/[0.03] hover:bg-white/[0.06] disabled:opacity-70 disabled:cursor-wait transition-colors cursor-pointer text-[10px] font-600 text-text-3/60 hover:text-text-2 shrink-0"
|
|
553
|
+
>
|
|
554
|
+
<ClipboardList className="h-3 w-3 shrink-0" aria-hidden="true" strokeWidth={2.2} />
|
|
555
|
+
<span>{contextPackLoading ? 'Packing' : contextPackCopied ? 'Copied' : 'Context pack'}</span>
|
|
556
|
+
</button>
|
|
557
|
+
</Tip>
|
|
558
|
+
)}
|
|
526
559
|
{hasMemoryLink && (
|
|
527
560
|
<Tip label="View agent memories">
|
|
528
561
|
<button
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
summarizeEvalRuns,
|
|
16
16
|
summarizeRunHealth,
|
|
17
17
|
} from '@/lib/quality/quality-summary'
|
|
18
|
+
import type { ArchitectureHealthReport, ArchitectureHealthStatus } from '@/lib/quality/architecture-health'
|
|
18
19
|
import type { ReleaseReadinessReport, ReleaseReadinessStatus } from '@/lib/quality/release-readiness'
|
|
19
20
|
import { cn } from '@/lib/utils'
|
|
20
21
|
import { useAppStore } from '@/stores/use-app-store'
|
|
@@ -142,6 +143,18 @@ function readinessScoreTone(status: ReleaseReadinessStatus): string {
|
|
|
142
143
|
return 'text-rose-300'
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
function architectureStatusClass(status: ArchitectureHealthStatus): string {
|
|
147
|
+
if (status === 'healthy') return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-200'
|
|
148
|
+
if (status === 'watch') return 'border-amber-500/25 bg-amber-500/10 text-amber-200'
|
|
149
|
+
return 'border-rose-500/25 bg-rose-500/10 text-rose-200'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function architectureScoreTone(status: ArchitectureHealthStatus): string {
|
|
153
|
+
if (status === 'healthy') return 'text-emerald-300'
|
|
154
|
+
if (status === 'watch') return 'text-amber-300'
|
|
155
|
+
return 'text-rose-300'
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
function ReleaseReadinessPanel({
|
|
146
159
|
report,
|
|
147
160
|
loading,
|
|
@@ -254,6 +267,118 @@ function ReleaseReadinessPanel({
|
|
|
254
267
|
)
|
|
255
268
|
}
|
|
256
269
|
|
|
270
|
+
function ArchitectureHealthPanel({
|
|
271
|
+
report,
|
|
272
|
+
loading,
|
|
273
|
+
onRefresh,
|
|
274
|
+
onOpenHref,
|
|
275
|
+
}: {
|
|
276
|
+
report: ArchitectureHealthReport | null
|
|
277
|
+
loading: boolean
|
|
278
|
+
onRefresh: () => void
|
|
279
|
+
onOpenHref: (href: string) => void
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<section className="rounded-[16px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
283
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
284
|
+
<div>
|
|
285
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-accent-bright/70">Architecture Health</div>
|
|
286
|
+
<h2 className="mt-1 font-display text-[17px] font-700 text-text">Runtime ownership map</h2>
|
|
287
|
+
<p className="mt-1 max-w-[680px] text-[12px] leading-relaxed text-text-3/65">
|
|
288
|
+
Inventories dispatch, memory, startup, and quality surfaces with owners, guardrails, and test evidence.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={onRefresh}
|
|
294
|
+
disabled={loading}
|
|
295
|
+
className="shrink-0 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-800 text-text-2 transition-colors hover:bg-white/[0.08] disabled:opacity-40"
|
|
296
|
+
>
|
|
297
|
+
{loading ? 'Checking' : 'Refresh map'}
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{!report ? (
|
|
302
|
+
<div className="mt-4 rounded-[12px] border border-dashed border-white/[0.08] bg-white/[0.02] px-4 py-5 text-[12px] text-text-3/65">
|
|
303
|
+
{loading ? 'Building architecture health report...' : 'No architecture health report is available yet.'}
|
|
304
|
+
</div>
|
|
305
|
+
) : (
|
|
306
|
+
<div className="mt-4 grid gap-4 xl:grid-cols-[260px_1fr]">
|
|
307
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
308
|
+
<span className={cn('inline-flex rounded-full border px-2.5 py-1 text-[10px] font-800 uppercase tracking-[0.1em]', architectureStatusClass(report.status))}>
|
|
309
|
+
{report.status}
|
|
310
|
+
</span>
|
|
311
|
+
<div className={cn('mt-4 font-display text-[42px] font-700 tracking-[-0.04em]', architectureScoreTone(report.status))}>{report.score}</div>
|
|
312
|
+
<div className="mt-1 text-[12px] text-text-3/65">health score</div>
|
|
313
|
+
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
314
|
+
<div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
|
|
315
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Surfaces</div>
|
|
316
|
+
<div className="mt-1 text-[18px] font-800 text-text">{report.surfaceCount}</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
|
|
319
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Guardrails</div>
|
|
320
|
+
<div className="mt-1 text-[18px] font-800 text-text">{report.guardrailCount}</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div className="grid gap-3 lg:grid-cols-2">
|
|
326
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
|
|
327
|
+
<div className="text-[12px] font-800 text-text">Domains</div>
|
|
328
|
+
<div className="mt-3 grid gap-2">
|
|
329
|
+
{report.domains.map((domain) => (
|
|
330
|
+
<div key={domain.id} className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-2">
|
|
331
|
+
<div className="flex items-center justify-between gap-2">
|
|
332
|
+
<div className="text-[12px] font-800 text-text">{domain.title}</div>
|
|
333
|
+
<span className={cn('rounded-full border px-2 py-0.5 text-[9px] font-800 uppercase tracking-[0.08em]', architectureStatusClass(domain.status))}>
|
|
334
|
+
{domain.status}
|
|
335
|
+
</span>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="mt-1 text-[11px] leading-relaxed text-text-3/65">{domain.surfaces.length} surfaces, {domain.testPaths.length} evidence paths</div>
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
|
|
344
|
+
<div className="text-[12px] font-800 text-text">Checks</div>
|
|
345
|
+
<div className="mt-3 flex flex-col gap-2">
|
|
346
|
+
{report.nextActions.length === 0 ? (
|
|
347
|
+
report.checks.filter((check) => check.status === 'healthy').slice(0, 4).map((check) => (
|
|
348
|
+
<button
|
|
349
|
+
key={check.code}
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={() => check.href && onOpenHref(check.href)}
|
|
352
|
+
className="rounded-[10px] border border-emerald-500/20 bg-emerald-500/[0.05] px-3 py-2 text-left text-emerald-200 transition-colors hover:bg-emerald-500/[0.08]"
|
|
353
|
+
>
|
|
354
|
+
<div className="text-[11px] font-800 uppercase tracking-[0.08em]">{check.status}</div>
|
|
355
|
+
<div className="mt-1 text-[12px] font-700 text-text">{check.title}</div>
|
|
356
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{check.summary}</div>
|
|
357
|
+
</button>
|
|
358
|
+
))
|
|
359
|
+
) : (
|
|
360
|
+
report.nextActions.slice(0, 5).map((action) => (
|
|
361
|
+
<button
|
|
362
|
+
key={action.id}
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => onOpenHref(action.href)}
|
|
365
|
+
className={cn('rounded-[10px] border px-3 py-2 text-left transition-colors hover:bg-white/[0.08]', architectureStatusClass(action.severity))}
|
|
366
|
+
>
|
|
367
|
+
<div className="text-[11px] font-800 uppercase tracking-[0.08em]">{action.severity}</div>
|
|
368
|
+
<div className="mt-1 text-[12px] font-700 text-text">{action.title}</div>
|
|
369
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{action.summary}</div>
|
|
370
|
+
</button>
|
|
371
|
+
))
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</section>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
257
382
|
function EvalEnvironmentPanel({ plan, loading, onRefresh }: {
|
|
258
383
|
plan: EvalEnvironmentPlan | null
|
|
259
384
|
loading: boolean
|
|
@@ -471,6 +596,8 @@ export function QualityWorkspace() {
|
|
|
471
596
|
const [evalBaselineBusy, setEvalBaselineBusy] = useState(false)
|
|
472
597
|
const [releaseReadiness, setReleaseReadiness] = useState<ReleaseReadinessReport | null>(null)
|
|
473
598
|
const [releaseReadinessLoading, setReleaseReadinessLoading] = useState(false)
|
|
599
|
+
const [architectureHealth, setArchitectureHealth] = useState<ArchitectureHealthReport | null>(null)
|
|
600
|
+
const [architectureHealthLoading, setArchitectureHealthLoading] = useState(false)
|
|
474
601
|
const [approvalBusy, setApprovalBusy] = useState<string | null>(null)
|
|
475
602
|
|
|
476
603
|
useEffect(() => {
|
|
@@ -578,10 +705,27 @@ export function QualityWorkspace() {
|
|
|
578
705
|
}
|
|
579
706
|
}, [evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
|
|
580
707
|
|
|
708
|
+
const loadArchitectureHealth = useCallback(async () => {
|
|
709
|
+
setArchitectureHealthLoading(true)
|
|
710
|
+
try {
|
|
711
|
+
const report = await api<ArchitectureHealthReport>('GET', '/quality/architecture-health')
|
|
712
|
+
setArchitectureHealth(report)
|
|
713
|
+
} catch (err) {
|
|
714
|
+
setArchitectureHealth(null)
|
|
715
|
+
toast.error(err instanceof Error ? err.message : 'Unable to check architecture health')
|
|
716
|
+
} finally {
|
|
717
|
+
setArchitectureHealthLoading(false)
|
|
718
|
+
}
|
|
719
|
+
}, [])
|
|
720
|
+
|
|
581
721
|
useEffect(() => {
|
|
582
722
|
void loadQualityData()
|
|
583
723
|
}, [loadQualityData])
|
|
584
724
|
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
void loadArchitectureHealth()
|
|
727
|
+
}, [loadArchitectureHealth])
|
|
728
|
+
|
|
585
729
|
useWs('runs', () => { void loadQualityData({ silent: true }) }, 5000)
|
|
586
730
|
|
|
587
731
|
useEffect(() => {
|
|
@@ -746,7 +890,11 @@ export function QualityWorkspace() {
|
|
|
746
890
|
{refreshing && <span className="text-[11px] text-text-3/60">Refreshing...</span>}
|
|
747
891
|
<button
|
|
748
892
|
type="button"
|
|
749
|
-
onClick={() =>
|
|
893
|
+
onClick={() => {
|
|
894
|
+
void loadQualityData({ silent: true })
|
|
895
|
+
void loadArchitectureHealth()
|
|
896
|
+
void loadReleaseReadiness()
|
|
897
|
+
}}
|
|
750
898
|
className="inline-flex items-center gap-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-700 text-text-2 transition-colors hover:bg-white/[0.08]"
|
|
751
899
|
>
|
|
752
900
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -790,6 +938,12 @@ export function QualityWorkspace() {
|
|
|
790
938
|
onRefresh={() => void loadReleaseReadiness()}
|
|
791
939
|
onOpenHref={(href) => router.push(href)}
|
|
792
940
|
/>
|
|
941
|
+
<ArchitectureHealthPanel
|
|
942
|
+
report={architectureHealth}
|
|
943
|
+
loading={architectureHealthLoading}
|
|
944
|
+
onRefresh={() => void loadArchitectureHealth()}
|
|
945
|
+
onOpenHref={(href) => router.push(href)}
|
|
946
|
+
/>
|
|
793
947
|
|
|
794
948
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
795
949
|
<StatTile
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildArchitectureHealthReport,
|
|
6
|
+
DEFAULT_ARCHITECTURE_HEALTH_INVENTORY,
|
|
7
|
+
} from './architecture-health'
|
|
8
|
+
|
|
9
|
+
describe('architecture health report', () => {
|
|
10
|
+
it('summarizes the default runtime architecture inventory', () => {
|
|
11
|
+
const report = buildArchitectureHealthReport({
|
|
12
|
+
generatedAt: 100_000,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
assert.equal(report.generatedAt, 100_000)
|
|
16
|
+
assert.equal(report.status, 'healthy')
|
|
17
|
+
assert.equal(report.score, 100)
|
|
18
|
+
assert.equal(report.domainCount, DEFAULT_ARCHITECTURE_HEALTH_INVENTORY.length)
|
|
19
|
+
assert.ok(report.surfaceCount >= 10)
|
|
20
|
+
assert.ok(report.guardrailCount >= 8)
|
|
21
|
+
assert.ok(report.checks.some((check) => check.code === 'dispatch_guardrail_coverage'))
|
|
22
|
+
assert.ok(report.checks.some((check) => check.code === 'memory_authority'))
|
|
23
|
+
assert.ok(report.checks.some((check) => check.code === 'startup_surface_inventory'))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('warns when a domain has an unguarded surface', () => {
|
|
27
|
+
const report = buildArchitectureHealthReport({
|
|
28
|
+
generatedAt: 100_000,
|
|
29
|
+
inventory: [{
|
|
30
|
+
id: 'dispatch',
|
|
31
|
+
title: 'Dispatch',
|
|
32
|
+
summary: 'Test dispatch surface',
|
|
33
|
+
owner: 'runtime',
|
|
34
|
+
surfaces: [{
|
|
35
|
+
id: 'direct',
|
|
36
|
+
title: 'Direct run',
|
|
37
|
+
kind: 'dispatch',
|
|
38
|
+
path: 'src/lib/server/test.ts',
|
|
39
|
+
description: 'A dispatch path without a guardrail.',
|
|
40
|
+
guardrails: [],
|
|
41
|
+
evidence: ['No policy attached'],
|
|
42
|
+
}],
|
|
43
|
+
testPaths: ['src/lib/server/test.test.ts'],
|
|
44
|
+
}],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
assert.equal(report.status, 'watch')
|
|
48
|
+
assert.ok(report.score < 100)
|
|
49
|
+
assert.equal(report.warningCount, 1)
|
|
50
|
+
assert.ok(report.checks.some((check) => check.code === 'dispatch_unguarded_surface'))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('marks missing test coverage as an architecture risk', () => {
|
|
54
|
+
const report = buildArchitectureHealthReport({
|
|
55
|
+
generatedAt: 100_000,
|
|
56
|
+
inventory: [{
|
|
57
|
+
id: 'startup',
|
|
58
|
+
title: 'Startup',
|
|
59
|
+
summary: 'Startup entry points',
|
|
60
|
+
owner: 'runtime',
|
|
61
|
+
surfaces: [{
|
|
62
|
+
id: 'cli',
|
|
63
|
+
title: 'CLI',
|
|
64
|
+
kind: 'startup',
|
|
65
|
+
path: 'src/cli/index.js',
|
|
66
|
+
description: 'CLI startup surface.',
|
|
67
|
+
guardrails: ['route coverage'],
|
|
68
|
+
evidence: ['CLI starts the server'],
|
|
69
|
+
}],
|
|
70
|
+
testPaths: [],
|
|
71
|
+
}],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
assert.equal(report.status, 'risk')
|
|
75
|
+
assert.ok(report.score <= 70)
|
|
76
|
+
assert.equal(report.riskCount, 1)
|
|
77
|
+
assert.ok(report.checks.some((check) => check.code === 'startup_missing_tests'))
|
|
78
|
+
})
|
|
79
|
+
})
|