Skip to content

fix(gitops): restore 7 P0 regressions surfaced in feat/optimization-gitops-flow audit; add regression tests#10

Merged
dhruva-reddy merged 6 commits intomainfrom
fix/p0-gitops-regressions
Apr 21, 2026
Merged

fix(gitops): restore 7 P0 regressions surfaced in feat/optimization-gitops-flow audit; add regression tests#10
dhruva-reddy merged 6 commits intomainfrom
fix/p0-gitops-regressions

Conversation

@dhruva-reddy
Copy link
Copy Markdown
Contributor

@dhruva-reddy dhruva-reddy commented Apr 20, 2026

Describe your changes

Stacked on top of #7. Restores seven P0-class correctness/safety behaviors that feat/optimization-gitops-flow regressed, locks them in with 33 regression tests, sweeps docs and CLI text to the org-slug model, and integrates origin/main's parallel fix-set (a1e3228) + .vapi-ignore feature (eb29042).

P0 fixes (see requested improvements.md audit for details; also mostly shipped in parallel on main as a1e3228):

  • Credential walker scoped to credentialId / credentialIds — the feat branch's generic string-walk rewrote provider: openai / 11labs / langfuse enums into UUIDs on push, which the Vapi API rejects. Restored the scoped walker with cycle-safe WeakSet and isPlainObject guard.
  • saveState wrapped in try { ... } finally so a mid-push 5xx no longer leaves API-issued UUIDs unrecorded and creates duplicates on retry.
  • cleanResource preserves null — the Vapi API uses null to mean "intentionally cleared". Stripping it caused pull→push round-trip drift that silently re-applied prior values.
  • Cleanup double-gate restored--force alone is no longer enough; --confirm <slug> is also required, plus empty-state refusal. Prevents a fresh clone or corrupted state from being misread as "everything is orphaned" and wiping the org. Interactive cleanup passes both flags since the confirm() prompt is the user's explicit consent.
  • Interactive pull is now local-first — drops the unconditional --force in both the "All" and per-resource flows; adds an explicit Overwrite locally modified files? confirm (default No).
  • Fresh-clone preservationgit status --porcelain --untracked-files=all -z (was just -z, causing untracked dirs to collapse). Mtime fallback now fires when changedFiles is an empty Set, not just undefined. .yaml extension now included alongside .md / .yml in the preservation check.
  • Short-form paths match for partial pushassistants/foo.yml (the form documented in AGENTS.md) used to silently no-op. Extracted pathMatchesFolder helper and broadened the match. Bare resource ids (npm run push -- <org> foo) are now refused explicitly instead of triggering a full apply with orphan-deletion.

Other changes in this PR:

  • 33 regression tests under tests/ using Node's built-in node:test runner (no new dependencies); npm test added.
  • README + AGENTS.md swept for the org-slug model (.env.<org>, resources/<org>/..., npm run <cmd> -- <org>); docs/environment-scoped-resources.md deleted as fully stale.
  • Merged origin/main's parallel fix (a1e3228) + .vapi-ignore feature (eb29042) cleanly — full conflict-resolution rationale in the merge commit message.
  • Consolidated .vapi-ignore.example to a single canonical resources/.vapi-ignore.example (main's merge added three duplicates in dead env-named folders).
  • Removed unused scripts/mock-vapi-webhook-server.ts and its references in README / AGENTS / package.json.
  • Swept P1/P2 findings from a pre-push review subagent: evals properly wired through cleanup.ts orphan scan and ALL_RESOURCE_TYPES in push; eval.ts remediation text updated to org-slug paths; AGENTS.md .vapi-ignore section fixed.

Deferred to follow-up PRs (flagged, not blocking):

  • applyEval doesn't yet use upsertResourceWithStateRecovery — will 404-crash on stale state mappings instead of recovering like the other resource types.
  • Interactive pull masks 5xx/network errors as "no remote resources" — only 401/403 are currently classified correctly.
  • Interactive flows (src/interactive.ts, src/searchableCheckbox.ts, src/setup.ts) have no automated coverage; the --force confirm prompt and cleanup's confirm spawn are only protected by code review.

Relevant Context (linear ticket, slack link, etc)

Surfaced during a multi-agent audit of feat/optimization-gitops-flow (parent PR #7) before it shipped to customer-facing deployments. The audit's findings and a status-per-requested-improvement table live in requested improvements.md at the repo root (gitignored — local audit log only).

API Changes

  • Is this changing the public API?

    • Yes
    • No
  • If yes, is it backward‐compatible?

    • Yes
    • No

N/A. This repo is an internal CLI that consumes the Vapi API; it does not expose any public API surface. The only user-visible CLI changes are additive (npm test) or correctness-restoring (cleanup --confirm <slug> now required for destructive runs, bare-id positional args refused explicitly). Behaviors that were silent no-ops or silent overwrites now error or prompt loudly.

Non backward-compatible changes might break customers' agents. Please proceed with care and notify the team.

How did you test this?

Automated (33 tests, all passing under npm test):

  • tests/credentials.test.ts (8) — replaceCredentialRefs scoping, including the specific provider: openai regression that motivated the walker revert; cycle safety; Date/Buffer pass-through; symmetric round-trip.
  • tests/clean-resource.test.ts (4) — null preservation, undefined stripping, EXCLUDED_FIELDS handling, nested structures.
  • tests/path-matching.test.ts (11) — pathMatchesFolder across long-form, short-form, absolute, Windows-style, nested-folder, and segment-boundary cases.
  • tests/cleanup-safety.test.ts (4) — spawn-based integration: cleanup --force refuses without --confirm <slug> / with wrong slug / with empty state; dry-run allowed without the gates.
  • tests/cli-arg-parsing.test.ts (6) — spawn-based integration: bare-id refusal, misspelled-type refusal, valid positional types / file paths / --confirm slug consumption all accepted.

Verified locally (no network):

  • npm run build (tsc --noEmit) clean at every commit.
  • speaker and mic load under createRequire(import.meta.url) in ESM (per the requested improvements.md smoke test command).
  • git status --porcelain --untracked-files=all -z expands untracked dirs to individual files on macOS/APFS (the mechanism the P0-6 fix relies on).

Manual smoke test flagged for reviewer (requires real terminal / real org):

  • Interactive pull on a populated test org: confirm the Overwrite locally modified files? prompt defaults to No and only forwards --force when user explicitly answers Yes.
  • Interactive cleanup on a populated test org: confirm the spawn passes --force --confirm <slug> so the destructive subprocess proceeds.
  • Fresh-clone preservation: delete resources/<org>/ and .vapi-state.<org>.json, run npm run pull -- <org> --bootstrap, then npm run pull -- <org>, edit one assistant, run pull again and confirm ✏️ <id> (locally modified, preserving) fires (P0-6 end-to-end).

Copy link
Copy Markdown
Contributor Author

dhruva-reddy commented Apr 20, 2026

@dhruva-reddy dhruva-reddy changed the title docs: format call-duration tables and fix bold marker fix(gitops): restore 7 P0 regressions surfaced in feat/optimization-gitops-flow audit; add regression tests Apr 20, 2026
@dhruva-reddy dhruva-reddy force-pushed the fix/p0-gitops-regressions branch from 2f63e7c to a1da236 Compare April 20, 2026 18:43
@dhruva-reddy dhruva-reddy requested a review from vtkovapi April 20, 2026 19:22
Copy link
Copy Markdown
Contributor Author

dhruva-reddy commented Apr 21, 2026

Merge activity

  • Apr 21, 6:14 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 21, 6:15 AM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 21, 6:16 AM UTC: @dhruva-reddy merged this pull request with Graphite.

@dhruva-reddy dhruva-reddy changed the base branch from feat/optimization-gitops-flow to graphite-base/10 April 21, 2026 06:14
@dhruva-reddy dhruva-reddy changed the base branch from graphite-base/10 to main April 21, 2026 06:14
…ow audit

A multi-agent audit of feat/optimization-gitops-flow found seven P0-class
regressions that would cause data loss, silent failure, or API rejection
in real workflows. Each is restored or fixed below.

P0-1 (src/credentials.ts, src/pull.ts, src/push.ts):
  Restore the scoped `replaceCredentialRefs` walker. The branch had
  replaced it with a generic `deepReplaceValues` string-walk that swapped
  every value matching a credential slug. Combined with `pullCredentials`
  auto-slugifying provider-named credentials (`openai`, `11labs`,
  `langfuse`, etc.), this rewrote `voice.provider`, `model.provider`, and
  `observabilityPlan.provider` enum values into UUIDs on push, which the
  Vapi API rejects. The scoped walker matches only at exactly
  `credentialId` / `credentialIds` keys and uses a WeakSet for cycle
  safety. The deleted warning comment is restored.

P0-2 (src/push.ts):
  Wrap the apply phases in try / finally so `await saveState(state)`
  always flushes even on partial failure. Without it, a 5xx mid-push
  leaves API-issued UUIDs unrecorded, and the next run creates duplicates
  on the platform. Save errors are caught and surfaced with a recovery
  hint pointing at `npm run pull -- <org> --bootstrap`.

P0-3 (src/pull.ts):
  Restore null preservation in `cleanResource`. The Vapi API uses `null`
  to represent intentionally cleared fields (`voicemailMessage`,
  `endCallMessage`, etc.). Stripping null on pull caused the next push to
  drop the clear and the platform silently re-applied the prior value.

P0-4 (src/cleanup.ts, src/interactive.ts):
  Restore both pre-existing safety gates that the branch removed:
  `--confirm <slug>` double-gate (so a stray `--force` from another
  command can't go destructive) and the empty-state refusal (so a fresh
  clone or corrupted state file can't be misread as 'all remote
  resources are orphaned' and wipe the org). The interactive cleanup
  wrapper now passes both `--force` and `--confirm <slug>` since the
  user's confirm() prompt is the explicit consent.

P0-5 (src/interactive.ts):
  Drop the unconditional `--force` in interactive pull (both the All
  flow and the per-resource picker). Default is now local-first; an
  explicit `Overwrite locally modified files?` confirm gates the
  `--force` flag. This restores Section 4 of the improvements doc that
  the interactive flow was bypassing.

P0-6 (src/pull.ts):
  Use `git status --porcelain --untracked-files=all -z` so untracked
  files are listed individually rather than as a collapsed `?? dir/`
  entry, and gate the mtime fallback on `(!changedFiles ||
  changedFiles.size === 0)` so it fires when git has no useful info.
  Also include `.yaml` (in addition to `.md` and `.yml`) when checking
  the git-changed-files preservation set, to avoid duplicate-resourceId
  errors on the next push for assistants stored as `.yaml`.

P0-7 (src/push.ts, src/config.ts):
  Extract `pathMatchesFolder` and broaden the match: short-form paths
  like `assistants/foo.yml` (the form documented in AGENTS.md) now
  correctly match the assistants type instead of silently no-op'ing.
  In `parseFlags`, refuse unrecognized bare positional args (e.g.
  `npm run push -- <org> foo`) instead of silently dropping them and
  triggering a full apply with full orphan-deletion check. Allow
  unrecognized long flags (starting with `--`) to pass through so
  command-specific flags like `--confirm` reach their consumer.

P1 cleanups in the same files:
  - `src/config.ts`: hardcoded `.env.dev` in the missing-token error
    message becomes `.env.${VAPI_ENV}` so the user is told to create the
    correct env file for their slug.
  - `src/config.ts`: `--confirm <slug>` is now consumed by `parseFlags`
    so cleanup.ts's flag forwarding doesn't trip the new strict-arg
    refusal.
Locks in the P0 behaviors restored in the previous commit so future
refactors can't silently regress them.

Tests use Node's built-in test runner (no new dependencies):
- tests/credentials.test.ts: 8 unit tests for replaceCredentialRefs,
  including the key P0-1 case that asserts provider enums are NOT
  swapped even when the credential slug exactly matches a provider name
  like `openai`. Also covers cycle safety, non-plain-object pass-through
  (Date, Buffer), and symmetric round-trip behavior.
- tests/clean-resource.test.ts: 4 unit tests pinning that
  `cleanResource` strips EXCLUDED_FIELDS and undefined values but
  preserves null (P0-3).
- tests/path-matching.test.ts: 11 unit tests for the new
  `pathMatchesFolder` helper covering long-form, short-form, absolute,
  Windows-style, nested, and the segment-boundary edge cases (P0-7).
- tests/cleanup-safety.test.ts: 4 spawn-based integration tests
  asserting that `cleanup --force` refuses without `--confirm <slug>`,
  refuses with the wrong slug, refuses on empty state, and does NOT
  refuse a default dry-run (P0-4).
- tests/cli-arg-parsing.test.ts: 6 spawn-based integration tests
  covering bare-id refusal, misspelled-type refusal, and that valid
  args (positional types, file paths, `--confirm` slug consumption) are
  all accepted at parse time (P0-7).

Run with `npm test`. All 33 tests pass under tsx.
`requested improvements.md` is a local audit log used during the P0
review of feat/optimization-gitops-flow. Keep it out of the upstream
repo so it doesn't get pushed accidentally; reviewers who want it can
fetch the audit summary from the related conversation.
The README still listed the old cleanup direct command without
`--confirm <slug>`, didn't mention the new interactive `Overwrite
locally modified files?` prompt, omitted the new keyboard shortcuts
(Ctrl+G, ←/→) that searchableCheckbox actually supports, and pointed
both AGENTS.md and the project-structure tree at the now-stale
`docs/environment-scoped-resources.md`. Sweep:

- README commands table: cleanup direct form is now
  `--force --confirm <org>`; add `npm test` row.
- README Interactive Mode: document the new pull and cleanup prompts;
  flesh out the keyboard navigation list (Ctrl+G group toggle,
  arrow-key collapse/expand).
- README "Pulling Without Losing Local Work": explain the two-layer
  detection (git + mtime fallback) so the fresh-clone case is covered
  in the docs as well as the engine.
- README "Selective Push": call out that short-form paths
  (`assistants/foo.yml`) are supported, and that bare resource ids are
  rejected explicitly with a hint.
- README troubleshooting: add entries for "Refusing to run destructive
  cleanup" and "Unrecognized argument / push appears to do nothing".
- README project structure: drop the stale
  `docs/environment-scoped-resources.md` line; add `docs/learnings/`
  and `tests/` with brief annotations.
- AGENTS.md: drop the stale env-scoped-resources line from the docs
  tree.
- Delete `docs/environment-scoped-resources.md` entirely. The whole
  file walked users through `push:dev` / `pull:dev` / `push:stg` /
  `cp resources/dev/...` workflows that no longer exist in this repo;
  keeping it would actively misroute anyone who landed on it.

No source changes; npm run build and npm test still pass.
…lder

Two small cleanups surfaced during the merge from origin/main:

.vapi-ignore example
  main added three identical .vapi-ignore.example files in
  resources/{dev,stg,prod}/. Those directories exist as dead artifacts
  from before the org-slug refactor — nothing in this branch's code
  reads them, and the example content still referenced the deleted
  `pull:<env>` command form.

  Replaced with a single canonical resources/.vapi-ignore.example that:
    - Tells the user to copy it to `resources/<org>/.vapi-ignore`
      (matching how config.ts actually resolves the ignore file).
    - Uses `npm run pull -- <org>` in the description instead of
      `pull:<env>` (which no longer exists).
    - Adds `evals/` to the list of known folder paths (was missing).

scripts/mock-vapi-webhook-server.ts removed
  The mock webhook receiver has not been used in any real pilot and
  lived in a dedicated folder that was otherwise empty. Removed:
    - scripts/mock-vapi-webhook-server.ts
    - `mock:webhook` npm script
    - README commands-table row, project-structure entry, and the
      "Webhook Local Testing" section
    - AGENTS.md "When to use" row, commands list entry, project-
      structure entry, and the "Mock Server Testing" subsection

Build + tests still pass (tsc --noEmit clean, 33/33 tests).
Sweep of the lowest-cost, highest-value items from the pre-push review:

P1-3  src/push.ts
  Append `"evals"` to ALL_RESOURCE_TYPES and add a comment anchoring it to
  VALID_RESOURCE_TYPES in types.ts. Without this, hasAnyLoadedResources,
  getTargetedResourceTypes, getMissingCredentialNames, and
  getInvalidStateMappings all skipped evals — so `npm run push -- <org>
  evals` on a fresh clone would bypass the bootstrap pre-flight check and
  proceed with uninitialized state.

P1-2  src/cleanup.ts
  Add evals to the orphan-scan resourceTypes array. cleanup.ts already
  counted evals in stateIds, but the scan never fetched /eval from the
  platform, so eval orphans were silently un-detectable via direct
  cleanup. (Orphans during `push --force` were already handled by
  delete.ts.)

P1-4  src/eval.ts
  Rewrite the empty-state remediation text and the printUsage examples to
  match the org-slug command surface. Was:
    - "Eval files go in: resources/evals/" (pre-org-scoped path)
    - "tsx src/eval.ts dev -s ..." (direct-tsx invocation; no longer
      documented)
  Now:
    - "Eval files go in: resources/<org>/evals/" with config.env
      interpolation for the actual org
    - "npm run eval -- <org> -s ..." canonical command form

P1-5  AGENTS.md
  .vapi-ignore section pointed at `resources/<env>/.vapi-ignore` and
  `resources/<env>/.vapi-ignore.example`. Both are wrong for this repo:
  the env placeholder should be <org>, and the canonical example lives at
  `resources/.vapi-ignore.example` (no subfolder) after the consolidation
  in 8b0fc88. Fixed both references.

P2-2  src/config.ts
  `loadEnvFile` comments still cited `.env.dev`/`.env.stg`/`.env.prod` as
  examples. Replaced with `.env.my-org` / `.env.my-org.local` so the
  inline doc matches the org-slug model the surrounding code uses.

P2-3  src/resources.ts
  `resourceId` computation comment showed example paths without the
  `<org>` segment. Updated to the current layout.

P2-5  src/call.ts
  Mic-permission hint mixed "System Preferences" (pre-Ventura) and
  "System Settings" (current) in adjacent log lines. Normalized to
  "System Settings" everywhere.

Deferred to follow-up PRs (non-trivial):
  - P1-1 applyEval needs upsertResourceWithStateRecovery pattern
  - P1-6 interactive pull classify-and-surface 5xx/network errors
  - P1-7 automated coverage for interactive flows via @inquirer/testing

Build + tests still pass (tsc --noEmit clean, 33/33).
@dhruva-reddy dhruva-reddy force-pushed the fix/p0-gitops-regressions branch from a1da236 to 1f226b9 Compare April 21, 2026 06:15
@dhruva-reddy dhruva-reddy merged commit a761bb8 into main Apr 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants