Skip to content

Commit 3afa63e

Browse files
bloveclaude
andauthored
feat(ci): working aimock drift detection (#846)
* docs: design spec for working aimock drift detection Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: implementation plan for aimock drift detection Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(examples-chat): record mode for the aimock e2e runner * feat(examples-chat): AIMOCK_MODE=record wiring in e2e global-setup * feat(examples-chat): tag the contract-only @drift e2e subset * feat(examples-chat): structural fixture differ replacing the broken drift script * feat(ci): weekly aimock drift run — record-proxy e2e subset + structural diff * feat(examples-chat): differ reports incomplete recordings separately from drift The aimock recorder emits { content: '' } with a 'fixture may be incomplete' warning when it cannot parse tool-call deltas from a stream. Observed live: both tool-calling entries in the first record run collapsed to empty text while the @drift specs passed. A recorder artifact, not model drift — reported in its own category so it cannot masquerade as a kind change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 6c0685b commit 3afa63e

12 files changed

Lines changed: 922 additions & 98 deletions

.github/workflows/aimock-drift.yml

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ name: aimock fixture drift
22

33
on:
44
workflow_dispatch:
5-
# NOTE: scheduled cron is intentionally omitted. The committed fixtures are
6-
# handwritten seeds — drift detection only becomes meaningful once a phase
7-
# lands recorded fixtures. Re-enable a `schedule:` trigger here at that
8-
# point (weekly cron is a reasonable starting cadence).
5+
schedule:
6+
# Weekly, Monday 09:00 UTC. Advisory only — never a merge gate. The @drift
7+
# e2e subset runs against the LIVE provider through aimock's record proxy;
8+
# a red run means today's model no longer satisfies our contract
9+
# assertions. Recordings are uploaded as an artifact either way.
10+
- cron: '0 9 * * 1'
911

1012
concurrency:
1113
group: ${{ github.workflow }}-${{ github.ref }}
@@ -27,37 +29,61 @@ jobs:
2729
with:
2830
node-version: 22
2931
cache: npm
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
3034
- run: npm ci
31-
- name: Run drift check
35+
- name: Cache examples-chat python venv
36+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
37+
with:
38+
path: examples/chat/python/.venv
39+
key: uv-examples-chat-${{ runner.os }}-${{ hashFiles('examples/chat/python/uv.lock') }}
40+
- name: Sync examples-chat python
41+
working-directory: examples/chat/python
42+
run: uv sync
43+
- run: npx playwright install --with-deps chromium
44+
- name: Run @drift subset against the live provider
45+
id: drift-run
3246
env:
3347
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
34-
run: npx nx run examples-chat-angular:drift --skip-nx-cache
48+
AIMOCK_MODE: record
49+
AIMOCK_RECORD_DIR: ${{ runner.temp }}/recordings
50+
working-directory: examples/chat/angular
51+
run: npx playwright test --config e2e/playwright.config.ts --grep @drift
52+
- name: Structural diff vs committed fixtures
53+
if: always()
54+
working-directory: examples/chat/angular
55+
run: |
56+
mkdir -p "${{ runner.temp }}/recordings"
57+
npx tsx e2e/scripts/drift.ts "${{ runner.temp }}/recordings" | tee "${{ runner.temp }}/drift-report.json"
58+
- name: Upload recordings artifact
59+
if: always()
60+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
61+
with:
62+
name: aimock-recordings
63+
path: ${{ runner.temp }}/recordings
64+
if-no-files-found: warn
3565
- name: Open issue on drift
3666
if: failure()
3767
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
3868
with:
3969
script: |
70+
const fs = require('fs');
4071
const { owner, repo } = context.repo;
41-
const today = new Date().toISOString().slice(0, 10);
42-
const title = `aimock fixture drift detected — ${today}`;
43-
const body = [
44-
'The scheduled fixture drift check failed.',
45-
'',
46-
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
47-
'',
48-
'Investigate which fixture drifted and either refresh it intentionally',
49-
'or open a PR fixing the regression on the LLM side.',
50-
].join('\n');
51-
const existing = await github.rest.issues.listForRepo({
52-
owner, repo, labels: 'aimock-drift', state: 'open',
72+
let report = '(drift report unavailable)';
73+
try { report = fs.readFileSync(process.env.RUNNER_TEMP + '/drift-report.json', 'utf8'); } catch {}
74+
const trigger = context.eventName === 'schedule' ? 'scheduled' : 'manually dispatched';
75+
await github.rest.issues.create({
76+
owner, repo,
77+
title: 'aimock drift: @drift subset failed against the live provider',
78+
body: [
79+
`The ${trigger} fixture drift check failed.`,
80+
'',
81+
`Run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
82+
'',
83+
'Structural diff of recordings vs committed fixtures:',
84+
'```json',
85+
report.slice(0, 6000),
86+
'```',
87+
].join('\n'),
88+
labels: ['drift'],
5389
});
54-
if (existing.data.length === 0) {
55-
await github.rest.issues.create({
56-
owner, repo, title, body, labels: ['aimock-drift'],
57-
});
58-
} else {
59-
await github.rest.issues.createComment({
60-
owner, repo, issue_number: existing.data[0].number,
61-
body: `Drift again on ${today}. ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
62-
});
63-
}

0 commit comments

Comments
 (0)