diff --git a/src/agent/infra/harness/harness-outcome-recorder.ts b/src/agent/infra/harness/harness-outcome-recorder.ts index 5db51cc73..b9444efe8 100644 --- a/src/agent/infra/harness/harness-outcome-recorder.ts +++ b/src/agent/infra/harness/harness-outcome-recorder.ts @@ -92,7 +92,7 @@ class Semaphore { // --------------------------------------------------------------------------- /** Synthetic outcome count per verdict (§C2 weighting policy). */ -const BAD_SYNTHETIC_COUNT = 3 +export const BAD_SYNTHETIC_COUNT = 3 /** * Maximum feedback-sourced synthetic outcomes in the H window. @@ -105,7 +105,7 @@ const FEEDBACK_SYNTHETIC_CAP = 10 const FEEDBACK_LIST_LIMIT = 100 /** Synthetic outcome count for 'good' verdict — asymmetric with BAD (3:1) per §C2. */ -const GOOD_SYNTHETIC_COUNT = 1 +export const GOOD_SYNTHETIC_COUNT = 1 /** H window size — matches the synthesizer's OUTCOMES_WINDOW. */ const H_WINDOW_SIZE = 50 @@ -114,7 +114,7 @@ const MAX_OUTCOMES_PER_SESSION = 50 const SEMAPHORE_PERMITS = 5 /** Delimiter between the original outcome ID and the synthetic suffix. */ -const SYNTHETIC_DELIMITER = '__synthetic_' +export const SYNTHETIC_DELIMITER = '__synthetic_' // --------------------------------------------------------------------------- // Recorder diff --git a/src/oclif/commands/curate/index.ts b/src/oclif/commands/curate/index.ts index 75ca8d12f..dd541bd2f 100644 --- a/src/oclif/commands/curate/index.ts +++ b/src/oclif/commands/curate/index.ts @@ -17,12 +17,18 @@ import { providerMissingMessage, withDaemonRetry, } from '../../lib/daemon-client.js' +import { + attachFeedbackFromCli, + FeedbackError, + type FeedbackVerdict, +} from '../../lib/harness-feedback.js' import {writeJsonResponse} from '../../lib/json-response.js' import {DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, type ToolCallRecord, waitForTaskCompletion} from '../../lib/task-client.js' /** Parsed flags type */ type CurateFlags = { detach?: boolean + feedback?: FeedbackVerdict files?: string[] folder?: string[] format?: 'json' | 'text' @@ -72,6 +78,11 @@ Bad examples: default: false, description: 'Queue task and exit without waiting for completion', }), + feedback: Flags.string({ + description: + 'After the curate completes, flag the most-recent outcome for AutoHarness learning. "bad" inserts 3 synthetic failures (weighted heavier); "good" inserts 1 synthetic success.', + options: ['good', 'bad'], + }), files: Flags.string({ char: 'f', description: 'Include specific file paths for critical context (max 5 files)', @@ -101,8 +112,14 @@ Bad examples: public async run(): Promise { const {args, flags: rawFlags} = await this.parse(Curate) + // oclif's `options: ['good', 'bad']` validator rejects anything + // else before we reach here, so the cast is type-narrowing, not + // input validation. + const feedbackVerdict: FeedbackVerdict | undefined = + rawFlags.feedback === 'good' || rawFlags.feedback === 'bad' ? rawFlags.feedback : undefined const flags: CurateFlags = { detach: rawFlags.detach, + feedback: feedbackVerdict, files: rawFlags.files, folder: rawFlags.folder, format: rawFlags.format === 'json' ? 'json' : rawFlags.format === 'text' ? 'text' : undefined, @@ -121,9 +138,19 @@ Bad examples: let providerContext: ProviderErrorContext | undefined + // Capture projectRoot out of the daemon callback so feedback can + // run AFTER withDaemonRetry resolves. If feedback ran inside the + // callback, `this.error(..., {exit: 1})` would be caught by the + // outer try/catch below and routed to `reportError`, which + // swallows the exit code — the CLI would exit 0 on a + // NO_RECENT_OUTCOME path. + let capturedProjectRoot: string | undefined + let daemonSucceeded = false try { await withDaemonRetry( async (client, projectRoot, worktreeRoot) => { + capturedProjectRoot = projectRoot + const active = await client.requestWithAck( TransportStateEventNames.GET_PROVIDER_CONFIG, ) @@ -140,6 +167,7 @@ Bad examples: } await this.submitTask({client, content: resolvedContent, flags, format, projectRoot, taskType, worktreeRoot}) + daemonSucceeded = true }, { ...this.getDaemonClientOptions(), @@ -152,6 +180,16 @@ Bad examples: ) } catch (error) { this.reportError(error, format, providerContext) + return + } + + // Feedback attaches only on a successful primary run. + if ( + daemonSucceeded && + flags.feedback !== undefined && + capturedProjectRoot !== undefined + ) { + await this.handleFeedback(capturedProjectRoot, flags, format) } } @@ -247,6 +285,70 @@ Bad examples: return filePath.slice(idx + marker.length) } + /** + * Attach the `--feedback` verdict to the most-recent curate outcome. + * + * Surface contract (handoff §C1): + * - HARNESS_DISABLED → warn, exit 0 (primary curate already succeeded) + * - NO_RECENT_OUTCOME / NO_STORAGE → `this.error` with exit 1 + * - detach mode → skipped with a hint (no outcome to flag yet) + */ + private async handleFeedback( + projectRoot: string, + flags: CurateFlags, + format: 'json' | 'text', + ): Promise { + if (flags.feedback === undefined) return + + if (flags.detach === true) { + if (format === 'text') { + this.warn('--feedback skipped: detach mode — no completed outcome to flag yet.') + } + + return + } + + try { + const result = await attachFeedbackFromCli(projectRoot, 'curate', flags.feedback) + if (format === 'json') { + writeJsonResponse({ + command: 'curate:feedback', + data: { + outcomeId: result.outcomeId, + syntheticCount: result.syntheticCount, + verdict: result.verdict, + }, + success: true, + }) + } else { + this.log( + `feedback attached: ${result.verdict} → outcome ${result.outcomeId} (${result.syntheticCount} synthetic row${result.syntheticCount === 1 ? '' : 's'} inserted for heuristic weighting)`, + ) + } + } catch (error) { + if (error instanceof FeedbackError) { + if (error.code === 'HARNESS_DISABLED') { + if (format === 'json') { + writeJsonResponse({ + command: 'curate:feedback', + data: {reason: error.message, skipped: true}, + success: true, + }) + } else { + this.warn(`--feedback ignored: ${error.message}`) + } + + return + } + + // NO_RECENT_OUTCOME / NO_STORAGE — user-input error per §C1. + this.error(error.message, {exit: 1}) + } + + throw error + } + } + /** * Print a human-readable pending review summary to stdout. * Called after successful curate completion when review is required. diff --git a/src/oclif/commands/query.ts b/src/oclif/commands/query.ts index ba4e486ac..5a62602c6 100644 --- a/src/oclif/commands/query.ts +++ b/src/oclif/commands/query.ts @@ -13,11 +13,17 @@ import { providerMissingMessage, withDaemonRetry, } from '../lib/daemon-client.js' +import { + attachFeedbackFromCli, + FeedbackError, + type FeedbackVerdict, +} from '../lib/harness-feedback.js' import {writeJsonResponse} from '../lib/json-response.js' import {DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion} from '../lib/task-client.js' /** Parsed flags type */ type QueryFlags = { + feedback?: FeedbackVerdict format?: 'json' | 'text' timeout?: number } @@ -46,6 +52,11 @@ Bad: '<%= config.bin %> <%= command.id %> "How does auth work?" --format json', ] public static flags = { + feedback: Flags.string({ + description: + 'After the query completes, flag the most-recent outcome for AutoHarness learning. "bad" inserts 3 synthetic failures (weighted heavier); "good" inserts 1 synthetic success.', + options: ['good', 'bad'], + }), format: Flags.string({ default: 'text', description: 'Output format (text or json)', @@ -66,16 +77,33 @@ Bad: public async run(): Promise { const {args, flags: rawFlags} = await this.parse(Query) - const flags = rawFlags as QueryFlags - const format = (flags.format ?? 'text') as 'json' | 'text' + // oclif's `options:` validators reject unknown values before we + // reach here; the branches below narrow the string types without + // `as` casts (matching the curate command's pattern). + const feedbackVerdict: FeedbackVerdict | undefined = + rawFlags.feedback === 'good' || rawFlags.feedback === 'bad' ? rawFlags.feedback : undefined + const flags: QueryFlags = { + feedback: feedbackVerdict, + format: rawFlags.format === 'json' ? 'json' : rawFlags.format === 'text' ? 'text' : undefined, + timeout: rawFlags.timeout, + } + const format: 'json' | 'text' = flags.format ?? 'text' if (!this.validateInput(args.query, format)) return let providerContext: ProviderErrorContext | undefined + // Captured from the daemon callback so feedback runs AFTER + // withDaemonRetry resolves. Running it inside the callback would + // let `this.error({exit: 1})` get caught by the outer try/catch + // and routed to `reportError`, which swallows the exit code. + let capturedProjectRoot: string | undefined + let daemonSucceeded = false try { await withDaemonRetry( async (client, projectRoot, worktreeRoot) => { + capturedProjectRoot = projectRoot + const active = await client.requestWithAck( TransportStateEventNames.GET_PROVIDER_CONFIG, ) @@ -99,6 +127,7 @@ Bad: timeoutMs: (flags.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000, worktreeRoot, }) + daemonSucceeded = true }, { ...this.getDaemonClientOptions(), @@ -111,6 +140,68 @@ Bad: ) } catch (error) { this.reportError(error, format, providerContext) + return + } + + if ( + daemonSucceeded && + feedbackVerdict !== undefined && + capturedProjectRoot !== undefined + ) { + await this.handleFeedback(capturedProjectRoot, feedbackVerdict, format) + } + } + + /** + * Attach the `--feedback` verdict to the most-recent query outcome. + * + * Surface contract (handoff §C1): + * - HARNESS_DISABLED → warn, exit 0 (primary query already succeeded) + * - NO_RECENT_OUTCOME / NO_STORAGE → `this.error` with exit 1 + */ + private async handleFeedback( + projectRoot: string, + verdict: FeedbackVerdict, + format: 'json' | 'text', + ): Promise { + try { + const result = await attachFeedbackFromCli(projectRoot, 'query', verdict) + if (format === 'json') { + writeJsonResponse({ + command: 'query:feedback', + data: { + outcomeId: result.outcomeId, + syntheticCount: result.syntheticCount, + verdict: result.verdict, + }, + success: true, + }) + } else { + this.log( + `feedback attached: ${result.verdict} → outcome ${result.outcomeId} (${result.syntheticCount} synthetic row${result.syntheticCount === 1 ? '' : 's'} inserted for heuristic weighting)`, + ) + } + } catch (error) { + if (error instanceof FeedbackError) { + if (error.code === 'HARNESS_DISABLED') { + if (format === 'json') { + writeJsonResponse({ + command: 'query:feedback', + data: {reason: error.message, skipped: true}, + success: true, + }) + } else { + this.warn(`--feedback ignored: ${error.message}`) + } + + return + } + + // NO_RECENT_OUTCOME / NO_STORAGE — user-input error per §C1. + this.error(error.message, {exit: 1}) + } + + throw error } } diff --git a/src/oclif/lib/harness-feedback.ts b/src/oclif/lib/harness-feedback.ts new file mode 100644 index 000000000..7020a8eaf --- /dev/null +++ b/src/oclif/lib/harness-feedback.ts @@ -0,0 +1,182 @@ +/** + * AutoHarness V2 — CLI-side feedback attach helper. + * + * Shared logic for `brv curate --feedback` and `brv query --feedback` + * (Phase 7 Task 7.4, brutal-review Tier 2 D4). Implements §C5 of the + * handoff contract: + * + * - Target the MOST RECENT `CodeExecOutcome` for + * `(projectId, commandType)`. + * - Call the recorder's `attachFeedback` — Phase 6 Task 6.5's + * weighting policy (3x synthetic failures for `'bad'`, 1x + * synthetic success for `'good'`) lives there. + * - Repeat invocation with a different verdict replaces the + * previous synthetics (idempotent re-label). + * + * Harness-disabled / no-outcome cases surface as typed errors so the + * calling command can choose between "warn + exit 0" (disabled) and + * "error + exit 1" (missing outcome) per §C1. + */ + +import type {IHarnessStore} from '../../agent/core/interfaces/i-harness-store.js' +import type {ValidatedHarnessConfig} from '../../agent/infra/agent/agent-schemas.js' + +import {NoOpLogger} from '../../agent/core/interfaces/i-logger.js' +import {SessionEventBus} from '../../agent/infra/events/event-emitter.js' +import { + BAD_SYNTHETIC_COUNT, + GOOD_SYNTHETIC_COUNT, + HarnessOutcomeRecorder, + SYNTHETIC_DELIMITER, +} from '../../agent/infra/harness/harness-outcome-recorder.js' +import {openHarnessStoreForProject, readHarnessFeatureConfig} from './harness-cli.js' + +export type FeedbackVerdict = 'bad' | 'good' + +/** + * Scan depth when hunting for the most-recent NON-synthetic outcome. + * 10 feedback synthetics (bad=3, good=1 × several re-labels) can + * precede a real outcome in the worst case; 50 gives comfortable + * headroom without unbounded store reads. + * + * `BAD_SYNTHETIC_COUNT`, `GOOD_SYNTHETIC_COUNT`, and + * `SYNTHETIC_DELIMITER` are re-used from `HarnessOutcomeRecorder` + * (the canonical owner of the §C2 weighting policy) to prevent + * drift — redeclaring them here would silently diverge on a policy + * change. + */ +const FEEDBACK_SCAN_LIMIT = 50 + +export interface FeedbackResult { + readonly outcomeId: string + readonly syntheticCount: number + readonly verdict: FeedbackVerdict +} + +export type FeedbackErrorCode = 'HARNESS_DISABLED' | 'NO_RECENT_OUTCOME' | 'NO_STORAGE' + +export class FeedbackError extends Error { + constructor( + message: string, + public readonly code: FeedbackErrorCode, + public readonly details: Readonly> = {}, + ) { + super(message) + this.name = 'FeedbackError' + } +} + +/** + * Attach `verdict` to the most recent outcome for the current + * `(projectRoot, commandType)` pair. Returns the outcome id and the + * number of synthetic rows the recorder inserted. + * + * @throws {FeedbackError} `HARNESS_DISABLED` when `.brv/config.json` + * has `harness.enabled !== true` — the primary action already ran; + * caller should warn-log and exit 0. + * @throws {FeedbackError} `NO_STORAGE` when the project has no XDG + * storage dir yet (daemon never wrote for this project) — exit 1. + * @throws {FeedbackError} `NO_RECENT_OUTCOME` when `listOutcomes` + * returns empty for the pair — exit 1 with a hint to run + * curate/query first. + */ +export async function attachFeedbackFromCli( + projectRoot: string, + commandType: 'curate' | 'query', + verdict: FeedbackVerdict, +): Promise { + const config = await readHarnessFeatureConfig(projectRoot) + if (!config.enabled) { + throw new FeedbackError( + `harness is disabled — --feedback requires enabled harness in .brv/config.json.`, + 'HARNESS_DISABLED', + {commandType, projectRoot}, + ) + } + + const opened = await openHarnessStoreForProject(projectRoot) + if (opened === undefined) { + throw new FeedbackError( + `no harness storage for this project (${projectRoot}) — run curate/query first.`, + 'NO_STORAGE', + {projectRoot}, + ) + } + + try { + return await attachFeedbackToStore(opened.store, opened.projectId, commandType, verdict, config) + } finally { + opened.close() + } +} + +/** + * Pure-store variant: does the full most-recent-lookup + recorder + * delegation against an explicit `IHarnessStore`. Exported for unit + * tests that want to exercise the feedback logic without the XDG + * filesystem dance. + * + * Phase 6 Task 6.5's `HarnessOutcomeRecorder.attachFeedback` is the + * canonical path for the weighting policy (3x synthetic failures for + * `'bad'`, 1x synthetic success for `'good'`). We construct a + * minimal recorder here because `attachFeedback` only reads + * `this.store` and `this.logger` — `sessionEventBus` / `config` are + * untouched by that method. Duplicating the weighting logic here + * would drift from the recorder on a policy change. + */ +export async function attachFeedbackToStore( + store: IHarnessStore, + projectId: string, + commandType: 'curate' | 'query', + verdict: FeedbackVerdict, + feature: {readonly autoLearn: boolean; readonly enabled: boolean}, +): Promise { + // Skip feedback synthetics: they carry `Date.now()` timestamps so + // would otherwise shadow the real outcome in a "most recent" scan. + // A re-label (`--feedback good` then `--feedback bad`) must target + // the original user outcome, not the synthetic from the first call. + const recent = await store.listOutcomes(projectId, commandType, FEEDBACK_SCAN_LIMIT) + const mostRecent = recent.find((o) => !o.id.includes(SYNTHETIC_DELIMITER)) + if (mostRecent === undefined) { + throw new FeedbackError( + `no recent outcome to flag — run ${commandType} first.`, + 'NO_RECENT_OUTCOME', + {commandType, projectId}, + ) + } + + const recorder = buildCliRecorder(store, feature) + await recorder.attachFeedback(projectId, commandType, mostRecent.id, verdict) + + return { + outcomeId: mostRecent.id, + syntheticCount: verdict === 'bad' ? BAD_SYNTHETIC_COUNT : GOOD_SYNTHETIC_COUNT, + verdict, + } +} + +/** + * Construct a minimal `HarnessOutcomeRecorder` for CLI use. + * + * `attachFeedback` only reads `store` and `logger` off the recorder — + * verified in `harness-outcome-recorder.ts`. The `sessionEventBus` + * and hot-path `config.enabled` checks live in other methods, so + * the stubs here are safe for the feedback-attach path. + * + * Exposed for unit tests that want to seed the recorder from a + * test-double store; callers should prefer `attachFeedbackFromCli`. + */ +export function buildCliRecorder( + store: IHarnessStore, + feature: {readonly autoLearn: boolean; readonly enabled: boolean}, +): HarnessOutcomeRecorder { + const validatedConfig: ValidatedHarnessConfig = { + autoLearn: feature.autoLearn, + enabled: feature.enabled, + language: 'auto', + maxVersions: 20, + } + return new HarnessOutcomeRecorder(store, new SessionEventBus(), new NoOpLogger(), validatedConfig) +} + +export type {CodeExecOutcome} from '../../agent/core/domain/harness/types.js' diff --git a/test/unit/oclif/lib/harness-feedback.test.ts b/test/unit/oclif/lib/harness-feedback.test.ts new file mode 100644 index 000000000..0a2a54e22 --- /dev/null +++ b/test/unit/oclif/lib/harness-feedback.test.ts @@ -0,0 +1,137 @@ +import {expect} from 'chai' + +import type {CodeExecOutcome} from '../../../../src/agent/core/domain/harness/types.js' + +import {NoOpLogger} from '../../../../src/agent/core/interfaces/i-logger.js' +import {HarnessStore} from '../../../../src/agent/infra/harness/harness-store.js' +import {FileKeyStorage} from '../../../../src/agent/infra/storage/file-key-storage.js' +import { + attachFeedbackToStore, + FeedbackError, +} from '../../../../src/oclif/lib/harness-feedback.js' + +const PROJECT_ID = 'fixture-proj' +const SESSION_ID = 's-1' + +const ENABLED_FEATURE = {autoLearn: true, enabled: true} as const + +async function makeStore(): Promise { + const keyStorage = new FileKeyStorage({inMemory: true}) + await keyStorage.initialize() + return new HarnessStore(keyStorage, new NoOpLogger()) +} + +function makeOutcome(overrides: Partial = {}): CodeExecOutcome { + return { + code: 'ctx.tools.curate([])', + commandType: 'curate', + executionTimeMs: 12, + id: 'o-real', + projectId: PROJECT_ID, + projectType: 'typescript', + sessionId: SESSION_ID, + success: true, + timestamp: 1_700_000_000_000, + usedHarness: false, + ...overrides, + } +} + +describe('attachFeedbackToStore', () => { + it('1. "bad" verdict inserts 3 synthetic failure rows', async () => { + const store = await makeStore() + await store.saveOutcome(makeOutcome({id: 'o-target'})) + + const result = await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'bad', ENABLED_FEATURE) + + expect(result.outcomeId).to.equal('o-target') + expect(result.syntheticCount).to.equal(3) + expect(result.verdict).to.equal('bad') + + const all = await store.listOutcomes(PROJECT_ID, 'curate', 20) + const synthetics = all.filter((o) => o.id.startsWith('o-target__synthetic_bad_')) + expect(synthetics.length).to.equal(3) + for (const s of synthetics) { + expect(s.success).to.equal(false) + expect(s.userFeedback).to.equal('bad') + } + }) + + it('2. "good" verdict inserts 1 synthetic success row', async () => { + const store = await makeStore() + await store.saveOutcome(makeOutcome({id: 'o-target'})) + + const result = await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'good', ENABLED_FEATURE) + + expect(result.syntheticCount).to.equal(1) + + const all = await store.listOutcomes(PROJECT_ID, 'curate', 20) + const synthetics = all.filter((o) => o.id.startsWith('o-target__synthetic_good_')) + expect(synthetics.length).to.equal(1) + expect(synthetics[0].success).to.equal(true) + expect(synthetics[0].userFeedback).to.equal('good') + }) + + it('3. repeat with different verdict replaces the previous synthetics', async () => { + const store = await makeStore() + await store.saveOutcome(makeOutcome({id: 'o-target'})) + + await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'good', ENABLED_FEATURE) + const afterGood = await store.listOutcomes(PROJECT_ID, 'curate', 20) + expect(afterGood.filter((o) => o.id.startsWith('o-target__synthetic_good_')).length).to.equal(1) + + await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'bad', ENABLED_FEATURE) + const afterBad = await store.listOutcomes(PROJECT_ID, 'curate', 20) + + // Good synthetics cleared; bad synthetics inserted. + expect(afterBad.filter((o) => o.id.startsWith('o-target__synthetic_good_')).length).to.equal(0) + expect(afterBad.filter((o) => o.id.startsWith('o-target__synthetic_bad_')).length).to.equal(3) + // Original outcome's userFeedback field is now 'bad'. + const updated = afterBad.find((o) => o.id === 'o-target') + expect(updated?.userFeedback).to.equal('bad') + }) + + it('4. no recent outcome → throws FeedbackError with code NO_RECENT_OUTCOME', async () => { + const store = await makeStore() + + let caught: unknown + try { + await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'bad', ENABLED_FEATURE) + } catch (error) { + caught = error + } + + expect(caught).to.be.instanceOf(FeedbackError) + expect((caught as FeedbackError).code).to.equal('NO_RECENT_OUTCOME') + expect((caught as FeedbackError).details.commandType).to.equal('curate') + }) + + it('5. most-recent-by-timestamp wins when multiple outcomes exist', async () => { + const store = await makeStore() + await store.saveOutcome(makeOutcome({id: 'o-older', timestamp: 1000})) + await store.saveOutcome(makeOutcome({id: 'o-newer', timestamp: 2000})) + + const result = await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'bad', ENABLED_FEATURE) + + expect(result.outcomeId).to.equal('o-newer') + }) + + it('6. commandType partitioning: curate feedback does not touch query outcomes', async () => { + const store = await makeStore() + // One outcome per commandType, same project. + await store.saveOutcome(makeOutcome({commandType: 'curate', id: 'o-curate'})) + await store.saveOutcome( + makeOutcome({commandType: 'query', id: 'o-query', timestamp: 1000}), + ) + + await attachFeedbackToStore(store, PROJECT_ID, 'curate', 'bad', ENABLED_FEATURE) + + const curateSide = await store.listOutcomes(PROJECT_ID, 'curate', 20) + const querySide = await store.listOutcomes(PROJECT_ID, 'query', 20) + + // Curate got 3 synthetic failures; query is untouched. + expect(curateSide.filter((o) => o.id.startsWith('o-curate__synthetic_bad_')).length).to.equal(3) + expect(querySide.length).to.equal(1) + expect(querySide[0].userFeedback).to.equal(undefined) + }) +})