Commit e907e347 authored by AI-甘富林's avatar AI-甘富林

test(desktop): add expert bootstrap smoke coverage

parent 82f454d3
......@@ -497,11 +497,7 @@ export class ProjectStoreService {
async setActiveProject(projectId: string): Promise<ProjectSummary> {
await this.initialize();
const projects = await this.readProjects();
const project = projects.find((item) => item.id === projectId);
if (!project) {
throw new Error(`Project ${projectId} was not found.`);
}
const project = await this.getProjectById(projectId);
await writeJsonFile(await this.getActiveProjectFilePath(), { projectId });
return project;
}
......
......@@ -9,6 +9,7 @@ import {
import { ProjectContextService } from "../../apps/desktop/src/main/services/project-context.js";
import { ProjectExecutionRouter } from "../../apps/desktop/src/main/services/project-execution-router.js";
import { ProjectStoreService } from "../../apps/desktop/src/main/services/project-store.js";
import type { SystemSummary } from "../../packages/shared-types/src/index.js";
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
......@@ -55,7 +56,18 @@ async function main(): Promise<void> {
await writeFile(readmePath, `# Default Chat Smoke\n\n${readmeMarkerBefore}\n`, "utf8");
const projectContextService = new ProjectContextService(projectStore);
const projectExecutionRouter = new ProjectExecutionRouter();
const systemSummary: SystemSummary = {
appName: "QianjiangClaw",
appVersion: "0.1.0-smoke",
isPackaged: false,
platform: process.platform,
arch: process.arch,
appPath: path.join(repoRoot, "apps", "desktop"),
resourcesPath: path.join(repoRoot, "apps", "desktop"),
userDataPath,
logsPath: path.join(tempRoot, "logs")
};
const projectExecutionRouter = new ProjectExecutionRouter(systemSummary);
const session = await projectStore.createSession("Default Chat Smoke", project.id);
async function prepare(prompt: string) {
......
param(
[int]$GatewayPort = 18889,
[string]$GatewayToken = 'qjc-bundled-runtime-token',
[int]$SmokePort = 4318,
[string]$SmokeToken = 'smoke-token',
[string]$BaseOutputDir,
[int]$TimeoutSeconds = 240,
[switch]$SkipMaterializeRuntime
)
$ErrorActionPreference = 'Stop'
function Write-Utf8File {
param([string]$FilePath, [string]$Content)
$encoding = New-Object System.Text.UTF8Encoding $false
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($FilePath)) | Out-Null
[System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}
function New-ExpertFixtureProject {
param(
[string]$ProjectsRoot,
[string]$ProjectId,
[string]$ProjectName,
[string]$Platform,
[string]$Description,
[string]$ReadmeBody,
[string]$UpdatedAt
)
$projectRoot = Join-Path $ProjectsRoot $ProjectId
New-Item -ItemType Directory -Force -Path $projectRoot, (Join-Path $projectRoot 'memory') | Out-Null
$projectPayload = [ordered]@{
id = $ProjectId
name = $ProjectName
description = $Description
platform = $Platform
ready = $true
updatedAt = $UpdatedAt
boundSkillIds = @()
}
Write-Utf8File (Join-Path $projectRoot 'project.json') ($projectPayload | ConvertTo-Json -Depth 8)
Write-Utf8File (Join-Path $projectRoot 'README.md') ("# $ProjectName`n`n$ReadmeBody")
}
function Initialize-SmokeUserData {
param([string]$UserDataPath)
$projectsRoot = Join-Path $UserDataPath 'projects'
$manifestsRoot = Join-Path $UserDataPath 'manifests'
if (Test-Path $UserDataPath) {
Remove-Item $UserDataPath -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $UserDataPath, $projectsRoot, $manifestsRoot | Out-Null
New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'content-account-planning-smoke' -ProjectName 'Content Account Planning Expert Workspace' -Platform 'content-account-planning' -Description 'Standalone expert fixture for account planning UI smoke.' -ReadmeBody 'Used to validate bootstrap prompt injection in the real desktop UI flow.' -UpdatedAt '2026-04-16T00:00:00.000Z'
New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'zhihu-smoke' -ProjectName 'Zhihu Expert Workspace' -Platform 'zhihu' -Description 'Standalone expert fixture for Zhihu UI smoke.' -ReadmeBody 'Used to validate bootstrap prompt injection in the real desktop UI flow.' -UpdatedAt '2026-04-16T00:01:00.000Z'
New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'douyin-expert-smoke' -ProjectName 'Douyin Expert Workspace' -Platform 'douyin' -Description 'Control fixture to keep workspace non-trivial.' -ReadmeBody 'Ensures workspace still contains other expert-like projects.' -UpdatedAt '2026-04-16T00:02:00.000Z'
Write-Utf8File (Join-Path $manifestsRoot 'active-project.json') (@{ projectId = 'douyin-expert-smoke' } | ConvertTo-Json -Depth 3)
}
function Invoke-BootstrapPromptScenario {
param(
[string]$ScenarioName,
[string]$SmokeExpertEntryId,
[string]$Prompt,
[string]$ExpectedPromptSnippet,
[string]$BaseOutputDir,
[string]$ElectronSmokeScript,
[int]$SmokePort,
[string]$SmokeToken,
[int]$TimeoutSeconds
)
$scenarioRoot = Join-Path $BaseOutputDir $ScenarioName
$userDataPath = Join-Path $scenarioRoot 'user-data'
$logsPath = Join-Path $scenarioRoot 'logs'
$smokeOutput = Join-Path $scenarioRoot 'result.json'
Initialize-SmokeUserData -UserDataPath $userDataPath
if (Test-Path $logsPath) {
Remove-Item $logsPath -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $scenarioRoot, $logsPath | Out-Null
powershell -ExecutionPolicy Bypass -File $ElectronSmokeScript @(
'-SmokeOutput', $smokeOutput,
'-SmokePort', $SmokePort,
'-SmokeToken', $SmokeToken,
'-UserDataPath', $userDataPath,
'-LogsPath', $logsPath,
'-RuntimeMode', 'bundled-runtime',
'-ExpectBundledRuntime',
'-PreserveUserData',
'-SmokeViewMode', 'chat',
'-SmokeExpertEntryId', $SmokeExpertEntryId,
'-SmokeSendAfterExpertEntry',
'-SmokePrompt', $Prompt,
'-TimeoutSeconds', $TimeoutSeconds
)
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$summaryScript = @"
const fs = require('fs');
const [scenarioName, smokeOutput, expectedExpertEntryId, expectedPrompt, expectedPromptSnippet] = process.argv.slice(1);
const result = JSON.parse(fs.readFileSync(smokeOutput, 'utf8'));
if (!result.ok) {
throw new Error(result.error || (scenarioName + ' smoke failed.'));
}
const sendResult = result.sendResult || {};
const finalState = result.finalState || {};
const streamSmoke = sendResult.streamSmoke || {};
const expertEntry = sendResult.expertEntry || {};
const assistantContent = String((sendResult.lastAssistantMessage && sendResult.lastAssistantMessage.content) || streamSmoke.finalContent || streamSmoke.renderedContent || '');
if (String(sendResult.smokeExpertEntryId || '') !== expectedExpertEntryId) {
throw new Error('Smoke did not report the expected expert entry id: ' + String(sendResult.smokeExpertEntryId || ''));
}
if (String(sendResult.smokeExpertEntryAction || '') !== 'activate-and-send') {
throw new Error('Smoke expert entry action mismatch: ' + String(sendResult.smokeExpertEntryAction || ''));
}
if (String(expertEntry.entryMode || '') !== 'standalone') {
throw new Error('Expert entryMode mismatch: ' + String(expertEntry.entryMode || ''));
}
if (String(finalState.viewMode || '') !== 'experts') {
throw new Error('Final viewMode mismatch: ' + String(finalState.viewMode || ''));
}
if (String(streamSmoke.phase || '') !== 'completed') {
throw new Error('Stream did not complete: ' + String(streamSmoke.phase || ''));
}
if (!assistantContent.includes(expectedPrompt)) {
throw new Error('Assistant content did not echo the submitted prompt.');
}
if (!assistantContent.includes('[expert prompt]')) {
throw new Error('Assistant content did not echo the injected expert prompt section.');
}
if (!assistantContent.includes(expectedPromptSnippet)) {
throw new Error('Assistant content did not echo the expected bootstrap prompt snippet.');
}
console.log(JSON.stringify({
ok: true,
scenarioName,
smokeOutput,
expertEntryId: expectedExpertEntryId,
currentProjectId: expertEntry.currentProjectId || null,
sessionId: sendResult.sessionId || streamSmoke.sessionId || null,
streamPhase: streamSmoke.phase || null,
assistantEchoedPrompt: assistantContent.includes(expectedPrompt),
assistantEchoedExpertSection: assistantContent.includes('[expert prompt]'),
assistantEchoedBootstrapPrompt: assistantContent.includes(expectedPromptSnippet)
}, null, 2));
"@
$summary = & node -e $summaryScript $ScenarioName $smokeOutput $SmokeExpertEntryId $Prompt $ExpectedPromptSnippet
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Write-Output $summary
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
$BaseOutputDir = Join-Path $repoRoot '.tmp\desktop-expert-bootstrap-ui-smoke'
}
$BaseOutputDir = [System.IO.Path]::GetFullPath($BaseOutputDir)
$electronSmokeScript = Join-Path $repoRoot 'build\scripts\electron-smoke.ps1'
$contentPlanningPromptFile = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('5YaF5a656LSm5Y+36KeE5YiS5LiT5a62cHJvbXB0Lm1k'))
$zhihuPromptFile = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('55+l5LmO5LiT5a62cHJvbXB0Lm1k'))
$contentPlanningSnippet = (Get-Content -Path (Join-Path $repoRoot (Join-Path 'apps\desktop\bootstrap\prompts' $contentPlanningPromptFile)) -Encoding UTF8 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim()
$zhihuSnippet = (Get-Content -Path (Join-Path $repoRoot (Join-Path 'apps\desktop\bootstrap\prompts' $zhihuPromptFile)) -Encoding UTF8 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim()
if (Test-Path $BaseOutputDir) {
Remove-Item $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $BaseOutputDir | Out-Null
if (-not $SkipMaterializeRuntime) {
Write-Host "Materializing bundled runtime payload on port $GatewayPort"
powershell -ExecutionPolicy Bypass -File (Join-Path $repoRoot 'build\scripts\materialize-runtime-payload.ps1') -GatewayPort $GatewayPort -GatewayToken $GatewayToken
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
$contentPlanningPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('VUkgc21va2U6IOivt+W4ruaIkeinhOWIkuS4gOS4quWuoOeJqeS4u+eQhuS6uuWGheWuuei0puWPt+OAgg=='))
Invoke-BootstrapPromptScenario -ScenarioName 'standalone-content-account-planning-bootstrap' -SmokeExpertEntryId 'content-account-planning' -Prompt $contentPlanningPrompt -ExpectedPromptSnippet $contentPlanningSnippet -BaseOutputDir $BaseOutputDir -ElectronSmokeScript $electronSmokeScript -SmokePort $SmokePort -SmokeToken $SmokeToken -TimeoutSeconds $TimeoutSeconds
$zhihuPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('VUkgc21va2U6IOivt+W4ruaIkeWGmeS4gOS4quefpeS5juWbnuetlOW8gOWktOOAgg=='))
Invoke-BootstrapPromptScenario -ScenarioName 'standalone-zhihu-bootstrap' -SmokeExpertEntryId 'zhihu' -Prompt $zhihuPrompt -ExpectedPromptSnippet $zhihuSnippet -BaseOutputDir $BaseOutputDir -ElectronSmokeScript $electronSmokeScript -SmokePort $SmokePort -SmokeToken $SmokeToken -TimeoutSeconds $TimeoutSeconds
......@@ -19,6 +19,8 @@ param(
[string]$WorkspaceMarkerFile = 'AGENT.md',
[string]$SmokeViewMode = 'chat',
[string]$SmokeProjectId,
[string]$SmokeExpertEntryId,
[switch]$SmokeSendAfterExpertEntry,
[ValidateSet('continue-home', 'switch-expert', 'dismiss', '')]
[string]$SmokeSuggestionAction = '',
[string]$ExpectedBundleSourceUrl,
......@@ -197,6 +199,18 @@ if ($PSBoundParameters.ContainsKey('SmokeViewMode')) {
if ($PSBoundParameters.ContainsKey('SmokeProjectId')) {
$env:QJCLAW_SMOKE_PROJECT_ID = $SmokeProjectId
}
if ($PSBoundParameters.ContainsKey('SmokeExpertEntryId')) {
if ([string]::IsNullOrWhiteSpace($SmokeExpertEntryId)) {
Remove-Item Env:QJCLAW_SMOKE_EXPERT_ENTRY_ID -ErrorAction SilentlyContinue
} else {
$env:QJCLAW_SMOKE_EXPERT_ENTRY_ID = $SmokeExpertEntryId
}
}
if ($SmokeSendAfterExpertEntry) {
$env:QJCLAW_SMOKE_SEND_AFTER_EXPERT_ENTRY = '1'
} else {
Remove-Item Env:QJCLAW_SMOKE_SEND_AFTER_EXPERT_ENTRY -ErrorAction SilentlyContinue
}
if ($PSBoundParameters.ContainsKey('SmokeSuggestionAction')) {
if ([string]::IsNullOrWhiteSpace($SmokeSuggestionAction)) {
Remove-Item Env:QJCLAW_SMOKE_SUGGESTION_ACTION -ErrorAction SilentlyContinue
......@@ -391,31 +405,107 @@ if (smokeViewMode === 'skills') {
: [];
const workspaceLaunchAccepted = process.env.QJCLAW_SMOKE_ACCEPT_WORKSPACE_LAUNCH === '1'
&& statusLabels.some((label) => label.includes('Launching project workspace'));
if (streamSmoke.phase !== 'completed' && !workspaceLaunchAccepted) {
if (sendResult.smokeExpertEntryId) {
const finalWorkspace = finalState.workspaceSummary || {};
const experts = finalState.experts || {};
const expertEntry = sendResult.expertEntry || {};
const standaloneIds = Array.isArray(experts.standaloneIds)
? experts.standaloneIds.map((value) => String(value || '')).sort()
: [];
const homeShortcutIds = Array.isArray(experts.homeShortcutIds)
? experts.homeShortcutIds.map((value) => String(value || '')).sort()
: [];
const standalonePromptAvailableIds = Array.isArray(experts.standalonePromptAvailableIds)
? experts.standalonePromptAvailableIds.map((value) => String(value || '')).sort()
: [];
const smokeExpertEntryId = String(sendResult.smokeExpertEntryId || '');
if (!smokeExpertEntryId) {
throw new Error('Expert-entry smoke did not report smokeExpertEntryId.');
}
if (standaloneIds.length !== 2) {
throw new Error('Expert-entry smoke did not expose exactly 2 standalone experts: ' + JSON.stringify(standaloneIds));
}
if (homeShortcutIds.length !== 6) {
throw new Error('Expert-entry smoke did not expose exactly 6 home shortcuts: ' + JSON.stringify(homeShortcutIds));
}
if (standalonePromptAvailableIds.length !== 2) {
throw new Error('Expert-entry smoke did not expose prompt availability for both standalone experts: ' + JSON.stringify(standalonePromptAvailableIds));
}
if (String(expertEntry.expertId || '') !== smokeExpertEntryId) {
throw new Error('Expert-entry smoke returned mismatched expert id: ' + String(expertEntry.expertId || ''));
}
if (String(expertEntry.entryMode || '') === 'standalone') {
if (String(finalState.viewMode || '') !== 'experts') {
throw new Error('Standalone expert entry did not switch to experts view.');
}
if (String(expertEntry.viewMode || '') !== 'experts') {
throw new Error('Standalone expert action result did not report experts view.');
}
if (!String(expertEntry.currentProjectId || '')) {
throw new Error('Standalone expert entry did not resolve a project id.');
}
if (!standaloneIds.includes(smokeExpertEntryId)) {
throw new Error('Standalone expert id was not published in standaloneIds: ' + smokeExpertEntryId);
}
if (!standalonePromptAvailableIds.includes(smokeExpertEntryId)) {
throw new Error('Standalone expert id did not report prompt availability: ' + smokeExpertEntryId);
}
if (String(finalWorkspace.currentProjectId || '') !== String(expertEntry.currentProjectId || '')) {
throw new Error('Standalone expert entry did not activate the expected project. final=' + String(finalWorkspace.currentProjectId || '') + ' expected=' + String(expertEntry.currentProjectId || ''));
}
} else if (String(expertEntry.entryMode || '') === 'home-chat-shortcut') {
if (String(finalState.viewMode || '') !== 'chat') {
throw new Error('Home shortcut expert entry did not switch to chat view.');
}
if (String(expertEntry.viewMode || '') !== 'chat') {
throw new Error('Home shortcut expert action result did not report chat view.');
}
if (!homeShortcutIds.includes(smokeExpertEntryId)) {
throw new Error('Home shortcut id was not published in homeShortcutIds: ' + smokeExpertEntryId);
}
if (String(finalState.ui && finalState.ui.sessionScopeProjectId || '') !== 'home-chat') {
throw new Error('Home shortcut expert entry changed away from home-chat session scope: ' + String(finalState.ui && finalState.ui.sessionScopeProjectId || ''));
}
if (String(expertEntry.sessionScopeProjectId || '') !== 'home-chat') {
throw new Error('Home shortcut expert post-action session scope mismatch: ' + String(expertEntry.sessionScopeProjectId || ''));
}
if (!String(expertEntry.prompt || '').trim()) {
throw new Error('Home shortcut expert did not provide a starter prompt.');
}
if (Number(sendResult.messageCount || 0) !== 0) {
throw new Error('Home shortcut expert unexpectedly sent messages: ' + Number(sendResult.messageCount || 0));
}
if (sendResult.homeIntentSuggestionVisible) {
throw new Error('Home shortcut expert unexpectedly surfaced home intent suggestion state.');
}
} else {
throw new Error('Expert-entry smoke returned unsupported entryMode: ' + String(expertEntry.entryMode || ''));
}
} else if (streamSmoke.phase !== 'completed' && !workspaceLaunchAccepted) {
throw new Error('Renderer stream smoke did not complete successfully: ' + streamSmoke.phase);
}
if (streamSmoke.fallbackUsed) {
if (!sendResult.smokeExpertEntryId && streamSmoke.fallbackUsed) {
throw new Error('Renderer stream smoke fell back to non-streaming sendPrompt.');
}
if (!['cloud-default', 'cloud-skill-binding'].includes(executionPolicySource)) {
if (!sendResult.smokeExpertEntryId && !['cloud-default', 'cloud-skill-binding'].includes(executionPolicySource)) {
throw new Error('Unexpected stream execution policy source: ' + executionPolicySource);
}
if (sendResult.selectedSkillId && streamSmoke.selectedSkillId !== sendResult.selectedSkillId) {
throw new Error('Renderer stream selectedSkillId does not match smoke selection.');
}
if (Number(streamSmoke.startedEventCount || 0) < 1) {
if (!sendResult.smokeExpertEntryId && Number(streamSmoke.startedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a started event.');
}
if (!workspaceLaunchAccepted && Number(streamSmoke.deltaEventCount || 0) < 1 && !String(streamSmoke.finalContent || '')) {
if (!sendResult.smokeExpertEntryId && !workspaceLaunchAccepted && Number(streamSmoke.deltaEventCount || 0) < 1 && !String(streamSmoke.finalContent || '')) {
throw new Error('Renderer stream smoke did not observe a delta event or final assistant content.');
}
if (!workspaceLaunchAccepted && Number(streamSmoke.completedEventCount || 0) < 1) {
if (!sendResult.smokeExpertEntryId && !workspaceLaunchAccepted && Number(streamSmoke.completedEventCount || 0) < 1) {
throw new Error('Renderer stream smoke did not observe a completed event.');
}
if (Number(streamSmoke.errorEventCount || 0) !== 0) {
if (!sendResult.smokeExpertEntryId && Number(streamSmoke.errorEventCount || 0) !== 0) {
throw new Error('Renderer stream smoke observed unexpected error events: ' + streamSmoke.errorEventCount);
}
if (!workspaceLaunchAccepted && !String(streamSmoke.renderedContent || streamSmoke.finalContent || '')) {
if (!sendResult.smokeExpertEntryId && !workspaceLaunchAccepted && !String(streamSmoke.renderedContent || streamSmoke.finalContent || '')) {
throw new Error('Renderer stream smoke did not render assistant content.');
}
}
......@@ -441,10 +531,10 @@ if (!['skills', 'settings'].includes(smokeViewMode)) {
if (Number(runtimeTelemetry.heartbeatSuccessCount || 0) < 1) {
throw new Error('Runtime telemetry did not record a successful heartbeat.');
}
if (!acceptWorkspaceLaunch && Number(runtimeTelemetry.totalAcceptedEventCount || 0) < 3) {
if (!acceptWorkspaceLaunch && !sendResult.smokeExpertEntryId && Number(runtimeTelemetry.totalAcceptedEventCount || 0) < 3) {
throw new Error('Runtime telemetry did not accept the expected event batch count: ' + runtimeTelemetry.totalAcceptedEventCount);
}
if (!acceptWorkspaceLaunch && Number(runtimeTelemetry.configSyncSuccessCount || 0) < 1) {
if (!acceptWorkspaceLaunch && !sendResult.smokeExpertEntryId && Number(runtimeTelemetry.configSyncSuccessCount || 0) < 1) {
throw new Error('Runtime telemetry did not record a successful config sync.');
}
if (!diagnostics || !diagnostics.runtimeTelemetry) {
......@@ -661,6 +751,8 @@ finally {
Remove-Item Env:QJCLAW_SMOKE_SKILL_ID -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_VIEW_MODE -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_PROJECT_ID -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_EXPERT_ENTRY_ID -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_SEND_AFTER_EXPERT_ENTRY -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SMOKE_STARTUP_ONLY -ErrorAction SilentlyContinue
Remove-Item Env:QJCLAW_SECRET_BACKEND -ErrorAction SilentlyContinue
}
......
param(
[string]$SmokeOutput
)
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$sourcePath = Join-Path $repoRoot 'build\scripts\expert-bootstrap-prompt-smoke.ts'
$tempRoot = Join-Path $repoRoot '.tmp\expert-bootstrap-prompt-smoke'
$compileRoot = Join-Path $tempRoot 'compiled'
$entryPath = Join-Path $compileRoot 'build\scripts\expert-bootstrap-prompt-smoke.js'
$compilePackagePath = Join-Path $compileRoot 'package.json'
$resolvedResultPath = if ($SmokeOutput) { [System.IO.Path]::GetFullPath($SmokeOutput) } else { Join-Path $tempRoot 'result.json' }
function Write-Utf8File {
param([string]$FilePath, [string]$Content)
$encoding = New-Object System.Text.UTF8Encoding $false
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($FilePath)) | Out-Null
[System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}
if (-not (Test-Path $sourcePath)) {
throw "Expert bootstrap prompt smoke source was not found: $sourcePath"
}
if (Test-Path $compileRoot) {
Remove-Item $compileRoot -Recurse -Force
}
New-Item -ItemType Directory -Path $compileRoot -Force | Out-Null
$compileArgs = @(
'pnpm',
'--dir', (Join-Path $repoRoot 'apps\desktop'),
'exec',
'tsc',
'--module', 'ES2022',
'--moduleResolution', 'node',
'--target', 'ES2022',
'--lib', 'ES2022',
'--types', 'node',
'--esModuleInterop',
'--allowSyntheticDefaultImports',
'--skipLibCheck',
'--outDir', $compileRoot,
$sourcePath
)
Write-Host 'Compiling expert-bootstrap-prompt smoke with local TypeScript'
corepack @compileArgs
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if (-not (Test-Path $entryPath)) {
throw "Expert bootstrap prompt smoke entry was not emitted: $entryPath"
}
Write-Utf8File -FilePath $compilePackagePath -Content '{"type":"module"}'
Write-Host 'Running expert-bootstrap-prompt smoke'
node $entryPath $resolvedResultPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if (-not (Test-Path $resolvedResultPath)) {
throw "Expert bootstrap prompt smoke did not produce a result file: $resolvedResultPath"
}
import { mkdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { AppConfigService } from "../../apps/desktop/src/main/services/app-config.js";
import { loadBootstrapExpertPrompt } from "../../apps/desktop/src/main/services/bootstrap-expert-prompts.js";
import { ProjectContextService } from "../../apps/desktop/src/main/services/project-context.js";
import { ProjectExecutionRouter } from "../../apps/desktop/src/main/services/project-execution-router.js";
import { ProjectStoreService } from "../../apps/desktop/src/main/services/project-store.js";
import type { SystemSummary } from "../../packages/shared-types/src/index.js";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function buildSystemSummary(repoRoot: string, userDataPath: string, logsPath: string): SystemSummary {
return {
appName: "QianjiangClaw",
appVersion: "0.1.0-smoke",
isPackaged: false,
platform: process.platform,
arch: process.arch,
appPath: path.join(repoRoot, "apps", "desktop"),
resourcesPath: path.join(repoRoot, "apps", "desktop"),
userDataPath,
logsPath
};
}
async function main(): Promise<void> {
const repoRoot = process.cwd();
const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "expert-bootstrap-prompt-smoke", "result.json"));
const tempRoot = path.dirname(resultPath);
const userDataPath = path.join(tempRoot, "user-data");
const logsPath = path.join(tempRoot, "logs");
await rm(tempRoot, { recursive: true, force: true });
await mkdir(userDataPath, { recursive: true });
await mkdir(logsPath, { recursive: true });
const configService = new AppConfigService(userDataPath);
await configService.load();
const projectStore = new ProjectStoreService(configService);
await projectStore.initialize();
const projectContextService = new ProjectContextService(projectStore);
const systemSummary = buildSystemSummary(repoRoot, userDataPath, logsPath);
const projectExecutionRouter = new ProjectExecutionRouter(systemSummary);
const scenarios = [
{
projectId: "content-account-planning",
projectName: "Content Account Planning Expert",
prompt: "帮我规划一个母婴账号的人设、栏目和更新节奏。"
},
{
projectId: "zhihu",
projectName: "Zhihu Expert",
prompt: "帮我写一个知乎回答开头,主题是副业转型。"
}
] as const;
const results: Array<Record<string, unknown>> = [];
for (const scenario of scenarios) {
const promptText = await loadBootstrapExpertPrompt(systemSummary, scenario.projectId);
assert(promptText, `Bootstrap expert prompt was not found for ${scenario.projectId}.`);
const project = await projectStore.upsertProject({
id: scenario.projectId,
name: scenario.projectName,
description: `Smoke fixture for ${scenario.projectId}.`,
ready: true,
boundSkillIds: []
});
const projectRoot = await projectStore.getProjectRoot(project.id);
await writeFile(path.join(projectRoot, "README.md"), `# ${scenario.projectName}\n\nSmoke README for ${scenario.projectId}.\n`, "utf8");
const session = await projectStore.createSession(`${scenario.projectName} Smoke`, project.id);
const snapshot = await projectContextService.getSnapshot(project.id);
const decision = await projectExecutionRouter.decide({
sessionId: session.id,
projectId: project.id,
projectRoot,
userPrompt: scenario.prompt,
context: snapshot,
selectedSkillId: null,
projectConfig: await projectStore.getProjectPackageConfig(project.id)
});
assert(decision.kind === "chat-fallback", `${scenario.projectId} should route through chat-fallback in this smoke fixture.`);
assert(decision.preparedPrompt.includes("[expert prompt]"), `${scenario.projectId} preparedPrompt did not include the expert prompt section marker.`);
assert(decision.preparedPrompt.includes(promptText), `${scenario.projectId} preparedPrompt did not include the bootstrap expert prompt body.`);
assert(decision.preparedPrompt.includes(scenario.prompt), `${scenario.projectId} preparedPrompt did not preserve the user prompt.`);
assert(decision.preparedPrompt.includes(`Current project: ${project.name} (${project.id})`), `${scenario.projectId} preparedPrompt did not include the project identity.`);
results.push({
projectId: project.id,
decisionKind: decision.kind,
promptLoaded: true,
preparedPromptIncludesExpertSection: decision.preparedPrompt.includes("[expert prompt]"),
preparedPromptIncludesBootstrapPrompt: decision.preparedPrompt.includes(promptText),
preparedPromptIncludesUserPrompt: decision.preparedPrompt.includes(scenario.prompt)
});
}
const ordinaryProject = await projectStore.upsertProject({
id: "generic-smoke",
name: "Generic Smoke",
description: "Control project without standalone expert bootstrap prompt.",
ready: true,
boundSkillIds: []
});
const ordinaryProjectRoot = await projectStore.getProjectRoot(ordinaryProject.id);
const ordinarySession = await projectStore.createSession("Generic Smoke", ordinaryProject.id);
const ordinarySnapshot = await projectContextService.getSnapshot(ordinaryProject.id);
const ordinaryDecision = await projectExecutionRouter.decide({
sessionId: ordinarySession.id,
projectId: ordinaryProject.id,
projectRoot: ordinaryProjectRoot,
userPrompt: "普通项目的默认对话",
context: ordinarySnapshot,
selectedSkillId: null,
projectConfig: await projectStore.getProjectPackageConfig(ordinaryProject.id)
});
assert(!ordinaryDecision.preparedPrompt.includes("[expert prompt]"), "Non-expert project unexpectedly included expert prompt content.");
const summary = {
ok: true,
userDataPath,
logsPath,
results,
controlProjectId: ordinaryProject.id,
controlPreparedPromptIncludesExpertSection: ordinaryDecision.preparedPrompt.includes("[expert prompt]")
};
await mkdir(path.dirname(resultPath), { recursive: true });
await writeFile(resultPath, JSON.stringify(summary, null, 2), "utf8");
console.log(JSON.stringify(summary, null, 2));
}
main().catch(async (error) => {
const repoRoot = process.cwd();
const resultPath = path.resolve(process.argv[2] ?? path.join(repoRoot, ".tmp", "expert-bootstrap-prompt-smoke", "result.json"));
const failure = {
ok: false,
error: error instanceof Error ? error.stack ?? error.message : String(error)
};
await mkdir(path.dirname(resultPath), { recursive: true });
await writeFile(resultPath, JSON.stringify(failure, null, 2), "utf8");
console.error(failure.error);
process.exitCode = 1;
});
......@@ -8,6 +8,7 @@ import { ProjectExecutionRouter } from "../../apps/desktop/src/main/services/pro
import { ProjectIntentRouterService } from "../../apps/desktop/src/main/services/project-intent-router.js";
import { ProjectSkillRouterService } from "../../apps/desktop/src/main/services/project-skill-router.js";
import { ProjectStoreService } from "../../apps/desktop/src/main/services/project-store.js";
import type { SystemSummary } from "../../packages/shared-types/src/index.js";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
......@@ -110,7 +111,18 @@ description: douyin script writing skill
const intentRouter = new ProjectIntentRouterService(projectStore);
const skillRouter = new ProjectSkillRouterService(projectStore);
const projectContextService = new ProjectContextService(projectStore);
const projectExecutionRouter = new ProjectExecutionRouter();
const systemSummary: SystemSummary = {
appName: "QianjiangClaw",
appVersion: "0.1.0-smoke",
isPackaged: false,
platform: process.platform,
arch: process.arch,
appPath: path.join(repoRoot, "apps", "desktop"),
resourcesPath: path.join(repoRoot, "apps", "desktop"),
userDataPath,
logsPath: path.join(tempRoot, "logs")
};
const projectExecutionRouter = new ProjectExecutionRouter(systemSummary);
const chatTargetResolver = new ProjectChatTargetResolverService(projectStore, intentRouter);
await projectStore.setActiveProject(douyin.id);
......
......@@ -24,6 +24,8 @@
"launch:xhs-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/xhs-expert-manual-launch.ps1",
"launch:douyin-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-manual-launch.ps1",
"smoke:default-chat": "powershell -ExecutionPolicy Bypass -File build/scripts/default-chat-smoke.ps1",
"smoke:expert-bootstrap-prompt": "powershell -ExecutionPolicy Bypass -File build/scripts/expert-bootstrap-prompt-smoke.ps1",
"smoke:desktop-expert-bootstrap-ui": "powershell -ExecutionPolicy Bypass -File build/scripts/desktop-expert-bootstrap-ui-smoke.ps1",
"smoke:settings": "powershell -ExecutionPolicy Bypass -File build/scripts/settings-smoke.ps1",
"smoke:project-routing": "powershell -ExecutionPolicy Bypass -File build/scripts/project-routing-smoke.ps1",
"smoke:project-package-orchestrator": "powershell -ExecutionPolicy Bypass -File build/scripts/project-package-orchestrator-smoke.ps1",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment