diff --git a/llp/0011-setup-and-onboarding.decision.md b/llp/0011-setup-and-onboarding.decision.md index b65a3e2..cc35558 100644 --- a/llp/0011-setup-and-onboarding.decision.md +++ b/llp/0011-setup-and-onboarding.decision.md @@ -20,6 +20,10 @@ > realizing the plugin-contributed picks this document already named. The > prose below is rewritten with the implementation. +> **Amended by [LLP 0137](./0137-onboarding-retention-defaults.decision.md)**: +> the "Cache retention (days)" question is removed; the pathway sets the +> default instead (30-day team / 120-day local). + ## Interactive walkthrough The primary way to get a HypAware install on the ground is the interactive @@ -27,14 +31,16 @@ setup (`npx hypaware`): ```text What do you want to collect? (Claude / Codex / raw Anthropic+OpenAI / OTEL) -Where should HypAware export? (local cache only / local Parquet / central / later) -Cache retention (days, default 30): 30 ✓ Wrote ~/.hyp/hypaware-config.json ✓ Wired Claude Code to use the local proxy ✓ Started the HypAware daemon ``` +Export and retention are not asked: export defaults to local Parquet, and +the retention window comes from the pathway chosen at the fork +([LLP 0137](./0137-onboarding-retention-defaults.decision.md#pathway-defaults)). + The walkthrough is the canonical first-run experience. It composes **plugin-contributed picks** — each source/client plugin registers what it collects; each sink plugin registers what it exports to — and writes a config @@ -92,7 +98,9 @@ HypAware picks the plugin set. There are no names like "standalone" or a source on, never hides one. Raw proxy sources and OTEL are never autodetected (no installed tool to find). See [LLP 0012](./0012-sources.spec.md#source-kinds). - **Default** is a fixed starting selection not derived from system state: - export defaults to local Parquet, retention defaults to 30 days. + export defaults to local Parquet; retention defaults by pathway, 30 days + on a team install and 120 days on a local-only one + ([LLP 0137](./0137-onboarding-retention-defaults.decision.md#pathway-defaults)). (Canonical definitions live in `CONTEXT.md`.) diff --git a/llp/0137-onboarding-retention-defaults.decision.md b/llp/0137-onboarding-retention-defaults.decision.md new file mode 100644 index 0000000..2856bef --- /dev/null +++ b/llp/0137-onboarding-retention-defaults.decision.md @@ -0,0 +1,53 @@ +# LLP 0137: Onboarding never asks for retention; the pathway sets the default + +**Type:** Decision +**Status:** Active +**Systems:** Onboarding, CLI, Config +**Author:** Phil / Claude +**Date:** 2026-07-24 +**Related:** LLP 0011, LLP 0013, LLP 0129, LLP 0135 + +> Amends [LLP 0011](./0011-setup-and-onboarding.decision.md): the +> "Cache retention (days)" question is removed from the interactive +> walkthrough, and the flat 30-day default becomes pathway-scoped. + +## Decision + +**Onboarding never asks for a cache +retention window. The pathway chosen at the fork +([LLP 0129](./0129-init-wizard-fork.decision.md#fork)) decides the +default instead:** + +- **Join a team: 30 days.** The org server holds the durable copy of + forwarded history, so the local cache is a working window, not the + record. +- **Local install: 120 days.** Nothing leaves the machine; the local + cache is the only copy of history, so it keeps a much longer window. +- A managed machine's **scoped re-entry** counts as the team side + (30 days), same rationale. + +The wizard orchestrator passes the local default into the pick phase as +`retentionDefault`; team and scoped runs fall through to the pick +phase's `DEFAULT_RETENTION_DAYS`. The superseded programmatic +walkthrough (`runPickerWalkthrough`), which has no fork, keeps the flat +30-day default. + +## Why not ask + +Retention was the last free-text question in onboarding, and it asked +for a judgment the user has no basis to make on first run (how many +days of an as-yet-empty cache to keep). Every other pick already +defaults (export defaults to local Parquet, +[LLP 0011](./0011-setup-and-onboarding.decision.md#autodetect-vs-default)); +the window is a config value (`query.cache.retention.default_days`, +[LLP 0013](./0013-local-query-cache.decision.md#retention-is-the-central-tradeoff)) +that remains freely editable after the fact, so asking up front bought +nothing but friction. + +## Overrides unchanged + +`hyp init --retention-days ` still sets the window explicitly on the +non-interactive path (its flag default stays 30), and editing +`query.cache.retention` in the written config remains the post-install +knob. Fleet-enforced retention stays deliberately deferred +([LLP 0031](./0031-layered-config.decision.md#open-questions--deferred)). diff --git a/src/core/cli/types.d.ts b/src/core/cli/types.d.ts index a0ab8dc..4e05017 100644 --- a/src/core/cli/types.d.ts +++ b/src/core/cli/types.d.ts @@ -14,7 +14,6 @@ import type { KernelRuntime } from '../runtime/types.d.ts' import type { createCommandRegistry } from '../registry/commands.js' export type AsyncPickPrompt = (question: WalkthroughQuestion) => Promise -export type AsyncRetentionPrompt = (prompt: string, defaultDays: number) => Promise export type AsyncBackfillConsentPrompt = (args: { providers: string[] retentionDays: number @@ -82,7 +81,6 @@ export interface WalkthroughOptions { env: NodeJS.ProcessEnv /** Override prompt resolver (tests pre-bake answers). */ prompt?: AsyncPickPrompt - retentionPrompt?: AsyncRetentionPrompt } export interface WalkthroughResult { @@ -143,7 +141,6 @@ export interface RunPickerWalkthroughOptions { */ exportOrigin?: PickerExportOrigin prompt?: AsyncPickPrompt - retentionPrompt?: AsyncRetentionPrompt /** * Override the system source detector. Defaults to a catalog-backed * wrapper around the real filesystem-based `detectPickerSources`. diff --git a/src/core/cli/walkthrough.js b/src/core/cli/walkthrough.js index a3cbfc1..ec63bc3 100644 --- a/src/core/cli/walkthrough.js +++ b/src/core/cli/walkthrough.js @@ -12,7 +12,7 @@ import { discoverBundledPlugins } from '../runtime/bundled.js' import { isWithinDir } from '../runtime/contribution_names.js' import { buildPluginCatalog } from '../plugin_catalog.js' import { detectPickerSources } from './detect.js' -import { multiselect, select, text } from './tui/index.js' +import { multiselect, select } from './tui/index.js' import { isPromptCancelledError } from './tui/runtime.js' import { shouldUseTui } from './tui-router.js' import { copyDir } from '../util/fs_copy.js' @@ -35,7 +35,6 @@ export const WALKTHROUGH_CANCEL_EXIT_CODE = 130 * @import { * AsyncBackfillConsentPrompt, * AsyncPickPrompt, - * AsyncRetentionPrompt, * BackfillFinaleResult, * PickerBackfillRunner, * PickerSource, @@ -50,7 +49,14 @@ export const WALKTHROUGH_CANCEL_EXIT_CODE = 130 * } from '../../../src/core/cli/types.js' */ +// Onboarding never asks for a retention window; these are the pathway +// defaults the wizard applies instead. A team (or managed) install keeps +// the 30-day window because the org server holds the durable copy; a +// local-only install keeps 120 days because the local cache is the only +// copy of history. `hyp init --retention-days ` remains the override. +// @ref LLP 0137#pathway-defaults [implements]: no retention question; 30-day team / 120-day local defaults export const DEFAULT_RETENTION_DAYS = 30 +export const LOCAL_INSTALL_RETENTION_DAYS = 120 /** * Resolve the HYP_HOME root the same way the kernel does (matches @@ -103,28 +109,6 @@ function legacyNumberedPromptFactory(opts) { } } -/** - * @param {Pick} opts - * @returns {AsyncRetentionPrompt} - */ -function legacyRetentionPromptFactory(opts) { - const input = /** @type {NodeJS.ReadableStream} */ (opts.stdin ?? process.stdin) - const output = /** @type {NodeJS.WritableStream} */ (opts.stdout) - return async function (prompt, defaultDays) { - const rl = readline.createInterface({ input, output, terminal: false }) - try { - const answer = await rl.question(`${prompt} [${defaultDays}]: `) - const trimmed = answer.trim() - if (!trimmed) return defaultDays - const parsed = Number.parseInt(trimmed, 10) - if (!Number.isInteger(parsed) || parsed < 0) return defaultDays - return parsed - } finally { - rl.close() - } - } -} - /** * Build the interactive "overwrite existing config?" confirm. Defaults * to **no** (a bare Enter keeps the existing config), so a stray @@ -177,37 +161,6 @@ function tuiPromptFactory(opts) { } } -/** - * Prompt for the cache retention window through the TUI text input. - * Empty input falls through to the supplied default to match the legacy - * behavior. - * - * @param {Pick} opts - * @returns {AsyncRetentionPrompt} - */ -function tuiRetentionPromptFactory(opts) { - return async function (prompt, defaultDays) { - const v = await text({ - title: prompt, - default: String(defaultDays), - validate: (s) => { - if (s.trim() === '') return null - const n = Number.parseInt(s.trim(), 10) - return Number.isInteger(n) && n >= 0 ? null : 'enter a non-negative integer' - }, - clearOnResolve: true, - stdin: opts.stdin ?? process.stdin, - stdout: /** @type {NodeJS.WritableStream} */ (/** @type {unknown} */ (opts.stdout)), - env: opts.env, - }) - const trimmed = v.trim() - if (trimmed === '') return defaultDays - const parsed = Number.parseInt(trimmed, 10) - if (!Number.isInteger(parsed) || parsed < 0) return defaultDays - return parsed - } -} - /** * Route between the TUI and legacy prompts. Tests and CI keep getting * the legacy numbered list, but only real TTYs without `HYP_NO_TUI=1` see @@ -221,15 +174,6 @@ export function defaultPromptFactory(opts) { return legacyNumberedPromptFactory(opts) } -/** - * @param {Pick} opts - * @returns {AsyncRetentionPrompt} - */ -export function defaultRetentionPromptFactory(opts) { - if (shouldUseTui(opts)) return tuiRetentionPromptFactory(opts) - return legacyRetentionPromptFactory(opts) -} - /** * Build the interactive backfill-consent prompt. Routes to the TUI * arrow-navigable yes/no select on a real TTY, else a legacy readline @@ -423,7 +367,6 @@ export async function runPickerWalkthrough(opts) { exportOrigin = opts.exportOrigin ?? 'default' } else { const ask = opts.prompt ?? defaultPromptFactory(opts) - const retentionAsk = opts.retentionPrompt ?? defaultRetentionPromptFactory(opts) stdout.write('Welcome to HypAware - the local logs+telemetry collector.\n\n') @@ -450,8 +393,10 @@ export async function runPickerWalkthrough(opts) { // editing the written config later. const exportChoice = /** @type {PickerExport} */ ('local-parquet') - const retentionDays = await retentionAsk('Cache retention (days)', DEFAULT_RETENTION_DAYS) - picks = { sources, exportChoice, retentionDays } + // Retention is not asked either (LLP 0137): this legacy surface has + // no pathway fork, so it takes the flat default. The wizard applies + // the pathway-aware defaults; `--retention-days` overrides via picks. + picks = { sources, exportChoice, retentionDays: DEFAULT_RETENTION_DAYS } } catch (err) { if (isPromptCancelledError(err)) { return await cancelledResult(opts) diff --git a/src/core/cli/wizard/index.js b/src/core/cli/wizard/index.js index e037a6f..f4312e5 100644 --- a/src/core/cli/wizard/index.js +++ b/src/core/cli/wizard/index.js @@ -18,7 +18,7 @@ import { discoverBundledPlugins } from '../../runtime/bundled.js' import { buildPluginCatalog } from '../../plugin_catalog.js' import { collectHypAwareStatus } from '../../daemon/status.js' import { formatFirstSyncDeadline, readFirstSyncDeadline } from '../../usage-policy/first_sync_hold.js' -import { runPickerFinale, writeWalkthroughRunSummary } from '../walkthrough.js' +import { LOCAL_INSTALL_RETENTION_DAYS, runPickerFinale, writeWalkthroughRunSummary } from '../walkthrough.js' import { LOGIN_ORG_SELECTION_MESSAGE } from '../remote_commands.js' import { evaluateReturningGate, runWizardFork } from './fork.js' import { computeCentralLockedSources, runWizardJoin } from './join.js' @@ -127,7 +127,12 @@ export async function runInitWizard(opts) { ...(opts.exportOrigin ? { exportOrigin: opts.exportOrigin } : {}), ...(opts.force ? { force: opts.force } : {}), ...(opts.prompt ? { prompt: opts.prompt } : {}), - ...(opts.retentionPrompt ? { retentionPrompt: opts.retentionPrompt } : {}), + // Retention is never asked; the pathway decides the default. A local + // install keeps the longer 120-day window (the cache is the only copy + // of history); team and scoped runs fall through to the pick phase's + // 30-day default (the org server holds the durable copy). + // @ref LLP 0137#pathway-defaults [implements]: 30-day team / 120-day local retention defaults + ...(pathway === 'local' ? { retentionDefault: LOCAL_INSTALL_RETENTION_DAYS } : {}), ...(opts.detect ? { detect: opts.detect } : {}), ...(opts.confirmOverwrite ? { confirmOverwrite: opts.confirmOverwrite } : {}), }) diff --git a/src/core/cli/wizard/pick.js b/src/core/cli/wizard/pick.js index 2b0faf5..271aabf 100644 --- a/src/core/cli/wizard/pick.js +++ b/src/core/cli/wizard/pick.js @@ -14,7 +14,6 @@ import { defaultOverwriteConfirmFactory, defaultPickerDetect, defaultPromptFactory, - defaultRetentionPromptFactory, loadPickerDescriptors, orderPickerDescriptors, resolveHypHome, @@ -133,7 +132,6 @@ export async function runWizardPick(opts) { exportOrigin = opts.exportOrigin ?? 'default' } else { const ask = opts.prompt ?? defaultPromptFactory(opts) - const retentionAsk = opts.retentionPrompt ?? defaultRetentionPromptFactory(opts) opts.stdout.write('Welcome to HypAware - the local logs+telemetry collector.\n\n') @@ -151,7 +149,11 @@ export async function runWizardPick(opts) { // Parquet exports so the first run produces durable files out of the // box. Other destinations remain available via `hyp init --export`. exportChoice = /** @type {PickerExport} */ ('local-parquet') - retentionDays = await retentionAsk('Cache retention (days)', DEFAULT_RETENTION_DAYS) + // Retention is not asked either: the orchestrator supplies the + // pathway default (30-day team / 120-day local), overridable only + // via `hyp init --retention-days` on the non-interactive path. + // @ref LLP 0137#pathway-defaults [implements]: the retention question is removed from onboarding + retentionDays = opts.retentionDefault ?? DEFAULT_RETENTION_DAYS } catch (err) { if (isPromptCancelledError(err)) return cancelledResult(opts) throw err diff --git a/src/core/cli/wizard/types.d.ts b/src/core/cli/wizard/types.d.ts index c38562c..96df0ab 100644 --- a/src/core/cli/wizard/types.d.ts +++ b/src/core/cli/wizard/types.d.ts @@ -4,7 +4,6 @@ import type { PickerDescriptor, PluginCatalog } from '../../types.d.ts' import type { AsyncBackfillConsentPrompt, AsyncPickPrompt, - AsyncRetentionPrompt, FinaleSummary, PickerBackfillRunner, PickerExport, @@ -251,8 +250,12 @@ export interface RunWizardPickOptions { exportOrigin?: PickerExportOrigin /** Override the source prompt (tests pre-bake answers). */ prompt?: AsyncPickPrompt - /** Override the retention prompt (tests pre-bake answers). */ - retentionPrompt?: AsyncRetentionPrompt + /** + * The retention window applied without asking (LLP 0137): the + * orchestrator passes the pathway default (120-day local; team and + * scoped runs omit it and take the 30-day `DEFAULT_RETENTION_DAYS`). + */ + retentionDefault?: number /** Override the system source detector (interactive only). */ detect?: (opts: { env: NodeJS.ProcessEnv }) => Promise> /** Overwrite an existing local config non-interactively (`--force`). */ @@ -313,7 +316,6 @@ export interface RunInitWizardOptions { finaleRunner?: (args: Record) => Promise /** Pick-phase prompt seams, threaded through unchanged (tests). */ prompt?: AsyncPickPrompt - retentionPrompt?: AsyncRetentionPrompt detect?: (opts: { env: NodeJS.ProcessEnv }) => Promise> confirmOverwrite?: (targetPath: string) => Promise backfillConsentPrompt?: AsyncBackfillConsentPrompt diff --git a/test/core/cli/wizard/index.test.js b/test/core/cli/wizard/index.test.js index 4e71b31..209a4b7 100644 --- a/test/core/cli/wizard/index.test.js +++ b/test/core/cli/wizard/index.test.js @@ -152,6 +152,9 @@ test('runInitWizard: local pathway runs pick -> configure -> finale, no join', a assert.deepEqual(calls, ['gate', 'fork', 'pick', 'configure', 'finale']) assert.equal(result.pathway, 'local') assert.equal(opts._pickOpts.managed, undefined) + // The local pathway supplies the 120-day retention default without a + // prompt (LLP 0137 #pathway-defaults). + assert.equal(opts._pickOpts.retentionDefault, 120) }) test('runInitWizard: fork quit exits 0 before the pick phase', async () => { @@ -172,6 +175,9 @@ test('runInitWizard: team pathway threads locked + managed into the pick phase', assert.equal(result.pathway, 'team') assert.deepEqual(opts._pickOpts.locked, ['claude']) assert.equal(opts._pickOpts.managed, true) + // The team pathway takes the pick phase's 30-day default: no + // retentionDefault override (LLP 0137 #pathway-defaults). + assert.equal(opts._pickOpts.retentionDefault, undefined) }) test('runInitWizard: a failed join explains and returns to the fork', async () => { diff --git a/test/core/cli/wizard/pick.test.js b/test/core/cli/wizard/pick.test.js index bb125d9..8303a3a 100644 --- a/test/core/cli/wizard/pick.test.js +++ b/test/core/cli/wizard/pick.test.js @@ -118,7 +118,6 @@ test('runWizardPick: interactive prompt options pre-check detected sources', asy const { prompt, state } = capturingPrompt(['codex']) const result = await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(['codex']), })) // The codex row came pre-checked from detection. @@ -129,13 +128,53 @@ test('runWizardPick: interactive prompt options pre-check detected sources', asy assert.equal(result.retentionDays, 30) }) +// --- retention defaults (LLP 0137): never asked, pathway-supplied --- +// @ref LLP 0137#pathway-defaults [tests]: + +test('runWizardPick: interactive runs take the 30-day default without a retention prompt', async () => { + const tmp = await mkTmp() + const catalog = await realCatalog() + const { prompt } = capturingPrompt(['otel']) + const result = await runWizardPick(/** @type {any} */ ({ + stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, + detect: async () => new Set(), + })) + assert.equal(result.retentionDays, 30) + const written = JSON.parse(await fs.readFile(result.configPath, 'utf8')) + assert.equal(written.query.cache.retention.default_days, 30) +}) + +test('runWizardPick: retentionDefault (the local pathway) lands in the composed config', async () => { + const tmp = await mkTmp() + const catalog = await realCatalog() + const { prompt } = capturingPrompt(['otel']) + const result = await runWizardPick(/** @type {any} */ ({ + stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, + detect: async () => new Set(), + retentionDefault: 120, + })) + assert.equal(result.retentionDays, 120) + const written = JSON.parse(await fs.readFile(result.configPath, 'utf8')) + assert.equal(written.query.cache.retention.default_days, 120) +}) + +test('runWizardPick: pre-baked picks override retentionDefault', async () => { + const tmp = await mkTmp() + const catalog = await realCatalog() + const result = await runWizardPick(/** @type {any} */ ({ + stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, + retentionDefault: 120, + picks: { sources: /** @type {PickerSource[]} */ (['otel']), exportChoice: 'local-parquet', retentionDays: 7 }, + })) + assert.equal(result.retentionDays, 7) +}) + test('runWizardPick: options come from catalog.pickerDescriptors, not a hardcoded table', async () => { const tmp = await mkTmp() const catalog = await realCatalog() const { prompt, state } = capturingPrompt([]) await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(), })) const ids = state.question.options.map((/** @type {any} */ o) => o.value).sort() @@ -150,7 +189,6 @@ test('runWizardPick: a locked row renders checked, disabled, and fleet-labeled', const { prompt, state } = capturingPrompt(['claude']) await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(), locked: ['claude'], })) @@ -168,7 +206,6 @@ test('runWizardPick: a locked source is filtered out of the returned picks and c const { prompt } = capturingPrompt(['claude', 'codex']) const result = await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(), locked: ['claude'], })) @@ -188,7 +225,6 @@ test('runWizardPick: an unknown locked id is ignored, not surfaced as a row', as const { prompt, state } = capturingPrompt([]) const result = await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(), locked: ['does-not-exist'], })) @@ -205,7 +241,6 @@ test('runWizardPick: on a managed machine, non-locked rows say "stays on this ma const { prompt, state } = capturingPrompt([]) await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(['codex']), locked: ['claude'], managed: true, @@ -228,7 +263,6 @@ test('runWizardPick: an unmanaged (solo) machine never shows the local-only suff const { prompt, state } = capturingPrompt([]) await runWizardPick(/** @type {any} */ ({ stdout: makeBuf(), stderr: makeBuf(), env: hermeticEnv(tmp), catalog, prompt, - retentionPrompt: async (/** @type {string} */ _p, /** @type {number} */ d) => d, detect: async () => new Set(['claude']), })) for (const row of state.question.options) { diff --git a/test/core/init-guard.test.js b/test/core/init-guard.test.js index aece9f5..b2c6b99 100644 --- a/test/core/init-guard.test.js +++ b/test/core/init-guard.test.js @@ -126,7 +126,6 @@ function interactiveOpts(hypHome) { env: { ...process.env, HYP_HOME: hypHome, HYP_CONFIG: '' }, detect: async () => new Set(), prompt: async () => [], - retentionPrompt: async () => 30, }, } } diff --git a/test/core/walkthrough-backfill.test.js b/test/core/walkthrough-backfill.test.js index f4c3ab4..092100c 100644 --- a/test/core/walkthrough-backfill.test.js +++ b/test/core/walkthrough-backfill.test.js @@ -191,7 +191,6 @@ test('interactive onboarding defaults backfill to enabled (consent yes runs it)' env, // No `picks` ⇒ interactive: prompts are driven by injected resolvers. prompt: async (q) => (q.pickType === 'sources' ? ['claude'] : ['keep-local']), - retentionPrompt: async () => 30, backfillConsentPrompt: async (args) => { consentCalls.push(args) return true @@ -219,7 +218,6 @@ test('interactive onboarding lets the user decline backfill', async () => { stderr, env, prompt: async (q) => (q.pickType === 'sources' ? ['claude'] : ['keep-local']), - retentionPrompt: async () => 30, backfillConsentPrompt: async () => false, backfill, finale: { skipDaemon: true }, @@ -243,7 +241,6 @@ test('interactive onboarding maps cancelled backfill consent to the cancel exit stderr, env, prompt: async (q) => (q.pickType === 'sources' ? ['claude'] : ['keep-local']), - retentionPrompt: async () => 30, backfillConsentPrompt: async () => { throw new PromptCancelledError() }, @@ -422,7 +419,6 @@ test('interactive onboarding prompts codex backfill consent and runs it on yes', env, // No `picks` ⇒ interactive: the source resolver picks codex. prompt: async (q) => (q.pickType === 'sources' ? ['codex'] : ['keep-local']), - retentionPrompt: async () => 30, backfillConsentPrompt: async (args) => { consentCalls.push(args) return true diff --git a/test/core/walkthrough-detect.test.js b/test/core/walkthrough-detect.test.js index dfe6e90..dbc6225 100644 --- a/test/core/walkthrough-detect.test.js +++ b/test/core/walkthrough-detect.test.js @@ -42,7 +42,6 @@ test('picker pre-checks detected sources, labels them, and defaults export to lo env: await tmpEnv(), detect: async () => new Set(['claude']), prompt, - retentionPrompt: async (_p, d) => d, }) const sources = seen.find((q) => q.pickType === 'sources') @@ -79,7 +78,6 @@ test('nothing detected → no source pre-checked, export still defaults to local env: await tmpEnv(), detect: async () => new Set(), prompt, - retentionPrompt: async (_p, d) => d, }) const sources = seen.find((q) => q.pickType === 'sources') diff --git a/test/core/walkthrough-prompt.test.js b/test/core/walkthrough-prompt.test.js index 322d218..d390ffa 100644 --- a/test/core/walkthrough-prompt.test.js +++ b/test/core/walkthrough-prompt.test.js @@ -12,9 +12,9 @@ import { runPickerWalkthrough } from '../../src/core/cli/walkthrough.js' test('picker prompt prints context under source options and defaults export to local-parquet', async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'hypaware-walkthrough-prompt-')) const input = new PassThrough() - // Only the source question and the retention prompt are asked; the - // export question was removed in favour of the local-parquet default. - const stdout = answerDrivenOutput(input, ['3\n', '\n']) + // Only the source question is asked; export defaults to local-parquet + // and retention takes its default without a prompt (LLP 0137). + const stdout = answerDrivenOutput(input, ['3\n']) const stderr = makeBuf() const result = await runPickerWalkthrough({ @@ -37,6 +37,8 @@ test('picker prompt prints context under source options and defaults export to l // The export question is no longer rendered. assert.doesNotMatch(text, /keep local query cache only/) assert.doesNotMatch(text, /Where should HypAware export/) + // Neither is the retention question (LLP 0137). + assert.doesNotMatch(text, /Cache retention/) assert.equal(stderr.text(), '') }) @@ -50,7 +52,7 @@ function answerDrivenOutput(input, answers) { write(chunk) { const text = String(chunk) value += text - if (text.includes('select (e.g. 1,3 or "all"): ') || text.includes('Cache retention (days)')) { + if (text.includes('select (e.g. 1,3 or "all"): ')) { const answer = answers.shift() if (answer !== undefined) input.write(answer) if (answers.length === 0) input.end() diff --git a/test/core/walkthrough-tui-happy.test.js b/test/core/walkthrough-tui-happy.test.js index 6b25120..c0191c5 100644 --- a/test/core/walkthrough-tui-happy.test.js +++ b/test/core/walkthrough-tui-happy.test.js @@ -86,15 +86,11 @@ test('runPickerWalkthrough drives the TUI multiselect end-to-end when stdin+stdo // Sources prompt (PICKER_SOURCES: claude, codex, raw-anthropic, // raw-openai, otel). Move down twice to land on raw-anthropic, - // toggle, then enter. + // toggle, then enter. It is the only prompt: export defaults to + // local-parquet and retention is never asked (LLP 0137). await settle() await feed(io.stdin, ['\x1b[B', '\x1b[B', ' ', '\r']) - // No export prompt: the picker always defaults to local-parquet now. - // Retention prompt: empty buffer + enter accepts the 30-day default. - await settle() - await feed(io.stdin, ['\r']) - const result = await promise assert.equal(result.exitCode, 0) assert.deepEqual(result.sourcesPicked, ['raw-anthropic']) @@ -177,10 +173,10 @@ test('runPickerWalkthrough falls back to the legacy numbered prompt under HYP_NO const input = new PassThrough() // Mark BOTH ends as TTYs so the only signal that flips the router is // the HYP_NO_TUI escape. This proves the env override wins over the - // TTY probe. Answers: source '3' (raw-anthropic), then retention - // default: the export question is no longer asked. + // TTY probe. One answer: source '3' (raw-anthropic). Neither export + // nor retention is asked. Object.defineProperty(input, 'isTTY', { value: true }) - const stdout = answerDrivenOutput(input, ['3\n', '\n'], true) + const stdout = answerDrivenOutput(input, ['3\n'], true) const stderr = makeBuf() // HYP_NO_TUI flows through opts.env: the same channel real callers @@ -214,7 +210,7 @@ function answerDrivenOutput(input, answers, withIsTty = false) { write(chunk) { const text = String(chunk) value += text - if (text.includes('select (e.g. 1,3 or "all"): ') || text.includes('Cache retention (days)')) { + if (text.includes('select (e.g. 1,3 or "all"): ')) { const answer = answers.shift() if (answer !== undefined) input.write(answer) if (answers.length === 0) input.end()