|
|
@@ -4,18 +4,20 @@
|
|
|
// the vendored Loader (the same include boot AppCLIEntry drives), patched the
|
|
|
// snapshot way — so a real chromium exercises the real HTTP/SSE wire, the
|
|
|
// api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT:
|
|
|
-// replay (default, keyless: llm-deepseek row disabled, dsh-llm-replay row
|
|
|
-// inserted in providers mode), record (real adapter + key, harvests fixtures
|
|
|
-// from live session memory), refresh (keyless replay that rewrites goldens).
|
|
|
+// replay (default, keyless: normally disables the llm-deepseek row and
|
|
|
+// inserts dsh-llm-replay in providers mode), record (real adapter + key,
|
|
|
+// harvests fixtures from live session memory), refresh (keyless replay that
|
|
|
+// rewrites goldens). A first-run option keeps the real adapter mounted while
|
|
|
+// masking its credential, without making a model call.
|
|
|
//
|
|
|
// Composition divergences from `dsh web`, all deliberate, all via include
|
|
|
// patches over the SAME tree (never a second yml): temp persistenceRoot;
|
|
|
// workspace-context disabled (recorded fixtures must not embed this repo's
|
|
|
// AGENTS.md); session-title-llm disabled (its fire-and-forget title call
|
|
|
// would race the loop for the session's replay cursor); webserver pinned to
|
|
|
-// port 0 with the built dist; keyless modes disable llm-deepseek and fill
|
|
|
-// the open llm seam post-boot with installLlmReplay on the settled root ctx
|
|
|
-// (the plugin-row path discards the ReplayHandle; the direct install keeps
|
|
|
+// port 0 with the built dist; ordinary keyless modes disable llm-deepseek and
|
|
|
+// fill 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 } from 'node:fs'
|
|
|
import { mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
|
|
|
@@ -125,6 +127,12 @@ export interface LaunchOptions {
|
|
|
* remain reconstructable without making the tools a product default.
|
|
|
*/
|
|
|
cordisTools?: boolean
|
|
|
+ /**
|
|
|
+ * Keep the shipped DeepSeek adapter mounted while masking the process
|
|
|
+ * environment's DEEPSEEK_API_KEY for this scaffold lifetime. This is the
|
|
|
+ * keyless first-run configuration lane; the default disables the adapter.
|
|
|
+ */
|
|
|
+ deepSeekMissingCredential?: boolean
|
|
|
}
|
|
|
|
|
|
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
|
|
|
@@ -151,6 +159,21 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
throw new Error('web e2e record mode needs DEEPSEEK_API_KEY (env or repo-root .env)')
|
|
|
}
|
|
|
}
|
|
|
+ if (mode === 'record' && options.deepSeekMissingCredential === true) {
|
|
|
+ throw new Error('deepSeekMissingCredential is a keyless replay/refresh option')
|
|
|
+ }
|
|
|
+ const maskDeepSeekCredential = mode !== 'record' && options.deepSeekMissingCredential === true
|
|
|
+ const originalDeepSeekCredential = process.env.DEEPSEEK_API_KEY
|
|
|
+ let credentialEnvironmentRestored = false
|
|
|
+ const restoreCredentialEnvironment = (): void => {
|
|
|
+ if (credentialEnvironmentRestored || !maskDeepSeekCredential) return
|
|
|
+ credentialEnvironmentRestored = true
|
|
|
+ if (originalDeepSeekCredential === undefined) {
|
|
|
+ Reflect.deleteProperty(process.env, 'DEEPSEEK_API_KEY')
|
|
|
+ } else {
|
|
|
+ process.env.DEEPSEEK_API_KEY = originalDeepSeekCredential
|
|
|
+ }
|
|
|
+ }
|
|
|
const workspaceCwd = await realpath(await mkdtemp(join(tmpdir(), 'dsh-web-e2e-ws-')))
|
|
|
// Isolated harness home: the settings/credentials rows resolve $DSH_HOME
|
|
|
// paths at load, and an in-process boot must NEVER touch the developer's
|
|
|
@@ -165,6 +188,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
|
|
|
throw error
|
|
|
}
|
|
|
+ if (maskDeepSeekCredential) Reflect.deleteProperty(process.env, 'DEEPSEEK_API_KEY')
|
|
|
|
|
|
// The include patch set — the same mechanism AppCLIEntry and the ACP
|
|
|
// snapshot overlay use, applied over the SAME shipped tree (a patch id that
|
|
|
@@ -187,7 +211,9 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
...options.cordisTools === true
|
|
|
? [{ insert: [{ id: 'tool-cordis', name: 'cordis:tool-cordis' }] }]
|
|
|
: [],
|
|
|
- ...mode === 'record' ? [] : [{ id: 'llm-deepseek', disabled: true }],
|
|
|
+ ...mode === 'record' || options.deepSeekMissingCredential === true
|
|
|
+ ? []
|
|
|
+ : [{ id: 'llm-deepseek', disabled: true }],
|
|
|
]
|
|
|
|
|
|
// Sessions inherit the gateway's process.cwd() default; run the boot from
|
|
|
@@ -216,10 +242,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
}
|
|
|
port = boundPort
|
|
|
|
|
|
- // Fill the open llm seam on the settled root ctx (llm-deepseek is disabled
|
|
|
- // in keyless modes; a scenario with no fixture leaves the seam empty so a
|
|
|
- // stray stream fails loud with NO_ADAPTER). The direct install, unlike the
|
|
|
- // plugin row, returns the ReplayHandle for the teardown consumption check.
|
|
|
+ // Fill the open llm seam on the settled root ctx. Ordinary keyless modes
|
|
|
+ // disable llm-deepseek; the first-run lane keeps it mounted but has no
|
|
|
+ // replay fixture and never streams. The direct install, unlike the plugin
|
|
|
+ // row, returns the ReplayHandle for the teardown consumption check.
|
|
|
if (mode !== 'record' && options.replayFixture !== undefined) {
|
|
|
replayHandle = installLlmReplay(ctx, {
|
|
|
file: options.replayFixture,
|
|
|
@@ -231,6 +257,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
} catch (error) {
|
|
|
if (process.cwd() !== originalCwd) process.chdir(originalCwd)
|
|
|
const cleanupFailures = await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot)
|
|
|
+ restoreCredentialEnvironment()
|
|
|
if (cleanupFailures.length > 0) {
|
|
|
throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete')
|
|
|
}
|
|
|
@@ -279,7 +306,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|
|
} catch (error) {
|
|
|
failures.push(error)
|
|
|
}
|
|
|
- failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
|
|
+ try {
|
|
|
+ failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
|
|
+ } finally {
|
|
|
+ restoreCredentialEnvironment()
|
|
|
+ }
|
|
|
if (failures.length > 0) throw new AggregateError(failures, 'web scaffold teardown failed')
|
|
|
},
|
|
|
}
|