diff --git a/.gitignore b/.gitignore index 9520d34..7e16615 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules/ pnpm-lock.yaml # Environment files (secrets - never commit these!) -# Covers dev/stg/prod and any org slug (e.g. .env.roofr-production) +# Covers dev/stg/prod and any org slug (e.g. .env.acme-production) .env .env.* !.env.example diff --git a/README.md b/README.md index f3401c4..88dd33e 100644 --- a/README.md +++ b/README.md @@ -757,7 +757,7 @@ server: credentialId: my-server-credential # State file (environment-specific) -# "my-server-credential": "2f6db611-ad08-4099-8bd8-74db37b0a07e" +# "my-server-credential": "11111111-1111-1111-1111-111111111111" ``` ### State File diff --git a/src/audit.ts b/src/audit.ts index 9ca5a73..a2274c6 100644 --- a/src/audit.ts +++ b/src/audit.ts @@ -156,8 +156,8 @@ function extractRemoteName(resource: VapiResource): string | undefined { // Build the candidate resourceId(s) that a dashboard-orphan UUID would map to, // so we can check them against `.vapi-ignore`. Two shapes are produced because // real customer .vapi-ignore patterns target either form: -// - the bare name-slug (e.g. `assistants/iform-triage-classifier`) -// - the `-` form pull.ts emits (`assistants/iform-...-d98136d9`) +// - the bare name-slug (e.g. `assistants/support-triage-classifier`) +// - the `-` form pull.ts emits (`assistants/support-...-1a2b3c4d`) function candidateResourceIdsForRemote(resource: VapiResource): string[] { const name = extractRemoteName(resource); const shortId = resource.id.slice(0, 8); diff --git a/src/credentials.ts b/src/credentials.ts index c352deb..21e7a23 100644 --- a/src/credentials.ts +++ b/src/credentials.ts @@ -4,7 +4,7 @@ import type { StateFile } from "./types.ts"; // Credential Resolution — resolve org-specific credential UUIDs across environments // // Credentials are pulled from the API and stored in state (name-slug → UUID). -// Resource files store credential NAMES (e.g., "roofr-server-credential"). +// Resource files store credential NAMES (e.g., "acme-server-credential"). // Push resolves names → UUIDs. Pull resolves UUIDs → names. // // Replacement is scoped to `credentialId` / `credentialIds` fields only. diff --git a/src/new-file-gate.ts b/src/new-file-gate.ts index 654781d..94fff3f 100644 --- a/src/new-file-gate.ts +++ b/src/new-file-gate.ts @@ -10,7 +10,7 @@ // (c) MOVED file — file copied without the state entry being rekeyed // // Silently treating every orphan as case (a) is what produced the duplicate -// fleet we surfaced during the gitops-mudflap working session 2026-05-13. +// fleet we surfaced in a customer working session on 2026-05-13. // Flow F (`mv foo.md bar.md` + push), Flow G (dashboard rename → pull writes // new file but leaves stale YAML), and Flow M (`apply` compresses Flow G into // one click) all share this shape. diff --git a/src/pull.ts b/src/pull.ts index 4e71fea..7f3ecb7 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -409,7 +409,7 @@ export function listExistingResourceIds(resourceType: ResourceType): string[] { } // When pulling a new environment, a resource may already exist on disk under a -// different UUID suffix (e.g., `end-call-tool-8102e715` from dev). Match by +// different UUID suffix (e.g., `end-call-tool-9f8e7d6c` from dev). Match by // name-slug so we reuse the existing file instead of creating a duplicate. // // State-awareness guard: if a name-matching file is already claimed in state @@ -851,8 +851,8 @@ export async function pullResourceType( // - `state[resourceType]` carries prior-pull claims loaded from // disk. Without this, if the dashboard returns the new same-name // twin BEFORE the tracked one, the new twin sees `newStateSection` - // empty and clobbers the tracked file. The customer's mudflap-prod - // 5-Rileys investigation surfaced this ordering dependency. + // empty and clobbers the tracked file. A customer-org investigation + // into five same-name assistants surfaced this ordering dependency. // - `newStateSection` carries intra-pull claims from earlier // iterations. Handles the converse (tracked-then-twin order). // Spread `newStateSection` last so it wins when both have the same diff --git a/src/push.ts b/src/push.ts index 3270e9e..2aa34c0 100644 --- a/src/push.ts +++ b/src/push.ts @@ -1674,8 +1674,8 @@ async function main(): Promise { // Orphan-YAML pre-flight gate. Runs ONCE for ALL resource types after // bootstrap (so state-recovery has a chance to rekey first) and BEFORE // any apply phase. Halts push when local files exist with no state entry - // — the duplicate-creation pattern we surfaced during the gitops-mudflap - // working session 2026-05-13 (see src/new-file-gate.ts for context). + // — the duplicate-creation pattern we surfaced in a customer working + // session on 2026-05-13 (see src/new-file-gate.ts for context). // // Skipped during explicit `--bootstrap` runs: a bootstrap is supposed to // populate state from scratch, so every local file legitimately lacks a diff --git a/src/types.ts b/src/types.ts index d285a43..f0c8ed4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,7 +60,7 @@ export type ResourceType = | "simulationSuites" | "evals"; -// Any slug-like string: "dev", "prod", "roofr-production", etc. +// Any slug-like string: "dev", "prod", "acme-production", etc. export type Environment = string; // Well-known names kept for backward-compatible npm scripts diff --git a/tests/apply-scoped-pull.test.ts b/tests/apply-scoped-pull.test.ts index bae62d0..b1f01ea 100644 --- a/tests/apply-scoped-pull.test.ts +++ b/tests/apply-scoped-pull.test.ts @@ -11,31 +11,31 @@ const { preserveExplicitOursPaths } = await import("../src/pull.ts"); test("parseResourceFilePath: long-form assistant path", () => { const parsed = parseResourceFilePath( - "resources/test-fixture-org/assistants/call-transfer-test-c95f4c6b.md", + "resources/test-fixture-org/assistants/call-transfer-test-d3adb33f.md", ); assert.deepEqual(parsed, { type: "assistants", - resourceId: "call-transfer-test-c95f4c6b", + resourceId: "call-transfer-test-d3adb33f", }); }); test("parseResourceFilePath: short-form assistant path", () => { const parsed = parseResourceFilePath( - "assistants/call-transfer-test-c95f4c6b.md", + "assistants/call-transfer-test-d3adb33f.md", ); assert.deepEqual(parsed, { type: "assistants", - resourceId: "call-transfer-test-c95f4c6b", + resourceId: "call-transfer-test-d3adb33f", }); }); test("resolvePullScopeFromFilePaths: maps file paths to dashboard UUIDs by state", () => { const scope = resolvePullScopeFromFilePaths( - ["resources/test-fixture-org/assistants/call-transfer-test-c95f4c6b.md"], + ["resources/test-fixture-org/assistants/call-transfer-test-d3adb33f.md"], { credentials: {}, assistants: { - "call-transfer-test-c95f4c6b": { + "call-transfer-test-d3adb33f": { uuid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", }, }, diff --git a/tests/audit.test.ts b/tests/audit.test.ts index d348bd2..fbd2537 100644 --- a/tests/audit.test.ts +++ b/tests/audit.test.ts @@ -237,13 +237,13 @@ test("state-uuid-collision: 1 slug per uuid → 0 findings", async () => { // Rule: content-identical // ───────────────────────────────────────────────────────────────────────────── -test("content-identical: 4 entries with same lastPulledHash (riley fixture) → 1 warn, 4 slugs", async () => { +test("content-identical: 4 entries with same lastPulledHash (taylor fixture) → 1 warn, 4 slugs", async () => { const state = makeStateFile({ assistants: { - "riley-1": makeStateEntry("uuid-r1", "hash-shared"), - "riley-2": makeStateEntry("uuid-r2", "hash-shared"), - "riley-3": makeStateEntry("uuid-r3", "hash-shared"), - "riley-4": makeStateEntry("uuid-r4", "hash-shared"), + "taylor-1": makeStateEntry("uuid-r1", "hash-shared"), + "taylor-2": makeStateEntry("uuid-r2", "hash-shared"), + "taylor-3": makeStateEntry("uuid-r3", "hash-shared"), + "taylor-4": makeStateEntry("uuid-r4", "hash-shared"), }, }); const findings = await runAudit(baseOpts(state)); @@ -252,10 +252,10 @@ test("content-identical: 4 entries with same lastPulledHash (riley fixture) → assert.equal(identicals[0]!.severity, "warn"); assert.equal(identicals[0]!.resourceIds.length, 4); assert.deepEqual(identicals[0]!.resourceIds, [ - "riley-1", - "riley-2", - "riley-3", - "riley-4", + "taylor-1", + "taylor-2", + "taylor-3", + "taylor-4", ]); }); @@ -294,9 +294,9 @@ test("content-identical: 2 entries share hash, 1 entry has distinct hash → 1 f test("sibling-base-slug: bare + 2 suffixed entries cluster under same base → 1 finding, 3 slugs", async () => { const state = makeStateFile({ assistants: { - "iform-barge": makeStateEntry("uuid-1"), - "iform-barge-d98136d9": makeStateEntry("uuid-2"), - "iform-barge-f6b53e27": makeStateEntry("uuid-3"), + "triage-bot": makeStateEntry("uuid-1"), + "triage-bot-1a2b3c4d": makeStateEntry("uuid-2"), + "triage-bot-5e6f7a8b": makeStateEntry("uuid-3"), }, }); const findings = await runAudit(baseOpts(state)); @@ -304,9 +304,9 @@ test("sibling-base-slug: bare + 2 suffixed entries cluster under same base → 1 assert.equal(siblings.length, 1); assert.equal(siblings[0]!.resourceIds.length, 3); assert.deepEqual(siblings[0]!.resourceIds, [ - "iform-barge", - "iform-barge-d98136d9", - "iform-barge-f6b53e27", + "triage-bot", + "triage-bot-1a2b3c4d", + "triage-bot-5e6f7a8b", ]); // No content-identical overlap here → message does NOT contain cross-ref. assert.equal( @@ -318,8 +318,8 @@ test("sibling-base-slug: bare + 2 suffixed entries cluster under same base → 1 test("sibling-base-slug: siblings that share a hash get cross-reference to content-identical", async () => { const state = makeStateFile({ assistants: { - "iform-barge": makeStateEntry("uuid-1", "hash-shared"), - "iform-barge-d98136d9": makeStateEntry("uuid-2", "hash-shared"), + "triage-bot": makeStateEntry("uuid-1", "hash-shared"), + "triage-bot-1a2b3c4d": makeStateEntry("uuid-2", "hash-shared"), }, }); const findings = await runAudit(baseOpts(state)); @@ -336,7 +336,7 @@ test("sibling-base-slug: siblings that share a hash get cross-reference to conte test("sibling-base-slug: only one entry (no siblings) → 0 findings", async () => { const state = makeStateFile({ assistants: { - "iform-barge": makeStateEntry("uuid-1"), + "triage-bot": makeStateEntry("uuid-1"), }, }); const findings = await runAudit(baseOpts(state)); @@ -500,16 +500,16 @@ test("integration: orphan-yaml + collision + content-identical(4) + sibling-base "coll-a": makeStateEntry("dup-uuid"), "coll-b": makeStateEntry("dup-uuid"), // 4 distinct slugs sharing hash H1 → 1 content-identical (warn, 4 slugs) - "riley-1": makeStateEntry("uuid-r1", "H1"), - "riley-2": makeStateEntry("uuid-r2", "H1"), - "riley-3": makeStateEntry("uuid-r3", "H1"), - "riley-4": makeStateEntry("uuid-r4", "H1"), - // 3 slugs sharing base "iform-barge"; 2 of them share hash H2 so + "taylor-1": makeStateEntry("uuid-r1", "H1"), + "taylor-2": makeStateEntry("uuid-r2", "H1"), + "taylor-3": makeStateEntry("uuid-r3", "H1"), + "taylor-4": makeStateEntry("uuid-r4", "H1"), + // 3 slugs sharing base "triage-bot"; 2 of them share hash H2 so // sibling-base-slug message picks up the cross-ref AND we get one // extra content-identical finding for those 2. - "iform-barge": makeStateEntry("uuid-s1", "H2"), - "iform-barge-d98136d9": makeStateEntry("uuid-s2", "H2"), - "iform-barge-f6b53e27": makeStateEntry("uuid-s3"), + "triage-bot": makeStateEntry("uuid-s1", "H2"), + "triage-bot-1a2b3c4d": makeStateEntry("uuid-s2", "H2"), + "triage-bot-5e6f7a8b": makeStateEntry("uuid-s3"), }, }); @@ -544,7 +544,7 @@ test("integration: orphan-yaml + collision + content-identical(4) + sibling-base ]); // The sibling finding must carry the cross-ref token because 2 of the 3 - // siblings (iform-barge, iform-barge-d98136d9) also appear in a + // siblings (triage-bot, triage-bot-1a2b3c4d) also appear in a // content-identical cluster. const sibling = findings.find((f) => f.rule === "sibling-base-slug")!; assert.ok(sibling.message.includes("overlaps with content-identical")); diff --git a/tests/credentials.test.ts b/tests/credentials.test.ts index 109e7d6..4e611d6 100644 --- a/tests/credentials.test.ts +++ b/tests/credentials.test.ts @@ -47,12 +47,12 @@ function forwardMap(state: StateFile): Map { test("replaceCredentialRefs swaps at credentialId keys", () => { const state = makeState({ - "roofr-server-credential": "11111111-1111-1111-1111-111111111111", + "acme-server-credential": "11111111-1111-1111-1111-111111111111", }); const input = { server: { url: "https://example.com", - credentialId: "roofr-server-credential", + credentialId: "acme-server-credential", }, }; const out = replaceCredentialRefs(input, forwardMap(state)); diff --git a/tests/dep-dedup.test.ts b/tests/dep-dedup.test.ts index 40db393..a1cfce1 100644 --- a/tests/dep-dedup.test.ts +++ b/tests/dep-dedup.test.ts @@ -65,7 +65,7 @@ test("findExistingResourceByName: assistant payload (top-level name only)", () = test("findExistingResourceByName: state-only match", () => { const m = findExistingResourceByName({ - localResourceId: "b2b-invoice-end-call", + localResourceId: "billing-end-call", localPayload: { function: { name: "end-call" } }, stateSection: { "end-call-67aea057": { uuid: "uuid-aaa" }, @@ -157,7 +157,7 @@ test("findExistingResourceByName: ambiguous across state vs dashboard → lex-sm // the SAME uuid appears in both — here the winner appears in only one, // so source is whichever side it came from. const m = findExistingResourceByName({ - localResourceId: "b2b-invoice-end-call", + localResourceId: "billing-end-call", localPayload: { function: { name: "end-call" } }, stateSection: { "end-call-67aea057": { uuid: "uuid-zzz" } }, remoteList: [{ id: "uuid-aaa", function: { name: "end-call" } }], diff --git a/tests/drift.test.ts b/tests/drift.test.ts index 73fbf61..b4a7788 100644 --- a/tests/drift.test.ts +++ b/tests/drift.test.ts @@ -786,7 +786,7 @@ test("canonicalizeForHash: strips server-managed fields (id, orgId, createdAt, u // short-circuit baseline preservation. // // Regression coverage for a bug introduced by the drift-direction-classifier -// PR (#38) and caught by the E2E both-diverged smoke test on mudflap-iform-test: +// PR (#38) and caught by the E2E both-diverged smoke test on a customer test org: // pull rebuilds each state section from EMPTY, and the classifier short-circuit // branches wrote back a bare `{ uuid }` — dropping the baseline, so the next // pull classified the resource as `no-baseline` and could never detect drift diff --git a/tests/find-existing-resource-id.test.ts b/tests/find-existing-resource-id.test.ts index 2fd4cba..bb91e2f 100644 --- a/tests/find-existing-resource-id.test.ts +++ b/tests/find-existing-resource-id.test.ts @@ -26,50 +26,50 @@ function stateEntry(uuid: string): ResourceState { // ───────────────────────────────────────────────────────────────────────────── test("adoption: state empty, one file matches name → adopt (cross-env pull)", () => { - // Classic cross-env pull: dev shipped a file `riley-8102e715.md`, prod is + // Classic cross-env pull: dev shipped a file `taylor-9f8e7d6c.md`, prod is // a fresh clone with no state entry for it. Pull should reuse the file // even though the UUID-suffix on disk differs from prod's UUID. - const onDisk = ["riley-8102e715"]; + const onDisk = ["taylor-9f8e7d6c"]; const newState: Record = {}; const result = findExistingResourceId( onDisk, - resource("uuid-prod-aaaa", "Riley"), + resource("uuid-prod-aaaa", "Taylor"), newState, ); - assert.equal(result, "riley-8102e715"); + assert.equal(result, "taylor-9f8e7d6c"); }); test("adoption: file claimed in state by the SAME UUID → adopt (re-pull is idempotent)", () => { // Second pull of an already-tracked resource. State maps the slug to // THIS resource's UUID, so reusing the slug is correct. - const onDisk = ["riley"]; + const onDisk = ["taylor"]; const newState: Record = { - riley: stateEntry("uuid-aaaa"), + taylor: stateEntry("uuid-aaaa"), }; const result = findExistingResourceId( onDisk, - resource("uuid-aaaa", "Riley"), + resource("uuid-aaaa", "Taylor"), newState, ); - assert.equal(result, "riley"); + assert.equal(result, "taylor"); }); test("adoption: two matches but only one is adoptable (the unclaimed one) → adopt it", () => { // The dashboard has 2 same-named resources. The first was already - // processed earlier in this same pull loop and adopted `riley.md`. - // Now a different file `riley-deadbeef.md` (e.g. from cross-env pull + // processed earlier in this same pull loop and adopted `taylor.md`. + // Now a different file `taylor-deadbeef.md` (e.g. from cross-env pull // history) is on disk, unclaimed in state, and the current resource // can adopt it without conflict. - const onDisk = ["riley", "riley-deadbeef"]; + const onDisk = ["taylor", "taylor-deadbeef"]; const newState: Record = { - riley: stateEntry("uuid-aaaa"), + taylor: stateEntry("uuid-aaaa"), }; const result = findExistingResourceId( onDisk, - resource("uuid-bbbb", "Riley"), + resource("uuid-bbbb", "Taylor"), newState, ); - assert.equal(result, "riley-deadbeef"); + assert.equal(result, "taylor-deadbeef"); }); // ───────────────────────────────────────────────────────────────────────────── @@ -78,15 +78,15 @@ test("adoption: two matches but only one is adoptable (the unclaimed one) → ad test("no adoption: file claimed by DIFFERENT UUID → undefined (the fix's main case)", () => { // This is the clobber scenario the fix prevents: state already maps - // `riley` to UUID A; a new resource with the same name but a NEW UUID - // (B) must NOT adopt `riley.md` — doing so would overwrite A's content. - const onDisk = ["riley"]; + // `taylor` to UUID A; a new resource with the same name but a NEW UUID + // (B) must NOT adopt `taylor.md` — doing so would overwrite A's content. + const onDisk = ["taylor"]; const newState: Record = { - riley: stateEntry("uuid-aaaa"), + taylor: stateEntry("uuid-aaaa"), }; const result = findExistingResourceId( onDisk, - resource("uuid-bbbb", "Riley"), + resource("uuid-bbbb", "Taylor"), newState, ); assert.equal(result, undefined); @@ -98,14 +98,14 @@ test("no adoption: N+ matches with mixed claims → undefined", () => { // third match exists, claimed by yet another different UUID. With // multiple adoptable candidates the 1:1 ambiguity guard still kicks // in. Here we test a related shape: 2 adoptable matches → ambiguous. - const onDisk = ["riley", "riley-aaaa1111", "riley-bbbb2222"]; + const onDisk = ["taylor", "taylor-aaaa1111", "taylor-bbbb2222"]; const newState: Record = { - riley: stateEntry("uuid-other"), - // riley-aaaa1111 and riley-bbbb2222 are both unclaimed → 2 adoptable + taylor: stateEntry("uuid-other"), + // taylor-aaaa1111 and taylor-bbbb2222 are both unclaimed → 2 adoptable }; const result = findExistingResourceId( onDisk, - resource("uuid-cccc", "Riley"), + resource("uuid-cccc", "Taylor"), newState, ); assert.equal(result, undefined); @@ -115,14 +115,14 @@ test("no adoption: N+ matches but all claimed by other UUIDs → undefined", () // Every name-matching file is claimed by some other UUID. No file is // adoptable for the current resource; fall through to // generateResourceId in the caller. - const onDisk = ["riley", "riley-aaaa1111"]; + const onDisk = ["taylor", "taylor-aaaa1111"]; const newState: Record = { - riley: stateEntry("uuid-aaaa"), - "riley-aaaa1111": stateEntry("uuid-bbbb"), + taylor: stateEntry("uuid-aaaa"), + "taylor-aaaa1111": stateEntry("uuid-bbbb"), }; const result = findExistingResourceId( onDisk, - resource("uuid-cccc", "Riley"), + resource("uuid-cccc", "Taylor"), newState, ); assert.equal(result, undefined); @@ -133,7 +133,7 @@ test("no adoption: no name-matching files on disk → undefined", () => { const newState: Record = {}; const result = findExistingResourceId( onDisk, - resource("uuid-aaaa", "Riley"), + resource("uuid-aaaa", "Taylor"), newState, ); assert.equal(result, undefined); @@ -147,7 +147,7 @@ test("regression: resource without a name → undefined (unchanged)", () => { // Tools store their name under function.name (see extractName). A // resource with neither a top-level name nor a function.name is // un-adoptable by design — no slug to compute. - const onDisk = ["riley"]; + const onDisk = ["taylor"]; const newState: Record = {}; const result = findExistingResourceId( onDisk, @@ -161,11 +161,11 @@ test("regression: two same-name files, state empty → undefined (unchanged ambi // Pre-fix behavior: 2+ matches without a state discriminator → ambiguous, // refuse adoption. Fix should preserve this — both files are adoptable // (unclaimed), so `adoptable.length === 2` and the 1:1 guard fires. - const onDisk = ["riley", "riley-deadbeef"]; + const onDisk = ["taylor", "taylor-deadbeef"]; const newState: Record = {}; const result = findExistingResourceId( onDisk, - resource("uuid-aaaa", "Riley"), + resource("uuid-aaaa", "Taylor"), newState, ); assert.equal(result, undefined); diff --git a/tests/pull-rename-preserves-filename.test.ts b/tests/pull-rename-preserves-filename.test.ts index 7405380..bbc6c51 100644 --- a/tests/pull-rename-preserves-filename.test.ts +++ b/tests/pull-rename-preserves-filename.test.ts @@ -22,12 +22,12 @@ import { Worker } from "node:worker_threads"; // rename or recreate the local file. The filename slug is a stable local // handle, decoupled from the dashboard `name`. // -// Scenario: state maps `call-transfer-test-c95f4c6b` → UUID X. On disk, -// `call-transfer-test-c95f4c6b.md` holds X's content. The dashboard renames +// Scenario: state maps `call-transfer-test-d3adb33f` → UUID X. On disk, +// `call-transfer-test-d3adb33f.md` holds X's content. The dashboard renames // the resource to "Call Transfer Test 1". A correct pull must: -// 1. Keep `call-transfer-test-c95f4c6b.md` (update its content in place). -// 2. NOT create a second file `call-transfer-test-1-c95f4c6b.md`. -// 3. Keep state keyed `call-transfer-test-c95f4c6b` → X. +// 1. Keep `call-transfer-test-d3adb33f.md` (update its content in place). +// 2. NOT create a second file `call-transfer-test-1-d3adb33f.md`. +// 3. Keep state keyed `call-transfer-test-d3adb33f` → X. // // Before the fix, pull discarded the tracked resourceId on name mismatch, // minted a name-derived slug, and wrote a duplicate file — leaving two files @@ -86,8 +86,8 @@ function startStub( } const ENV = "test-rename"; -const UUID_X = "c95f4c6b-bfde-4e90-af7e-ea8870b9f2d6"; -const TRACKED_SLUG = "call-transfer-test-c95f4c6b"; +const UUID_X = "d3adb33f-0000-4000-8000-000000000000"; +const TRACKED_SLUG = "call-transfer-test-d3adb33f"; // The name-derived slug that the buggy behavior would have produced. const NAME_DERIVED_SLUG = `call-transfer-test-1-${UUID_X.slice(0, 8)}`; diff --git a/tests/pull-same-name-clobber.test.ts b/tests/pull-same-name-clobber.test.ts index de1f852..a5f3f3f 100644 --- a/tests/pull-same-name-clobber.test.ts +++ b/tests/pull-same-name-clobber.test.ts @@ -20,17 +20,17 @@ import { Worker } from "node:worker_threads"; // ───────────────────────────────────────────────────────────────────────────── // Integration test for the state-aware adoption fix in pull.ts. // -// Scenario: dashboard has 2 assistants both named "Riley" (UUID A and B). -// State already maps the slug `riley` → A. On disk, `riley.md` holds A's +// Scenario: dashboard has 2 assistants both named "Taylor" (UUID A and B). +// State already maps the slug `taylor` → A. On disk, `taylor.md` holds A's // content. A bug-free pull must: -// 1. Preserve `riley.md` unchanged (still A's content) — NOT clobber it. -// 2. Create a fresh `riley-.md` for the new resource B. -// 3. Persist both mappings in state: `riley → A` AND `riley- → B`. +// 1. Preserve `taylor.md` unchanged (still A's content) — NOT clobber it. +// 2. Create a fresh `taylor-.md` for the new resource B. +// 3. Persist both mappings in state: `taylor → A` AND `taylor- → B`. // -// Without the fix, B silently overwrites `riley.md` and the state mapping -// for `riley` flips to B — orphaning A's UUID with no on-disk artifact. +// Without the fix, B silently overwrites `taylor.md` and the state mapping +// for `taylor` flips to B — orphaning A's UUID with no on-disk artifact. // -// Reproduces the mudflap "5 Rileys" customer scenario that will keep getting +// Reproduces the "five same-name assistants" customer scenario that will keep getting // triggered as Vapi auto-seeds same-named twins for new orgs. // ───────────────────────────────────────────────────────────────────────────── @@ -95,11 +95,11 @@ const UUID_B = "bbbbbbbb-2222-2222-2222-222222222222"; // Minimal assistant body the API would return. Includes a distinctive // marker so we can assert which body landed in each file. -function rileyDashboardBody(uuid: string, marker: string) { +function taylorDashboardBody(uuid: string, marker: string) { return { id: uuid, orgId: "org-test", - name: "Riley", + name: "Taylor", model: { provider: "openai", model: "gpt-4o", @@ -109,13 +109,13 @@ function rileyDashboardBody(uuid: string, marker: string) { }; } -// Pre-pull on-disk content for `riley.md`. Uses A's marker so we can tell +// Pre-pull on-disk content for `taylor.md`. Uses A's marker so we can tell // whether B clobbered it. -const PREEXISTING_RILEY_MD = `--- +const PREEXISTING_TAYLOR_MD = `--- model: provider: openai model: gpt-4o -name: Riley +name: Taylor voice: provider: 11labs voiceId: burt @@ -125,7 +125,7 @@ marker:A-original `; // Run the full clobber scenario with a configurable dashboard-response -// ordering. Asserts the fix works regardless of which Riley the API +// ordering. Asserts the fix works regardless of which Taylor the API // returns first — this is the H1 case from code review: without merging // the prior-pull state into the adoption guard, B-first ordering would // clobber A's file because `newStateSection` is empty when B is @@ -146,12 +146,12 @@ async function runClobberScenario( "dir", ); - // Seed the resource tree: existing `riley.md` holding A's content. + // Seed the resource tree: existing `taylor.md` holding A's content. const assistantsDir = join(dir, "resources", ENV, "assistants"); mkdirSync(assistantsDir, { recursive: true }); - writeFileSync(join(assistantsDir, "riley.md"), PREEXISTING_RILEY_MD); + writeFileSync(join(assistantsDir, "taylor.md"), PREEXISTING_TAYLOR_MD); - // Seed state: slug `riley` already maps to UUID A. + // Seed state: slug `taylor` already maps to UUID A. // The drift baseline lives in the hash store, not the state file. The // engine runs from the copied src/, so its store resolves under `dir`. const hashStore = join(dir, ".vapi-state-hash", ENV); @@ -164,7 +164,7 @@ async function runClobberScenario( { credentials: {}, assistants: { - riley: { uuid: UUID_A }, + taylor: { uuid: UUID_A }, }, structuredOutputs: {}, tools: {}, @@ -180,18 +180,18 @@ async function runClobberScenario( ), ); - // HTTP stub returns BOTH Rileys (A and B) for the /assistant list call, + // HTTP stub returns BOTH Taylors (A and B) for the /assistant list call, // in the configured order. The fix must produce the correct outcome // regardless of which one the dashboard returns first. const orderedBodies = dashboardOrder === "A-first" ? [ - rileyDashboardBody(UUID_A, "A-fresh-from-platform"), - rileyDashboardBody(UUID_B, "B-new-twin"), + taylorDashboardBody(UUID_A, "A-fresh-from-platform"), + taylorDashboardBody(UUID_B, "B-new-twin"), ] : [ - rileyDashboardBody(UUID_B, "B-new-twin"), - rileyDashboardBody(UUID_A, "A-fresh-from-platform"), + taylorDashboardBody(UUID_B, "B-new-twin"), + taylorDashboardBody(UUID_A, "A-fresh-from-platform"), ]; const { worker, port } = await startStub([ { @@ -213,8 +213,8 @@ async function runClobberScenario( try { // Run pull via the CLI entrypoint (same path real customers exercise). // --force so the mtime-based "locally modified" guard does not kick in - // and short-circuit the platform overwrite of riley.md (we want pull - // to actually try to write riley.md — the question is whether B's + // and short-circuit the platform overwrite of taylor.md (we want pull + // to actually try to write taylor.md — the question is whether B's // content lands there or A's content stays). const res = spawnSync( "node", @@ -238,25 +238,25 @@ async function runClobberScenario( ); // ── Filesystem assertions ──────────────────────────────────────────── - // riley.md must still exist AND hold A's content (the platform's + // taylor.md must still exist AND hold A's content (the platform's // A-fresh-from-platform marker, since --force overwrites with platform // state — but NOT B's content). - const rileyPath = join(assistantsDir, "riley.md"); - assert.ok(existsSync(rileyPath), `[${testName}] riley.md must still exist`); - const rileyContent = readFileSync(rileyPath, "utf-8"); + const taylorPath = join(assistantsDir, "taylor.md"); + assert.ok(existsSync(taylorPath), `[${testName}] taylor.md must still exist`); + const taylorContent = readFileSync(taylorPath, "utf-8"); assert.match( - rileyContent, + taylorContent, /marker:A-fresh-from-platform/, - `[${testName}] riley.md must hold A's content (the file mapped to A in state); got:\n${rileyContent}`, + `[${testName}] taylor.md must hold A's content (the file mapped to A in state); got:\n${taylorContent}`, ); assert.doesNotMatch( - rileyContent, + taylorContent, /marker:B-new-twin/, - `[${testName}] riley.md must NOT have been clobbered by B; got:\n${rileyContent}`, + `[${testName}] taylor.md must NOT have been clobbered by B; got:\n${taylorContent}`, ); - // B must have landed in its own file `riley-.md`. - const expectedBSlug = `riley-${UUID_B.slice(0, 8)}`; + // B must have landed in its own file `taylor-.md`. + const expectedBSlug = `taylor-${UUID_B.slice(0, 8)}`; const bPath = join(assistantsDir, `${expectedBSlug}.md`); assert.ok( existsSync(bPath), @@ -274,9 +274,9 @@ async function runClobberScenario( readFileSync(join(dir, `.vapi-state.${ENV}.json`), "utf-8"), ); assert.equal( - finalState.assistants.riley?.uuid, + finalState.assistants.taylor?.uuid, UUID_A, - `[${testName}] state[riley] must still map to A (${UUID_A}); got ${JSON.stringify(finalState.assistants.riley)}`, + `[${testName}] state[taylor] must still map to A (${UUID_A}); got ${JSON.stringify(finalState.assistants.taylor)}`, ); assert.equal( finalState.assistants[expectedBSlug]?.uuid, @@ -306,6 +306,6 @@ test("pull: 2 same-name resources (B-first ordering) — fix prevents clobber re // Regression guard for the H1 finding from code review: without merging // `state[resourceType]` into the adoption guard, B-first ordering would // clobber A's file (B is processed while `newStateSection` is still empty, - // sees `riley.md` as "unclaimed in flight" — but prior state has it). + // sees `taylor.md` as "unclaimed in flight" — but prior state has it). await runClobberScenario("B-first", "B-first"); }); diff --git a/tests/push-stale-baseline-noop.test.ts b/tests/push-stale-baseline-noop.test.ts index 882ce3d..c46fa8f 100644 --- a/tests/push-stale-baseline-noop.test.ts +++ b/tests/push-stale-baseline-noop.test.ts @@ -32,8 +32,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, ".."); const ENV = "test-stale-baseline"; -const UUID = "c95f4c6b-bfde-4e90-af7e-ea8870b9f2d6"; -const SLUG = "stale-baseline-bot-c95f4c6b"; +const UUID = "d3adb33f-0000-4000-8000-000000000000"; +const SLUG = "stale-baseline-bot-d3adb33f"; // Dashboard payload. cleanResource strips id/orgId; the remaining shape must // canonicalize to exactly what the local .md parses to (see LOCAL_MD). diff --git a/tests/recanonicalize.test.ts b/tests/recanonicalize.test.ts index 479513f..91fb690 100644 --- a/tests/recanonicalize.test.ts +++ b/tests/recanonicalize.test.ts @@ -48,14 +48,14 @@ function makeFileExists(paths: Set) { // ───────────────────────────────────────────────────────────────────────────── test("recanonicalize: collapses UUID-suffixed key to canonical when local file present and canonical slot empty (the duplicate-generation root cause)", () => { - // State has `foo-vmd-004c5108` (rekey'd by a prior pull during a name + // State has `foo-vmd-0a1b2c3d` (rekey'd by a prior pull during a name // collision). The conflicting twin has since been deleted on the // dashboard. Local has only `squads/foo-vmd.md`. Canonical slot in state // is empty. We should collapse. const state = makeStateFile({ squads: { - "foo-vmd-004c5108": makeStateEntry( - "004c5108-aaaa-bbbb-cccc-dddddddddddd", + "foo-vmd-0a1b2c3d": makeStateEntry( + "0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd", ), }, }); @@ -64,13 +64,13 @@ test("recanonicalize: collapses UUID-suffixed key to canonical when local file p fileExists: makeFileExists(new Set(["squads/foo-vmd.yml"])), }); assert.equal(report.rekeys.length, 1); - assert.equal(report.rekeys[0]!.fromKey, "foo-vmd-004c5108"); + assert.equal(report.rekeys[0]!.fromKey, "foo-vmd-0a1b2c3d"); assert.equal(report.rekeys[0]!.toKey, "foo-vmd"); assert.equal(report.conflicts.length, 0); assert.deepEqual(Object.keys(state.squads), ["foo-vmd"]); assert.equal( state.squads["foo-vmd"]!.uuid, - "004c5108-aaaa-bbbb-cccc-dddddddddddd", + "0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd", ); }); @@ -141,12 +141,12 @@ test("recanonicalize: applies uniformly across every resource type", () => { test("recanonicalize: refuses when UUID suffix doesn't match entry's UUID prefix (user-named resource that coincidentally ends in -<8hex>)", () => { // The key looks suffixed but the captured 8 hex chars (`deadbeef`) are - // NOT the prefix of the entry's UUID (`004c5108...`). This is a + // NOT the prefix of the entry's UUID (`0a1b2c3d...`). This is a // user-given name like "my-tool-deadbeef" — DO NOT touch it. const state = makeStateFile({ tools: { "my-tool-deadbeef": makeStateEntry( - "004c5108-aaaa-bbbb-cccc-dddddddddddd", + "0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd", ), }, }); @@ -237,15 +237,15 @@ test("recanonicalize: refuses when canonical local file is missing (would create test("recanonicalize: skips UUID-suffixed keys when only the matching local file exists (normal post-pull layout)", () => { const state = makeStateFile({ assistants: { - "call-transfer-test-c95f4c6b": makeStateEntry( - "c95f4c6b-bfde-4e90-af7e-ea8870b9f2d6", + "call-transfer-test-d3adb33f": makeStateEntry( + "d3adb33f-0000-4000-8000-000000000000", ), }, }); const report = recanonicalizeStateKeys({ state, fileExists: makeFileExists( - new Set(["assistants/call-transfer-test-c95f4c6b.md"]), + new Set(["assistants/call-transfer-test-d3adb33f.md"]), ), }); assert.equal(report.rekeys.length, 0); @@ -354,28 +354,28 @@ test("recanonicalize: handles UUID prefix match case-insensitively", () => { test("recanonicalize: collapses multi-dash base slug ('foo-vmd-') — the exact shape the orphan-gate pairing missed", () => { // From the live incident: state key was - // `iform-voicemail-triage-squad-llm-only-vmd-004c5108`. The orphan-gate's + // `voicemail-triage-squad-llm-only-vmd-0a1b2c3d`. The orphan-gate's // extractBaseSlug pairing failed because base = "...-vmd" not "...". // This pass operates on raw UUID-suffix shape, so it recanonicalizes // regardless of how many dash-segments precede the UUID8 — as long as // the canonical local file exists. const state = makeStateFile({ squads: { - "iform-voicemail-triage-squad-llm-only-vmd-004c5108": makeStateEntry( - "004c5108-aaaa-bbbb-cccc-dddddddddddd", + "voicemail-triage-squad-llm-only-vmd-0a1b2c3d": makeStateEntry( + "0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd", ), }, }); const report = recanonicalizeStateKeys({ state, fileExists: makeFileExists( - new Set(["squads/iform-voicemail-triage-squad-llm-only-vmd.yml"]), + new Set(["squads/voicemail-triage-squad-llm-only-vmd.yml"]), ), }); assert.equal(report.rekeys.length, 1); assert.equal( report.rekeys[0]!.toKey, - "iform-voicemail-triage-squad-llm-only-vmd", + "voicemail-triage-squad-llm-only-vmd", ); assert.equal(report.conflicts.length, 0); }); diff --git a/tests/slug-utils.test.ts b/tests/slug-utils.test.ts index cd0dde0..a8a19e2 100644 --- a/tests/slug-utils.test.ts +++ b/tests/slug-utils.test.ts @@ -72,7 +72,7 @@ test("UUID_SUFFIX_RE: case-insensitive on hex", () => { test("extractBaseSlug: strips engine-shape suffix", () => { assert.equal(extractBaseSlug("end-call-67aea057"), "end-call"); - assert.equal(extractBaseSlug("foo-vmd-004c5108"), "foo-vmd"); + assert.equal(extractBaseSlug("foo-vmd-0a1b2c3d"), "foo-vmd"); }); test("extractBaseSlug: returns input unchanged when no suffix present", () => { @@ -160,11 +160,11 @@ test("isEngineSuffixedSlug: strips UUID dashes defensively (malformed UUID with test("isEngineSuffixedSlug: handles multi-segment base", () => { const result = isEngineSuffixedSlug( - "iform-voicemail-triage-squad-llm-only-vmd-004c5108", - "004c5108-aaaa-bbbb-cccc-dddddddddddd", + "voicemail-triage-squad-llm-only-vmd-0a1b2c3d", + "0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd", ); assert.deepEqual(result, { - base: "iform-voicemail-triage-squad-llm-only-vmd", - suffix: "004c5108", + base: "voicemail-triage-squad-llm-only-vmd", + suffix: "0a1b2c3d", }); }); diff --git a/tests/so-assistant-omit.test.ts b/tests/so-assistant-omit.test.ts index a8f74b9..0ead17d 100644 --- a/tests/so-assistant-omit.test.ts +++ b/tests/so-assistant-omit.test.ts @@ -36,7 +36,7 @@ const UUID_2 = "1a2b3c4d-5e6f-4a1b-9c2d-3e4f5a6b7c8d"; test("one unresolved ref among the authored assistant_ids drops the whole assistantIds key", () => { const original = { - assistant_ids: ["front-desk", "clinical-stage-1"], + assistant_ids: ["front-desk", "intake-stage-1"], }; const payload = { name: "intake-schema", @@ -53,7 +53,7 @@ test("one unresolved ref among the authored assistant_ids drops the whole assist }); test("a fully resolved assistantIds array is sent unchanged", () => { - const original = { assistant_ids: ["front-desk", "clinical-stage-1"] }; + const original = { assistant_ids: ["front-desk", "intake-stage-1"] }; const payload = { assistantIds: [UUID, UUID_2] }; const result = omitUnresolvedAssistantIds(payload, original); @@ -186,12 +186,12 @@ test("updateStructuredOutputAssistantRefs: a partial resolution (one of two) ski const state = emptyState(); state.structuredOutputs["intake-schema"] = { uuid: UUID }; state.assistants["front-desk"] = { uuid: UUID_2 }; - // "clinical-stage-1" is deliberately untracked. + // "intake-stage-1" is deliberately untracked. const so: ResourceFile = { resourceId: "intake-schema", filePath: "/fake/structured-outputs/intake-schema.yml", - data: { assistant_ids: ["front-desk", "clinical-stage-1"] }, + data: { assistant_ids: ["front-desk", "intake-stage-1"] }, }; await withFetchAndWarnRecorders(async ({ fetchCalls, warnings }) => { @@ -208,15 +208,15 @@ test("updateStructuredOutputAssistantRefs: a partial resolution (one of two) ski (arg) => typeof arg === "string" && arg.includes("intake-schema") && - arg.includes("clinical-stage-1") && + arg.includes("intake-stage-1") && !arg.includes("front-desk"), ), ), // Checks that the resolved slug never appears in the warning at all — // not just that it doesn't appear in one particular ordering. The // implementation preserves authored order, so a buggy message naming - // both refs would read "front-desk, clinical-stage-1" (authored order) - // and slip past a check that only excluded "clinical-stage-1, front-desk". + // both refs would read "front-desk, intake-stage-1" (authored order) + // and slip past a check that only excluded "intake-stage-1, front-desk". "the warning should name the unresolved ref but not the resolved one", ); }); diff --git a/tests/tool-assistant-cycle.test.ts b/tests/tool-assistant-cycle.test.ts index c0c5ee4..150497b 100644 --- a/tests/tool-assistant-cycle.test.ts +++ b/tests/tool-assistant-cycle.test.ts @@ -11,7 +11,7 @@ import test, { after } from "node:test"; // once every assistant exists. The UPDATE path had no equivalent, so a first // push into an empty org sent the raw slug and the API answered: // -// PATCH /tool/ → 400 Assistant with ID "clinical-stage-1-a4598432" not found +// PATCH /tool/ → 400 Assistant with ID "intake-stage-1-7c6d5e4f" not found // // which aborted the push before the linking pass could run. // ───────────────────────────────────────────────────────────────────────────── @@ -41,11 +41,11 @@ test("an unresolved assistant destination drops the whole destinations key", asy // assistant is not in state yet. const original = { type: "transferCall", - destinations: [{ type: "assistant", assistantId: "clinical-stage-1" }], + destinations: [{ type: "assistant", assistantId: "intake-stage-1" }], }; const payload = { type: "transferCall", - destinations: [{ type: "assistant", assistantId: "clinical-stage-1" }], + destinations: [{ type: "assistant", assistantId: "intake-stage-1" }], }; const result = omitUnresolvedDestinations(payload, original); @@ -59,7 +59,7 @@ test("an unresolved assistant destination drops the whole destinations key", asy test("a resolved destination is sent unchanged", async () => { const original = { - destinations: [{ type: "assistant", assistantId: "clinical-stage-1" }], + destinations: [{ type: "assistant", assistantId: "intake-stage-1" }], }; const payload = { destinations: [{ type: "assistant", assistantId: UUID }], @@ -73,12 +73,12 @@ test("a resolved destination is sent unchanged", async () => { }); test("a trailing YAML comment on the reference still counts as resolved", async () => { - // `assistantId: clinical-stage-1 ## human note` — the comment is part of the + // `assistantId: intake-stage-1 ## human note` — the comment is part of the // authored string, so the comparison has to strip it or every reference would // look unresolved. const original = { destinations: [ - { type: "assistant", assistantId: "clinical-stage-1 ## stage one" }, + { type: "assistant", assistantId: "intake-stage-1 ## stage one" }, ], }; const payload = { destinations: [{ type: "assistant", assistantId: UUID }] }; @@ -94,7 +94,7 @@ test("an unresolved slug authored with a trailing YAML comment still drops the w // must clean both sides before checking equality, or the commented, // uncleaned resolved value never matches the cleaned original and the // destination is wrongly classified as resolved. - const commented = "clinical-stage-1 ## stage one"; + const commented = "intake-stage-1 ## stage one"; const original = { destinations: [{ type: "assistant", assistantId: commented }], }; @@ -164,16 +164,16 @@ test("non-assistant destinations never block the update", async () => { test("unresolvedDestinationSlugs: a slug entry is reported", () => { const result = unresolvedDestinationSlugs([ - { type: "assistant", assistantId: "clinical-stage-1" }, + { type: "assistant", assistantId: "intake-stage-1" }, ]); - assert.deepEqual(result, ["clinical-stage-1"]); + assert.deepEqual(result, ["intake-stage-1"]); }); test("unresolvedDestinationSlugs: a trailing YAML comment is stripped before reporting", () => { const result = unresolvedDestinationSlugs([ - { type: "assistant", assistantId: "clinical-stage-1 ## stage one" }, + { type: "assistant", assistantId: "intake-stage-1 ## stage one" }, ]); - assert.deepEqual(result, ["clinical-stage-1"]); + assert.deepEqual(result, ["intake-stage-1"]); }); test("unresolvedDestinationSlugs: an array of only UUID assistantIds reports nothing", () => { @@ -255,7 +255,7 @@ function emptyState(): StateFile { test("updateToolAssistantRefs: skips the PATCH and warns when the referenced assistant is genuinely absent", async () => { const state = emptyState(); state.tools["router"] = { uuid: UUID }; - // Deliberately no entry under state.assistants for "clinical-stage-1" — the + // Deliberately no entry under state.assistants for "intake-stage-1" — the // assistant is genuinely absent, not merely not-yet-applied. const tool: ResourceFile = { @@ -263,7 +263,7 @@ test("updateToolAssistantRefs: skips the PATCH and warns when the referenced ass filePath: "/fake/tools/router.yml", data: { type: "transferCall", - destinations: [{ type: "assistant", assistantId: "clinical-stage-1" }], + destinations: [{ type: "assistant", assistantId: "intake-stage-1" }], }, }; @@ -298,7 +298,7 @@ test("updateToolAssistantRefs: skips the PATCH and warns when the referenced ass (arg) => typeof arg === "string" && arg.includes("router") && - arg.includes("clinical-stage-1"), + arg.includes("intake-stage-1"), ), ), "a warning naming the tool and the unresolved slug should be logged",