Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress

This commit is contained in:
Chinesezjc
2026-07-27 18:45:47 +08:00
32 changed files with 465 additions and 425 deletions

View File

@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
/**
@@ -22,25 +22,18 @@ import { describe, expect, it } from 'vitest'
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
/** Run the built bin with PIPED stdio; resolve with output + exit code. */
function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [dshBin], { stdio: ['pipe', 'pipe', 'pipe'] })
let stdout = ''
let stderr = ''
child.stdout.setEncoding('utf8')
child.stdout.on('data', (c: string) => { stdout += c })
child.stderr.setEncoding('utf8')
child.stderr.on('data', (c: string) => { stderr += c })
const timer = setTimeout(() => {
child.kill('SIGKILL')
reject(new Error(`dsh built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`))
}, 25_000)
// Resolve on `close` (all stdio drained), not `exit`, so captured output is complete.
child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
child.on('error', (err) => { clearTimeout(timer); reject(err) })
child.stdin.end()
/** Run the built bin with PIPED stdio (stdin closed at EOF); resolve with output + exit code. */
async function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
const result = await execa(process.execPath, [dshBin], {
input: '',
timeout: 25_000,
killSignal: 'SIGKILL',
reject: false,
})
if (result.timedOut) {
throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
}
return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }
}
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {

View File

@@ -17,7 +17,7 @@
// the open llm seam post-boot with installLlmReplay on the settled root ctx
// (the plugin-row path discards the ReplayHandle; the direct install keeps
// assertConsumed for the teardown fixture-consumption check).
import { existsSync, readFileSync } from 'node:fs'
import { existsSync } from 'node:fs'
import { mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
@@ -69,16 +69,6 @@ const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/cordis.yml')
// contextWindow keeps that pressure path provably inert for small fixtures.
const REPLAY_PROVIDERS = [{ id: 'deepseek', name: 'DeepSeek', models: [{ id: 'deepseek-v4-flash', contextWindow: 128_000 }] }]
/** Repo-root .env → process.env for record mode (never overrides set vars); the smoke-real convention. */
function loadRootEnv(): void {
const envPath = join(REPO_ROOT, '.env')
if (!existsSync(envPath)) return
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim())
if (m !== null && process.env[m[1]!] === undefined) process.env[m[1]!] = m[2]
}
}
/** A booted web scaffold: real composition, mode-selected model backend, temp world. */
export interface WebScaffold {
/** The active snapshot mode this scaffold booted under. */
@@ -142,7 +132,8 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
requireDist()
const mode = webSnapshotMode()
if (mode === 'record') {
loadRootEnv()
// Both owning vitest configs (web unconditionally, snapshot in record
// mode) load the repo-root .env before this file runs.
if (process.env.DEEPSEEK_API_KEY === undefined || process.env.DEEPSEEK_API_KEY.length === 0) {
throw new Error('web e2e record mode needs DEEPSEEK_API_KEY (env or repo-root .env)')
}

View File

@@ -1,9 +1,9 @@
// W5 real-host smoke: spawn `dsh web` with a real key, walk the full W5 flow
// list in a real chromium, screenshot every screen into .artifacts/ for the
// figma comparison pass. Self-skips without DEEPSEEK_API_KEY (repo e2e
// convention); the runner loads the repo-root .env explicitly because the CLI
// only auto-loads .env from its cwd (a temp dir here, so sessions never land
// in the repo's .sessions).
// convention); vitest.web.config.ts loads the repo-root .env before this file
// runs (the CLI only auto-loads .env from its cwd a temp dir here, so
// sessions never land in the repo's .sessions).
//
// Selector convention: CSS Modules hash as [hash]_[local], so class-substring
// selectors are unreliable — anchor on data-* attributes (data-variant /
@@ -26,17 +26,6 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { REPO_ROOT, connectFreshWorkspace, probeFreePort, requireDist, saveFailureShot } from './support.ts'
/** Repo-root .env → process.env (never overrides an already-set variable). */
function loadRootEnv(): void {
const envPath = join(REPO_ROOT, '.env')
if (!existsSync(envPath)) return
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim())
if (m !== null && process.env[m[1]!] === undefined) process.env[m[1]!] = m[2]
}
}
loadRootEnv()
function waitForReadyLine(child: ChildProcess): Promise<string> {
return new Promise((resolveReady, reject) => {
let out = ''