fix(ci): telemetry switch trivially satisfied without the row; coverage-lane test fixes
A custom profile that mounts no telemetry-otel row exports nothing, so DSH_TELEMETRY_DISABLED must not fail its boot (CI exports the switch globally, which broke the lifecycle-fixture profile). The web-app dist resolution test accepts the fail-loud unbuilt outcome the CI coverage lane sees before any build, and the headless spec covers the idle-anchor and pre-start skip branches under the per-file gate.
This commit is contained in:
@@ -49,18 +49,16 @@ const PROFILE_ROOT_FILENAME = 'cordis.yml'
|
|||||||
/**
|
/**
|
||||||
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
||||||
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
||||||
* off-by-mistake over on-by-mistake. Throws when the switch is set but the
|
* off-by-mistake over on-by-mistake. A composition without the telemetry row
|
||||||
* row is absent — a silently no-op "disabled" privacy switch would keep
|
* exports nothing, so the switch is then trivially satisfied and no patch is
|
||||||
* exporting while the user believes it is off.
|
* generated — custom profiles need not mount telemetry to run with the
|
||||||
|
* switch set.
|
||||||
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
||||||
* @param hasRow - whether the composition carries the telemetry row.
|
* @param hasRow - whether the composition carries the telemetry row.
|
||||||
* @returns the disable patch, or `undefined` when telemetry stays enabled.
|
* @returns the disable patch, or `undefined` when telemetry stays enabled or is not mounted.
|
||||||
*/
|
*/
|
||||||
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
|
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
|
||||||
if ((disabledEnv ?? '') === '') return undefined
|
if ((disabledEnv ?? '') === '' || !hasRow) return undefined
|
||||||
if (!hasRow) {
|
|
||||||
throw new Error(`dsh: DSH_TELEMETRY_DISABLED is set but row "${TELEMETRY_ROW_ID}" is not in this composition`)
|
|
||||||
}
|
|
||||||
return { id: TELEMETRY_ROW_ID, disabled: true }
|
return { id: TELEMETRY_ROW_ID, disabled: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ describe('resolveTelemetryPatch', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fails loud when the switch is set but the row is absent', () => {
|
it('is trivially satisfied by a composition without the telemetry row', () => {
|
||||||
expect(() => resolveTelemetryPatch('1', false)).toThrow('DSH_TELEMETRY_DISABLED is set but row "telemetry-otel" is not in this composition')
|
// A custom profile need not mount telemetry: nothing exports, so the
|
||||||
})
|
// privacy switch has nothing to disable and generates no patch.
|
||||||
|
expect(resolveTelemetryPatch('1', false)).toBeUndefined()
|
||||||
it('ignores a missing row while the switch is unset', () => {
|
|
||||||
expect(resolveTelemetryPatch(undefined, false)).toBeUndefined()
|
expect(resolveTelemetryPatch(undefined, false)).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ async function run(events: ScriptedEvent[], options: { promptFails?: boolean } =
|
|||||||
ctx.provide('httpServer', { port: 12345 } as never)
|
ctx.provide('httpServer', { port: 12345 } as never)
|
||||||
apply(ctx, { task: 'do the thing' })
|
apply(ctx, { task: 'do the thing' })
|
||||||
// Quiescence is out of band: give the scripted stream a beat to drain, then
|
// Quiescence is out of band: give the scripted stream a beat to drain, then
|
||||||
// flip the agent idle exactly as the loop would.
|
// flip the agent idle exactly as the loop would. Foreign agents and
|
||||||
|
// non-idle transitions must not settle the run.
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
ctx.emit('agent/status', { id: 'OTHER' } as Agent, 'idle')
|
||||||
|
ctx.emit('agent/status', { id: 'S1' } as Agent, 'running')
|
||||||
ctx.emit('agent/status', { id: 'S1' } as Agent, 'idle')
|
ctx.emit('agent/status', { id: 'S1' } as Agent, 'idle')
|
||||||
const code = await exited
|
const code = await exited
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
@@ -86,6 +89,8 @@ const end = (turn: number, reason: string): ScriptedEvent => ({ type: 'turn/end'
|
|||||||
describe('headless runner', () => {
|
describe('headless runner', () => {
|
||||||
it('aggregates to quiescence: last text wins across turns, final turn-end reason maps to exit 0', async () => {
|
it('aggregates to quiescence: last text wins across turns, final turn-end reason maps to exit 0', async () => {
|
||||||
const { code, out, err } = await run([
|
const { code, out, err } = await run([
|
||||||
|
// Frames before the first turn/start are outside the task interval.
|
||||||
|
{ type: 'assistant/message', data: { turn: 0, message: { content: [{ type: 'text', text: 'pre-task noise' }] } } },
|
||||||
startupTurn,
|
startupTurn,
|
||||||
// Off-session, non-text, and text-empty frames never affect the aggregate.
|
// Off-session, non-text, and text-empty frames never affect the aggregate.
|
||||||
{ type: 'assistant/message', sessionId: 'OTHER', data: { turn: 1, message: { content: [{ type: 'text', text: 'other session' }] } } },
|
{ type: 'assistant/message', sessionId: 'OTHER', data: { turn: 1, message: { content: [{ type: 'text', text: 'other session' }] } } },
|
||||||
|
|||||||
@@ -163,9 +163,15 @@ describe('web-app runtime glue', () => {
|
|||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resolves the real built frontend dist through the package exports', () => {
|
it('resolves the real built frontend dist through the package exports, failing loud unbuilt', () => {
|
||||||
// The production resolver (not the test seam): this checkout builds the
|
// The production resolver (not the test seam). A built checkout resolves
|
||||||
// dist, so the resolved path must be the frontend package's index.html.
|
// the frontend package's index.html; a dist-less one (the CI coverage
|
||||||
expect(originalResolve()).toMatch(/dist[/\\]index\.html$/)
|
// lane runs before any build) must fail with the build hint, never a
|
||||||
|
// silent fallback.
|
||||||
|
try {
|
||||||
|
expect(originalResolve()).toMatch(/dist[/\\]index\.html$/)
|
||||||
|
} catch (error) {
|
||||||
|
expect((error as Error).message).toContain('frontend dist not built')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user