Skip to content

Commit 5286740

Browse files
committed
fix: fingerprint the built daemon's real import graph (#1545)
The static-import regex in computeDaemonCodeSignature required whitespace around `from` and after `import`/`export`. The built daemon entry is a minified bundle (tsdown/rolldown `minify: true`), whose real import statements have zero whitespace (e.g. `import{o as e}from"../foo.js"`), so the regex never matched any dependency edge and the graph walk silently degraded to fingerprinting only the entry file (`graph:1`) instead of its true ~50-file runtime graph. That defeats the safety net the signature exists for: a daemon started from one build can look identical to a client running a materially different build as long as the entry file's own size/mtime happen to coincide, and the "code-signature mismatch" takeover this check drives was one of the mechanisms observed in #1545's worktree dev-daemon multiplication.
1 parent b59b4e5 commit 5286740

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

‎src/daemon/code-signature.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ import fs from 'node:fs';
33
import path from 'node:path';
44
import { findProjectRoot } from '../utils/version.ts';
55

6+
// `\s*` (not `\s+`) around each optional part: the built daemon entry is a
7+
// minified bundle (tsdown/rolldown `minify: true`), so real import/export
8+
// statements have zero whitespace around `from` and even after the keyword
9+
// itself (e.g. `import{o as e}from"../foo.js"`). `\s+` here would never match
10+
// minified output, silently degrading the whole graph walk to just the entry
11+
// file (see #1545). The `(?![\w$])` boundary after the keyword still rejects
12+
// identifiers like `importantValue` that merely start with "import".
613
const STATIC_IMPORT_RE =
7-
/(?:^|[^\w$.])(?:import|export)\s+(?:type\s+)?(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]/gm;
14+
/(?:^|[^\w$.])(?:import|export)(?![\w$])\s*(?:type\s+)?(?:[^'"`]*?\s*from\s*)?['"]([^'"]+)['"]/gm;
815
const DYNAMIC_IMPORT_RE = /import\(\s*['"]([^'"]+)['"]\s*\)/gm;
916
const RESOLVABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const;
1017

‎src/utils/__tests__/daemon-client.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,6 +1776,47 @@ test('computeDaemonCodeSignature fingerprints the daemon runtime import graph',
17761776
}
17771777
});
17781778

1779+
test('computeDaemonCodeSignature walks minified bundler output (#1545)', () => {
1780+
// The published/built daemon entry is a minified bundle (tsdown/rolldown
1781+
// `minify: true`): real import/export statements have zero whitespace
1782+
// around `from` or even after the keyword itself, e.g.
1783+
// `import{o as e}from"../foo.js"`. Before this regression fix, the
1784+
// whitespace-requiring regex never matched any of that, so the signature
1785+
// silently degraded to fingerprinting only the entry file — real
1786+
// dependency changes went undetected while the entry file's own rebuilt
1787+
// mtime still flipped the signature on every unrelated rebuild.
1788+
const root = mkdtempForTestSync('agent-device-daemon-signature-minified-');
1789+
try {
1790+
const daemonEntryPath = path.join(root, 'daemon.js');
1791+
const depPath = path.join(root, 'dep.js');
1792+
const reExportPath = path.join(root, 're-export.js');
1793+
const sideEffectPath = path.join(root, 'side-effect.js');
1794+
fs.writeFileSync(
1795+
daemonEntryPath,
1796+
[
1797+
`import{a as x}from"./dep.js";`,
1798+
`export*from"./re-export.js";`,
1799+
`import"./side-effect.js";`,
1800+
`const importantValue="not-an-import";`,
1801+
`export{x};`,
1802+
].join(''),
1803+
'utf8',
1804+
);
1805+
fs.writeFileSync(depPath, `export const a=1;`, 'utf8');
1806+
fs.writeFileSync(reExportPath, `export const b=1;`, 'utf8');
1807+
fs.writeFileSync(sideEffectPath, `globalThis.sideEffect=1;`, 'utf8');
1808+
1809+
const signature = computeDaemonCodeSignature(daemonEntryPath, root);
1810+
assert.match(signature, /^graph:4:[0-9a-f]{40}$/);
1811+
1812+
fs.writeFileSync(depPath, `export const a=2;`, 'utf8');
1813+
const changed = computeDaemonCodeSignature(daemonEntryPath, root);
1814+
assert.notEqual(changed, signature);
1815+
} finally {
1816+
fs.rmSync(root, { recursive: true, force: true });
1817+
}
1818+
});
1819+
17791820
test('stopDaemonProcessForTakeover terminates a matching daemon process', async (t) => {
17801821
const root = mkdtempForTestSync('agent-device-daemon-test-');
17811822
const daemonDir = path.join(root, 'agent-device', 'dist', 'src', 'internal');

0 commit comments

Comments
 (0)