mixdog 0.9.49 → 0.9.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -152,12 +152,12 @@ current Anthropic models.
|
|
|
152
152
|
## Scripts
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
|
-
npm run smoke
|
|
156
|
-
npm run smoke:all
|
|
157
|
-
npm run smoke:tui
|
|
158
|
-
npm run
|
|
159
|
-
npm run build:tui
|
|
160
|
-
npm run audit:models
|
|
155
|
+
npm run smoke # fast core feature smoke
|
|
156
|
+
npm run smoke:all # feature-surface smoke suite
|
|
157
|
+
npm run smoke:tui # TUI feature smoke
|
|
158
|
+
npm run test:tool-contracts # optional tool contract suite
|
|
159
|
+
npm run build:tui # build the bundled Ink TUI
|
|
160
|
+
npm run audit:models # inspect model catalog metadata
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
Additional diagnostics and benchmarks live under `scripts/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"prepack": "npm run build:tui",
|
|
37
37
|
"prepublishOnly": "node -e \"if(!process.env.CI){console.error('local npm publish is disabled — run npm run release:patch');process.exit(1)}\"",
|
|
38
38
|
"start": "node src/cli.mjs",
|
|
39
|
-
"smoke": "node scripts/smoke.mjs
|
|
40
|
-
"smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:
|
|
39
|
+
"smoke": "node scripts/smoke.mjs",
|
|
40
|
+
"smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:patch && npm run smoke:output && npm run smoke:tui && npm run smoke:live-worker",
|
|
41
41
|
"smoke:boot": "node scripts/boot-smoke.mjs",
|
|
42
42
|
"smoke:compact": "node scripts/compact-smoke.mjs",
|
|
43
43
|
"smoke:loop": "node scripts/smoke-loop.mjs",
|
|
44
|
-
"smoke:loop:final": "node scripts/smoke-loop-report.mjs --require-complete --min-elapsed 5h --min-iterations 400 --max-gap 60s --max-smoke-ms 10000 --max-avg-smoke-ms 8500 --max-step-ms
|
|
44
|
+
"smoke:loop:final": "node scripts/smoke-loop-report.mjs --require-complete --min-elapsed 5h --min-iterations 400 --max-gap 60s --max-smoke-ms 10000 --max-avg-smoke-ms 8500 --max-step-ms smoke.mjs=4000 --max-step-ms boot-smoke.mjs=8000 --max-rss-mb 140 --max-rss-growth-mb 50",
|
|
45
45
|
"smoke:loop:report": "node scripts/smoke-loop-report.mjs",
|
|
46
|
-
"
|
|
46
|
+
"test:tool-contracts": "node scripts/tool-smoke.mjs",
|
|
47
47
|
"smoke:patch": "node scripts/apply-patch-edit-smoke.mjs",
|
|
48
48
|
"smoke:output": "node scripts/output-style-smoke.mjs",
|
|
49
49
|
"smoke:tui": "node scripts/tui-render-smoke.mjs && npm run test:tui-input-render && npm run test:tui-streaming-window && npm run test:tui-queue",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
"test:anthropic-oauth-race": "node --test scripts/anthropic-oauth-refresh-race-test.mjs",
|
|
63
63
|
"test:grok-oauth-race": "node --test scripts/grok-oauth-refresh-race-test.mjs",
|
|
64
64
|
"test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
|
|
65
|
-
"test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/
|
|
65
|
+
"test:memory-routing": "node --test scripts/memory-cycle-routing-test.mjs scripts/maintenance-default-routes-test.mjs scripts/embedding-worker-exit-test.mjs",
|
|
66
66
|
"test:code-graph-dispatch": "node --test scripts/code-graph-dispatch-test.mjs",
|
|
67
67
|
"test:code-graph-clean-cache": "node --test scripts/code-graph-dispatch-test.mjs",
|
|
68
68
|
"test:tui-queue": "node --test scripts/submit-commandbusy-race-test.mjs scripts/steering-drain-buckets-test.mjs scripts/abort-recovery-test.mjs scripts/execution-pending-resume-kick-test.mjs scripts/execution-resume-esc-integration-test.mjs",
|
|
69
69
|
"test:tui-input-render": "node --test scripts/prompt-immediate-render-test.mjs",
|
|
70
70
|
"test:tui-streaming-window": "node --test scripts/streaming-tail-window-test.mjs",
|
|
71
71
|
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/verify-release-assets-test.mjs && node --test scripts/verify-release-assets-test.mjs",
|
|
72
|
-
"test:release-focused": "npm run test:release-assets && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact &&
|
|
72
|
+
"test:release-focused": "npm run test:release-assets && npm run test:patch-binary-cache && npm run test:providers && npm run test:deferred-tools && npm run smoke:compact && node --test scripts/code-graph-aggregate-cwd-test.mjs && npm run test:code-graph-dispatch && node --test scripts/code-graph-disk-hit-test.mjs && npm run test:shellhardening && node --test scripts/windows-hide-spawn-options-test.mjs && node --test scripts/tui-transcript-perf-test.mjs",
|
|
73
73
|
"test:native-edit-wire": "node --test scripts/native-edit-wire-test.mjs",
|
|
74
74
|
"test:patch-binary-cache": "node --test scripts/patch-binary-cache-test.mjs",
|
|
75
75
|
"test:session": "node --test scripts/session-orphan-sweep-test.mjs",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const providerUrl = new URL('../src/runtime/memory/lib/embedding-provider.mjs', import.meta.url)
|
|
9
|
+
const CHILD_DEADLINE_MS = 3_000
|
|
10
|
+
|
|
11
|
+
async function createExitWorkerFixture() {
|
|
12
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'mixdog-embed-worker-exit-'))
|
|
13
|
+
const workerPath = join(tempDir, 'exit-before-reply.mjs')
|
|
14
|
+
await writeFile(workerPath, `
|
|
15
|
+
import { parentPort } from 'node:worker_threads'
|
|
16
|
+
parentPort.once('message', () => process.exit(0))
|
|
17
|
+
`)
|
|
18
|
+
const source = await readFile(providerUrl, 'utf8')
|
|
19
|
+
const instrumented = source
|
|
20
|
+
.replace("'./model-profile.mjs'", JSON.stringify(new URL('../src/runtime/memory/lib/model-profile.mjs', import.meta.url).href))
|
|
21
|
+
.replace("'./embedding-model-config.mjs'", JSON.stringify(new URL('../src/runtime/memory/lib/embedding-model-config.mjs', import.meta.url).href))
|
|
22
|
+
.replace(/^const WORKER_PATH = .*$/m, `const WORKER_PATH = ${JSON.stringify(workerPath)}`)
|
|
23
|
+
const providerModule = `data:text/javascript;base64,${Buffer.from(instrumented).toString('base64')}#${Date.now()}`
|
|
24
|
+
const fixturePath = join(tempDir, 'run-exit-test.mjs')
|
|
25
|
+
await writeFile(fixturePath, `
|
|
26
|
+
import { embedText } from ${JSON.stringify(providerModule)}
|
|
27
|
+
const started = Date.now()
|
|
28
|
+
const settled = await Promise.allSettled([embedText('first'), embedText('second')])
|
|
29
|
+
if (Date.now() - started >= 2_000) throw new Error('worker exit did not reject pending embeds promptly')
|
|
30
|
+
if (settled.some(({ status }) => status !== 'rejected')) throw new Error('worker exit resolved a pending embed')
|
|
31
|
+
for (const result of settled) {
|
|
32
|
+
if (result.reason?.message !== 'Worker exited with code 0') throw result.reason
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write('ok')
|
|
35
|
+
`)
|
|
36
|
+
return { fixturePath, tempDir }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runFixture(fixturePath) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(process.execPath, [fixturePath], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
42
|
+
let stdout = ''
|
|
43
|
+
let stderr = ''
|
|
44
|
+
let timedOut = false
|
|
45
|
+
const deadline = setTimeout(() => {
|
|
46
|
+
timedOut = true
|
|
47
|
+
child.kill()
|
|
48
|
+
}, CHILD_DEADLINE_MS)
|
|
49
|
+
child.stdout.on('data', (chunk) => { stdout += chunk })
|
|
50
|
+
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
51
|
+
child.once('error', (error) => {
|
|
52
|
+
clearTimeout(deadline)
|
|
53
|
+
reject(error)
|
|
54
|
+
})
|
|
55
|
+
child.once('close', (code, signal) => {
|
|
56
|
+
clearTimeout(deadline)
|
|
57
|
+
if (timedOut) {
|
|
58
|
+
reject(new Error(`exit-worker fixture exceeded ${CHILD_DEADLINE_MS}ms and was killed`))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
resolve({ code, signal, stdout, stderr })
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('code-zero worker exit before reply promptly rejects every pending embed', async () => {
|
|
67
|
+
const { fixturePath, tempDir } = await createExitWorkerFixture()
|
|
68
|
+
try {
|
|
69
|
+
const result = await runFixture(fixturePath)
|
|
70
|
+
assert.equal(result.code, 0, result.stderr)
|
|
71
|
+
assert.equal(result.signal, null, result.stderr)
|
|
72
|
+
assert.equal(result.stdout, 'ok')
|
|
73
|
+
} finally {
|
|
74
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
75
|
+
}
|
|
76
|
+
})
|
package/scripts/smoke-loop.mjs
CHANGED
|
@@ -58,7 +58,7 @@ function runNode(args, label, timeoutMs = 180_000) {
|
|
|
58
58
|
function runSmokeAll(iteration) {
|
|
59
59
|
let totalMs = 0;
|
|
60
60
|
const steps = [];
|
|
61
|
-
for (const script of ['scripts/
|
|
61
|
+
for (const script of ['scripts/smoke.mjs', 'scripts/boot-smoke.mjs']) {
|
|
62
62
|
const result = runNode([script], `${script} iteration ${iteration}`);
|
|
63
63
|
totalMs += result.ms;
|
|
64
64
|
steps.push({
|
|
@@ -161,8 +161,14 @@ while (Date.now() < deadline && iteration < maxIterations) {
|
|
|
161
161
|
const iterStartedAt = Date.now();
|
|
162
162
|
const smoke = runSmokeAll(iteration);
|
|
163
163
|
smokeTimes.push(smoke.ms);
|
|
164
|
-
const failure = runNode(['scripts/tool-failures.mjs', '--since', since, '--limit', '1'], `failures iteration ${iteration}`, 60_000);
|
|
165
|
-
|
|
164
|
+
const failure = runNode(['scripts/tool-failures.mjs', '--since', since, '--limit', '1', '--json'], `failures iteration ${iteration}`, 60_000);
|
|
165
|
+
let failureSummary;
|
|
166
|
+
try {
|
|
167
|
+
failureSummary = JSON.parse(failure.stdout);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(`tool failures returned malformed JSON:\n${failure.stdout}`);
|
|
170
|
+
}
|
|
171
|
+
if (failureSummary?.actionable_failures?.matched !== 0) {
|
|
166
172
|
throw new Error(`tool failures appeared after loop start:\n${failure.stdout}`);
|
|
167
173
|
}
|
|
168
174
|
const currentRss = rssMb();
|
package/scripts/smoke.mjs
CHANGED
|
@@ -19,120 +19,19 @@ function runNode(args, label, options = {}) {
|
|
|
19
19
|
return child;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
const isolatedStatuslineEnv = {
|
|
24
|
-
MIXDOG_DATA_DIR: resolve(root, '.mixdog-smoke-data-empty'),
|
|
25
|
-
MIXDOG_HOME: resolve(root, '.mixdog-smoke-home-empty'),
|
|
26
|
-
MIXDOG_CONFIG_DIR: resolve(root, '.mixdog-smoke-config-empty'),
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
if (!child.stdout.includes('standalone mixdog CLI/TUI coding agent')) {
|
|
30
|
-
process.stderr.write(`unexpected help output:\n${child.stdout}`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
22
|
+
runNode(['src/cli.mjs', '--help'], 'help smoke');
|
|
33
23
|
|
|
34
24
|
runNode(['--input-type=module', '-e', `
|
|
35
25
|
const mod = await import('./src/tui/dist/index.mjs');
|
|
36
26
|
if (typeof mod.runTui !== 'function') throw new Error('runTui export missing');
|
|
37
27
|
`], 'tui bundle import smoke');
|
|
38
28
|
|
|
39
|
-
runNode(['--
|
|
40
|
-
|
|
41
|
-
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
42
|
-
const line = await renderStatusline({
|
|
43
|
-
provider: 'openai',
|
|
44
|
-
model: 'gpt-5.5',
|
|
45
|
-
contextWindow: 1000000,
|
|
46
|
-
stats: { currentContextTokens: 999 },
|
|
47
|
-
});
|
|
48
|
-
if (/▓/.test(line) || !line.includes('░') || !line.includes('0.1%')) throw new Error('sub-1% context bar should stay empty: ' + JSON.stringify(line));
|
|
49
|
-
`], 'statusline sub-percent context smoke', { env: isolatedStatuslineEnv });
|
|
50
|
-
|
|
51
|
-
runNode(['--input-type=module', '-e', `
|
|
52
|
-
process.stdout.columns = 120;
|
|
53
|
-
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
54
|
-
const line = await renderStatusline({
|
|
55
|
-
provider: 'openai',
|
|
56
|
-
model: 'gpt-5.5',
|
|
57
|
-
contextWindow: 950000,
|
|
58
|
-
rawContextWindow: 1000000,
|
|
59
|
-
stats: { currentContextTokens: 900000 },
|
|
60
|
-
});
|
|
61
|
-
if (!line.includes('94%') || line.includes('100%')) throw new Error('statusline context% should use effective compact capacity, not the 90% auto-compact trigger: ' + JSON.stringify(line));
|
|
62
|
-
`], 'statusline compact-capacity context smoke', { env: isolatedStatuslineEnv });
|
|
63
|
-
|
|
64
|
-
runNode(['--input-type=module', '-e', `
|
|
65
|
-
process.stdout.columns = 120;
|
|
66
|
-
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
67
|
-
const line = await renderStatusline({
|
|
68
|
-
provider: 'openai',
|
|
69
|
-
model: 'gpt-5.5',
|
|
70
|
-
contextWindow: 950000,
|
|
71
|
-
stats: { currentContextTokens: 350000, currentContextSource: 'estimated' },
|
|
72
|
-
});
|
|
73
|
-
if (line.includes('37%') || !line.includes('0%')) throw new Error('statusline must not show local estimated context as session usage: ' + JSON.stringify(line));
|
|
74
|
-
`], 'statusline estimated-context isolation smoke', { env: isolatedStatuslineEnv });
|
|
75
|
-
|
|
76
|
-
runNode(['--input-type=module', '-e', `
|
|
77
|
-
process.stdout.columns = 120;
|
|
78
|
-
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
79
|
-
const line = await renderStatusline({
|
|
80
|
-
provider: 'openai',
|
|
81
|
-
model: 'gpt-5.5',
|
|
82
|
-
contextWindow: 950000,
|
|
83
|
-
stats: { currentContextTokens: 360000, currentContextSource: 'post_compact_estimate' },
|
|
84
|
-
});
|
|
85
|
-
if (!line.includes('37%')) throw new Error('statusline must show post-compact estimated context after stale API usage: ' + JSON.stringify(line));
|
|
86
|
-
`], 'statusline post-compact context smoke', { env: isolatedStatuslineEnv });
|
|
87
|
-
|
|
88
|
-
runNode(['--input-type=module', '-e', `
|
|
89
|
-
process.stdout.columns = 120;
|
|
90
|
-
const { renderStatusline } = await import('./src/ui/statusline.mjs');
|
|
91
|
-
const line = await renderStatusline({
|
|
92
|
-
provider: 'openai',
|
|
93
|
-
model: 'gpt-5.5',
|
|
94
|
-
contextWindow: 950000,
|
|
95
|
-
stats: { currentContextTokens: 0 },
|
|
96
|
-
agentJobs: [{ task_id: 'task_statusline_smoke', status: 'running', tag: 'bench-agent', startedAt: new Date().toISOString() }],
|
|
97
|
-
});
|
|
98
|
-
if (!line.includes('Running 1 Agent') || line.includes('bench-agent')) throw new Error('statusline must render live agent count without task tags: ' + JSON.stringify(line));
|
|
99
|
-
`], 'statusline live agent task smoke', { env: isolatedStatuslineEnv });
|
|
100
|
-
|
|
101
|
-
runNode(['--input-type=module', '-e', `
|
|
102
|
-
const { mkdtempSync, mkdirSync, writeFileSync } = await import('node:fs');
|
|
103
|
-
const { tmpdir } = await import('node:os');
|
|
104
|
-
const { join } = await import('node:path');
|
|
105
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-status-smoke-'));
|
|
106
|
-
mkdirSync(join(root, 'runtime'), { recursive: true });
|
|
107
|
-
writeFileSync(join(root, 'runtime', 'active-instance.json'), JSON.stringify({
|
|
108
|
-
gateway_port: 3468,
|
|
109
|
-
gateway_server_pid: process.pid,
|
|
110
|
-
gateway_provider: 'openai',
|
|
111
|
-
gateway_model: 'gpt-5.5',
|
|
112
|
-
gateway_context_window: 950000,
|
|
113
|
-
gateway_raw_context_window: 1000000,
|
|
114
|
-
gateway_auto_compact_token_limit: 950000,
|
|
115
|
-
gateway_context_used_pct: 37,
|
|
116
|
-
gateway_cc_session_id: 'other-session',
|
|
117
|
-
gateway_updated_at: Date.now()
|
|
118
|
-
}));
|
|
119
|
-
process.env.MIXDOG_RUNTIME_ROOT = join(root, 'runtime');
|
|
120
|
-
const { loadGatewayStatus } = await import('./src/vendor/statusline/bin/statusline-route.mjs');
|
|
121
|
-
const status = loadGatewayStatus({ sessionId: 'fresh-session', clientHostPid: process.pid, activeContextTokens: 0 });
|
|
122
|
-
if (status?.contextUsedPct === 37) throw new Error('statusline leaked active-instance context from another session');
|
|
123
|
-
`], 'statusline session metrics isolation smoke', { env: isolatedStatuslineEnv });
|
|
124
|
-
|
|
125
|
-
const boot = spawnSync(process.execPath, ['src/cli.mjs', '--help'], {
|
|
126
|
-
cwd: root,
|
|
127
|
-
env: { ...process.env, MIXDOG_BOOT_PROFILE: '1' },
|
|
128
|
-
encoding: 'utf8',
|
|
129
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
-
timeout: 10000,
|
|
29
|
+
const boot = runNode(['src/cli.mjs', '--help'], 'boot profile smoke', {
|
|
30
|
+
env: { MIXDOG_BOOT_PROFILE: '1' },
|
|
131
31
|
});
|
|
132
|
-
|
|
133
|
-
if (boot.status !== 0 || !boot.stderr.includes('[mixdog-boot]') || !boot.stderr.includes('app:run:start')) {
|
|
32
|
+
if (!boot.stderr.includes('[mixdog-boot]')) {
|
|
134
33
|
process.stderr.write(boot.stderr || boot.stdout || 'boot profile smoke failed\n');
|
|
135
|
-
process.exit(
|
|
34
|
+
process.exit(1);
|
|
136
35
|
}
|
|
137
36
|
|
|
138
37
|
process.stdout.write('smoke passed ✓\n');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { mkdtemp,
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
@@ -59,303 +59,6 @@ function graphFixture() {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function yamlScalar(value) {
|
|
63
|
-
const trimmed = value.trim();
|
|
64
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
65
|
-
return trimmed.slice(1, -1).replaceAll("''", "'");
|
|
66
|
-
}
|
|
67
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
68
|
-
return JSON.parse(trimmed);
|
|
69
|
-
}
|
|
70
|
-
return trimmed.replace(/\s+#.*$/, '').trim();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function yamlBlockEnd(lines, start, indent) {
|
|
74
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
75
|
-
const text = lines[index];
|
|
76
|
-
if (/^\s*(?:#.*)?$/.test(text)) continue;
|
|
77
|
-
if (text.match(/^\s*/)[0].length <= indent) return index;
|
|
78
|
-
}
|
|
79
|
-
return lines.length;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function yamlMappingEntry(text, indent) {
|
|
83
|
-
if (text.match(/^\s*/)[0].length !== indent) return undefined;
|
|
84
|
-
const match = text.slice(indent).match(/^("(?:\\.|[^"])*"|'(?:''|[^'])*'|[A-Za-z0-9_-]+):\s*(.*)$/);
|
|
85
|
-
if (!match) return undefined;
|
|
86
|
-
return { key: yamlScalar(match[1]), rawValue: match[2] };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function yamlChildren(lines, start, end, parentIndent) {
|
|
90
|
-
let childIndent;
|
|
91
|
-
const children = [];
|
|
92
|
-
for (let index = start + 1; index < end; index += 1) {
|
|
93
|
-
const text = lines[index];
|
|
94
|
-
if (/^\s*(?:#.*)?$/.test(text)) continue;
|
|
95
|
-
const indent = text.match(/^\s*/)[0].length;
|
|
96
|
-
if (indent <= parentIndent) break;
|
|
97
|
-
childIndent ??= indent;
|
|
98
|
-
if (indent !== childIndent) continue;
|
|
99
|
-
const entry = yamlMappingEntry(text, indent);
|
|
100
|
-
if (entry) children.push({
|
|
101
|
-
...entry,
|
|
102
|
-
index,
|
|
103
|
-
indent,
|
|
104
|
-
end: yamlBlockEnd(lines, index, indent),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return children;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function yamlEnvironment(lines, parent) {
|
|
111
|
-
const env = {};
|
|
112
|
-
for (const entry of yamlChildren(lines, parent.index, parent.end, parent.indent)) {
|
|
113
|
-
env[entry.key] = yamlScalar(entry.rawValue);
|
|
114
|
-
}
|
|
115
|
-
return env;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function yamlSteps(lines, parent) {
|
|
119
|
-
const itemIndexes = [];
|
|
120
|
-
let itemIndent;
|
|
121
|
-
for (let index = parent.index + 1; index < parent.end; index += 1) {
|
|
122
|
-
const match = lines[index].match(/^(\s*)-\s+(.*)$/);
|
|
123
|
-
if (!match || /^\s*(?:#.*)?$/.test(lines[index])) continue;
|
|
124
|
-
const indent = match[1].length;
|
|
125
|
-
if (itemIndent === undefined) itemIndent = indent;
|
|
126
|
-
if (indent === itemIndent) itemIndexes.push(index);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return itemIndexes.map((start, position) => {
|
|
130
|
-
const end = itemIndexes[position + 1] ?? parent.end;
|
|
131
|
-
const fieldIndent = itemIndent + 2;
|
|
132
|
-
const fields = { env: {} };
|
|
133
|
-
for (let index = start; index < end; index += 1) {
|
|
134
|
-
const source = index === start
|
|
135
|
-
? lines[index].replace(/^\s*-\s+/, '')
|
|
136
|
-
: lines[index].slice(fieldIndent);
|
|
137
|
-
if (index !== start && lines[index].match(/^\s*/)[0].length !== fieldIndent) continue;
|
|
138
|
-
const field = source.match(/^(name|run|uses|env):\s*(.*)$/);
|
|
139
|
-
if (!field) continue;
|
|
140
|
-
const [, key, rawValue] = field;
|
|
141
|
-
if (key === 'env' && /^(?:#.*)?$/.test(rawValue)) {
|
|
142
|
-
fields.env = yamlEnvironment(lines, {
|
|
143
|
-
index,
|
|
144
|
-
indent: fieldIndent,
|
|
145
|
-
end: yamlBlockEnd(lines, index, fieldIndent),
|
|
146
|
-
});
|
|
147
|
-
} else if (key === 'run' && /^[|>][-+0-9]*\s*(?:#.*)?$/.test(rawValue)) {
|
|
148
|
-
const commands = [];
|
|
149
|
-
for (let blockIndex = index + 1; blockIndex < end; blockIndex += 1) {
|
|
150
|
-
const blockLine = lines[blockIndex];
|
|
151
|
-
if (blockLine.trim() === '' || /^\s*#/.test(blockLine)) continue;
|
|
152
|
-
if (blockLine.match(/^\s*/)[0].length <= fieldIndent) break;
|
|
153
|
-
commands.push(blockLine.trim());
|
|
154
|
-
}
|
|
155
|
-
fields.run = commands.join('\n');
|
|
156
|
-
} else {
|
|
157
|
-
fields[key] = yamlScalar(rawValue);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return fields;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function parseWorkflowSubset(workflow) {
|
|
165
|
-
const lines = workflow.replaceAll('\r\n', '\n').split('\n');
|
|
166
|
-
const roots = lines
|
|
167
|
-
.map((line, index) => ({ line, index }))
|
|
168
|
-
.filter(({ line }) => yamlMappingEntry(line, 0)?.key === 'jobs');
|
|
169
|
-
assert.equal(roots.length, 1, 'workflow must define exactly one jobs mapping');
|
|
170
|
-
const jobsBlock = {
|
|
171
|
-
index: roots[0].index,
|
|
172
|
-
indent: 0,
|
|
173
|
-
end: yamlBlockEnd(lines, roots[0].index, 0),
|
|
174
|
-
};
|
|
175
|
-
const jobs = yamlChildren(lines, jobsBlock.index, jobsBlock.end, jobsBlock.indent).map((entry) => {
|
|
176
|
-
const children = yamlChildren(lines, entry.index, entry.end, entry.indent);
|
|
177
|
-
const envEntry = children.find((child) => child.key === 'env');
|
|
178
|
-
const stepsEntry = children.find((child) => child.key === 'steps');
|
|
179
|
-
const strategyEntry = children.find((child) => child.key === 'strategy');
|
|
180
|
-
const matrixEntry = strategyEntry
|
|
181
|
-
? yamlChildren(lines, strategyEntry.index, strategyEntry.end, strategyEntry.indent)
|
|
182
|
-
.find((child) => child.key === 'matrix')
|
|
183
|
-
: undefined;
|
|
184
|
-
const matrixLines = matrixEntry
|
|
185
|
-
? lines.slice(matrixEntry.index + 1, matrixEntry.end)
|
|
186
|
-
.filter((line) => !/^\s*(?:#.*)?$/.test(line))
|
|
187
|
-
.map((line) => line.trim())
|
|
188
|
-
: [];
|
|
189
|
-
const includeEntry = matrixEntry
|
|
190
|
-
? yamlChildren(lines, matrixEntry.index, matrixEntry.end, matrixEntry.indent)
|
|
191
|
-
.find((child) => child.key === 'include')
|
|
192
|
-
: undefined;
|
|
193
|
-
const matrixInclude = includeEntry
|
|
194
|
-
? lines.slice(includeEntry.index + 1, includeEntry.end)
|
|
195
|
-
.filter((line) => /^\s*-\s+/.test(line) && !/^\s*-\s*#/.test(line))
|
|
196
|
-
.map((line) => line.replace(/^\s*-\s+/, '').trim())
|
|
197
|
-
: [];
|
|
198
|
-
return {
|
|
199
|
-
name: entry.key,
|
|
200
|
-
env: envEntry ? yamlEnvironment(lines, envEntry) : {},
|
|
201
|
-
matrix: matrixEntry ? { lines: matrixLines, include: matrixInclude } : undefined,
|
|
202
|
-
steps: stepsEntry ? yamlSteps(lines, stepsEntry) : [],
|
|
203
|
-
};
|
|
204
|
-
});
|
|
205
|
-
return { jobs };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function workflowJob(workflow, name) {
|
|
209
|
-
const matches = parseWorkflowSubset(workflow).jobs.filter((job) => job.name === name);
|
|
210
|
-
assert.equal(matches.length, 1, `workflow must define exactly one jobs.${name}`);
|
|
211
|
-
return matches[0];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function namedStep(job, name) {
|
|
215
|
-
const matches = job.steps.filter((step) => step.name === name);
|
|
216
|
-
assert.equal(matches.length, 1, `jobs.${job.name} must contain exactly one "${name}" step`);
|
|
217
|
-
return matches[0];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function extractReleaseSteps(workflow) {
|
|
221
|
-
return workflowJob(workflow, 'release').steps;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function runLines(step) {
|
|
225
|
-
return step.run?.split('\n') ?? [];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function assertRunLine(step, line, message) {
|
|
229
|
-
assert.ok(runLines(step).includes(line), message);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function matrixRowsWithKey(matrix, key) {
|
|
233
|
-
return matrix.include.filter((row) => {
|
|
234
|
-
const fields = row.match(/^\{(.*)\}$/)?.[1] ?? row;
|
|
235
|
-
return new RegExp(`(?:^|,)\\s*${key}\\s*:`).test(fields);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function effectiveStepEnv(job, step, key) {
|
|
240
|
-
if (Object.hasOwn(step.env, key)) return step.env[key];
|
|
241
|
-
return job.env[key];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function cargoBuildCommands(step) {
|
|
245
|
-
return runLines(step).filter((line) => /\bcargo\s+build(?:\s|$)/.test(line));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function assertRepeatedCommand(commands, expected, count, message) {
|
|
249
|
-
assert.equal(commands.length, count, message);
|
|
250
|
-
for (const command of commands) {
|
|
251
|
-
assert.equal(command, expected, message);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function assertProvenanceDigestLinkage(step) {
|
|
256
|
-
for (const line of [
|
|
257
|
-
'FIRST=$(sha256 "_asset/$ASSET")',
|
|
258
|
-
'SECOND=$(sha256 "$BIN")',
|
|
259
|
-
'cmp "_asset/$ASSET" "$BIN"',
|
|
260
|
-
'test "$FIRST" = "$SECOND"',
|
|
261
|
-
'ASSET_DIGEST=$FIRST \\',
|
|
262
|
-
'assetDigest: process.env.ASSET_DIGEST,',
|
|
263
|
-
]) {
|
|
264
|
-
assertRunLine(step, line, `reproducibility step must preserve digest/provenance linkage: ${line}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function assertMatrixRustflags(job) {
|
|
269
|
-
assert.ok(job.matrix, 'jobs.build must define a strategy matrix');
|
|
270
|
-
assert.ok(job.matrix.include.length > 0, 'jobs.build matrix must define include rows');
|
|
271
|
-
assert.equal(
|
|
272
|
-
matrixRowsWithKey(job.matrix, 'rustflags').length,
|
|
273
|
-
job.matrix.include.length,
|
|
274
|
-
'every jobs.build matrix include row must define rustflags',
|
|
275
|
-
);
|
|
276
|
-
assert.equal(
|
|
277
|
-
job.env.RUSTFLAGS,
|
|
278
|
-
'${{ matrix.rustflags }}',
|
|
279
|
-
'jobs.build must set RUSTFLAGS from matrix.rustflags',
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function assertProtocolStep(job) {
|
|
284
|
-
const protocol = namedStep(job, 'Test protocol');
|
|
285
|
-
assert.equal(
|
|
286
|
-
protocol.env.RUSTFLAGS,
|
|
287
|
-
'',
|
|
288
|
-
'protocol tests must explicitly clear RUSTFLAGS for loadable host proc-macro dylibs',
|
|
289
|
-
);
|
|
290
|
-
assertRunLine(
|
|
291
|
-
protocol,
|
|
292
|
-
'cargo test --locked --manifest-path native/mixdog-graph/Cargo.toml',
|
|
293
|
-
'Test protocol step must own the locked graph protocol test command',
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function assertReproducibilityStep(job) {
|
|
298
|
-
const reproducibility = namedStep(job, 'Build twice and compare');
|
|
299
|
-
assert.equal(
|
|
300
|
-
effectiveStepEnv(job, reproducibility, 'RUSTFLAGS'),
|
|
301
|
-
'${{ matrix.rustflags }}',
|
|
302
|
-
'reproducibility builds must preserve matrix RUSTFLAGS',
|
|
303
|
-
);
|
|
304
|
-
assertRepeatedCommand(
|
|
305
|
-
cargoBuildCommands(reproducibility),
|
|
306
|
-
'cargo build --locked --release --target "${{ matrix.target }}" --manifest-path native/mixdog-graph/Cargo.toml',
|
|
307
|
-
2,
|
|
308
|
-
'Build twice and compare must own exactly two targeted locked release cargo build commands',
|
|
309
|
-
);
|
|
310
|
-
assertProvenanceDigestLinkage(reproducibility);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function assertGraphReleaseRustflagsContract(workflow) {
|
|
314
|
-
const build = workflowJob(workflow, 'build');
|
|
315
|
-
assertMatrixRustflags(build);
|
|
316
|
-
assertProtocolStep(build);
|
|
317
|
-
assertReproducibilityStep(build);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function assertReleaseWorkflowOrdering(workflow) {
|
|
321
|
-
const steps = extractReleaseSteps(workflow);
|
|
322
|
-
const required = [
|
|
323
|
-
['Verify bundled release assets', 'node scripts/verify-release-assets.mjs'],
|
|
324
|
-
['Install', 'npm ci'],
|
|
325
|
-
['Execute code graph from a clean cache', 'npm run test:code-graph-clean-cache'],
|
|
326
|
-
['Focused release regressions', 'npm run test:release-focused'],
|
|
327
|
-
['Smoke', 'npm run smoke'],
|
|
328
|
-
['Publish', 'npm publish --provenance --access public'],
|
|
329
|
-
];
|
|
330
|
-
const indexes = {};
|
|
331
|
-
for (const [name, command] of required) {
|
|
332
|
-
const matches = steps
|
|
333
|
-
.map((step, index) => ({ step, index }))
|
|
334
|
-
.filter(({ step }) => step.name === name);
|
|
335
|
-
assert.equal(matches.length, 1, `release workflow must contain exactly one "${name}" step`);
|
|
336
|
-
assert.ok(
|
|
337
|
-
matches[0].step.run?.split('\n').includes(command),
|
|
338
|
-
`"${name}" step must run ${command}`,
|
|
339
|
-
);
|
|
340
|
-
indexes[name] = matches[0].index;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const asset = indexes['Verify bundled release assets'];
|
|
344
|
-
const install = indexes.Install;
|
|
345
|
-
const cleanCache = indexes['Execute code graph from a clean cache'];
|
|
346
|
-
const focused = indexes['Focused release regressions'];
|
|
347
|
-
const smoke = indexes.Smoke;
|
|
348
|
-
const publish = indexes.Publish;
|
|
349
|
-
assert.ok(asset < install, 'release asset blocker must run before Install');
|
|
350
|
-
assert.ok(asset < cleanCache, 'release asset blocker must run before clean-cache graph execution');
|
|
351
|
-
assert.ok(install < cleanCache, 'Install must run before clean-cache graph execution');
|
|
352
|
-
assert.ok(cleanCache < focused, 'clean-cache graph execution must run before Focused release regressions');
|
|
353
|
-
assert.ok(focused < smoke, 'Focused release regressions must run before Smoke');
|
|
354
|
-
assert.ok(smoke < publish, 'Smoke must run before Publish');
|
|
355
|
-
assert.ok(asset < publish, 'release asset blocker must run before Publish');
|
|
356
|
-
assert.ok(cleanCache < publish, 'clean-cache graph execution must run before Publish');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
62
|
test('accepts independent strict patch, runtime, app, and graph versions', () => {
|
|
360
63
|
assert.equal(validatePatchManifest(patchFixture(), `[package]\nversion = "${VERSION}"\n`).version, VERSION);
|
|
361
64
|
assert.equal(validateRuntimeManifest(runtimeFixture()).release_tag, 'runtime-v1.2.3');
|
|
@@ -534,6 +237,13 @@ test('cancels immediately when a stream exceeds its declared size below the abso
|
|
|
534
237
|
|
|
535
238
|
test('full guard reads deterministic fixtures and downloads every declared asset', async () => {
|
|
536
239
|
const dir = await mkdtemp(join(tmpdir(), 'mixdog-release-assets-'));
|
|
240
|
+
const patch = patchFixture();
|
|
241
|
+
const runtime = runtimeFixture();
|
|
242
|
+
const graph = graphFixture();
|
|
243
|
+
const expectedUrls = new Set(
|
|
244
|
+
[...Object.values(patch.assets), ...Object.values(runtime.assets), ...Object.values(graph.assets)]
|
|
245
|
+
.map(({ url }) => url),
|
|
246
|
+
);
|
|
537
247
|
const paths = {
|
|
538
248
|
patchManifestPath: join(dir, 'patch.json'),
|
|
539
249
|
cargoPath: join(dir, 'Cargo.toml'),
|
|
@@ -542,21 +252,23 @@ test('full guard reads deterministic fixtures and downloads every declared asset
|
|
|
542
252
|
packagePath: join(dir, 'package.json'),
|
|
543
253
|
};
|
|
544
254
|
await Promise.all([
|
|
545
|
-
writeFile(paths.patchManifestPath, JSON.stringify(
|
|
255
|
+
writeFile(paths.patchManifestPath, JSON.stringify(patch)),
|
|
546
256
|
writeFile(paths.cargoPath, `[package]\nversion = "${VERSION}"\n`),
|
|
547
|
-
writeFile(paths.runtimeManifestPath, JSON.stringify(
|
|
548
|
-
writeFile(paths.graphManifestPath, JSON.stringify(
|
|
257
|
+
writeFile(paths.runtimeManifestPath, JSON.stringify(runtime)),
|
|
258
|
+
writeFile(paths.graphManifestPath, JSON.stringify(graph)),
|
|
549
259
|
writeFile(paths.packagePath, JSON.stringify({ version: APP_VERSION })),
|
|
550
260
|
]);
|
|
551
261
|
let downloads = 0;
|
|
262
|
+
const requestedUrls = [];
|
|
552
263
|
try {
|
|
553
264
|
await verifyReleaseAssets({
|
|
554
265
|
...paths,
|
|
555
266
|
downloadOptions: {
|
|
556
267
|
attempts: 1,
|
|
557
268
|
timeoutMs: 1000,
|
|
558
|
-
fetchImpl: async () => {
|
|
269
|
+
fetchImpl: async (url) => {
|
|
559
270
|
downloads += 1;
|
|
271
|
+
requestedUrls.push(url);
|
|
560
272
|
return new Response(bytes);
|
|
561
273
|
},
|
|
562
274
|
},
|
|
@@ -565,83 +277,5 @@ test('full guard reads deterministic fixtures and downloads every declared asset
|
|
|
565
277
|
await rm(dir, { recursive: true, force: true });
|
|
566
278
|
}
|
|
567
279
|
assert.equal(downloads, 15);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
test('release workflow orders the asset and clean-cache blockers before consumers', async () => {
|
|
571
|
-
const workflow = await readFile(new URL('../.github/workflows/release.yml', import.meta.url), 'utf8');
|
|
572
|
-
assertReleaseWorkflowOrdering(workflow);
|
|
573
|
-
|
|
574
|
-
const quotedNames = workflow
|
|
575
|
-
.replace('- name: Install', '- name: "Install"')
|
|
576
|
-
.replace('- name: Smoke', "- name: 'Smoke'");
|
|
577
|
-
assertReleaseWorkflowOrdering(quotedNames);
|
|
578
|
-
|
|
579
|
-
const wrongStep = workflow
|
|
580
|
-
.replace('run: npm run smoke', 'run: echo smoke')
|
|
581
|
-
.replace('run: npm run test:release-focused', 'run: npm run test:release-focused\n # npm run smoke');
|
|
582
|
-
assert.throws(() => assertReleaseWorkflowOrdering(wrongStep), /"Smoke" step must run npm run smoke/);
|
|
583
|
-
|
|
584
|
-
const otherJob = workflow
|
|
585
|
-
.replace('run: npm run smoke', 'run: echo smoke')
|
|
586
|
-
.replace(
|
|
587
|
-
'\n release:',
|
|
588
|
-
'\n decoy:\n steps:\n - name: Smoke\n run: npm run smoke\n release:',
|
|
589
|
-
);
|
|
590
|
-
assert.throws(() => assertReleaseWorkflowOrdering(otherJob), /"Smoke" step must run npm run smoke/);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test('graph release clears test RUSTFLAGS but keeps targeted reproducibility builds', async () => {
|
|
594
|
-
const workflow = await readFile(new URL('../.github/workflows/graph-release.yml', import.meta.url), 'utf8');
|
|
595
|
-
assertGraphReleaseRustflagsContract(workflow);
|
|
596
|
-
|
|
597
|
-
const quotedNames = workflow
|
|
598
|
-
.replace('- name: Test protocol', '- name: "Test protocol"')
|
|
599
|
-
.replace('- name: Build twice and compare', "- name: 'Build twice and compare'");
|
|
600
|
-
assertGraphReleaseRustflagsContract(quotedNames);
|
|
601
|
-
|
|
602
|
-
assert.throws(
|
|
603
|
-
() => assertGraphReleaseRustflagsContract(workflow.replace(' RUSTFLAGS: ""', ' RUSTFLAGS: "-C unsafe-for-host"')),
|
|
604
|
-
/protocol tests must explicitly clear RUSTFLAGS/,
|
|
605
|
-
);
|
|
606
|
-
assert.throws(
|
|
607
|
-
() => assertGraphReleaseRustflagsContract(workflow.replace('--release --target', '--release')),
|
|
608
|
-
/exactly two targeted locked release cargo build commands/,
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
const renamedBuild = workflow
|
|
612
|
-
.replace('\n build:\n', '\n renamed-build:\n')
|
|
613
|
-
.replace(
|
|
614
|
-
'\n gate:',
|
|
615
|
-
'\n decoy-build:\n env:\n RUSTFLAGS: ${{ matrix.rustflags }}\n'
|
|
616
|
-
+ ' steps:\n - name: Test protocol\n'
|
|
617
|
-
+ ' run: cargo test --locked --manifest-path native/mixdog-graph/Cargo.toml\n gate:',
|
|
618
|
-
);
|
|
619
|
-
assert.throws(
|
|
620
|
-
() => assertGraphReleaseRustflagsContract(renamedBuild),
|
|
621
|
-
/exactly one jobs\.build/,
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
const commentedBuild = workflow.replace(
|
|
625
|
-
' cargo build --locked --release --target "${{ matrix.target }}" --manifest-path native/mixdog-graph/Cargo.toml',
|
|
626
|
-
' # cargo build --locked --release --target "${{ matrix.target }}" --manifest-path native/mixdog-graph/Cargo.toml',
|
|
627
|
-
);
|
|
628
|
-
assert.throws(
|
|
629
|
-
() => assertGraphReleaseRustflagsContract(commentedBuild),
|
|
630
|
-
/exactly two targeted locked release cargo build commands/,
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
const commandInWrongStep = workflow
|
|
634
|
-
.replace(
|
|
635
|
-
' run: cargo test --locked --manifest-path native/mixdog-graph/Cargo.toml',
|
|
636
|
-
' run: echo protocol command moved',
|
|
637
|
-
)
|
|
638
|
-
.replace(
|
|
639
|
-
' test "$(git rev-parse HEAD)" = "${{ needs.gate.outputs.source_sha }}"',
|
|
640
|
-
' cargo test --locked --manifest-path native/mixdog-graph/Cargo.toml\n'
|
|
641
|
-
+ ' test "$(git rev-parse HEAD)" = "${{ needs.gate.outputs.source_sha }}"',
|
|
642
|
-
);
|
|
643
|
-
assert.throws(
|
|
644
|
-
() => assertGraphReleaseRustflagsContract(commandInWrongStep),
|
|
645
|
-
/Test protocol step must own/,
|
|
646
|
-
);
|
|
280
|
+
assert.deepEqual(new Set(requestedUrls), expectedUrls);
|
|
647
281
|
});
|
|
@@ -104,14 +104,15 @@ function ensureWorker() {
|
|
|
104
104
|
_restartCount++
|
|
105
105
|
})
|
|
106
106
|
worker.on('exit', (code) => {
|
|
107
|
+
const exitError = new Error(`Worker exited with code ${code}`)
|
|
107
108
|
if (code !== 0) {
|
|
108
109
|
__mixdogMemoryLog(`[embed] worker exited with code ${code}\n`)
|
|
109
|
-
for (const [, p] of _pending) p.reject(new Error(`Worker exited with code ${code}`))
|
|
110
|
-
_pending.clear()
|
|
111
110
|
_restartCount++
|
|
112
111
|
} else {
|
|
113
112
|
_restartCount = 0
|
|
114
113
|
}
|
|
114
|
+
for (const [, p] of _pending) p.reject(exitError)
|
|
115
|
+
_pending.clear()
|
|
115
116
|
worker = null
|
|
116
117
|
_modelReady = false
|
|
117
118
|
})
|